blob: 3799e92f8d750e139744a7b799eac0f6fe26e9f0 [file] [log] [blame]
Liz Kammer767fad42023-06-09 11:23:15 -04001#!/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
Cole Fausta57c8c22023-10-26 11:42:24 -070018import asyncio
Liz Kammer767fad42023-06-09 11:23:15 -040019import collections
20import json
Cole Faustbbe2cc62023-07-12 17:55:28 -070021import os
Cole Fausta57c8c22023-10-26 11:42:24 -070022import socket
Liz Kammer767fad42023-06-09 11:23:15 -040023import subprocess
Cole Faustbbe2cc62023-07-12 17:55:28 -070024import sys
Cole Fausta57c8c22023-10-26 11:42:24 -070025import textwrap
Liz Kammer767fad42023-06-09 11:23:15 -040026
Cole Faustbbe2cc62023-07-12 17:55:28 -070027def 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 Kammer767fad42023-06-09 11:23:15 -040034
Cole Fausta57c8c22023-10-26 11:42:24 -070035async 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 Kammer767fad42023-06-09 11:23:15 -040041 args = [
Cole Fausta57c8c22023-10-26 11:42:24 -070042 '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 Kammer767fad42023-06-09 11:23:15 -040071 "build/soong/soong_ui.bash",
72 "--make-mode",
73 "--skip-soong-tests",
74 ]
75 args.extend(targets)
Cole Fausta57c8c22023-10-26 11:42:24 -070076 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 Kammer767fad42023-06-09 11:23:15 -040087
88
Cole Fausta57c8c22023-10-26 11:42:24 -070089async def _find_outputs_for_modules(modules):
90 module_path = "out/soong/module-actions.json"
Liz Kammer767fad42023-06-09 11:23:15 -040091
92 if not os.path.exists(module_path):
Cole Fausta57c8c22023-10-26 11:42:24 -070093 await _build_with_soong('out', ["json-module-graph"])
Liz Kammer767fad42023-06-09 11:23:15 -040094
Cole Faustbbe2cc62023-07-12 17:55:28 -070095 with open(module_path) as f:
96 action_graph = json.load(f)
Liz Kammer767fad42023-06-09 11:23:15 -040097
98 module_to_outs = collections.defaultdict(set)
99 for mod in action_graph:
100 name = mod["Name"]
101 if name in modules:
Cole Fausta57c8c22023-10-26 11:42:24 -0700102 for act in (mod["Module"]["Actions"] or []):
Jason Macnak394f1b72023-06-15 09:28:26 -0700103 if "}generate" in act["Desc"]:
Liz Kammer767fad42023-06-09 11:23:15 -0400104 module_to_outs[name].update(act["Outputs"])
105 return module_to_outs
106
107
Cole Faustbbe2cc62023-07-12 17:55:28 -0700108def _compare_outputs(module_to_outs, tempdir) -> dict[str, list[str]]:
Liz Kammer767fad42023-06-09 11:23:15 -0400109 different_modules = collections.defaultdict(list)
110 for module, outs in module_to_outs.items():
111 for out in outs:
Cole Faustbbe2cc62023-07-12 17:55:28 -0700112 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 Kammer767fad42023-06-09 11:23:15 -0400116
Liz Kammer767fad42023-06-09 11:23:15 -0400117 return different_modules
118
119
Cole Fausta57c8c22023-10-26 11:42:24 -0700120async def main():
Liz Kammer767fad42023-06-09 11:23:15 -0400121 parser = argparse.ArgumentParser()
122 parser.add_argument(
Liz Kammer767fad42023-06-09 11:23:15 -0400123 "modules",
124 nargs="+",
125 help="modules to compare builds with genrule sandboxing enabled/not",
126 )
127 parser.add_argument(
Cole Fausta57c8c22023-10-26 11:42:24 -0700128 "--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 Kammer767fad42023-06-09 11:23:15 -0400133 "--show-diff",
134 "-d",
135 action="store_true",
Liz Kammer767fad42023-06-09 11:23:15 -0400136 help="whether to display differing files",
137 )
Liz Kammer2fb361c2023-06-09 11:29:48 -0400138 parser.add_argument(
139 "--output-paths-only",
140 "-o",
141 action="store_true",
Liz Kammer2fb361c2023-06-09 11:29:48 -0400142 help="Whether to only return the output paths per module",
143 )
Liz Kammer767fad42023-06-09 11:23:15 -0400144 args = parser.parse_args()
Cole Faustbbe2cc62023-07-12 17:55:28 -0700145 os.chdir(get_top())
Liz Kammer767fad42023-06-09 11:23:15 -0400146
Cole Fausta57c8c22023-10-26 11:42:24 -0700147 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 Kammer767fad42023-06-09 11:23:15 -0400151
Cole Faustbbe2cc62023-07-12 17:55:28 -0700152 print("finding output files for the modules...")
Cole Fausta57c8c22023-10-26 11:42:24 -0700153 module_to_outs = await _find_outputs_for_modules(set(args.modules))
Jason Macnak394f1b72023-06-15 09:28:26 -0700154 if not module_to_outs:
Cole Faustbbe2cc62023-07-12 17:55:28 -0700155 sys.exit("No outputs found")
Jason Macnak394f1b72023-06-15 09:28:26 -0700156
Liz Kammer2fb361c2023-06-09 11:29:48 -0400157 if args.output_paths_only:
158 for m, o in module_to_outs.items():
159 print(f"{m} outputs: {o}")
Cole Faustbbe2cc62023-07-12 17:55:28 -0700160 sys.exit(0)
Liz Kammer2fb361c2023-06-09 11:29:48 -0400161
Cole Faustbbe2cc62023-07-12 17:55:28 -0700162 all_outs = list(set.union(*module_to_outs.values()))
Cole Fausta57c8c22023-10-26 11:42:24 -0700163 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 Faustbbe2cc62023-07-12 17:55:28 -0700166
Cole Fausta57c8c22023-10-26 11:42:24 -0700167 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 Faustbbe2cc62023-07-12 17:55:28 -0700171
Cole Fausta57c8c22023-10-26 11:42:24 -0700172 # 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 Faustbbe2cc62023-07-12 17:55:28 -0700175
Cole Fausta57c8c22023-10-26 11:42:24 -0700176 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 Kammer767fad42023-06-09 11:23:15 -0400204
205
206if __name__ == "__main__":
Cole Fausta57c8c22023-10-26 11:42:24 -0700207 asyncio.run(main())