Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 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 | |
| 17 | import argparse |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 18 | import asyncio |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 19 | import collections |
| 20 | import json |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 21 | import os |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 22 | import socket |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 23 | import subprocess |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 24 | import sys |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 25 | import textwrap |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 26 | |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 27 | def 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) |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 34 | |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 35 | async 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. |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 41 | args = [ |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 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 | '--', |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 71 | "build/soong/soong_ui.bash", |
| 72 | "--make-mode", |
| 73 | "--skip-soong-tests", |
| 74 | ] |
| 75 | args.extend(targets) |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 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) |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 87 | |
| 88 | |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 89 | async def _find_outputs_for_modules(modules): |
| 90 | module_path = "out/soong/module-actions.json" |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 91 | |
| 92 | if not os.path.exists(module_path): |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 93 | await _build_with_soong('out', ["json-module-graph"]) |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 94 | |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 95 | with open(module_path) as f: |
| 96 | action_graph = json.load(f) |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 97 | |
| 98 | module_to_outs = collections.defaultdict(set) |
| 99 | for mod in action_graph: |
| 100 | name = mod["Name"] |
| 101 | if name in modules: |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 102 | for act in (mod["Module"]["Actions"] or []): |
Jason Macnak | 394f1b7 | 2023-06-15 09:28:26 -0700 | [diff] [blame] | 103 | if "}generate" in act["Desc"]: |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 104 | module_to_outs[name].update(act["Outputs"]) |
| 105 | return module_to_outs |
| 106 | |
| 107 | |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 108 | def _compare_outputs(module_to_outs, tempdir) -> dict[str, list[str]]: |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 109 | different_modules = collections.defaultdict(list) |
| 110 | for module, outs in module_to_outs.items(): |
| 111 | for out in outs: |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 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) |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 116 | |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 117 | return different_modules |
| 118 | |
| 119 | |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 120 | async def main(): |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 121 | parser = argparse.ArgumentParser() |
| 122 | parser.add_argument( |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 123 | "modules", |
| 124 | nargs="+", |
| 125 | help="modules to compare builds with genrule sandboxing enabled/not", |
| 126 | ) |
| 127 | parser.add_argument( |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 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( |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 133 | "--show-diff", |
| 134 | "-d", |
| 135 | action="store_true", |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 136 | help="whether to display differing files", |
| 137 | ) |
Liz Kammer | 2fb361c | 2023-06-09 11:29:48 -0400 | [diff] [blame] | 138 | parser.add_argument( |
| 139 | "--output-paths-only", |
| 140 | "-o", |
| 141 | action="store_true", |
Liz Kammer | 2fb361c | 2023-06-09 11:29:48 -0400 | [diff] [blame] | 142 | help="Whether to only return the output paths per module", |
| 143 | ) |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 144 | args = parser.parse_args() |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 145 | os.chdir(get_top()) |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 146 | |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 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')}'") |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 151 | |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 152 | print("finding output files for the modules...") |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 153 | module_to_outs = await _find_outputs_for_modules(set(args.modules)) |
Jason Macnak | 394f1b7 | 2023-06-15 09:28:26 -0700 | [diff] [blame] | 154 | if not module_to_outs: |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 155 | sys.exit("No outputs found") |
Jason Macnak | 394f1b7 | 2023-06-15 09:28:26 -0700 | [diff] [blame] | 156 | |
Liz Kammer | 2fb361c | 2023-06-09 11:29:48 -0400 | [diff] [blame] | 157 | if args.output_paths_only: |
| 158 | for m, o in module_to_outs.items(): |
| 159 | print(f"{m} outputs: {o}") |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 160 | sys.exit(0) |
Liz Kammer | 2fb361c | 2023-06-09 11:29:48 -0400 | [diff] [blame] | 161 | |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 162 | all_outs = list(set.union(*module_to_outs.values())) |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 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) |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 166 | |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 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 = {} |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 171 | |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 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) |
Cole Faust | bbe2cc6 | 2023-07-12 17:55:28 -0700 | [diff] [blame] | 175 | |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 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 | |
Liz Kammer | 767fad4 | 2023-06-09 11:23:15 -0400 | [diff] [blame] | 204 | |
| 205 | |
| 206 | if __name__ == "__main__": |
Cole Faust | a57c8c2 | 2023-10-26 11:42:24 -0700 | [diff] [blame] | 207 | asyncio.run(main()) |