diff options
author | 2024-11-13 23:24:46 +0000 | |
---|---|---|
committer | 2024-11-13 23:24:46 +0000 | |
commit | b8eee8a1ed55f68eac400ef5735ba23521f1ba89 (patch) | |
tree | 7ccebffcf1d4d42886a761f860ec73c48efa519b | |
parent | 81731ac62704a96471a6a531a3c1b9fb15417eca (diff) | |
parent | 06540bb808debb9efe410e518ce9f420bac9042c (diff) |
Merge "Add ninja determinism test" into main am: 06540bb808
Original change: https://android-review.googlesource.com/c/platform/build/soong/+/3348946
Change-Id: I163b8a7b71f9c5f14d6bb5093e9c805014d18d2f
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rwxr-xr-x | scripts/ninja_determinism_test.py | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/scripts/ninja_determinism_test.py b/scripts/ninja_determinism_test.py new file mode 100755 index 000000000..e207b9613 --- /dev/null +++ b/scripts/ninja_determinism_test.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 + +import asyncio +import argparse +import dataclasses +import hashlib +import os +import re +import socket +import subprocess +import sys +import zipfile + +from typing import List + +def get_top() -> str: + path = '.' + while not os.path.isfile(os.path.join(path, 'build/soong/soong_ui.bash')): + if os.path.abspath(path) == '/': + sys.exit('Could not find android source tree root.') + path = os.path.join(path, '..') + return os.path.abspath(path) + + +_PRODUCT_REGEX = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)(?:(?:-([a-zA-Z_][a-zA-Z0-9_]*))?-(user|userdebug|eng))?') + + +@dataclasses.dataclass(frozen=True) +class Product: + """Represents a TARGET_PRODUCT and TARGET_BUILD_VARIANT.""" + product: str + release: str + variant: str + + def __post_init__(self): + if not _PRODUCT_REGEX.match(str(self)): + raise ValueError(f'Invalid product name: {self}') + + def __str__(self): + return self.product + '-' + self.release + '-' + self.variant + + +async def run_make_nothing(product: Product, out_dir: str) -> bool: + """Runs a build and returns if it succeeded or not.""" + with open(os.path.join(out_dir, 'build.log'), 'wb') as f: + result = await asyncio.create_subprocess_exec( + 'prebuilts/build-tools/linux-x86/bin/nsjail', + '-q', + '--cwd', + os.getcwd(), + '-e', + '-B', + '/', + '-B', + f'{os.path.abspath(out_dir)}:{os.path.join(os.getcwd(), "out")}', + '--time_limit', + '0', + '--skip_setsid', + '--keep_caps', + '--disable_clone_newcgroup', + '--disable_clone_newnet', + '--rlimit_as', + 'soft', + '--rlimit_core', + 'soft', + '--rlimit_cpu', + 'soft', + '--rlimit_fsize', + 'soft', + '--rlimit_nofile', + 'soft', + '--proc_rw', + '--hostname', + socket.gethostname(), + '--', + 'build/soong/soong_ui.bash', + '--make-mode', + f'TARGET_PRODUCT={product.product}', + f'TARGET_RELEASE={product.release}', + f'TARGET_BUILD_VARIANT={product.variant}', + '--skip-ninja', + 'nothing', stdout=f, stderr=subprocess.STDOUT) + return await result.wait() == 0 + +SUBNINJA_OR_INCLUDE_REGEX = re.compile(rb'\n(?:include|subninja) ') + +def find_subninjas_and_includes(contents) -> List[str]: + results = [] + def get_path_from_directive(i): + j = contents.find(b'\n', i) + if j < 0: + path_bytes = contents[i:] + else: + path_bytes = contents[i:j] + path_bytes = path_bytes.removesuffix(b'\r') + path = path_bytes.decode() + if '$' in path: + sys.exit('includes/subninjas with variables are unsupported: '+path) + return path + + if contents.startswith(b"include "): + results.append(get_path_from_directive(len(b"include "))) + elif contents.startswith(b"subninja "): + results.append(get_path_from_directive(len(b"subninja "))) + + for match in SUBNINJA_OR_INCLUDE_REGEX.finditer(contents): + results.append(get_path_from_directive(match.end())) + + return results + + +def transitively_included_ninja_files(out_dir: str, ninja_file: str, seen): + with open(ninja_file, 'rb') as f: + contents = f.read() + + results = [ninja_file] + seen[ninja_file] = True + sub_files = find_subninjas_and_includes(contents) + for sub_file in sub_files: + sub_file = os.path.join(out_dir, sub_file.removeprefix('out/')) + if sub_file not in seen: + results.extend(transitively_included_ninja_files(out_dir, sub_file, seen)) + + return results + + +def hash_ninja_file(out_dir: str, ninja_file: str, hasher): + with open(ninja_file, 'rb') as f: + contents = f.read() + + sub_files = find_subninjas_and_includes(contents) + + hasher.update(contents) + + for sub_file in sub_files: + hash_ninja_file(out_dir, os.path.join(out_dir, sub_file.removeprefix('out/')), hasher) + + +def hash_files(files: List[str]) -> str: + hasher = hashlib.md5() + for file in files: + with open(file, 'rb') as f: + hasher.update(f.read()) + return hasher.hexdigest() + + +def dist_ninja_files(out_dir: str, zip_name: str, ninja_files: List[str]): + dist_dir = os.getenv('DIST_DIR', os.path.join(os.getenv('OUT_DIR', 'out'), 'dist')) + os.makedirs(dist_dir, exist_ok=True) + + with open(os.path.join(dist_dir, zip_name), 'wb') as f: + with zipfile.ZipFile(f, mode='w') as zf: + for ninja_file in ninja_files: + zf.write(ninja_file, arcname=os.path.basename(out_dir)+'/out/' + os.path.relpath(ninja_file, out_dir)) + + +async def main(): + parser = argparse.ArgumentParser() + args = parser.parse_args() + + os.chdir(get_top()) + subprocess.check_call(['touch', 'build/soong/Android.bp']) + + product = Product( + 'aosp_cf_x86_64_phone', + 'trunk_staging', + 'userdebug', + ) + os.environ['TARGET_PRODUCT'] = 'aosp_cf_x86_64_phone' + os.environ['TARGET_RELEASE'] = 'trunk_staging' + os.environ['TARGET_BUILD_VARIANT'] = 'userdebug' + + out_dir1 = os.path.join(os.getenv('OUT_DIR', 'out'), 'determinism_test_out1') + out_dir2 = os.path.join(os.getenv('OUT_DIR', 'out'), 'determinism_test_out2') + + os.makedirs(out_dir1, exist_ok=True) + os.makedirs(out_dir2, exist_ok=True) + + success1, success2 = await asyncio.gather( + run_make_nothing(product, out_dir1), + run_make_nothing(product, out_dir2)) + + if not success1: + with open(os.path.join(out_dir1, 'build.log'), 'r') as f: + print(f.read(), file=sys.stderr) + sys.exit('build failed') + if not success2: + with open(os.path.join(out_dir2, 'build.log'), 'r') as f: + print(f.read(), file=sys.stderr) + sys.exit('build failed') + + ninja_files1 = transitively_included_ninja_files(out_dir1, os.path.join(out_dir1, f'combined-{product.product}.ninja'), {}) + ninja_files2 = transitively_included_ninja_files(out_dir2, os.path.join(out_dir2, f'combined-{product.product}.ninja'), {}) + + dist_ninja_files(out_dir1, 'determinism_test_files_1.zip', ninja_files1) + dist_ninja_files(out_dir2, 'determinism_test_files_2.zip', ninja_files2) + + hash1 = hash_files(ninja_files1) + hash2 = hash_files(ninja_files2) + + if hash1 != hash2: + sys.exit("ninja files were not deterministic! See disted determinism_test_files_1/2.zip") + + print("Success, ninja files were deterministic") + + +if __name__ == "__main__": + asyncio.run(main()) + + |