1#!/usr/bin/env python3 2 3import asyncio 4import argparse 5import dataclasses 6import hashlib 7import os 8import re 9import socket 10import subprocess 11import sys 12import zipfile 13 14from typing import List 15 16def get_top() -> str: 17 path = '.' 18 while not os.path.isfile(os.path.join(path, 'build/soong/soong_ui.bash')): 19 if os.path.abspath(path) == '/': 20 sys.exit('Could not find android source tree root.') 21 path = os.path.join(path, '..') 22 return os.path.abspath(path) 23 24 25_PRODUCT_REGEX = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)(?:(?:-([a-zA-Z_][a-zA-Z0-9_]*))?-(user|userdebug|eng))?') 26 27 28@dataclasses.dataclass(frozen=True) 29class Product: 30 """Represents a TARGET_PRODUCT and TARGET_BUILD_VARIANT.""" 31 product: str 32 release: str 33 variant: str 34 35 def __post_init__(self): 36 if not _PRODUCT_REGEX.match(str(self)): 37 raise ValueError(f'Invalid product name: {self}') 38 39 def __str__(self): 40 return self.product + '-' + self.release + '-' + self.variant 41 42 43async def run_make_nothing(product: Product, out_dir: str) -> bool: 44 """Runs a build and returns if it succeeded or not.""" 45 with open(os.path.join(out_dir, 'build.log'), 'wb') as f: 46 result = await asyncio.create_subprocess_exec( 47 'prebuilts/build-tools/linux-x86/bin/nsjail', 48 '-q', 49 '--cwd', 50 os.getcwd(), 51 '-e', 52 '-B', 53 '/', 54 '-B', 55 f'{os.path.abspath(out_dir)}:{os.path.join(os.getcwd(), "out")}', 56 '--time_limit', 57 '0', 58 '--skip_setsid', 59 '--keep_caps', 60 '--disable_clone_newcgroup', 61 '--disable_clone_newnet', 62 '--rlimit_as', 63 'soft', 64 '--rlimit_core', 65 'soft', 66 '--rlimit_cpu', 67 'soft', 68 '--rlimit_fsize', 69 'soft', 70 '--rlimit_nofile', 71 'soft', 72 '--proc_rw', 73 '--hostname', 74 socket.gethostname(), 75 '--', 76 'build/soong/soong_ui.bash', 77 '--make-mode', 78 f'TARGET_PRODUCT={product.product}', 79 f'TARGET_RELEASE={product.release}', 80 f'TARGET_BUILD_VARIANT={product.variant}', 81 '--skip-ninja', 82 'nothing', stdout=f, stderr=subprocess.STDOUT) 83 return await result.wait() == 0 84 85SUBNINJA_OR_INCLUDE_REGEX = re.compile(rb'\n(?:include|subninja) ') 86 87def find_subninjas_and_includes(contents) -> List[str]: 88 results = [] 89 def get_path_from_directive(i): 90 j = contents.find(b'\n', i) 91 if j < 0: 92 path_bytes = contents[i:] 93 else: 94 path_bytes = contents[i:j] 95 path_bytes = path_bytes.removesuffix(b'\r') 96 path = path_bytes.decode() 97 if '$' in path: 98 sys.exit('includes/subninjas with variables are unsupported: '+path) 99 return path 100 101 if contents.startswith(b"include "): 102 results.append(get_path_from_directive(len(b"include "))) 103 elif contents.startswith(b"subninja "): 104 results.append(get_path_from_directive(len(b"subninja "))) 105 106 for match in SUBNINJA_OR_INCLUDE_REGEX.finditer(contents): 107 results.append(get_path_from_directive(match.end())) 108 109 return results 110 111 112def transitively_included_ninja_files(out_dir: str, ninja_file: str, seen): 113 with open(ninja_file, 'rb') as f: 114 contents = f.read() 115 116 results = [ninja_file] 117 seen[ninja_file] = True 118 sub_files = find_subninjas_and_includes(contents) 119 for sub_file in sub_files: 120 sub_file = os.path.join(out_dir, sub_file.removeprefix('out/')) 121 if sub_file not in seen: 122 results.extend(transitively_included_ninja_files(out_dir, sub_file, seen)) 123 124 return results 125 126 127def hash_ninja_file(out_dir: str, ninja_file: str, hasher): 128 with open(ninja_file, 'rb') as f: 129 contents = f.read() 130 131 sub_files = find_subninjas_and_includes(contents) 132 133 hasher.update(contents) 134 135 for sub_file in sub_files: 136 hash_ninja_file(out_dir, os.path.join(out_dir, sub_file.removeprefix('out/')), hasher) 137 138 139def hash_files(files: List[str]) -> str: 140 hasher = hashlib.md5() 141 for file in files: 142 with open(file, 'rb') as f: 143 hasher.update(f.read()) 144 return hasher.hexdigest() 145 146 147def dist_ninja_files(out_dir: str, zip_name: str, ninja_files: List[str]): 148 dist_dir = os.getenv('DIST_DIR', os.path.join(os.getenv('OUT_DIR', 'out'), 'dist')) 149 os.makedirs(dist_dir, exist_ok=True) 150 151 with open(os.path.join(dist_dir, zip_name), 'wb') as f: 152 with zipfile.ZipFile(f, mode='w') as zf: 153 for ninja_file in ninja_files: 154 zf.write(ninja_file, arcname=os.path.basename(out_dir)+'/out/' + os.path.relpath(ninja_file, out_dir)) 155 156 157async def main(): 158 parser = argparse.ArgumentParser() 159 args = parser.parse_args() 160 161 os.chdir(get_top()) 162 subprocess.check_call(['touch', 'build/soong/Android.bp']) 163 164 product = Product( 165 'aosp_cf_x86_64_phone', 166 'trunk_staging', 167 'userdebug', 168 ) 169 os.environ['TARGET_PRODUCT'] = 'aosp_cf_x86_64_phone' 170 os.environ['TARGET_RELEASE'] = 'trunk_staging' 171 os.environ['TARGET_BUILD_VARIANT'] = 'userdebug' 172 173 out_dir1 = os.path.join(os.getenv('OUT_DIR', 'out'), 'determinism_test_out1') 174 out_dir2 = os.path.join(os.getenv('OUT_DIR', 'out'), 'determinism_test_out2') 175 176 os.makedirs(out_dir1, exist_ok=True) 177 os.makedirs(out_dir2, exist_ok=True) 178 179 success1, success2 = await asyncio.gather( 180 run_make_nothing(product, out_dir1), 181 run_make_nothing(product, out_dir2)) 182 183 if not success1: 184 with open(os.path.join(out_dir1, 'build.log'), 'r') as f: 185 print(f.read(), file=sys.stderr) 186 sys.exit('build failed') 187 if not success2: 188 with open(os.path.join(out_dir2, 'build.log'), 'r') as f: 189 print(f.read(), file=sys.stderr) 190 sys.exit('build failed') 191 192 ninja_files1 = transitively_included_ninja_files(out_dir1, os.path.join(out_dir1, f'combined-{product.product}.ninja'), {}) 193 ninja_files2 = transitively_included_ninja_files(out_dir2, os.path.join(out_dir2, f'combined-{product.product}.ninja'), {}) 194 195 dist_ninja_files(out_dir1, 'determinism_test_files_1.zip', ninja_files1) 196 dist_ninja_files(out_dir2, 'determinism_test_files_2.zip', ninja_files2) 197 198 hash1 = hash_files(ninja_files1) 199 hash2 = hash_files(ninja_files2) 200 201 if hash1 != hash2: 202 sys.exit("ninja files were not deterministic! See disted determinism_test_files_1/2.zip") 203 204 print("Success, ninja files were deterministic") 205 206 207if __name__ == "__main__": 208 asyncio.run(main()) 209 210 211