|  | #!/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()) |