xref: /aosp_15_r20/external/skia/infra/bots/gen_tasks_logic/compile_cas.go (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package gen_tasks_logic
6
7import (
8	"log"
9	"os/exec"
10	"path/filepath"
11	"regexp"
12	"sort"
13	"strings"
14
15	"go.skia.org/infra/go/cas/rbe"
16	"go.skia.org/infra/task_scheduler/go/specs"
17)
18
19const (
20	// If a parent path contains more than this many immediate child paths (ie.
21	// files and dirs which are directly inside it as opposed to indirect
22	// descendants), we will include the parent in the isolate file instead of
23	// the children. This results in a simpler CasSpec which should need to be
24	// changed less often.
25	combinePathsThreshold = 3
26)
27
28var (
29	// Any files in Git which match these patterns will be included, either
30	// directly or indirectly via a parent dir.
31	pathRegexes = []*regexp.Regexp{
32		regexp.MustCompile(`.*\.c$`),
33		regexp.MustCompile(`.*\.cc$`),
34		regexp.MustCompile(`.*\.cpp$`),
35		regexp.MustCompile(`.*\.gn$`),
36		regexp.MustCompile(`.*\.gni$`),
37		regexp.MustCompile(`.*\.h$`),
38		regexp.MustCompile(`.*\.mm$`),
39		regexp.MustCompile(`.*\.storyboard$`),
40	}
41
42	// These paths are always added to the inclusion list. Note that they may
43	// not appear in the CasSpec if they are included indirectly via a parent
44	// dir.
45	explicitPaths = []string{
46		".bazelrc",
47		".bazelversion",
48		".clang-format",
49		".clang-tidy",
50		".vpython3",
51		"BUILD.bazel",
52		"DEPS", // Needed by bin/fetch-ninja
53		"WORKSPACE.bazel",
54		"bazel",
55		"bin/activate-emsdk",
56		"bin/fetch-clang-format",
57		"bin/fetch-gn",
58		"bin/fetch-ninja",
59		"buildtools",
60		"example",
61		"experimental/rust_png",
62		"go_repositories.bzl",
63		"infra/bots/assets/android_ndk_darwin/VERSION",
64		"infra/bots/assets/android_ndk_linux/VERSION",
65		"infra/bots/assets/android_ndk_windows/VERSION",
66		"infra/bots/assets/cast_toolchain/VERSION",
67		"infra/bots/assets/clang_linux/VERSION",
68		"infra/bots/assets/clang_win/VERSION",
69		"infra/bots/run_recipe.py",
70		"infra/bots/task_drivers",
71		"infra/canvaskit",
72		"infra/pathkit",
73		"package.json",
74		"package-lock.json",
75		"requirements.txt",
76		"resources",
77		"third_party/externals",
78		"toolchain",
79	}
80)
81
82// getAllCheckedInPaths returns every path checked in to the repo.
83func getAllCheckedInPaths(cfg *Config) []string {
84	cmd := exec.Command("git", "ls-files")
85	// Use cfg.PathToSkia to get to the Skia checkout, in case this is used by
86	// another repo.
87	cmd.Dir = filepath.Join(CheckoutRoot(), cfg.PathToSkia)
88	output, err := cmd.CombinedOutput()
89	if err != nil {
90		log.Fatal(err)
91	}
92	split := strings.Split(string(output), "\n")
93	rv := make([]string, 0, len(split))
94	for _, line := range split {
95		if line != "" {
96			rv = append(rv, line)
97		}
98	}
99	return rv
100}
101
102// getRelevantPaths returns all paths needed by compile tasks.
103func getRelevantPaths(cfg *Config) []string {
104	rv := []string{}
105	for _, path := range getAllCheckedInPaths(cfg) {
106		for _, regex := range pathRegexes {
107			if regex.MatchString(path) {
108				rv = append(rv, path)
109				break
110			}
111		}
112	}
113	return append(rv, explicitPaths...)
114}
115
116// node is a single node in a directory tree of task inputs.
117type node struct {
118	children map[string]*node
119	name     string
120	isLeaf   bool
121}
122
123// newNode returns a node instance.
124func newNode(name string) *node {
125	return &node{
126		children: map[string]*node{},
127		name:     name,
128		isLeaf:   false,
129	}
130}
131
132// isRoot returns true iff this is the root node.
133func (n *node) isRoot() bool {
134	return n.name == ""
135}
136
137// add the given entry (given as a slice of path components) to the node.
138func (n *node) add(entry []string) {
139	// Remove the first element if we're not the root node.
140	if !n.isRoot() {
141		if entry[0] != n.name {
142			log.Fatalf("Failed to compute compile CAS inputs; attempting to add entry %v to node %q", entry, n.name)
143		}
144		entry = entry[1:]
145
146		// If the entry is now empty, this node is a leaf.
147		if len(entry) == 0 {
148			n.isLeaf = true
149			return
150		}
151	}
152
153	// Add a child node.
154	if !n.isLeaf {
155		name := entry[0]
156		child, ok := n.children[name]
157		if !ok {
158			child = newNode(name)
159			n.children[name] = child
160		}
161		child.add(entry)
162
163		// If we have more than combinePathsThreshold immediate children,
164		// combine them into this node.
165		immediateChilden := 0
166		for _, child := range n.children {
167			if child.isLeaf {
168				immediateChilden++
169			}
170			if !n.isRoot() && immediateChilden >= combinePathsThreshold {
171				n.isLeaf = true
172				n.children = map[string]*node{}
173			}
174		}
175	}
176}
177
178// entries returns the entries represented by this node and its children.
179// Will not return children in the following cases:
180//   - This Node is a leaf, ie. it represents an entry which was explicitly
181//     inserted into the Tree, as opposed to only part of a path to other
182//     entries.
183//   - This Node has immediate children exceeding combinePathsThreshold and
184//     thus has been upgraded to a leaf node.
185func (n *node) entries() [][]string {
186	if n.isLeaf {
187		return [][]string{{n.name}}
188	}
189	rv := [][]string{}
190	for _, child := range n.children {
191		for _, entry := range child.entries() {
192			if !n.isRoot() {
193				entry = append([]string{n.name}, entry...)
194			}
195			rv = append(rv, entry)
196		}
197	}
198	return rv
199}
200
201// tree represents a directory tree of task inputs.
202type tree struct {
203	root *node
204}
205
206// newTree returns a tree instance.
207func newTree() *tree {
208	return &tree{
209		root: newNode(""),
210	}
211}
212
213// add the given path to the tree. Entries may be combined as defined by
214// combinePathsThreshold.
215func (t *tree) add(p string) {
216	split := strings.Split(p, "/")
217	t.root.add(split)
218}
219
220// entries returns all entries in the tree. Entries may be combined as defined
221// by combinePathsThreshold.
222func (t *tree) entries() []string {
223	entries := t.root.entries()
224	rv := make([]string, 0, len(entries))
225	for _, entry := range entries {
226		rv = append(rv, strings.Join(append([]string{"skia"}, entry...), "/"))
227	}
228	sort.Strings(rv)
229	return rv
230}
231
232// generateCompileCAS creates the CasSpec used for tasks which build Skia.
233func generateCompileCAS(b *specs.TasksCfgBuilder, cfg *Config) {
234	t := newTree()
235	for _, path := range getRelevantPaths(cfg) {
236		t.add(path)
237	}
238	spec := &specs.CasSpec{
239		Root:     "..",
240		Paths:    t.entries(),
241		Excludes: []string{rbe.ExcludeGitDir},
242	}
243	b.MustAddCasSpec(CAS_COMPILE, spec)
244}
245