| #!/usr/bin/env python | 
 | # | 
 | # Copyright 2019 Google LLC | 
 | # | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 |  | 
 | import difflib | 
 | import os | 
 | import re | 
 | import subprocess | 
 | import sys | 
 |  | 
 |  | 
 | # Any files in Git which match these patterns will be included, either directly | 
 | # or indirectly via a parent dir. | 
 | PATH_PATTERNS = [ | 
 |   r'.*\.c$', | 
 |   r'.*\.cc$', | 
 |   r'.*\.cpp$', | 
 |   r'.*\.gn$', | 
 |   r'.*\.gni$', | 
 |   r'.*\.h$', | 
 |   r'.*\.mm$', | 
 |   r'.*\.storyboard$', | 
 | ] | 
 |  | 
 | # These paths are always added to the inclusion list. Note that they may not | 
 | # appear in the isolate if they are included indirectly via a parent dir. | 
 | EXPLICIT_PATHS = [ | 
 |   '../.gclient', | 
 |   '.clang-format', | 
 |   '.clang-tidy', | 
 |   'bin/fetch-clang-format', | 
 |   'bin/fetch-gn', | 
 |   'buildtools', | 
 |   'infra/bots/assets/android_ndk_darwin/VERSION', | 
 |   'infra/bots/assets/android_ndk_linux/VERSION', | 
 |   'infra/bots/assets/android_ndk_windows/VERSION', | 
 |   'infra/bots/assets/cast_toolchain/VERSION', | 
 |   'infra/bots/assets/clang_linux/VERSION', | 
 |   'infra/bots/assets/clang_win/VERSION', | 
 |   'infra/canvaskit', | 
 |   'infra/pathkit', | 
 |   'resources', | 
 |   'third_party/externals', | 
 | ] | 
 |  | 
 | # If a parent path contains more than this many immediate child paths (ie. files | 
 | # and dirs which are directly inside it as opposed to indirect descendants), we | 
 | # will include the parent in the isolate file instead of the children. This | 
 | # results in a simpler isolate file which should need to be changed less often. | 
 | COMBINE_PATHS_THRESHOLD = 3 | 
 |  | 
 | # Template for the isolate file content. | 
 | ISOLATE_TMPL = '''{ | 
 |   'includes': [ | 
 |     'run_recipe.isolate', | 
 |   ], | 
 |   'variables': { | 
 |     'files': [ | 
 | %s | 
 |     ], | 
 |   }, | 
 | } | 
 | ''' | 
 |  | 
 | # Absolute path to the infra/bots dir. | 
 | INFRABOTS_DIR = os.path.realpath(os.path.dirname(os.path.abspath(__file__))) | 
 |  | 
 | # Absolute path to the compile.isolate file. | 
 | ISOLATE_FILE = os.path.join(INFRABOTS_DIR, 'compile.isolate') | 
 |  | 
 |  | 
 | def all_paths(): | 
 |   """Return all paths which are checked in to git.""" | 
 |   repo_root = os.path.abspath(os.path.join(INFRABOTS_DIR, os.pardir, os.pardir)) | 
 |   output = subprocess.check_output(['git', 'ls-files'], cwd=repo_root).rstrip() | 
 |   return output.splitlines() | 
 |  | 
 |  | 
 | def get_relevant_paths(): | 
 |   """Return all checked-in paths in PATH_PATTERNS or EXPLICIT_PATHS.""" | 
 |   paths = [] | 
 |   for f in all_paths(): | 
 |     for regexp in PATH_PATTERNS: | 
 |       if re.match(regexp, f): | 
 |         paths.append(f) | 
 |         break | 
 |  | 
 |   paths.extend(EXPLICIT_PATHS) | 
 |   return paths | 
 |  | 
 |  | 
 | class Tree(object): | 
 |   """Tree helps with deduplicating and collapsing paths.""" | 
 |   class Node(object): | 
 |     """Node represents an individual node in a Tree.""" | 
 |     def __init__(self, name): | 
 |       self._children = {} | 
 |       self._name = name | 
 |       self._is_leaf = False | 
 |  | 
 |     @property | 
 |     def is_root(self): | 
 |       """Return True iff this is the root node.""" | 
 |       return self._name is None | 
 |  | 
 |     def add(self, entry): | 
 |       """Add the given entry (given as a list of strings) to the Node.""" | 
 |       # Remove the first element if we're not the root node. | 
 |       if not self.is_root: | 
 |         if entry[0] != self._name: | 
 |           raise ValueError('Cannot add a non-matching entry to a Node!') | 
 |         entry = entry[1:] | 
 |  | 
 |         # If the entry is now empty, this node is a leaf. | 
 |         if not entry: | 
 |           self._is_leaf = True | 
 |           return | 
 |  | 
 |       # Add a child node. | 
 |       if not self._is_leaf: | 
 |         child = self._children.get(entry[0]) | 
 |         if not child: | 
 |           child = Tree.Node(entry[0]) | 
 |           self._children[entry[0]] = child | 
 |         child.add(entry) | 
 |  | 
 |         # If we have more than COMBINE_PATHS_THRESHOLD immediate children, | 
 |         # combine them into this node. | 
 |         immediate_children = 0 | 
 |         for child in self._children.itervalues(): | 
 |           if child._is_leaf: | 
 |             immediate_children += 1 | 
 |         if not self.is_root and immediate_children >= COMBINE_PATHS_THRESHOLD: | 
 |           self._is_leaf = True | 
 |           self._children = {} | 
 |  | 
 |     def entries(self): | 
 |       """Return the entries represented by this node and its children. | 
 |  | 
 |       Will not return children in the following cases: | 
 |         - This Node is a leaf, ie. it represents an entry which was explicitly | 
 |           inserted into the Tree, as opposed to only part of a path to other | 
 |           entries. | 
 |         - This Node has immediate children exceeding COMBINE_PATHS_THRESHOLD and | 
 |           thus has been upgraded to a leaf node. | 
 |       """ | 
 |       if self._is_leaf: | 
 |         return [self._name] | 
 |       rv = [] | 
 |       for child in self._children.itervalues(): | 
 |         for entry in child.entries(): | 
 |           if not self.is_root: | 
 |             entry = self._name + '/' + entry | 
 |           rv.append(entry) | 
 |       return rv | 
 |  | 
 |   def __init__(self): | 
 |     self._root = Tree.Node(None) | 
 |  | 
 |   def add(self, entry): | 
 |     """Add the given entry to the tree.""" | 
 |     split = entry.split('/') | 
 |     if split[-1] == '': | 
 |       split = split[:-1] | 
 |     self._root.add(split) | 
 |  | 
 |   def entries(self): | 
 |     """Return the list of entries in the tree. | 
 |  | 
 |     Entries will be de-duplicated as follows: | 
 |       - Any entry which is a sub-path of another entry will not be returned. | 
 |       - Any entry which was not explicitly inserted but has children exceeding | 
 |         the COMBINE_PATHS_THRESHOLD will be returned while its children will not | 
 |         be returned. | 
 |     """ | 
 |     return self._root.entries() | 
 |  | 
 |  | 
 | def relpath(repo_path): | 
 |   """Return a relative path to the given path within the repo. | 
 |  | 
 |   The path is relative to the infra/bots dir, where the compile.isolate file | 
 |   lives. | 
 |   """ | 
 |   repo_path = '../../' + repo_path | 
 |   repo_path = repo_path.replace('../../infra/', '../') | 
 |   repo_path = repo_path.replace('../bots/', '') | 
 |   return repo_path | 
 |  | 
 |  | 
 | def get_isolate_content(paths): | 
 |   """Construct the new content of the isolate file based on the given paths.""" | 
 |   lines = ['      \'%s\',' % relpath(p) for p in paths] | 
 |   lines.sort() | 
 |   return ISOLATE_TMPL % '\n'.join(lines) | 
 |  | 
 |  | 
 | def main(): | 
 |   """Regenerate the compile.isolate file, or verify that it hasn't changed.""" | 
 |   testing = False | 
 |   if len(sys.argv) == 2 and sys.argv[1] == 'test': | 
 |     testing = True | 
 |   elif len(sys.argv) != 1: | 
 |     print >> sys.stderr, 'Usage: %s [test]' % sys.argv[0] | 
 |     sys.exit(1) | 
 |  | 
 |   tree = Tree() | 
 |   for p in get_relevant_paths(): | 
 |     tree.add(p) | 
 |   content = get_isolate_content(tree.entries()) | 
 |  | 
 |   if testing: | 
 |     with open(ISOLATE_FILE, 'rb') as f: | 
 |       expect_content = f.read() | 
 |     if content != expect_content: | 
 |       print >> sys.stderr, 'Found diff in %s:' % ISOLATE_FILE | 
 |       a = expect_content.splitlines() | 
 |       b = content.splitlines() | 
 |       diff = difflib.context_diff(a, b, lineterm='') | 
 |       for line in diff: | 
 |         sys.stderr.write(line + '\n') | 
 |       print >> sys.stderr, 'You may need to run:\n\n\tpython %s' % sys.argv[0] | 
 |       sys.exit(1) | 
 |   else: | 
 |     with open(ISOLATE_FILE, 'wb') as f: | 
 |       f.write(content) | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |   main() |