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