blob: e207b961312fb03881be0c916ae1183a1d1b819b [file] [log] [blame]
Cole Faust4ed895d2024-11-13 11:11:11 -08001#!/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