xref: /aosp_15_r20/build/soong/tests/genrule_sandbox_test.py (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1*333d2b36SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*333d2b36SAndroid Build Coastguard Worker
3*333d2b36SAndroid Build Coastguard Worker# Copyright (C) 2023 The Android Open Source Project
4*333d2b36SAndroid Build Coastguard Worker#
5*333d2b36SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
6*333d2b36SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
7*333d2b36SAndroid Build Coastguard Worker# You may obtain a copy of the License at
8*333d2b36SAndroid Build Coastguard Worker#
9*333d2b36SAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
10*333d2b36SAndroid Build Coastguard Worker#
11*333d2b36SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
12*333d2b36SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
13*333d2b36SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*333d2b36SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
15*333d2b36SAndroid Build Coastguard Worker# limitations under the License.
16*333d2b36SAndroid Build Coastguard Worker
17*333d2b36SAndroid Build Coastguard Workerimport argparse
18*333d2b36SAndroid Build Coastguard Workerimport asyncio
19*333d2b36SAndroid Build Coastguard Workerimport collections
20*333d2b36SAndroid Build Coastguard Workerimport json
21*333d2b36SAndroid Build Coastguard Workerimport os
22*333d2b36SAndroid Build Coastguard Workerimport socket
23*333d2b36SAndroid Build Coastguard Workerimport subprocess
24*333d2b36SAndroid Build Coastguard Workerimport sys
25*333d2b36SAndroid Build Coastguard Workerimport textwrap
26*333d2b36SAndroid Build Coastguard Worker
27*333d2b36SAndroid Build Coastguard Workerdef get_top() -> str:
28*333d2b36SAndroid Build Coastguard Worker  path = '.'
29*333d2b36SAndroid Build Coastguard Worker  while not os.path.isfile(os.path.join(path, 'build/soong/tests/genrule_sandbox_test.py')):
30*333d2b36SAndroid Build Coastguard Worker    if os.path.abspath(path) == '/':
31*333d2b36SAndroid Build Coastguard Worker      sys.exit('Could not find android source tree root.')
32*333d2b36SAndroid Build Coastguard Worker    path = os.path.join(path, '..')
33*333d2b36SAndroid Build Coastguard Worker  return os.path.abspath(path)
34*333d2b36SAndroid Build Coastguard Worker
35*333d2b36SAndroid Build Coastguard Workerasync def _build_with_soong(out_dir, targets, *, extra_env={}):
36*333d2b36SAndroid Build Coastguard Worker  env = os.environ | extra_env
37*333d2b36SAndroid Build Coastguard Worker
38*333d2b36SAndroid Build Coastguard Worker  # Use nsjail to remap the out_dir to out/, because some genrules write the path to the out
39*333d2b36SAndroid Build Coastguard Worker  # dir into their artifacts, so if the out directories were different it would cause a diff
40*333d2b36SAndroid Build Coastguard Worker  # that doesn't really matter.
41*333d2b36SAndroid Build Coastguard Worker  args = [
42*333d2b36SAndroid Build Coastguard Worker      'prebuilts/build-tools/linux-x86/bin/nsjail',
43*333d2b36SAndroid Build Coastguard Worker      '-q',
44*333d2b36SAndroid Build Coastguard Worker      '--cwd',
45*333d2b36SAndroid Build Coastguard Worker      os.getcwd(),
46*333d2b36SAndroid Build Coastguard Worker      '-e',
47*333d2b36SAndroid Build Coastguard Worker      '-B',
48*333d2b36SAndroid Build Coastguard Worker      '/',
49*333d2b36SAndroid Build Coastguard Worker      '-B',
50*333d2b36SAndroid Build Coastguard Worker      f'{os.path.abspath(out_dir)}:{os.path.abspath("out")}',
51*333d2b36SAndroid Build Coastguard Worker      '--time_limit',
52*333d2b36SAndroid Build Coastguard Worker      '0',
53*333d2b36SAndroid Build Coastguard Worker      '--skip_setsid',
54*333d2b36SAndroid Build Coastguard Worker      '--keep_caps',
55*333d2b36SAndroid Build Coastguard Worker      '--disable_clone_newcgroup',
56*333d2b36SAndroid Build Coastguard Worker      '--disable_clone_newnet',
57*333d2b36SAndroid Build Coastguard Worker      '--rlimit_as',
58*333d2b36SAndroid Build Coastguard Worker      'soft',
59*333d2b36SAndroid Build Coastguard Worker      '--rlimit_core',
60*333d2b36SAndroid Build Coastguard Worker      'soft',
61*333d2b36SAndroid Build Coastguard Worker      '--rlimit_cpu',
62*333d2b36SAndroid Build Coastguard Worker      'soft',
63*333d2b36SAndroid Build Coastguard Worker      '--rlimit_fsize',
64*333d2b36SAndroid Build Coastguard Worker      'soft',
65*333d2b36SAndroid Build Coastguard Worker      '--rlimit_nofile',
66*333d2b36SAndroid Build Coastguard Worker      'soft',
67*333d2b36SAndroid Build Coastguard Worker      '--proc_rw',
68*333d2b36SAndroid Build Coastguard Worker      '--hostname',
69*333d2b36SAndroid Build Coastguard Worker      socket.gethostname(),
70*333d2b36SAndroid Build Coastguard Worker      '--',
71*333d2b36SAndroid Build Coastguard Worker      "build/soong/soong_ui.bash",
72*333d2b36SAndroid Build Coastguard Worker      "--make-mode",
73*333d2b36SAndroid Build Coastguard Worker      "--skip-soong-tests",
74*333d2b36SAndroid Build Coastguard Worker  ]
75*333d2b36SAndroid Build Coastguard Worker  args.extend(targets)
76*333d2b36SAndroid Build Coastguard Worker  process = await asyncio.create_subprocess_exec(
77*333d2b36SAndroid Build Coastguard Worker      *args,
78*333d2b36SAndroid Build Coastguard Worker      stdout=asyncio.subprocess.PIPE,
79*333d2b36SAndroid Build Coastguard Worker      stderr=asyncio.subprocess.PIPE,
80*333d2b36SAndroid Build Coastguard Worker      env=env,
81*333d2b36SAndroid Build Coastguard Worker  )
82*333d2b36SAndroid Build Coastguard Worker  stdout, stderr = await process.communicate()
83*333d2b36SAndroid Build Coastguard Worker  if process.returncode != 0:
84*333d2b36SAndroid Build Coastguard Worker    print(stdout)
85*333d2b36SAndroid Build Coastguard Worker    print(stderr)
86*333d2b36SAndroid Build Coastguard Worker    sys.exit(process.returncode)
87*333d2b36SAndroid Build Coastguard Worker
88*333d2b36SAndroid Build Coastguard Worker
89*333d2b36SAndroid Build Coastguard Workerasync def _find_outputs_for_modules(modules):
90*333d2b36SAndroid Build Coastguard Worker  module_path = "out/soong/module-actions.json"
91*333d2b36SAndroid Build Coastguard Worker
92*333d2b36SAndroid Build Coastguard Worker  if not os.path.exists(module_path):
93*333d2b36SAndroid Build Coastguard Worker    await _build_with_soong('out', ["json-module-graph"])
94*333d2b36SAndroid Build Coastguard Worker
95*333d2b36SAndroid Build Coastguard Worker  with open(module_path) as f:
96*333d2b36SAndroid Build Coastguard Worker    action_graph = json.load(f)
97*333d2b36SAndroid Build Coastguard Worker
98*333d2b36SAndroid Build Coastguard Worker  module_to_outs = collections.defaultdict(set)
99*333d2b36SAndroid Build Coastguard Worker  for mod in action_graph:
100*333d2b36SAndroid Build Coastguard Worker    name = mod["Name"]
101*333d2b36SAndroid Build Coastguard Worker    if name in modules:
102*333d2b36SAndroid Build Coastguard Worker      for act in (mod["Module"]["Actions"] or []):
103*333d2b36SAndroid Build Coastguard Worker        if "}generate" in act["Desc"]:
104*333d2b36SAndroid Build Coastguard Worker          module_to_outs[name].update(act["Outputs"])
105*333d2b36SAndroid Build Coastguard Worker  return module_to_outs
106*333d2b36SAndroid Build Coastguard Worker
107*333d2b36SAndroid Build Coastguard Worker
108*333d2b36SAndroid Build Coastguard Workerdef _compare_outputs(module_to_outs, tempdir) -> dict[str, list[str]]:
109*333d2b36SAndroid Build Coastguard Worker  different_modules = collections.defaultdict(list)
110*333d2b36SAndroid Build Coastguard Worker  for module, outs in module_to_outs.items():
111*333d2b36SAndroid Build Coastguard Worker    for out in outs:
112*333d2b36SAndroid Build Coastguard Worker      try:
113*333d2b36SAndroid Build Coastguard Worker        subprocess.check_output(["diff", os.path.join(tempdir, out), out])
114*333d2b36SAndroid Build Coastguard Worker      except subprocess.CalledProcessError as e:
115*333d2b36SAndroid Build Coastguard Worker        different_modules[module].append(e.stdout)
116*333d2b36SAndroid Build Coastguard Worker
117*333d2b36SAndroid Build Coastguard Worker  return different_modules
118*333d2b36SAndroid Build Coastguard Worker
119*333d2b36SAndroid Build Coastguard Worker
120*333d2b36SAndroid Build Coastguard Workerasync def main():
121*333d2b36SAndroid Build Coastguard Worker  parser = argparse.ArgumentParser()
122*333d2b36SAndroid Build Coastguard Worker  parser.add_argument(
123*333d2b36SAndroid Build Coastguard Worker      "modules",
124*333d2b36SAndroid Build Coastguard Worker      nargs="+",
125*333d2b36SAndroid Build Coastguard Worker      help="modules to compare builds with genrule sandboxing enabled/not",
126*333d2b36SAndroid Build Coastguard Worker  )
127*333d2b36SAndroid Build Coastguard Worker  parser.add_argument(
128*333d2b36SAndroid Build Coastguard Worker      "--check-determinism",
129*333d2b36SAndroid Build Coastguard Worker      action="store_true",
130*333d2b36SAndroid Build Coastguard Worker      help="Don't check for working sandboxing. Instead, run two default builds, and compare their outputs. This is used to check for nondeterminsim, which would also affect the sandboxed test.",
131*333d2b36SAndroid Build Coastguard Worker  )
132*333d2b36SAndroid Build Coastguard Worker  parser.add_argument(
133*333d2b36SAndroid Build Coastguard Worker      "--show-diff",
134*333d2b36SAndroid Build Coastguard Worker      "-d",
135*333d2b36SAndroid Build Coastguard Worker      action="store_true",
136*333d2b36SAndroid Build Coastguard Worker      help="whether to display differing files",
137*333d2b36SAndroid Build Coastguard Worker  )
138*333d2b36SAndroid Build Coastguard Worker  parser.add_argument(
139*333d2b36SAndroid Build Coastguard Worker      "--output-paths-only",
140*333d2b36SAndroid Build Coastguard Worker      "-o",
141*333d2b36SAndroid Build Coastguard Worker      action="store_true",
142*333d2b36SAndroid Build Coastguard Worker      help="Whether to only return the output paths per module",
143*333d2b36SAndroid Build Coastguard Worker  )
144*333d2b36SAndroid Build Coastguard Worker  args = parser.parse_args()
145*333d2b36SAndroid Build Coastguard Worker  os.chdir(get_top())
146*333d2b36SAndroid Build Coastguard Worker
147*333d2b36SAndroid Build Coastguard Worker  if "TARGET_PRODUCT" not in os.environ:
148*333d2b36SAndroid Build Coastguard Worker    sys.exit("Please run lunch first")
149*333d2b36SAndroid Build Coastguard Worker  if os.environ.get("OUT_DIR", "out") != "out":
150*333d2b36SAndroid Build Coastguard Worker    sys.exit(f"This script expects OUT_DIR to be 'out', got: '{os.environ.get('OUT_DIR')}'")
151*333d2b36SAndroid Build Coastguard Worker
152*333d2b36SAndroid Build Coastguard Worker  print("finding output files for the modules...")
153*333d2b36SAndroid Build Coastguard Worker  module_to_outs = await _find_outputs_for_modules(set(args.modules))
154*333d2b36SAndroid Build Coastguard Worker  if not module_to_outs:
155*333d2b36SAndroid Build Coastguard Worker    sys.exit("No outputs found")
156*333d2b36SAndroid Build Coastguard Worker
157*333d2b36SAndroid Build Coastguard Worker  if args.output_paths_only:
158*333d2b36SAndroid Build Coastguard Worker    for m, o in module_to_outs.items():
159*333d2b36SAndroid Build Coastguard Worker      print(f"{m} outputs: {o}")
160*333d2b36SAndroid Build Coastguard Worker    sys.exit(0)
161*333d2b36SAndroid Build Coastguard Worker
162*333d2b36SAndroid Build Coastguard Worker  all_outs = list(set.union(*module_to_outs.values()))
163*333d2b36SAndroid Build Coastguard Worker  for i, out in enumerate(all_outs):
164*333d2b36SAndroid Build Coastguard Worker    if not out.startswith("out/"):
165*333d2b36SAndroid Build Coastguard Worker      sys.exit("Expected output file to start with out/, found: " + out)
166*333d2b36SAndroid Build Coastguard Worker
167*333d2b36SAndroid Build Coastguard Worker  other_out_dir = "out_check_determinism" if args.check_determinism else "out_not_sandboxed"
168*333d2b36SAndroid Build Coastguard Worker  other_env = {"GENRULE_SANDBOXING": "false"}
169*333d2b36SAndroid Build Coastguard Worker  if args.check_determinism:
170*333d2b36SAndroid Build Coastguard Worker    other_env = {}
171*333d2b36SAndroid Build Coastguard Worker
172*333d2b36SAndroid Build Coastguard Worker  # nsjail will complain if the out dir doesn't exist
173*333d2b36SAndroid Build Coastguard Worker  os.makedirs("out", exist_ok=True)
174*333d2b36SAndroid Build Coastguard Worker  os.makedirs(other_out_dir, exist_ok=True)
175*333d2b36SAndroid Build Coastguard Worker
176*333d2b36SAndroid Build Coastguard Worker  print("building...")
177*333d2b36SAndroid Build Coastguard Worker  await asyncio.gather(
178*333d2b36SAndroid Build Coastguard Worker    _build_with_soong("out", all_outs),
179*333d2b36SAndroid Build Coastguard Worker    _build_with_soong(other_out_dir, all_outs, extra_env=other_env)
180*333d2b36SAndroid Build Coastguard Worker  )
181*333d2b36SAndroid Build Coastguard Worker
182*333d2b36SAndroid Build Coastguard Worker  diffs = collections.defaultdict(dict)
183*333d2b36SAndroid Build Coastguard Worker  for module, outs in module_to_outs.items():
184*333d2b36SAndroid Build Coastguard Worker    for out in outs:
185*333d2b36SAndroid Build Coastguard Worker      try:
186*333d2b36SAndroid Build Coastguard Worker        subprocess.check_output(["diff", os.path.join(other_out_dir, out.removeprefix("out/")), out])
187*333d2b36SAndroid Build Coastguard Worker      except subprocess.CalledProcessError as e:
188*333d2b36SAndroid Build Coastguard Worker        diffs[module][out] = e.stdout
189*333d2b36SAndroid Build Coastguard Worker
190*333d2b36SAndroid Build Coastguard Worker  if len(diffs) == 0:
191*333d2b36SAndroid Build Coastguard Worker    print("All modules are correct")
192*333d2b36SAndroid Build Coastguard Worker  elif args.show_diff:
193*333d2b36SAndroid Build Coastguard Worker    for m, files in diffs.items():
194*333d2b36SAndroid Build Coastguard Worker      print(f"Module {m} has diffs:")
195*333d2b36SAndroid Build Coastguard Worker      for f, d in files.items():
196*333d2b36SAndroid Build Coastguard Worker        print("  "+f+":")
197*333d2b36SAndroid Build Coastguard Worker        print(textwrap.indent(d, "    "))
198*333d2b36SAndroid Build Coastguard Worker  else:
199*333d2b36SAndroid Build Coastguard Worker    print(f"Modules {list(diffs.keys())} have diffs in these files:")
200*333d2b36SAndroid Build Coastguard Worker    all_diff_files = [f for m in diffs.values() for f in m]
201*333d2b36SAndroid Build Coastguard Worker    for f in all_diff_files:
202*333d2b36SAndroid Build Coastguard Worker      print(f)
203*333d2b36SAndroid Build Coastguard Worker
204*333d2b36SAndroid Build Coastguard Worker
205*333d2b36SAndroid Build Coastguard Worker
206*333d2b36SAndroid Build Coastguard Workerif __name__ == "__main__":
207*333d2b36SAndroid Build Coastguard Worker  asyncio.run(main())
208