1#!/usr/bin/env python3 2# 3# Copyright (C) 2021 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""" 18This scripts compiles Java files which are needed to execute run-tests. 19It is intended to be used only from soong genrule. 20""" 21 22import argparse 23import functools 24import glob 25import os 26import pathlib 27import re 28import shlex 29import shutil 30import subprocess 31import sys 32import zipfile 33 34from argparse import ArgumentParser 35from concurrent.futures import ThreadPoolExecutor 36from fcntl import lockf, LOCK_EX, LOCK_NB 37from importlib.machinery import SourceFileLoader 38from os import environ, getcwd, chdir, cpu_count, chmod 39from os.path import relpath 40from pathlib import Path 41from pprint import pprint 42from re import match 43from shutil import copytree, rmtree 44from subprocess import run 45from tempfile import TemporaryDirectory, NamedTemporaryFile 46from typing import Dict, List, Union, Set, Optional 47 48USE_RBE = 100 # Percentage of tests that can use RBE (between 0 and 100) 49 50lock_file = None # Keep alive as long as this process is alive. 51 52RBE_COMPARE = False # Debugging: Check that RBE and local output are identical. 53 54RBE_D8_DISABLED_FOR = { 55 "952-invoke-custom", # b/228312861: RBE uses wrong inputs. 56 "979-const-method-handle", # b/228312861: RBE uses wrong inputs. 57} 58 59# Debug option. Report commands that are taking a lot of user CPU time. 60REPORT_SLOW_COMMANDS = False 61 62class BuildTestContext: 63 def __init__(self, args, android_build_top, test_dir): 64 self.android_build_top = android_build_top.absolute() 65 self.bootclasspath = args.bootclasspath.absolute() 66 self.test_name = test_dir.name 67 self.test_dir = test_dir.absolute() 68 self.mode = args.mode 69 self.jvm = (self.mode == "jvm") 70 self.host = (self.mode == "host") 71 self.target = (self.mode == "target") 72 assert self.jvm or self.host or self.target 73 74 self.java_home = Path(os.environ.get("JAVA_HOME")).absolute() 75 self.java_path = self.java_home / "bin/java" 76 self.javac_path = self.java_home / "bin/javac" 77 self.javac_args = "-g -Xlint:-options" 78 79 # Helper functions to execute tools. 80 self.d8_path = args.d8.absolute() 81 self.d8 = functools.partial(self.run, args.d8.absolute()) 82 self.jasmin = functools.partial(self.run, args.jasmin.absolute()) 83 self.javac = functools.partial(self.run, self.javac_path) 84 self.smali_path = args.smali.absolute() 85 self.rbe_rewrapper = args.rewrapper.absolute() 86 self.smali = functools.partial(self.run, args.smali.absolute()) 87 self.soong_zip = functools.partial(self.run, args.soong_zip.absolute()) 88 self.zipalign = functools.partial(self.run, args.zipalign.absolute()) 89 if args.hiddenapi: 90 self.hiddenapi = functools.partial(self.run, args.hiddenapi.absolute()) 91 92 # RBE wrapper for some of the tools. 93 if "RBE_server_address" in os.environ and USE_RBE > (hash(self.test_name) % 100): 94 self.rbe_exec_root = os.environ.get("RBE_exec_root") 95 96 # TODO(b/307932183) Regression: RBE produces wrong output for D8 in ART 97 disable_d8 = any((self.test_dir / n).exists() for n in ["classes", "src2", "src-art"]) 98 99 if self.test_name not in RBE_D8_DISABLED_FOR and not disable_d8: 100 self.d8 = functools.partial(self.rbe_d8, args.d8.absolute()) 101 self.javac = functools.partial(self.rbe_javac, self.javac_path) 102 self.smali = functools.partial(self.rbe_smali, args.smali.absolute()) 103 104 # Minimal environment needed for bash commands that we execute. 105 self.bash_env = { 106 "ANDROID_BUILD_TOP": self.android_build_top, 107 "D8": args.d8.absolute(), 108 "JAVA": self.java_path, 109 "JAVAC": self.javac_path, 110 "JAVAC_ARGS": self.javac_args, 111 "JAVA_HOME": self.java_home, 112 "PATH": os.environ["PATH"], 113 "PYTHONDONTWRITEBYTECODE": "1", 114 "SMALI": args.smali.absolute(), 115 "SOONG_ZIP": args.soong_zip.absolute(), 116 "TEST_NAME": self.test_name, 117 } 118 119 def bash(self, cmd): 120 return subprocess.run(cmd, 121 shell=True, 122 cwd=self.test_dir, 123 env=self.bash_env, 124 check=True) 125 126 def run(self, executable: pathlib.Path, args: List[Union[pathlib.Path, str]]): 127 assert isinstance(executable, pathlib.Path), executable 128 cmd: List[Union[pathlib.Path, str]] = [] 129 if REPORT_SLOW_COMMANDS: 130 cmd += ["/usr/bin/time"] 131 if executable.suffix == ".sh": 132 cmd += ["/bin/bash"] 133 cmd += [executable] 134 cmd += args 135 env = self.bash_env 136 env.update({k: v for k, v in os.environ.items() if k.startswith("RBE_")}) 137 # Make paths relative as otherwise we could create too long command line. 138 for i, arg in enumerate(cmd): 139 if isinstance(arg, pathlib.Path): 140 assert arg.absolute(), arg 141 cmd[i] = relpath(arg, self.test_dir) 142 elif isinstance(arg, list): 143 assert all(p.absolute() for p in arg), arg 144 cmd[i] = ":".join(relpath(p, self.test_dir) for p in arg) 145 else: 146 assert isinstance(arg, str), arg 147 p = subprocess.run(cmd, 148 encoding=sys.stdout.encoding, 149 cwd=self.test_dir, 150 env=self.bash_env, 151 stderr=subprocess.STDOUT, 152 stdout=subprocess.PIPE) 153 if REPORT_SLOW_COMMANDS: 154 m = re.search("([0-9\.]+)user", p.stdout) 155 assert m, p.stdout 156 t = float(m.group(1)) 157 if t > 1.0: 158 cmd_text = " ".join(map(str, cmd[1:]))[:100] 159 print(f"[{self.test_name}] Command took {t:.2f}s: {cmd_text}") 160 161 if p.returncode != 0: 162 raise Exception("Command failed with exit code {}\n$ {}\n{}".format( 163 p.returncode, " ".join(map(str, cmd)), p.stdout)) 164 return p 165 166 def rbe_wrap(self, args, inputs: Set[pathlib.Path]=None): 167 with NamedTemporaryFile(mode="w+t") as input_list: 168 inputs = inputs or set() 169 for i in inputs: 170 assert i.exists(), i 171 for i, arg in enumerate(args): 172 if isinstance(arg, pathlib.Path): 173 assert arg.absolute(), arg 174 inputs.add(arg) 175 elif isinstance(arg, list): 176 assert all(p.absolute() for p in arg), arg 177 inputs.update(arg) 178 input_list.writelines([relpath(i, self.rbe_exec_root)+"\n" for i in inputs]) 179 input_list.flush() 180 dbg_args = ["-compare", "-num_local_reruns=1", "-num_remote_reruns=1"] if RBE_COMPARE else [] 181 return self.run(self.rbe_rewrapper, [ 182 "--platform=" + os.environ["RBE_platform"], 183 "--input_list_paths=" + input_list.name, 184 ] + dbg_args + args) 185 186 def rbe_javac(self, javac_path:Path, args): 187 output = relpath(Path(args[args.index("-d") + 1]), self.rbe_exec_root) 188 return self.rbe_wrap(["--output_directories", output, javac_path] + args) 189 190 def rbe_d8(self, d8_path:Path, args): 191 inputs = set([d8_path.parent.parent / "framework/d8.jar"]) 192 output = relpath(Path(args[args.index("--output") + 1]), self.rbe_exec_root) 193 return self.rbe_wrap([ 194 "--output_files" if output.endswith(".jar") else "--output_directories", output, 195 "--toolchain_inputs=prebuilts/jdk/jdk17/linux-x86/bin/java", 196 d8_path] + args, inputs) 197 198 def rbe_smali(self, smali_path:Path, args): 199 # The output of smali is non-deterministic, so create wrapper script, 200 # which runs D8 on the output to normalize it. 201 api = args[args.index("--api") + 1] 202 output = Path(args[args.index("--output") + 1]) 203 wrapper = output.with_suffix(".sh") 204 wrapper.write_text(''' 205 set -e 206 {smali} $@ 207 mkdir dex_normalize 208 {d8} --min-api {api} --output dex_normalize {output} 209 cp dex_normalize/classes.dex {output} 210 rm -rf dex_normalize 211 '''.strip().format( 212 smali=relpath(self.smali_path, self.test_dir), 213 d8=relpath(self.d8_path, self.test_dir), 214 api=api, 215 output=relpath(output, self.test_dir), 216 )) 217 218 inputs = set([ 219 wrapper, 220 self.smali_path, 221 self.smali_path.parent.parent / "framework/android-smali.jar", 222 self.d8_path, 223 self.d8_path.parent.parent / "framework/d8.jar", 224 ]) 225 res = self.rbe_wrap([ 226 "--output_files", relpath(output, self.rbe_exec_root), 227 "--toolchain_inputs=prebuilts/jdk/jdk17/linux-x86/bin/java", 228 "/bin/bash", wrapper] + args, inputs) 229 wrapper.unlink() 230 return res 231 232 def build(self) -> None: 233 script = self.test_dir / "build.py" 234 if script.exists(): 235 module = SourceFileLoader("build_" + self.test_name, 236 str(script)).load_module() 237 module.build(self) 238 else: 239 self.default_build() 240 241 def default_build( 242 self, 243 use_desugar=True, 244 use_hiddenapi=True, 245 need_dex=None, 246 zip_compression_method="deflate", 247 zip_align_bytes=None, 248 api_level:Union[int, str]=26, # Can also be named alias (string). 249 javac_args=[], 250 javac_classpath: List[Path]=[], 251 d8_flags=[], 252 d8_dex_container=True, 253 smali_args=[], 254 use_smali=True, 255 use_jasmin=True, 256 javac_source_arg="1.8", 257 javac_target_arg="1.8" 258 ): 259 javac_classpath = javac_classpath.copy() # Do not modify default value. 260 261 # Wrap "pathlib.Path" with our own version that ensures all paths are absolute. 262 # Plain filenames are assumed to be relative to self.test_dir and made absolute. 263 class Path(pathlib.Path): 264 def __new__(cls, filename: str): 265 path = pathlib.Path(filename) 266 return path if path.is_absolute() else (self.test_dir / path) 267 268 need_dex = (self.host or self.target) if need_dex is None else need_dex 269 270 if self.jvm: 271 # No desugaring on jvm because it supports the latest functionality. 272 use_desugar = False 273 274 # Set API level for smali and d8. 275 if isinstance(api_level, str): 276 API_LEVEL = { 277 "default-methods": 24, 278 "parameter-annotations": 25, 279 "agents": 26, 280 "method-handles": 26, 281 "var-handles": 28, 282 "const-method-type": 28, 283 } 284 api_level = API_LEVEL[api_level] 285 assert isinstance(api_level, int), api_level 286 287 def zip(zip_target: Path, *files: Path): 288 zip_args = ["-o", zip_target, "-C", zip_target.parent] 289 if zip_compression_method == "store": 290 zip_args.extend(["-L", "0"]) 291 for f in files: 292 zip_args.extend(["-f", f]) 293 self.soong_zip(zip_args) 294 295 if zip_align_bytes: 296 # zipalign does not operate in-place, so write results to a temp file. 297 with TemporaryDirectory() as tmp_dir: 298 tmp_file = Path(tmp_dir) / "aligned.zip" 299 self.zipalign(["-f", str(zip_align_bytes), zip_target, tmp_file]) 300 # replace original zip target with our temp file. 301 tmp_file.rename(zip_target) 302 303 304 def make_jasmin(dst_dir: Path, src_dir: Path) -> Optional[Path]: 305 if not use_jasmin or not src_dir.exists(): 306 return None # No sources to compile. 307 dst_dir.mkdir() 308 self.jasmin(["-d", dst_dir] + sorted(src_dir.glob("**/*.j"))) 309 return dst_dir 310 311 def make_smali(dst_dex: Path, src_dir: Path) -> Optional[Path]: 312 if not use_smali or not src_dir.exists(): 313 return None # No sources to compile. 314 p = self.smali(["-JXmx512m", "assemble"] + smali_args + ["--api", str(api_level)] + 315 ["--output", dst_dex] + sorted(src_dir.glob("**/*.smali"))) 316 assert dst_dex.exists(), p.stdout # NB: smali returns 0 exit code even on failure. 317 return dst_dex 318 319 def make_java(dst_dir: Path, *src_dirs: Path) -> Optional[Path]: 320 if not any(src_dir.exists() for src_dir in src_dirs): 321 return None # No sources to compile. 322 dst_dir.mkdir(exist_ok=True) 323 args = self.javac_args.split(" ") + javac_args 324 args += ["-implicit:none", "-encoding", "utf8", "-d", dst_dir] 325 args += ["-source", javac_source_arg, "-target", javac_target_arg] 326 if not self.jvm and float(javac_target_arg) < 17.0: 327 args += ["-bootclasspath", self.bootclasspath] 328 if javac_classpath: 329 args += ["-classpath", javac_classpath] 330 for src_dir in src_dirs: 331 args += sorted(src_dir.glob("**/*.java")) 332 self.javac(args) 333 javac_post = Path("javac_post.sh") 334 if javac_post.exists(): 335 self.run(javac_post, [dst_dir]) 336 return dst_dir 337 338 339 # Make a "dex" file given a directory of classes. This will be 340 # packaged in a jar file. 341 def make_dex(src_dir: Path): 342 dst_jar = Path(src_dir.name + ".jar") 343 args = [] 344 if d8_dex_container: 345 args += ["-JDcom.android.tools.r8.dexContainerExperiment"] 346 args += d8_flags + ["--min-api", str(api_level), "--output", dst_jar] 347 args += ["--lib", self.bootclasspath] if use_desugar else ["--no-desugaring"] 348 args += sorted(src_dir.glob("**/*.class")) 349 self.d8(args) 350 351 # D8 outputs to JAR files today rather than DEX files as DX used 352 # to. To compensate, we extract the DEX from d8's output to meet the 353 # expectations of make_dex callers. 354 dst_dex = Path(src_dir.name + ".dex") 355 with TemporaryDirectory() as tmp_dir: 356 zipfile.ZipFile(dst_jar, "r").extractall(tmp_dir) 357 (Path(tmp_dir) / "classes.dex").rename(dst_dex) 358 359 # Merge all the dex files. 360 # Skip non-existing files, but at least 1 file must exist. 361 def make_dexmerge(dst_dex: Path, *src_dexs: Path): 362 # Include destination. Skip any non-existing files. 363 srcs = [f for f in [dst_dex] + list(src_dexs) if f.exists()] 364 365 # NB: We merge even if there is just single input. 366 # It is useful to normalize non-deterministic smali output. 367 tmp_dir = self.test_dir / "dexmerge" 368 tmp_dir.mkdir() 369 flags = [] 370 if d8_dex_container: 371 flags += ["-JDcom.android.tools.r8.dexContainerExperiment"] 372 flags += ["--min-api", str(api_level), "--output", tmp_dir] 373 self.d8(flags + srcs) 374 assert not (tmp_dir / "classes2.dex").exists() 375 for src_file in srcs: 376 src_file.unlink() 377 (tmp_dir / "classes.dex").rename(dst_dex) 378 tmp_dir.rmdir() 379 380 381 def make_hiddenapi(*dex_files: Path): 382 if not use_hiddenapi or not Path("hiddenapi-flags.csv").exists(): 383 return # Nothing to do. 384 args: List[Union[str, Path]] = ["encode"] 385 for dex_file in dex_files: 386 args.extend(["--input-dex=" + str(dex_file), "--output-dex=" + str(dex_file)]) 387 args.append("--api-flags=hiddenapi-flags.csv") 388 args.append("--no-force-assign-all") 389 self.hiddenapi(args) 390 391 392 if Path("classes.dex").exists(): 393 zip(Path(self.test_name + ".jar"), Path("classes.dex")) 394 return 395 396 if Path("classes.dm").exists(): 397 zip(Path(self.test_name + ".jar"), Path("classes.dm")) 398 return 399 400 if make_jasmin(Path("jasmin_classes"), Path("jasmin")): 401 javac_classpath.append(Path("jasmin_classes")) 402 403 if make_jasmin(Path("jasmin_classes2"), Path("jasmin-multidex")): 404 javac_classpath.append(Path("jasmin_classes2")) 405 406 # To allow circular references, compile src/, src-multidex/, src-aotex/, 407 # src-bcpex/, src-ex/ together and pass the output as class path argument. 408 # Replacement sources in src-art/, src2/ and src-ex2/ can replace symbols 409 # used by the other src-* sources we compile here but everything needed to 410 # compile the other src-* sources should be present in src/ (and jasmin*/). 411 extra_srcs = ["src-multidex", "src-aotex", "src-bcpex", "src-ex"] 412 replacement_srcs = ["src2", "src-ex2"] + ([] if self.jvm else ["src-art"]) 413 if (Path("src").exists() and 414 any(Path(p).exists() for p in extra_srcs + replacement_srcs)): 415 make_java(Path("classes-tmp-all"), Path("src"), *map(Path, extra_srcs)) 416 javac_classpath.append(Path("classes-tmp-all")) 417 418 if make_java(Path("classes-aotex"), Path("src-aotex")) and need_dex: 419 make_dex(Path("classes-aotex")) 420 # rename it so it shows up as "classes.dex" in the zip file. 421 Path("classes-aotex.dex").rename(Path("classes.dex")) 422 zip(Path(self.test_name + "-aotex.jar"), Path("classes.dex")) 423 424 if make_java(Path("classes-bcpex"), Path("src-bcpex")) and need_dex: 425 make_dex(Path("classes-bcpex")) 426 # rename it so it shows up as "classes.dex" in the zip file. 427 Path("classes-bcpex.dex").rename(Path("classes.dex")) 428 zip(Path(self.test_name + "-bcpex.jar"), Path("classes.dex")) 429 430 make_java(Path("classes"), Path("src")) 431 432 if not self.jvm: 433 # Do not attempt to build src-art directories on jvm, 434 # since it would fail without libcore. 435 make_java(Path("classes"), Path("src-art")) 436 437 if make_java(Path("classes2"), Path("src-multidex")) and need_dex: 438 make_dex(Path("classes2")) 439 440 make_java(Path("classes"), Path("src2")) 441 442 # If the classes directory is not-empty, package classes in a DEX file. 443 # NB: some tests provide classes rather than java files. 444 if any(Path("classes").glob("*")) and need_dex: 445 make_dex(Path("classes")) 446 447 if Path("jasmin_classes").exists(): 448 # Compile Jasmin classes as if they were part of the classes.dex file. 449 if need_dex: 450 make_dex(Path("jasmin_classes")) 451 make_dexmerge(Path("classes.dex"), Path("jasmin_classes.dex")) 452 else: 453 # Move jasmin classes into classes directory so that they are picked up 454 # with -cp classes. 455 Path("classes").mkdir(exist_ok=True) 456 copytree(Path("jasmin_classes"), Path("classes"), dirs_exist_ok=True) 457 458 if need_dex and make_smali(Path("smali_classes.dex"), Path("smali")): 459 # Merge smali files into classes.dex, 460 # this takes priority over any jasmin files. 461 make_dexmerge(Path("classes.dex"), Path("smali_classes.dex")) 462 463 # Compile Jasmin classes in jasmin-multidex as if they were part of 464 # the classes2.jar 465 if Path("jasmin-multidex").exists(): 466 if need_dex: 467 make_dex(Path("jasmin_classes2")) 468 make_dexmerge(Path("classes2.dex"), Path("jasmin_classes2.dex")) 469 else: 470 # Move jasmin classes into classes2 directory so that 471 # they are picked up with -cp classes2. 472 Path("classes2").mkdir() 473 copytree(Path("jasmin_classes2"), Path("classes2"), dirs_exist_ok=True) 474 rmtree(Path("jasmin_classes2")) 475 476 if need_dex and make_smali(Path("smali_classes2.dex"), Path("smali-multidex")): 477 # Merge smali_classes2.dex into classes2.dex 478 make_dexmerge(Path("classes2.dex"), Path("smali_classes2.dex")) 479 480 make_java(Path("classes-ex"), Path("src-ex")) 481 482 make_java(Path("classes-ex"), Path("src-ex2")) 483 484 if Path("classes-ex").exists() and need_dex: 485 make_dex(Path("classes-ex")) 486 487 if need_dex and make_smali(Path("smali_classes-ex.dex"), Path("smali-ex")): 488 # Merge smali files into classes-ex.dex. 489 make_dexmerge(Path("classes-ex.dex"), Path("smali_classes-ex.dex")) 490 491 if Path("classes-ex.dex").exists(): 492 # Apply hiddenapi on the dex files if the test has API list file(s). 493 make_hiddenapi(Path("classes-ex.dex")) 494 495 # quick shuffle so that the stored name is "classes.dex" 496 Path("classes.dex").rename(Path("classes-1.dex")) 497 Path("classes-ex.dex").rename(Path("classes.dex")) 498 zip(Path(self.test_name + "-ex.jar"), Path("classes.dex")) 499 Path("classes.dex").rename(Path("classes-ex.dex")) 500 Path("classes-1.dex").rename(Path("classes.dex")) 501 502 # Apply hiddenapi on the dex files if the test has API list file(s). 503 if need_dex: 504 if any(Path(".").glob("*-multidex")): 505 make_hiddenapi(Path("classes.dex"), Path("classes2.dex")) 506 else: 507 make_hiddenapi(Path("classes.dex")) 508 509 # Create a single dex jar with two dex files for multidex. 510 if need_dex: 511 if Path("classes2.dex").exists(): 512 zip(Path(self.test_name + ".jar"), Path("classes.dex"), Path("classes2.dex")) 513 else: 514 zip(Path(self.test_name + ".jar"), Path("classes.dex")) 515 516 517# If we build just individual shard, we want to split the work among all the cores, 518# but if the build system builds all shards, we don't want to overload the machine. 519# We don't know which situation we are in, so as simple work-around, we use a lock 520# file to allow only one shard to use multiprocessing at the same time. 521def use_multiprocessing(mode: str) -> bool: 522 if "RBE_server_address" in os.environ: 523 return True 524 global lock_file 525 lock_path = Path(environ["TMPDIR"]) / ("art-test-run-test-build-py-" + mode) 526 lock_file = open(lock_path, "w") 527 try: 528 lockf(lock_file, LOCK_EX | LOCK_NB) 529 return True # We are the only instance of this script in the build system. 530 except BlockingIOError: 531 return False # Some other instance is already running. 532 533 534def main() -> None: 535 parser = ArgumentParser(description=__doc__) 536 parser.add_argument("--out", type=Path, help="Final zip file") 537 parser.add_argument("--mode", choices=["host", "jvm", "target"]) 538 parser.add_argument("--bootclasspath", type=Path) 539 parser.add_argument("--d8", type=Path) 540 parser.add_argument("--hiddenapi", type=Path) 541 parser.add_argument("--jasmin", type=Path) 542 parser.add_argument("--rewrapper", type=Path) 543 parser.add_argument("--smali", type=Path) 544 parser.add_argument("--soong_zip", type=Path) 545 parser.add_argument("--zipalign", type=Path) 546 parser.add_argument("--test-dir-regex") 547 parser.add_argument("srcs", nargs="+", type=Path) 548 args = parser.parse_args() 549 550 android_build_top = Path(getcwd()).absolute() 551 ziproot = args.out.absolute().parent / "zip" 552 test_dir_regex = re.compile(args.test_dir_regex) if args.test_dir_regex else re.compile(".*") 553 srcdirs = set( 554 s.parents[-4].absolute() 555 for s in args.srcs 556 if test_dir_regex.search(str(s)) 557 ) 558 559 # Special hidden-api shard: If the --hiddenapi flag is provided, build only 560 # hiddenapi tests. Otherwise exclude all hiddenapi tests from normal shards. 561 def filter_by_hiddenapi(srcdir: Path) -> bool: 562 return (args.hiddenapi != None) == ("hiddenapi" in srcdir.name) 563 564 # Initialize the test objects. 565 # We need to do this before we change the working directory below. 566 tests: List[BuildTestContext] = [] 567 for srcdir in filter(filter_by_hiddenapi, srcdirs): 568 dstdir = ziproot / args.mode / srcdir.name 569 copytree(srcdir, dstdir) 570 tests.append(BuildTestContext(args, android_build_top, dstdir)) 571 572 # We can not change the working directory per each thread since they all run in parallel. 573 # Create invalid read-only directory to catch accidental use of current working directory. 574 with TemporaryDirectory("-do-not-use-cwd") as invalid_tmpdir: 575 os.chdir(invalid_tmpdir) 576 os.chmod(invalid_tmpdir, 0) 577 with ThreadPoolExecutor(cpu_count() if use_multiprocessing(args.mode) else 1) as pool: 578 jobs = {} 579 for ctx in tests: 580 jobs[ctx.test_name] = pool.submit(ctx.build) 581 for test_name, job in jobs.items(): 582 try: 583 job.result() 584 except Exception as e: 585 raise Exception("Failed to build " + test_name) from e 586 587 # Create the final zip file which contains the content of the temporary directory. 588 proc = run([android_build_top / args.soong_zip, "-o", android_build_top / args.out, 589 "-C", ziproot, "-D", ziproot], check=True) 590 591 592if __name__ == "__main__": 593 main() 594