|  | #!/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()) | 
|  |  | 
|  |  |