xref: /aosp_15_r20/art/test/run_test_build.py (revision 795d594fd825385562da6b089ea9b2033f3abf5a)
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