662 lines
23 KiB
Python
662 lines
23 KiB
Python
|
#!/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
|