xref: /aosp_15_r20/external/skia/infra/bots/task_drivers/common/bazel_utils.go (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1// Copyright 2023 Google LLC
2//
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5
6package common
7
8import (
9	"archive/zip"
10	"context"
11	"fmt"
12	"io"
13	"os"
14	"path/filepath"
15	"regexp"
16	"strings"
17
18	"go.skia.org/infra/go/skerr"
19	"go.skia.org/infra/go/util"
20	"go.skia.org/infra/task_driver/go/lib/os_steps"
21	"go.skia.org/infra/task_driver/go/td"
22)
23
24// validBazelLabelRegexps represent valid, fully-qualified Bazel labels.
25var validBazelLabelRegexps = []*regexp.Regexp{
26	regexp.MustCompile(`^//:[a-zA-Z0-9_-]+$`),                  // Matches "//:foo".
27	regexp.MustCompile(`^/(/[a-zA-Z0-9_-]+)+:[a-zA-Z0-9_-]+$`), // Matches "//foo:bar", "//foo/bar:baz", etc.
28}
29
30// ValidateLabelAndReturnOutputsZipPath validates the given Bazel label and returns the path within
31// the checkout directory where the ZIP archive with undeclared test outputs will be found, if
32// applicable.
33func ValidateLabelAndReturnOutputsZipPath(checkoutDir, label string) (string, error) {
34	valid := false
35	for _, re := range validBazelLabelRegexps {
36		if re.MatchString(label) {
37			valid = true
38			break
39		}
40	}
41	if !valid {
42		return "", skerr.Fmt("invalid label: %q", label)
43	}
44
45	return filepath.Join(
46		checkoutDir,
47		"bazel-testlogs",
48		strings.ReplaceAll(strings.TrimPrefix(label, "//"), ":", "/"),
49		"test.outputs",
50		"outputs.zip"), nil
51}
52
53// ExtractOutputsZip extracts the undeclared outputs ZIP archive into a temporary directory, and
54// returns the path to said directory.
55func ExtractOutputsZip(ctx context.Context, outputsZipPath string) (string, error) {
56	// Create extraction directory.
57	extractionDir, err := os_steps.TempDir(ctx, "", "bazel-test-output-dir-*")
58	if err != nil {
59		return "", skerr.Wrap(err)
60	}
61
62	// Extract ZIP archive.
63	if err := td.Do(ctx, td.Props(fmt.Sprintf("Extract undeclared outputs archive %s into %s", outputsZipPath, extractionDir)), func(ctx context.Context) error {
64		outputsZip, err := zip.OpenReader(outputsZipPath)
65		if err != nil {
66			return skerr.Wrap(err)
67		}
68		defer util.Close(outputsZip)
69
70		for _, file := range outputsZip.File {
71			// Skip directories. We assume all output files are at the root directory of the archive.
72			if file.FileInfo().IsDir() {
73				if err := td.Do(ctx, td.Props(fmt.Sprintf("Not extracting subdirectory: %s", file.Name)), func(ctx context.Context) error { return nil }); err != nil {
74					return skerr.Wrap(err)
75				}
76				continue
77			}
78
79			// Skip files within subdirectories.
80			if strings.Contains(file.Name, "/") {
81				if err := td.Do(ctx, td.Props(fmt.Sprintf("Not extracting file within subdirectory: %s", file.Name)), func(ctx context.Context) error { return nil }); err != nil {
82					return skerr.Wrap(err)
83				}
84				continue
85			}
86
87			// Ignore anything that is not a PNG or JSON file.
88			if !strings.HasSuffix(strings.ToLower(file.Name), ".png") && !strings.HasSuffix(strings.ToLower(file.Name), ".json") {
89				if err := td.Do(ctx, td.Props(fmt.Sprintf("Not extracting non-PNG / non-JSON file: %s", file.Name)), func(ctx context.Context) error { return nil }); err != nil {
90					return skerr.Wrap(err)
91				}
92				continue
93			}
94
95			// Extract file.
96			if err := td.Do(ctx, td.Props(fmt.Sprintf("Extracting file: %s", file.Name)), func(ctx context.Context) error {
97				reader, err := file.Open()
98				if err != nil {
99					return skerr.Wrap(err)
100				}
101				defer util.Close(reader)
102
103				bytes, err := io.ReadAll(reader)
104				if err != nil {
105					return skerr.Wrap(err)
106				}
107				return skerr.Wrap(os.WriteFile(filepath.Join(extractionDir, file.Name), bytes, 0644))
108			}); err != nil {
109				return skerr.Wrap(err)
110			}
111		}
112
113		return nil
114	}); err != nil {
115		return "", skerr.Wrap(err)
116	}
117
118	return extractionDir, nil
119}
120