"""
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, andfor 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. Asin 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"asin 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.
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.
These flags will compare the final staging directories for partitions, as well as the APKs, apexes, testcases, and the like (the full directory
list isin 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).
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.
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:
import argparse import itertools import os import re 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/",
)
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 andnot 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 andnot args.incremental:
sys.stderr.write("The --incremental flag is required if the --touch flag is passed.")
sys.exit(1)
# 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" ifnot 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 isnotNone: 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 ifnot 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.""" ifnot 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) ifnot result:
sys.stderr.write("error: Can't determine %s. Please run lunch first.\n" % name)
sys.exit(1) return result
def GetBuildVar(out_dir, name): """Gets a variable from the build system."""
returncode, output = RunSoong(out_dir, ["--dumpvar-mode", name], True) if returncode != 0: returnNone 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) ifnot value:
sys.stderr.write("error: Can't determine %s. Please run lunch first.\n" % name)
sys.exit(1) return value
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]
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): returnFalse
remaining -= size returnTrue
def __next__(self): # os.walk's iterator will eventually terminate by raising StopIteration whileTrue: 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 ifnot 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, exceptfor 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: returnFalse # Skiplist for skip in BUILD_INTERNALS_PREFIX_SKIP: if relative.startswith(skip): returnFalse for skip in BUILD_INTERNALS_SUFFIX_SKIP: if relative.endswith(skip): returnFalse for skip in BUILD_INTERNALS_REGEX_SKIP: if re.search(skip, relative): returnFalse returnTrue
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): returnTrue returnFalse
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
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.22 Sekunden
(vorverarbeitet am 2026-06-28)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.