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