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