platform_build_soong/tests/genrule_sandbox_test.py

208 lines
6 KiB
Python
Raw Normal View History

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