| #!/usr/bin/env -S python3 -u | 
 |  | 
 | """ | 
 | This script helps find various build behaviors that make builds less hermetic | 
 | and repeatable. Depending on the flags, it runs a sequence of builds and looks | 
 | for files that have changed or have been improperly regenerated, updating | 
 | their timestamps incorrectly. It also looks for changes that the build has | 
 | done to the source tree, and for files whose contents are dependent on the | 
 | location of the out directory. | 
 |  | 
 | This utility has two major modes, full and incremental. By default, this tool | 
 | runs in full mode. To run in incremental mode, pass the --incremental flag. | 
 |  | 
 |  | 
 | FULL MODE | 
 |  | 
 | In full mode, this tool helps verify BUILD CORRECTNESS by examining its | 
 | REPEATABILITY. In full mode, this tool runs two complete builds in different | 
 | directories and compares the CONTENTS of the two directories. Lists of any | 
 | files that are added, removed or changed are printed, sorted by the timestamp | 
 | of that file, to aid finding which dependencies trigger the rebuilding of | 
 | other files. | 
 |  | 
 |  | 
 | INCREMENTAL MODE | 
 |  | 
 | In incremental mode, this tool helps verfiy the SPEED of the build. It runs two | 
 | builds and looks at the TIMESTAMPS of the generated files, and reports files | 
 | that were changed by the second build. In theory, an incremental build with no | 
 | source files touched should not have any generated targets changed. As in full | 
 | builds, the file list is returned sorted by timestamp. | 
 |  | 
 |  | 
 | OTHER CHECKS | 
 |  | 
 | In both full and incremental mode, this tool looks at the timestamps of all | 
 | source files in the tree, and reports on files that have been touched. In the | 
 | output, these are labeled with the header "Source files touched after start of | 
 | build." | 
 |  | 
 | In addition, by default, this tool sets the OUT_DIR environment variable to | 
 | something other than "out" in order to find build rules that are not respecting | 
 | the OUT_DIR. If you see these, you should fix them, but if your build can not | 
 | complete for some reason because of this, you can pass the --no-check-out-dir | 
 | flag to suppress this check. | 
 |  | 
 |  | 
 | OTHER FLAGS | 
 |  | 
 | In full mode, the --detect-embedded-paths flag does the two builds in different | 
 | directories, to help in finding rules that embed the out directory path into | 
 | the targets. | 
 |  | 
 | The --hide-build-output flag hides the output of successful bulds, to make | 
 | script output cleaner. The output of builds that fail is still shown. | 
 |  | 
 | The --no-build flag is useful if you have already done a build and would | 
 | just like to re-run the analysis. | 
 |  | 
 | The --target flag lets you specify a build target other than the default | 
 | full build (droid). You can pass "nothing" as in the example below, or a | 
 | specific target, to reduce the scope of the checks performed. | 
 |  | 
 | The --touch flag lets you specify a list of source files to touch between | 
 | the builds, to examine the consequences of editing a particular file. | 
 |  | 
 |  | 
 | EXAMPLE COMMANDLINES | 
 |  | 
 | Please run build/make/tools/compare_builds.py --help for a full listing | 
 | of the commandline flags. Here are a sampling of useful combinations. | 
 |  | 
 |   1. Find files changed during an incremental build that doesn't build | 
 |      any targets. | 
 |  | 
 |        build/make/tools/compare_builds.py --incremental --target nothing | 
 |  | 
 |      Long incremental build times, or consecutive builds that re-run build actions | 
 |      are usually caused by files being touched as part of loading the makefiles. | 
 |  | 
 |      The nothing build (m nothing) loads the make and blueprint files, generates | 
 |      the dependency graph, but then doesn't actually build any targets. Checking | 
 |      against this build is the fastest and easiest way to find files that are | 
 |      modified while makefiles are read, for example with $(shell) invocations. | 
 |  | 
 |   2. Find packaging targets that are different, ignoring intermediate files. | 
 |  | 
 |        build/make/tools/compare_builds.py --subdirs --detect-embedded-paths | 
 |  | 
 |      These flags will compare the final staging directories for partitions, | 
 |      as well as the APKs, apexes, testcases, and the like (the full directory | 
 |      list is in the DEFAULT_DIRS variable below). Since these are the files | 
 |      that are ultimately released, it is more important that these files be | 
 |      replicable, even if the intermediates that went into them are not (for | 
 |      example, when debugging symbols are stripped). | 
 |  | 
 |   3. Check that all targets are repeatable. | 
 |  | 
 |        build/make/tools/compare_builds.py --detect-embedded-paths | 
 |  | 
 |      This check will list all of the differences in built targets that it can | 
 |      find. Be aware that the AOSP tree still has quite a few targets that | 
 |      are flagged by this check, so OEM changes might be lost in that list. | 
 |      That said, each file shown here is a potential blocker for a repeatable | 
 |      build. | 
 |  | 
 |   4. See what targets are rebuilt when a file is touched between builds. | 
 |  | 
 |        build/make/tools/compare_builds.py --incremental \ | 
 |             --touch frameworks/base/core/java/android/app/Activity.java | 
 |  | 
 |      This check simulates the common engineer workflow of touching a single | 
 |      file and rebuilding the whole system. To see a restricted view, consider | 
 |      also passing a --target option for a common use case. For example: | 
 |  | 
 |        build/make/tools/compare_builds.py --incremental --target framework \ | 
 |             --touch frameworks/base/core/java/android/app/Activity.java | 
 | """ | 
 |  | 
 | import argparse | 
 | import itertools | 
 | import os | 
 | import shutil | 
 | import stat | 
 | import subprocess | 
 | import sys | 
 |  | 
 |  | 
 | # Soong | 
 | SOONG_UI = "build/soong/soong_ui.bash" | 
 |  | 
 |  | 
 | # Which directories to use if no --subdirs is supplied without explicit directories. | 
 | DEFAULT_DIRS = ( | 
 |     "apex", | 
 |     "data", | 
 |     "product", | 
 |     "ramdisk", | 
 |     "recovery", | 
 |     "root", | 
 |     "system", | 
 |     "system_ext", | 
 |     "system_other", | 
 |     "testcases", | 
 |     "vendor", | 
 | ) | 
 |  | 
 |  | 
 | # Files to skip for incremental timestamp checking | 
 | BUILD_INTERNALS_PREFIX_SKIP = ( | 
 |     "soong/.glob/", | 
 |     ".path/", | 
 | ) | 
 |  | 
 |  | 
 | BUILD_INTERNALS_SUFFIX_SKIP = ( | 
 |     "/soong/soong_build_metrics.pb", | 
 |     "/.installable_test_files", | 
 |     "/files.db", | 
 |     "/.blueprint.bootstrap", | 
 |     "/build_number.txt", | 
 |     "/build.ninja", | 
 |     "/.out-dir", | 
 |     "/build_fingerprint.txt", | 
 |     "/build_thumbprint.txt", | 
 |     "/.copied_headers_list", | 
 |     "/.installable_files", | 
 | ) | 
 |  | 
 |  | 
 | class DiffType(object): | 
 |   def __init__(self, code, message): | 
 |     self.code = code | 
 |     self.message = message | 
 |  | 
 | DIFF_NONE = DiffType("DIFF_NONE", "Files are the same") | 
 | DIFF_MODE = DiffType("DIFF_MODE", "Stat mode bits differ") | 
 | DIFF_SIZE = DiffType("DIFF_SIZE", "File size differs") | 
 | DIFF_SYMLINK = DiffType("DIFF_SYMLINK", "Symlinks point to different locations") | 
 | DIFF_CONTENTS = DiffType("DIFF_CONTENTS", "File contents differ") | 
 |  | 
 |  | 
 | def main(): | 
 |   argparser = argparse.ArgumentParser(description="Diff build outputs from two builds.", | 
 |                                       epilog="Run this command from the root of the tree." | 
 |                                         + " Before running this command, the build environment" | 
 |                                         + " must be set up, including sourcing build/envsetup.sh" | 
 |                                         + " and running lunch.") | 
 |   argparser.add_argument("--detect-embedded-paths", action="store_true", | 
 |       help="Use unique out dirs to detect paths embedded in binaries.") | 
 |   argparser.add_argument("--incremental", action="store_true", | 
 |       help="Compare which files are touched in two consecutive builds without a clean in between.") | 
 |   argparser.add_argument("--hide-build-output", action="store_true", | 
 |       help="Don't print the build output for successful builds") | 
 |   argparser.add_argument("--no-build", dest="run_build", action="store_false", | 
 |       help="Don't build or clean, but do everything else.") | 
 |   argparser.add_argument("--no-check-out-dir", dest="check_out_dir", action="store_false", | 
 |       help="Don't check for rules not honoring movable out directories.") | 
 |   argparser.add_argument("--subdirs", nargs="*", | 
 |       help="Only scan these subdirs of $PRODUCT_OUT instead of the whole out directory." | 
 |            + " The --subdirs argument with no listed directories will give a default list.") | 
 |   argparser.add_argument("--target", default="droid", | 
 |       help="Make target to run. The default is droid") | 
 |   argparser.add_argument("--touch", nargs="+", default=[], | 
 |       help="Files to touch between builds. Must pair with --incremental.") | 
 |   args = argparser.parse_args(sys.argv[1:]) | 
 |  | 
 |   if args.detect_embedded_paths and args.incremental: | 
 |     sys.stderr.write("Can't pass --detect-embedded-paths and --incremental together.\n") | 
 |     sys.exit(1) | 
 |   if args.detect_embedded_paths and not args.check_out_dir: | 
 |     sys.stderr.write("Can't pass --detect-embedded-paths and --no-check-out-dir together.\n") | 
 |     sys.exit(1) | 
 |   if args.touch and not args.incremental: | 
 |     sys.stderr.write("The --incremental flag is required if the --touch flag is passed.") | 
 |     sys.exit(1) | 
 |  | 
 |   AssertAtTop() | 
 |   RequireEnvVar("TARGET_PRODUCT") | 
 |   RequireEnvVar("TARGET_BUILD_VARIANT") | 
 |  | 
 |   # Out dir file names: | 
 |   #   - dir_prefix - The directory we'll put everything in (except for maybe the top level | 
 |   #     out/ dir). | 
 |   #   - *work_dir - The directory that we will build directly into. This is in dir_prefix | 
 |   #     unless --no-check-out-dir is set. | 
 |   #   - *out_dir - After building, if work_dir is different from out_dir, we move the out | 
 |   #     directory to here so we can do the comparisions. | 
 |   #   - timestamp_* - Files we touch so we know the various phases between the builds, so we | 
 |   #     can compare timestamps of files. | 
 |   if args.incremental: | 
 |     dir_prefix = "out_incremental" | 
 |     if args.check_out_dir: | 
 |       first_work_dir = first_out_dir = dir_prefix + "/out" | 
 |       second_work_dir = second_out_dir = dir_prefix + "/out" | 
 |     else: | 
 |       first_work_dir = first_out_dir = "out" | 
 |       second_work_dir = second_out_dir = "out" | 
 |   else: | 
 |     dir_prefix = "out_full" | 
 |     first_out_dir = dir_prefix + "/out_1" | 
 |     second_out_dir = dir_prefix + "/out_2" | 
 |     if not args.check_out_dir: | 
 |       first_work_dir = second_work_dir = "out" | 
 |     elif args.detect_embedded_paths: | 
 |       first_work_dir = first_out_dir | 
 |       second_work_dir = second_out_dir | 
 |     else: | 
 |       first_work_dir = dir_prefix + "/work" | 
 |       second_work_dir = dir_prefix + "/work" | 
 |   timestamp_start = dir_prefix + "/timestamp_start" | 
 |   timestamp_between = dir_prefix + "/timestamp_between" | 
 |   timestamp_end = dir_prefix + "/timestamp_end" | 
 |  | 
 |   if args.run_build: | 
 |     # Initial clean, if necessary | 
 |     print("Cleaning " + dir_prefix + "/") | 
 |     Clean(dir_prefix) | 
 |     print("Cleaning out/") | 
 |     Clean("out") | 
 |     CreateEmptyFile(timestamp_start) | 
 |     print("Running the first build in " + first_work_dir) | 
 |     RunBuild(first_work_dir, first_out_dir, args.target, args.hide_build_output) | 
 |     for f in args.touch: | 
 |       print("Touching " + f) | 
 |       TouchFile(f) | 
 |     CreateEmptyFile(timestamp_between) | 
 |     print("Running the second build in " + second_work_dir) | 
 |     RunBuild(second_work_dir, second_out_dir, args.target, args.hide_build_output) | 
 |     CreateEmptyFile(timestamp_end) | 
 |     print("Done building") | 
 |     print() | 
 |  | 
 |   # Which out directories to scan | 
 |   if args.subdirs is not None: | 
 |     if args.subdirs: | 
 |       subdirs = args.subdirs | 
 |     else: | 
 |       subdirs = DEFAULT_DIRS | 
 |     first_files = ProductFiles(RequireBuildVar(first_out_dir, "PRODUCT_OUT"), subdirs) | 
 |     second_files = ProductFiles(RequireBuildVar(second_out_dir, "PRODUCT_OUT"), subdirs) | 
 |   else: | 
 |     first_files = OutFiles(first_out_dir) | 
 |     second_files = OutFiles(second_out_dir) | 
 |  | 
 |   printer = Printer() | 
 |  | 
 |   if args.incremental: | 
 |     # Find files that were rebuilt unnecessarily | 
 |     touched_incrementally = FindOutFilesTouchedAfter(first_files, | 
 |                                                      GetFileTimestamp(timestamp_between)) | 
 |     printer.PrintList("Touched in incremental build", touched_incrementally) | 
 |   else: | 
 |     # Compare the two out dirs | 
 |     added, removed, changed = DiffFileList(first_files, second_files) | 
 |     printer.PrintList("Added", added) | 
 |     printer.PrintList("Removed", removed) | 
 |     printer.PrintList("Changed", changed, "%s %s") | 
 |  | 
 |   # Find files in the source tree that were touched | 
 |   touched_during = FindSourceFilesTouchedAfter(GetFileTimestamp(timestamp_start)) | 
 |   printer.PrintList("Source files touched after start of build", touched_during) | 
 |  | 
 |   # Find files and dirs that were output to "out" and didn't respect $OUT_DIR | 
 |   if args.check_out_dir: | 
 |     bad_out_dir_contents = FindFilesAndDirectories("out") | 
 |     printer.PrintList("Files and directories created by rules that didn't respect $OUT_DIR", | 
 |                       bad_out_dir_contents) | 
 |  | 
 |   # If we didn't find anything, print success message | 
 |   if not printer.printed_anything: | 
 |     print("No bad behaviors found.") | 
 |  | 
 |  | 
 | def AssertAtTop(): | 
 |   """If the current directory is not the top of an android source tree, print an error | 
 |      message and exit.""" | 
 |   if not os.access(SOONG_UI, os.X_OK): | 
 |     sys.stderr.write("FAILED: Please run from the root of the tree.\n") | 
 |     sys.exit(1) | 
 |  | 
 |  | 
 | def RequireEnvVar(name): | 
 |   """Gets an environment variable. If that fails, then print an error message and exit.""" | 
 |   result = os.environ.get(name) | 
 |   if not result: | 
 |     sys.stderr.write("error: Can't determine %s. Please run lunch first.\n" % name) | 
 |     sys.exit(1) | 
 |   return result | 
 |  | 
 |  | 
 | def RunSoong(out_dir, args, capture_output): | 
 |   env = dict(os.environ) | 
 |   env["OUT_DIR"] = out_dir | 
 |   args = [SOONG_UI,] + args | 
 |   if capture_output: | 
 |     proc = subprocess.Popen(args, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | 
 |     combined_output, none = proc.communicate() | 
 |     return proc.returncode, combined_output | 
 |   else: | 
 |     result = subprocess.run(args, env=env) | 
 |     return result.returncode, None | 
 |  | 
 |  | 
 | def GetBuildVar(out_dir, name): | 
 |   """Gets a variable from the build system.""" | 
 |   returncode, output = RunSoong(out_dir, ["--dumpvar-mode", name], True) | 
 |   if returncode != 0: | 
 |     return None | 
 |   else: | 
 |     return output.decode("utf-8").strip() | 
 |  | 
 |  | 
 | def RequireBuildVar(out_dir, name): | 
 |   """Gets a variable from the builds system. If that fails, then print an error | 
 |      message and exit.""" | 
 |   value = GetBuildVar(out_dir, name) | 
 |   if not value: | 
 |     sys.stderr.write("error: Can't determine %s. Please run lunch first.\n" % name) | 
 |     sys.exit(1) | 
 |   return value | 
 |  | 
 |  | 
 | def Clean(directory): | 
 |   """"Deletes the supplied directory.""" | 
 |   try: | 
 |     shutil.rmtree(directory) | 
 |   except FileNotFoundError: | 
 |     pass | 
 |  | 
 |  | 
 | def RunBuild(work_dir, out_dir, target, hide_build_output): | 
 |   """Runs a build. If the build fails, prints a message and exits.""" | 
 |   returncode, output = RunSoong(work_dir, | 
 |                     ["--build-mode", "--all-modules", "--dir=" + os.getcwd(), target], | 
 |                     hide_build_output) | 
 |   if work_dir != out_dir: | 
 |     os.replace(work_dir, out_dir) | 
 |   if returncode != 0: | 
 |     if hide_build_output: | 
 |       # The build output was hidden, so print it now for debugging | 
 |       sys.stderr.buffer.write(output) | 
 |     sys.stderr.write("FAILED: Build failed. Stopping.\n") | 
 |     sys.exit(1) | 
 |  | 
 |  | 
 | def DiffFileList(first_files, second_files): | 
 |   """Examines the files. | 
 |  | 
 |   Returns: | 
 |     Filenames of files in first_filelist but not second_filelist (added files) | 
 |     Filenames of files in second_filelist but not first_filelist (removed files) | 
 |     2-Tuple of filenames for the files that are in both but are different (changed files) | 
 |   """ | 
 |   # List of files, relative to their respective PRODUCT_OUT directories | 
 |   first_filelist = sorted([x for x in first_files], key=lambda x: x[1]) | 
 |   second_filelist = sorted([x for x in second_files], key=lambda x: x[1]) | 
 |  | 
 |   added = [] | 
 |   removed = [] | 
 |   changed = [] | 
 |  | 
 |   first_index = 0 | 
 |   second_index = 0 | 
 |  | 
 |   while first_index < len(first_filelist) and second_index < len(second_filelist): | 
 |     # Path relative to source root and path relative to PRODUCT_OUT | 
 |     first_full_filename, first_relative_filename = first_filelist[first_index] | 
 |     second_full_filename, second_relative_filename = second_filelist[second_index] | 
 |  | 
 |     if first_relative_filename < second_relative_filename: | 
 |       # Removed | 
 |       removed.append(first_full_filename) | 
 |       first_index += 1 | 
 |     elif first_relative_filename > second_relative_filename: | 
 |       # Added | 
 |       added.append(second_full_filename) | 
 |       second_index += 1 | 
 |     else: | 
 |       # Both present | 
 |       diff_type = DiffFiles(first_full_filename, second_full_filename) | 
 |       if diff_type != DIFF_NONE: | 
 |         changed.append((first_full_filename, second_full_filename)) | 
 |       first_index += 1 | 
 |       second_index += 1 | 
 |  | 
 |   while first_index < len(first_filelist): | 
 |     first_full_filename, first_relative_filename = first_filelist[first_index] | 
 |     removed.append(first_full_filename) | 
 |     first_index += 1 | 
 |  | 
 |   while second_index < len(second_filelist): | 
 |     second_full_filename, second_relative_filename = second_filelist[second_index] | 
 |     added.append(second_full_filename) | 
 |     second_index += 1 | 
 |  | 
 |   return (SortByTimestamp(added), | 
 |           SortByTimestamp(removed), | 
 |           SortByTimestamp(changed, key=lambda item: item[1])) | 
 |  | 
 |  | 
 | def FindOutFilesTouchedAfter(files, timestamp): | 
 |   """Find files in the given file iterator that were touched after timestamp.""" | 
 |   result = [] | 
 |   for full, relative in files: | 
 |     ts = GetFileTimestamp(full) | 
 |     if ts > timestamp: | 
 |       result.append(TouchedFile(full, ts)) | 
 |   return [f.filename for f in sorted(result, key=lambda f: f.timestamp)] | 
 |  | 
 |  | 
 | def GetFileTimestamp(filename): | 
 |   """Get timestamp for a file (just wraps stat).""" | 
 |   st = os.stat(filename, follow_symlinks=False) | 
 |   return st.st_mtime | 
 |  | 
 |  | 
 | def SortByTimestamp(items, key=lambda item: item): | 
 |   """Sort the list by timestamp of files. | 
 |   Args: | 
 |     items - the list of items to sort | 
 |     key - a function to extract a filename from each element in items | 
 |   """ | 
 |   return [x[0] for x in sorted([(item, GetFileTimestamp(key(item))) for item in items], | 
 |                                key=lambda y: y[1])] | 
 |  | 
 |  | 
 | def FindSourceFilesTouchedAfter(timestamp): | 
 |   """Find files in the source tree that have changed after timestamp. Ignores | 
 |   the out directory.""" | 
 |   result = [] | 
 |   for root, dirs, files in os.walk(".", followlinks=False): | 
 |     if root == ".": | 
 |       RemoveItemsFromList(dirs, (".repo", "out", "out_full", "out_incremental")) | 
 |     for f in files: | 
 |       full = os.path.sep.join((root, f))[2:] | 
 |       ts = GetFileTimestamp(full) | 
 |       if ts > timestamp: | 
 |         result.append(TouchedFile(full, ts)) | 
 |   return [f.filename for f in sorted(result, key=lambda f: f.timestamp)] | 
 |  | 
 |  | 
 | def FindFilesAndDirectories(directory): | 
 |   """Finds all files and directories inside a directory.""" | 
 |   result = [] | 
 |   for root, dirs, files in os.walk(directory, followlinks=False): | 
 |     result += [os.path.sep.join((root, x, "")) for x in dirs] | 
 |     result += [os.path.sep.join((root, x)) for x in files] | 
 |   return result | 
 |  | 
 |  | 
 | def CreateEmptyFile(filename): | 
 |   """Create an empty file with now as the timestamp at filename.""" | 
 |   try: | 
 |     os.makedirs(os.path.dirname(filename)) | 
 |   except FileExistsError: | 
 |     pass | 
 |   open(filename, "w").close() | 
 |   os.utime(filename) | 
 |  | 
 |  | 
 | def TouchFile(filename): | 
 |   os.utime(filename) | 
 |  | 
 |  | 
 | def DiffFiles(first_filename, second_filename): | 
 |   def AreFileContentsSame(remaining, first_filename, second_filename): | 
 |     """Compare the file contents. They must be known to be the same size.""" | 
 |     CHUNK_SIZE = 32*1024 | 
 |     with open(first_filename, "rb") as first_file: | 
 |       with open(second_filename, "rb") as second_file: | 
 |         while remaining > 0: | 
 |           size = min(CHUNK_SIZE, remaining) | 
 |           if first_file.read(CHUNK_SIZE) != second_file.read(CHUNK_SIZE): | 
 |             return False | 
 |           remaining -= size | 
 |         return True | 
 |  | 
 |   first_stat = os.stat(first_filename, follow_symlinks=False) | 
 |   second_stat = os.stat(first_filename, follow_symlinks=False) | 
 |  | 
 |   # Mode bits | 
 |   if first_stat.st_mode != second_stat.st_mode: | 
 |     return DIFF_MODE | 
 |  | 
 |   # File size | 
 |   if first_stat.st_size != second_stat.st_size: | 
 |     return DIFF_SIZE | 
 |  | 
 |   # Contents | 
 |   if stat.S_ISLNK(first_stat.st_mode): | 
 |     if os.readlink(first_filename) != os.readlink(second_filename): | 
 |       return DIFF_SYMLINK | 
 |   elif stat.S_ISREG(first_stat.st_mode): | 
 |     if not AreFileContentsSame(first_stat.st_size, first_filename, second_filename): | 
 |       return DIFF_CONTENTS | 
 |  | 
 |   return DIFF_NONE | 
 |  | 
 |  | 
 | class FileIterator(object): | 
 |   """Object that produces an iterator containing all files in a given directory. | 
 |  | 
 |   Each iteration yields a tuple containing: | 
 |  | 
 |   [0] (full) Path to file relative to source tree. | 
 |   [1] (relative) Path to the file relative to the base directory given in the | 
 |       constructor. | 
 |   """ | 
 |  | 
 |   def __init__(self, base_dir): | 
 |     self._base_dir = base_dir | 
 |  | 
 |   def __iter__(self): | 
 |     return self._Iterator(self, self._base_dir) | 
 |  | 
 |   def ShouldIncludeFile(self, root, path): | 
 |     return False | 
 |  | 
 |   class _Iterator(object): | 
 |     def __init__(self, parent, base_dir): | 
 |       self._parent = parent | 
 |       self._base_dir = base_dir | 
 |       self._walker = os.walk(base_dir, followlinks=False) | 
 |       self._current_index = 0 | 
 |       self._current_dir = [] | 
 |  | 
 |     def __iter__(self): | 
 |       return self | 
 |  | 
 |     def __next__(self): | 
 |       # os.walk's iterator will eventually terminate by raising StopIteration | 
 |       while True: | 
 |         if self._current_index >= len(self._current_dir): | 
 |           root, dirs, files = self._walker.__next__() | 
 |           full_paths = [os.path.sep.join((root, f)) for f in files] | 
 |           pairs = [(f, f[len(self._base_dir)+1:]) for f in full_paths] | 
 |           self._current_dir = [(full, relative) for full, relative in pairs | 
 |                                if self._parent.ShouldIncludeFile(root, relative)] | 
 |           self._current_index = 0 | 
 |           if not self._current_dir: | 
 |             continue | 
 |         index = self._current_index | 
 |         self._current_index += 1 | 
 |         return self._current_dir[index] | 
 |  | 
 |  | 
 | class OutFiles(FileIterator): | 
 |   """Object that produces an iterator containing all files in a given out directory, | 
 |   except for files which are known to be touched as part of build setup. | 
 |   """ | 
 |   def __init__(self, out_dir): | 
 |     super().__init__(out_dir) | 
 |     self._out_dir = out_dir | 
 |  | 
 |   def ShouldIncludeFile(self, root, relative): | 
 |     # Skip files in root, although note that this could actually skip | 
 |     # files that are sadly generated directly into that directory. | 
 |     if root == self._out_dir: | 
 |       return False | 
 |     # Skiplist | 
 |     for skip in BUILD_INTERNALS_PREFIX_SKIP: | 
 |       if relative.startswith(skip): | 
 |         return False | 
 |     for skip in BUILD_INTERNALS_SUFFIX_SKIP: | 
 |       if relative.endswith(skip): | 
 |         return False | 
 |     return True | 
 |  | 
 |  | 
 | class ProductFiles(FileIterator): | 
 |   """Object that produces an iterator containing files in listed subdirectories of $PRODUCT_OUT. | 
 |   """ | 
 |   def __init__(self, product_out, subdirs): | 
 |     super().__init__(product_out) | 
 |     self._subdirs = subdirs | 
 |  | 
 |   def ShouldIncludeFile(self, root, relative): | 
 |     for subdir in self._subdirs: | 
 |       if relative.startswith(subdir): | 
 |         return True | 
 |     return False | 
 |  | 
 |  | 
 | class TouchedFile(object): | 
 |   """A file in the out directory with a timestamp.""" | 
 |   def __init__(self, filename, timestamp): | 
 |     self.filename = filename | 
 |     self.timestamp = timestamp | 
 |  | 
 |  | 
 | def RemoveItemsFromList(haystack, needles): | 
 |   for needle in needles: | 
 |     try: | 
 |       haystack.remove(needle) | 
 |     except ValueError: | 
 |       pass | 
 |  | 
 |  | 
 | class Printer(object): | 
 |   def __init__(self): | 
 |     self.printed_anything = False | 
 |  | 
 |   def PrintList(self, title, items, fmt="%s"): | 
 |     if items: | 
 |       if self.printed_anything: | 
 |         sys.stdout.write("\n") | 
 |       sys.stdout.write("%s:\n" % title) | 
 |       for item in items: | 
 |         sys.stdout.write("  %s\n" % fmt % item) | 
 |       self.printed_anything = True | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |   try: | 
 |     main() | 
 |   except KeyboardInterrupt: | 
 |     pass | 
 |  | 
 |  | 
 | # vim: ts=2 sw=2 sts=2 nocindent |