blob: ea55908bea0858d98775a084ec6753f1588e93e2 [file] [log] [blame]
Eric Boren32c60992020-12-09 18:09:20 +00001#!/usr/bin/env python
2#
3# Copyright 2019 Google LLC
4#
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8
9import difflib
10import os
11import re
12import subprocess
13import sys
14
15
16# Any files in Git which match these patterns will be included, either directly
17# or indirectly via a parent dir.
18PATH_PATTERNS = [
19 r'.*\.c$',
20 r'.*\.cc$',
21 r'.*\.cpp$',
22 r'.*\.gn$',
23 r'.*\.gni$',
24 r'.*\.h$',
25 r'.*\.mm$',
26 r'.*\.storyboard$',
27]
28
29# These paths are always added to the inclusion list. Note that they may not
30# appear in the isolate if they are included indirectly via a parent dir.
31EXPLICIT_PATHS = [
32 '../.gclient',
33 '.clang-format',
34 '.clang-tidy',
35 'bin/fetch-clang-format',
36 'bin/fetch-gn',
37 'buildtools',
38 'infra/bots/assets/android_ndk_darwin/VERSION',
39 'infra/bots/assets/android_ndk_linux/VERSION',
40 'infra/bots/assets/android_ndk_windows/VERSION',
41 'infra/bots/assets/cast_toolchain/VERSION',
42 'infra/bots/assets/clang_linux/VERSION',
43 'infra/bots/assets/clang_win/VERSION',
44 'infra/canvaskit',
45 'infra/pathkit',
46 'resources',
47 'third_party/externals',
48]
49
50# If a parent path contains more than this many immediate child paths (ie. files
51# and dirs which are directly inside it as opposed to indirect descendants), we
52# will include the parent in the isolate file instead of the children. This
53# results in a simpler isolate file which should need to be changed less often.
54COMBINE_PATHS_THRESHOLD = 3
55
56# Template for the isolate file content.
57ISOLATE_TMPL = '''{
58 'includes': [
59 'run_recipe.isolate',
60 ],
61 'variables': {
62 'files': [
63%s
64 ],
65 },
66}
67'''
68
69# Absolute path to the infra/bots dir.
70INFRABOTS_DIR = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))
71
72# Absolute path to the compile.isolate file.
73ISOLATE_FILE = os.path.join(INFRABOTS_DIR, 'compile.isolate')
74
75
76def all_paths():
77 """Return all paths which are checked in to git."""
78 repo_root = os.path.abspath(os.path.join(INFRABOTS_DIR, os.pardir, os.pardir))
79 output = subprocess.check_output(['git', 'ls-files'], cwd=repo_root).rstrip()
80 return output.splitlines()
81
82
83def get_relevant_paths():
84 """Return all checked-in paths in PATH_PATTERNS or EXPLICIT_PATHS."""
85 paths = []
86 for f in all_paths():
87 for regexp in PATH_PATTERNS:
88 if re.match(regexp, f):
89 paths.append(f)
90 break
91
92 paths.extend(EXPLICIT_PATHS)
93 return paths
94
95
96class Tree(object):
97 """Tree helps with deduplicating and collapsing paths."""
98 class Node(object):
99 """Node represents an individual node in a Tree."""
100 def __init__(self, name):
101 self._children = {}
102 self._name = name
103 self._is_leaf = False
104
105 @property
106 def is_root(self):
107 """Return True iff this is the root node."""
108 return self._name is None
109
110 def add(self, entry):
111 """Add the given entry (given as a list of strings) to the Node."""
112 # Remove the first element if we're not the root node.
113 if not self.is_root:
114 if entry[0] != self._name:
115 raise ValueError('Cannot add a non-matching entry to a Node!')
116 entry = entry[1:]
117
118 # If the entry is now empty, this node is a leaf.
119 if not entry:
120 self._is_leaf = True
121 return
122
123 # Add a child node.
124 if not self._is_leaf:
125 child = self._children.get(entry[0])
126 if not child:
127 child = Tree.Node(entry[0])
128 self._children[entry[0]] = child
129 child.add(entry)
130
131 # If we have more than COMBINE_PATHS_THRESHOLD immediate children,
132 # combine them into this node.
133 immediate_children = 0
134 for child in self._children.itervalues():
135 if child._is_leaf:
136 immediate_children += 1
137 if not self.is_root and immediate_children >= COMBINE_PATHS_THRESHOLD:
138 self._is_leaf = True
139 self._children = {}
140
141 def entries(self):
142 """Return the entries represented by this node and its children.
143
144 Will not return children in the following cases:
145 - This Node is a leaf, ie. it represents an entry which was explicitly
146 inserted into the Tree, as opposed to only part of a path to other
147 entries.
148 - This Node has immediate children exceeding COMBINE_PATHS_THRESHOLD and
149 thus has been upgraded to a leaf node.
150 """
151 if self._is_leaf:
152 return [self._name]
153 rv = []
154 for child in self._children.itervalues():
155 for entry in child.entries():
156 if not self.is_root:
157 entry = self._name + '/' + entry
158 rv.append(entry)
159 return rv
160
161 def __init__(self):
162 self._root = Tree.Node(None)
163
164 def add(self, entry):
165 """Add the given entry to the tree."""
166 split = entry.split('/')
167 if split[-1] == '':
168 split = split[:-1]
169 self._root.add(split)
170
171 def entries(self):
172 """Return the list of entries in the tree.
173
174 Entries will be de-duplicated as follows:
175 - Any entry which is a sub-path of another entry will not be returned.
176 - Any entry which was not explicitly inserted but has children exceeding
177 the COMBINE_PATHS_THRESHOLD will be returned while its children will not
178 be returned.
179 """
180 return self._root.entries()
181
182
183def relpath(repo_path):
184 """Return a relative path to the given path within the repo.
185
186 The path is relative to the infra/bots dir, where the compile.isolate file
187 lives.
188 """
189 repo_path = '../../' + repo_path
190 repo_path = repo_path.replace('../../infra/', '../')
191 repo_path = repo_path.replace('../bots/', '')
192 return repo_path
193
194
195def get_isolate_content(paths):
196 """Construct the new content of the isolate file based on the given paths."""
197 lines = [' \'%s\',' % relpath(p) for p in paths]
198 lines.sort()
199 return ISOLATE_TMPL % '\n'.join(lines)
200
201
202def main():
203 """Regenerate the compile.isolate file, or verify that it hasn't changed."""
204 testing = False
205 if len(sys.argv) == 2 and sys.argv[1] == 'test':
206 testing = True
207 elif len(sys.argv) != 1:
208 print >> sys.stderr, 'Usage: %s [test]' % sys.argv[0]
209 sys.exit(1)
210
211 tree = Tree()
212 for p in get_relevant_paths():
213 tree.add(p)
214 content = get_isolate_content(tree.entries())
215
216 if testing:
217 with open(ISOLATE_FILE, 'rb') as f:
218 expect_content = f.read()
219 if content != expect_content:
220 print >> sys.stderr, 'Found diff in %s:' % ISOLATE_FILE
221 a = expect_content.splitlines()
222 b = content.splitlines()
223 diff = difflib.context_diff(a, b, lineterm='')
224 for line in diff:
225 sys.stderr.write(line + '\n')
226 print >> sys.stderr, 'You may need to run:\n\n\tpython %s' % sys.argv[0]
227 sys.exit(1)
228 else:
229 with open(ISOLATE_FILE, 'wb') as f:
230 f.write(content)
231
232
233if __name__ == '__main__':
234 main()