Eric Boren | 32c6099 | 2020-12-09 18:09:20 +0000 | [diff] [blame] | 1 | #!/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 | |
| 9 | import difflib |
| 10 | import os |
| 11 | import re |
| 12 | import subprocess |
| 13 | import sys |
| 14 | |
| 15 | |
| 16 | # Any files in Git which match these patterns will be included, either directly |
| 17 | # or indirectly via a parent dir. |
| 18 | PATH_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. |
| 31 | EXPLICIT_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. |
| 54 | COMBINE_PATHS_THRESHOLD = 3 |
| 55 | |
| 56 | # Template for the isolate file content. |
| 57 | ISOLATE_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. |
| 70 | INFRABOTS_DIR = os.path.realpath(os.path.dirname(os.path.abspath(__file__))) |
| 71 | |
| 72 | # Absolute path to the compile.isolate file. |
| 73 | ISOLATE_FILE = os.path.join(INFRABOTS_DIR, 'compile.isolate') |
| 74 | |
| 75 | |
| 76 | def 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 | |
| 83 | def 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 | |
| 96 | class 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 | |
| 183 | def 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 | |
| 195 | def 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 | |
| 202 | def 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 | |
| 233 | if __name__ == '__main__': |
| 234 | main() |