#!/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'.*\.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()