| #!/usr/bin/env python3 |
| |
| # Copyright (C) 2023 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import argparse |
| import asyncio |
| import collections |
| import json |
| import os |
| import socket |
| import subprocess |
| import sys |
| import textwrap |
| |
| def get_top() -> str: |
| path = '.' |
| while not os.path.isfile(os.path.join(path, 'build/soong/tests/genrule_sandbox_test.py')): |
| if os.path.abspath(path) == '/': |
| sys.exit('Could not find android source tree root.') |
| path = os.path.join(path, '..') |
| return os.path.abspath(path) |
| |
| async def _build_with_soong(out_dir, targets, *, extra_env={}): |
| env = os.environ | extra_env |
| |
| # Use nsjail to remap the out_dir to out/, because some genrules write the path to the out |
| # dir into their artifacts, so if the out directories were different it would cause a diff |
| # that doesn't really matter. |
| args = [ |
| 'prebuilts/build-tools/linux-x86/bin/nsjail', |
| '-q', |
| '--cwd', |
| os.getcwd(), |
| '-e', |
| '-B', |
| '/', |
| '-B', |
| f'{os.path.abspath(out_dir)}:{os.path.abspath("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", |
| "--skip-soong-tests", |
| ] |
| args.extend(targets) |
| process = await asyncio.create_subprocess_exec( |
| *args, |
| stdout=asyncio.subprocess.PIPE, |
| stderr=asyncio.subprocess.PIPE, |
| env=env, |
| ) |
| stdout, stderr = await process.communicate() |
| if process.returncode != 0: |
| print(stdout) |
| print(stderr) |
| sys.exit(process.returncode) |
| |
| |
| async def _find_outputs_for_modules(modules): |
| module_path = "out/soong/module-actions.json" |
| |
| if not os.path.exists(module_path): |
| await _build_with_soong('out', ["json-module-graph"]) |
| |
| with open(module_path) as f: |
| action_graph = json.load(f) |
| |
| module_to_outs = collections.defaultdict(set) |
| for mod in action_graph: |
| name = mod["Name"] |
| if name in modules: |
| for act in (mod["Module"]["Actions"] or []): |
| if "}generate" in act["Desc"]: |
| module_to_outs[name].update(act["Outputs"]) |
| return module_to_outs |
| |
| |
| def _compare_outputs(module_to_outs, tempdir) -> dict[str, list[str]]: |
| different_modules = collections.defaultdict(list) |
| for module, outs in module_to_outs.items(): |
| for out in outs: |
| try: |
| subprocess.check_output(["diff", os.path.join(tempdir, out), out]) |
| except subprocess.CalledProcessError as e: |
| different_modules[module].append(e.stdout) |
| |
| return different_modules |
| |
| |
| async def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "modules", |
| nargs="+", |
| help="modules to compare builds with genrule sandboxing enabled/not", |
| ) |
| parser.add_argument( |
| "--check-determinism", |
| action="store_true", |
| 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.", |
| ) |
| parser.add_argument( |
| "--show-diff", |
| "-d", |
| action="store_true", |
| help="whether to display differing files", |
| ) |
| parser.add_argument( |
| "--output-paths-only", |
| "-o", |
| action="store_true", |
| help="Whether to only return the output paths per module", |
| ) |
| args = parser.parse_args() |
| os.chdir(get_top()) |
| |
| if "TARGET_PRODUCT" not in os.environ: |
| sys.exit("Please run lunch first") |
| if os.environ.get("OUT_DIR", "out") != "out": |
| sys.exit(f"This script expects OUT_DIR to be 'out', got: '{os.environ.get('OUT_DIR')}'") |
| |
| print("finding output files for the modules...") |
| module_to_outs = await _find_outputs_for_modules(set(args.modules)) |
| if not module_to_outs: |
| sys.exit("No outputs found") |
| |
| if args.output_paths_only: |
| for m, o in module_to_outs.items(): |
| print(f"{m} outputs: {o}") |
| sys.exit(0) |
| |
| all_outs = list(set.union(*module_to_outs.values())) |
| for i, out in enumerate(all_outs): |
| if not out.startswith("out/"): |
| sys.exit("Expected output file to start with out/, found: " + out) |
| |
| other_out_dir = "out_check_determinism" if args.check_determinism else "out_not_sandboxed" |
| other_env = {"GENRULE_SANDBOXING": "false"} |
| if args.check_determinism: |
| other_env = {} |
| |
| # nsjail will complain if the out dir doesn't exist |
| os.makedirs("out", exist_ok=True) |
| os.makedirs(other_out_dir, exist_ok=True) |
| |
| print("building...") |
| await asyncio.gather( |
| _build_with_soong("out", all_outs), |
| _build_with_soong(other_out_dir, all_outs, extra_env=other_env) |
| ) |
| |
| diffs = collections.defaultdict(dict) |
| for module, outs in module_to_outs.items(): |
| for out in outs: |
| try: |
| subprocess.check_output(["diff", os.path.join(other_out_dir, out.removeprefix("out/")), out]) |
| except subprocess.CalledProcessError as e: |
| diffs[module][out] = e.stdout |
| |
| if len(diffs) == 0: |
| print("All modules are correct") |
| elif args.show_diff: |
| for m, files in diffs.items(): |
| print(f"Module {m} has diffs:") |
| for f, d in files.items(): |
| print(" "+f+":") |
| print(textwrap.indent(d, " ")) |
| else: |
| print(f"Modules {list(diffs.keys())} have diffs in these files:") |
| all_diff_files = [f for m in diffs.values() for f in m] |
| for f in all_diff_files: |
| print(f) |
| |
| |
| |
| if __name__ == "__main__": |
| asyncio.run(main()) |