blob: e1f6e731c23664179e5521f5e725ae452e642a98 [file] [log] [blame]
David Srbecky37274d52022-11-03 11:04:16 +00001#!/usr/bin/env python3
David Srbecky8106b382022-04-20 13:37:15 +01002#
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
David Srbecky37274d52022-11-03 11:04:16 +000017"""
18This scripts compiles Java files which are needed to execute run-tests.
19It is intended to be used only from soong genrule.
David Srbecky8106b382022-04-20 13:37:15 +010020"""
21
22import argparse
23import functools
24import glob
25import os
David Srbecky36b520b2022-10-27 14:43:12 +010026import pathlib
David Srbeckyc9453d32022-11-17 13:28:28 +000027import re
David Srbecky8106b382022-04-20 13:37:15 +010028import shlex
29import shutil
30import subprocess
David Srbecky6bd1cd12022-11-05 18:54:19 +000031import sys
David Srbecky8106b382022-04-20 13:37:15 +010032import zipfile
David Srbecky6bd1cd12022-11-05 18:54:19 +000033
David Srbecky37274d52022-11-03 11:04:16 +000034from argparse import ArgumentParser
David Srbeckyc9453d32022-11-17 13:28:28 +000035from concurrent.futures import ThreadPoolExecutor
David Srbecky37274d52022-11-03 11:04:16 +000036from fcntl import lockf, LOCK_EX, LOCK_NB
37from importlib.machinery import SourceFileLoader
David Srbecky36b520b2022-10-27 14:43:12 +010038from os import environ, getcwd, chdir, cpu_count, chmod
39from os.path import relpath
David Srbecky37274d52022-11-03 11:04:16 +000040from pathlib import Path
David Srbecky6bd1cd12022-11-05 18:54:19 +000041from pprint import pprint
David Srbeckye0c3cd82022-08-24 14:56:24 +010042from re import match
David Srbecky37274d52022-11-03 11:04:16 +000043from shutil import copytree, rmtree
44from subprocess import run
David Srbecky36b520b2022-10-27 14:43:12 +010045from tempfile import TemporaryDirectory, NamedTemporaryFile
David Srbecky100fe672022-11-14 11:32:27 +000046from typing import Dict, List, Union, Set, Optional
David Srbecky8106b382022-04-20 13:37:15 +010047
David Srbeckya42a2e32022-11-17 13:43:22 +000048USE_RBE = 100 # Percentage of tests that can use RBE (between 0 and 100)
David Srbecky37274d52022-11-03 11:04:16 +000049
50lock_file = None # Keep alive as long as this process is alive.
51
David Srbeckyf32b2582022-11-17 15:28:35 +000052RBE_D8_DISABLED_FOR = {
David Srbeckyf32b2582022-11-17 15:28:35 +000053 "952-invoke-custom", # b/228312861: RBE uses wrong inputs.
54 "979-const-method-handle", # b/228312861: RBE uses wrong inputs.
55}
David Srbecky89010a32022-07-14 16:47:30 +000056
David Srbeckyc9453d32022-11-17 13:28:28 +000057# Debug option. Report commands that are taking a lot of user CPU time.
58REPORT_SLOW_COMMANDS = False
59
David Srbecky51dec052022-10-27 14:43:12 +010060class BuildTestContext:
David Srbecky36b520b2022-10-27 14:43:12 +010061 def __init__(self, args, android_build_top, test_dir):
David Srbeckyfeed4af2022-11-17 10:44:39 +000062 self.android_build_top = android_build_top.absolute()
63 self.bootclasspath = args.bootclasspath.absolute()
David Srbecky6bd1cd12022-11-05 18:54:19 +000064 self.test_name = test_dir.name
65 self.test_dir = test_dir.absolute()
David Srbecky51dec052022-10-27 14:43:12 +010066 self.mode = args.mode
67 self.jvm = (self.mode == "jvm")
68 self.host = (self.mode == "host")
69 self.target = (self.mode == "target")
70 assert self.jvm or self.host or self.target
71
David Srbecky36b520b2022-10-27 14:43:12 +010072 self.java_home = Path(os.environ.get("JAVA_HOME")).absolute()
David Srbeckyfeed4af2022-11-17 10:44:39 +000073 self.java_path = self.java_home / "bin/java"
74 self.javac_path = self.java_home / "bin/javac"
Nicolas Geoffraybb8d6f62023-09-12 11:04:23 +010075 self.javac_args = "-g -Xlint:-options"
David Srbecky6bd1cd12022-11-05 18:54:19 +000076
David Srbeckyfeed4af2022-11-17 10:44:39 +000077 # Helper functions to execute tools.
78 self.d8 = functools.partial(self.run, args.d8.absolute())
79 self.jasmin = functools.partial(self.run, args.jasmin.absolute())
80 self.javac = functools.partial(self.run, self.javac_path)
81 self.smali = functools.partial(self.run, args.smali.absolute())
82 self.soong_zip = functools.partial(self.run, args.soong_zip.absolute())
83 self.zipalign = functools.partial(self.run, args.zipalign.absolute())
84 if args.hiddenapi:
85 self.hiddenapi = functools.partial(self.run, args.hiddenapi.absolute())
86
87 # RBE wrapper for some of the tools.
Hans Boehm6183e752023-01-11 00:45:46 +000088 if "RBE_server_address" in os.environ and USE_RBE > (hash(self.test_name) % 100):
David Srbeckyfeed4af2022-11-17 10:44:39 +000089 self.rbe_exec_root = os.environ.get("RBE_exec_root")
90 self.rbe_rewrapper = self.android_build_top / "prebuilts/remoteexecution-client/live/rewrapper"
David Srbecky17b50422023-10-26 15:25:41 +010091
92 # TODO(b/307932183) Regression: RBE produces wrong output for D8 in ART
93 disable_d8 = any((self.test_dir / n).exists() for n in ["classes", "src2", "src-art"])
94
95 if self.test_name not in RBE_D8_DISABLED_FOR and not disable_d8:
David Srbeckyf32b2582022-11-17 15:28:35 +000096 self.d8 = functools.partial(self.rbe_d8, args.d8.absolute())
David Srbeckya42a2e32022-11-17 13:43:22 +000097 self.javac = functools.partial(self.rbe_javac, self.javac_path)
98 self.smali = functools.partial(self.rbe_smali, args.smali.absolute())
David Srbecky51dec052022-10-27 14:43:12 +010099
100 # Minimal environment needed for bash commands that we execute.
101 self.bash_env = {
102 "ANDROID_BUILD_TOP": self.android_build_top,
David Srbeckyfeed4af2022-11-17 10:44:39 +0000103 "D8": args.d8.absolute(),
104 "JAVA": self.java_path,
105 "JAVAC": self.javac_path,
David Srbecky51dec052022-10-27 14:43:12 +0100106 "JAVAC_ARGS": self.javac_args,
David Srbecky6bd1cd12022-11-05 18:54:19 +0000107 "JAVA_HOME": self.java_home,
David Srbecky51dec052022-10-27 14:43:12 +0100108 "PATH": os.environ["PATH"],
109 "PYTHONDONTWRITEBYTECODE": "1",
David Srbeckyfeed4af2022-11-17 10:44:39 +0000110 "SMALI": args.smali.absolute(),
111 "SOONG_ZIP": args.soong_zip.absolute(),
David Srbecky51dec052022-10-27 14:43:12 +0100112 "TEST_NAME": self.test_name,
113 }
114
115 def bash(self, cmd):
David Srbecky36b520b2022-10-27 14:43:12 +0100116 return subprocess.run(cmd,
117 shell=True,
118 cwd=self.test_dir,
119 env=self.bash_env,
120 check=True)
David Srbecky51dec052022-10-27 14:43:12 +0100121
David Srbeckyf32b2582022-11-17 15:28:35 +0000122 def run(self, executable: pathlib.Path, args: List[Union[pathlib.Path, str]]):
David Srbeckyfeed4af2022-11-17 10:44:39 +0000123 assert isinstance(executable, pathlib.Path), executable
124 cmd: List[Union[pathlib.Path, str]] = []
David Srbeckyc9453d32022-11-17 13:28:28 +0000125 if REPORT_SLOW_COMMANDS:
126 cmd += ["/usr/bin/time"]
David Srbeckyfeed4af2022-11-17 10:44:39 +0000127 if executable.suffix == ".sh":
128 cmd += ["/bin/bash"]
129 cmd += [executable]
130 cmd += args
131 env = self.bash_env
132 env.update({k: v for k, v in os.environ.items() if k.startswith("RBE_")})
133 # Make paths relative as otherwise we could create too long command line.
134 for i, arg in enumerate(cmd):
135 if isinstance(arg, pathlib.Path):
136 assert arg.absolute(), arg
137 cmd[i] = relpath(arg, self.test_dir)
138 elif isinstance(arg, list):
139 assert all(p.absolute() for p in arg), arg
140 cmd[i] = ":".join(relpath(p, self.test_dir) for p in arg)
141 else:
142 assert isinstance(arg, str), arg
143 p = subprocess.run(cmd,
144 encoding=sys.stdout.encoding,
145 cwd=self.test_dir,
146 env=self.bash_env,
147 stderr=subprocess.STDOUT,
148 stdout=subprocess.PIPE)
David Srbeckyc9453d32022-11-17 13:28:28 +0000149 if REPORT_SLOW_COMMANDS:
150 m = re.search("([0-9\.]+)user", p.stdout)
151 assert m, p.stdout
152 t = float(m.group(1))
153 if t > 1.0:
154 cmd_text = " ".join(map(str, cmd[1:]))[:100]
155 print(f"[{self.test_name}] Command took {t:.2f}s: {cmd_text}")
156
David Srbeckyfeed4af2022-11-17 10:44:39 +0000157 if p.returncode != 0:
158 raise Exception("Command failed with exit code {}\n$ {}\n{}".format(
159 p.returncode, " ".join(map(str, cmd)), p.stdout))
160 return p
161
162 def rbe_wrap(self, args, inputs: Set[pathlib.Path]=None):
163 with NamedTemporaryFile(mode="w+t") as input_list:
164 inputs = inputs or set()
165 for i, arg in enumerate(args):
166 if isinstance(arg, pathlib.Path):
167 assert arg.absolute(), arg
168 inputs.add(arg)
169 elif isinstance(arg, list):
170 assert all(p.absolute() for p in arg), arg
171 inputs.update(arg)
172 input_list.writelines([relpath(i, self.rbe_exec_root)+"\n" for i in inputs])
173 input_list.flush()
174 return self.run(self.rbe_rewrapper, [
175 "--platform=" + os.environ["RBE_platform"],
176 "--input_list_paths=" + input_list.name,
177 ] + args)
178
David Srbeckya42a2e32022-11-17 13:43:22 +0000179 def rbe_javac(self, javac_path:Path, args):
David Srbeckyfeed4af2022-11-17 10:44:39 +0000180 output = relpath(Path(args[args.index("-d") + 1]), self.rbe_exec_root)
David Srbeckya42a2e32022-11-17 13:43:22 +0000181 return self.rbe_wrap(["--output_directories", output, javac_path] + args)
David Srbeckyfeed4af2022-11-17 10:44:39 +0000182
David Srbeckya42a2e32022-11-17 13:43:22 +0000183 def rbe_d8(self, d8_path:Path, args):
184 inputs = set([d8_path.parent.parent / "framework/d8.jar"])
David Srbeckyfeed4af2022-11-17 10:44:39 +0000185 output = relpath(Path(args[args.index("--output") + 1]), self.rbe_exec_root)
186 return self.rbe_wrap([
187 "--output_files" if output.endswith(".jar") else "--output_directories", output,
Sorin Bascae1f8e3c2022-12-21 15:58:58 +0000188 "--toolchain_inputs=prebuilts/jdk/jdk17/linux-x86/bin/java",
David Srbeckya42a2e32022-11-17 13:43:22 +0000189 d8_path] + args, inputs)
190
191 def rbe_smali(self, smali_path:Path, args):
192 inputs = set([smali_path.parent.parent / "framework/smali.jar"])
193 output = relpath(Path(args[args.index("--output") + 1]), self.rbe_exec_root)
194 return self.rbe_wrap([
195 "--output_files", output,
Sorin Bascae1f8e3c2022-12-21 15:58:58 +0000196 "--toolchain_inputs=prebuilts/jdk/jdk17/linux-x86/bin/java",
David Srbeckya42a2e32022-11-17 13:43:22 +0000197 smali_path] + args, inputs)
David Srbeckyfeed4af2022-11-17 10:44:39 +0000198
David Srbeckyd272f2d2022-11-15 19:13:34 +0000199 def build(self) -> None:
200 script = self.test_dir / "build.py"
201 if script.exists():
202 module = SourceFileLoader("build_" + self.test_name,
203 str(script)).load_module()
204 module.build(self)
205 else:
206 self.default_build()
207
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000208 def default_build(
David Srbeckyd272f2d2022-11-15 19:13:34 +0000209 self,
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000210 use_desugar=True,
211 use_hiddenapi=True,
212 need_dex=None,
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000213 zip_compression_method="deflate",
214 zip_align_bytes=None,
David Srbeckye82c87d2022-11-16 20:31:11 +0000215 api_level:Union[int, str]=26, # Can also be named alias (string).
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000216 javac_args=[],
David Srbeckyf32b2582022-11-17 15:28:35 +0000217 javac_classpath: List[Path]=[],
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000218 d8_flags=[],
219 smali_args=[],
220 use_smali=True,
221 use_jasmin=True,
Nicolas Geoffraybb8d6f62023-09-12 11:04:23 +0100222 javac_source_arg="1.8",
223 javac_target_arg="1.8"
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000224 ):
David Srbeckyf32b2582022-11-17 15:28:35 +0000225 javac_classpath = javac_classpath.copy() # Do not modify default value.
David Srbecky51dec052022-10-27 14:43:12 +0100226
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000227 # Wrap "pathlib.Path" with our own version that ensures all paths are absolute.
David Srbeckyd272f2d2022-11-15 19:13:34 +0000228 # Plain filenames are assumed to be relative to self.test_dir and made absolute.
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000229 class Path(pathlib.Path):
230 def __new__(cls, filename: str):
231 path = pathlib.Path(filename)
David Srbeckyd272f2d2022-11-15 19:13:34 +0000232 return path if path.is_absolute() else (self.test_dir / path)
David Srbecky8106b382022-04-20 13:37:15 +0100233
David Srbeckyd272f2d2022-11-15 19:13:34 +0000234 need_dex = (self.host or self.target) if need_dex is None else need_dex
David Srbecky8106b382022-04-20 13:37:15 +0100235
David Srbeckyd272f2d2022-11-15 19:13:34 +0000236 if self.jvm:
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000237 # No desugaring on jvm because it supports the latest functionality.
238 use_desugar = False
David Srbecky8106b382022-04-20 13:37:15 +0100239
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000240 # Set API level for smali and d8.
David Srbeckye82c87d2022-11-16 20:31:11 +0000241 if isinstance(api_level, str):
242 API_LEVEL = {
David Srbeckyd272f2d2022-11-15 19:13:34 +0000243 "default-methods": 24,
244 "parameter-annotations": 25,
245 "agents": 26,
246 "method-handles": 26,
247 "var-handles": 28,
248 }
David Srbeckye82c87d2022-11-16 20:31:11 +0000249 api_level = API_LEVEL[api_level]
250 assert isinstance(api_level, int), api_level
David Srbecky8106b382022-04-20 13:37:15 +0100251
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000252 def zip(zip_target: Path, *files: Path):
253 zip_args = ["-o", zip_target, "-C", zip_target.parent]
254 if zip_compression_method == "store":
255 zip_args.extend(["-L", "0"])
256 for f in files:
257 zip_args.extend(["-f", f])
David Srbeckyfeed4af2022-11-17 10:44:39 +0000258 self.soong_zip(zip_args)
David Srbecky8106b382022-04-20 13:37:15 +0100259
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000260 if zip_align_bytes:
261 # zipalign does not operate in-place, so write results to a temp file.
262 with TemporaryDirectory() as tmp_dir:
263 tmp_file = Path(tmp_dir) / "aligned.zip"
David Srbeckyfeed4af2022-11-17 10:44:39 +0000264 self.zipalign(["-f", str(zip_align_bytes), zip_target, tmp_file])
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000265 # replace original zip target with our temp file.
266 tmp_file.rename(zip_target)
267
268
269 def make_jasmin(dst_dir: Path, src_dir: Path) -> Optional[Path]:
270 if not use_jasmin or not src_dir.exists():
271 return None # No sources to compile.
272 dst_dir.mkdir()
David Srbeckyfeed4af2022-11-17 10:44:39 +0000273 self.jasmin(["-d", dst_dir] + sorted(src_dir.glob("**/*.j")))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000274 return dst_dir
275
276 def make_smali(dst_dex: Path, src_dir: Path) -> Optional[Path]:
277 if not use_smali or not src_dir.exists():
278 return None # No sources to compile.
David Srbeckyd7c770a2023-10-26 15:45:57 +0100279 p = self.smali(["-JXmx512m", "assemble"] + smali_args + ["--api", str(api_level)] +
280 ["--output", dst_dex] + sorted(src_dir.glob("**/*.smali")))
281 assert dst_dex.exists(), p.stdout # NB: smali returns 0 exit code even on failure.
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000282 return dst_dex
283
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000284 def make_java(dst_dir: Path, *src_dirs: Path) -> Optional[Path]:
285 if not any(src_dir.exists() for src_dir in src_dirs):
286 return None # No sources to compile.
287 dst_dir.mkdir(exist_ok=True)
David Srbeckyd272f2d2022-11-15 19:13:34 +0000288 args = self.javac_args.split(" ") + javac_args
289 args += ["-implicit:none", "-encoding", "utf8", "-d", dst_dir]
Nicolas Geoffraybb8d6f62023-09-12 11:04:23 +0100290 args += ["-source", javac_source_arg, "-target", javac_target_arg]
291 if not self.jvm and float(javac_target_arg) < 17.0:
David Srbeckyd272f2d2022-11-15 19:13:34 +0000292 args += ["-bootclasspath", self.bootclasspath]
David Srbeckyf32b2582022-11-17 15:28:35 +0000293 if javac_classpath:
294 args += ["-classpath", javac_classpath]
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000295 for src_dir in src_dirs:
296 args += sorted(src_dir.glob("**/*.java"))
David Srbeckyfeed4af2022-11-17 10:44:39 +0000297 self.javac(args)
David Srbeckyf32b2582022-11-17 15:28:35 +0000298 javac_post = Path("javac_post.sh")
299 if javac_post.exists():
300 self.run(javac_post, [dst_dir])
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000301 return dst_dir
302
303
304 # Make a "dex" file given a directory of classes. This will be
305 # packaged in a jar file.
306 def make_dex(src_dir: Path):
307 dst_jar = Path(src_dir.name + ".jar")
David Srbeckyd272f2d2022-11-15 19:13:34 +0000308 args = d8_flags + ["--min-api", str(api_level), "--output", dst_jar]
309 args += ["--lib", self.bootclasspath] if use_desugar else ["--no-desugaring"]
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000310 args += sorted(src_dir.glob("**/*.class"))
David Srbeckyfeed4af2022-11-17 10:44:39 +0000311 self.d8(args)
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000312
313 # D8 outputs to JAR files today rather than DEX files as DX used
314 # to. To compensate, we extract the DEX from d8's output to meet the
315 # expectations of make_dex callers.
316 dst_dex = Path(src_dir.name + ".dex")
David Srbecky36b520b2022-10-27 14:43:12 +0100317 with TemporaryDirectory() as tmp_dir:
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000318 zipfile.ZipFile(dst_jar, "r").extractall(tmp_dir)
319 (Path(tmp_dir) / "classes.dex").rename(dst_dex)
320
321 # Merge all the dex files.
322 # Skip non-existing files, but at least 1 file must exist.
323 def make_dexmerge(dst_dex: Path, *src_dexs: Path):
324 # Include destination. Skip any non-existing files.
325 srcs = [f for f in [dst_dex] + list(src_dexs) if f.exists()]
326
327 # NB: We merge even if there is just single input.
328 # It is useful to normalize non-deterministic smali output.
David Srbeckyd272f2d2022-11-15 19:13:34 +0000329 tmp_dir = self.test_dir / "dexmerge"
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000330 tmp_dir.mkdir()
David Srbeckyfeed4af2022-11-17 10:44:39 +0000331 self.d8(["--min-api", str(api_level), "--output", tmp_dir] + srcs)
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000332 assert not (tmp_dir / "classes2.dex").exists()
333 for src_file in srcs:
334 src_file.unlink()
335 (tmp_dir / "classes.dex").rename(dst_dex)
336 tmp_dir.rmdir()
David Srbecky8106b382022-04-20 13:37:15 +0100337
338
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000339 def make_hiddenapi(*dex_files: Path):
David Srbeckyd272f2d2022-11-15 19:13:34 +0000340 if not use_hiddenapi or not Path("hiddenapi-flags.csv").exists():
341 return # Nothing to do.
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000342 args: List[Union[str, Path]] = ["encode"]
343 for dex_file in dex_files:
344 args.extend(["--input-dex=" + str(dex_file), "--output-dex=" + str(dex_file)])
345 args.append("--api-flags=hiddenapi-flags.csv")
346 args.append("--no-force-assign-all")
David Srbeckyfeed4af2022-11-17 10:44:39 +0000347 self.hiddenapi(args)
David Srbecky8106b382022-04-20 13:37:15 +0100348
349
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000350 if Path("classes.dex").exists():
David Srbeckyd272f2d2022-11-15 19:13:34 +0000351 zip(Path(self.test_name + ".jar"), Path("classes.dex"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000352 return
353
354 if Path("classes.dm").exists():
David Srbeckyd272f2d2022-11-15 19:13:34 +0000355 zip(Path(self.test_name + ".jar"), Path("classes.dm"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000356 return
David Srbecky37274d52022-11-03 11:04:16 +0000357
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000358 if make_jasmin(Path("jasmin_classes"), Path("jasmin")):
David Srbeckyf32b2582022-11-17 15:28:35 +0000359 javac_classpath.append(Path("jasmin_classes"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000360
361 if make_jasmin(Path("jasmin_classes2"), Path("jasmin-multidex")):
David Srbeckyf32b2582022-11-17 15:28:35 +0000362 javac_classpath.append(Path("jasmin_classes2"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000363
David Srbeckyd272f2d2022-11-15 19:13:34 +0000364 # To allow circular references, compile src/, src-multidex/, src-aotex/,
365 # src-bcpex/, src-ex/ together and pass the output as class path argument.
366 # Replacement sources in src-art/, src2/ and src-ex2/ can replace symbols
367 # used by the other src-* sources we compile here but everything needed to
368 # compile the other src-* sources should be present in src/ (and jasmin*/).
369 extra_srcs = ["src-multidex", "src-aotex", "src-bcpex", "src-ex"]
370 replacement_srcs = ["src2", "src-ex2"] + ([] if self.jvm else ["src-art"])
371 if (Path("src").exists() and
372 any(Path(p).exists() for p in extra_srcs + replacement_srcs)):
373 make_java(Path("classes-tmp-all"), Path("src"), *map(Path, extra_srcs))
David Srbeckyf32b2582022-11-17 15:28:35 +0000374 javac_classpath.append(Path("classes-tmp-all"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000375
376 if make_java(Path("classes-aotex"), Path("src-aotex")) and need_dex:
377 make_dex(Path("classes-aotex"))
378 # rename it so it shows up as "classes.dex" in the zip file.
379 Path("classes-aotex.dex").rename(Path("classes.dex"))
David Srbeckyd272f2d2022-11-15 19:13:34 +0000380 zip(Path(self.test_name + "-aotex.jar"), Path("classes.dex"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000381
382 if make_java(Path("classes-bcpex"), Path("src-bcpex")) and need_dex:
383 make_dex(Path("classes-bcpex"))
384 # rename it so it shows up as "classes.dex" in the zip file.
385 Path("classes-bcpex.dex").rename(Path("classes.dex"))
David Srbeckyd272f2d2022-11-15 19:13:34 +0000386 zip(Path(self.test_name + "-bcpex.jar"), Path("classes.dex"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000387
388 make_java(Path("classes"), Path("src"))
389
David Srbeckyd272f2d2022-11-15 19:13:34 +0000390 if not self.jvm:
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000391 # Do not attempt to build src-art directories on jvm,
392 # since it would fail without libcore.
393 make_java(Path("classes"), Path("src-art"))
394
395 if make_java(Path("classes2"), Path("src-multidex")) and need_dex:
396 make_dex(Path("classes2"))
397
398 make_java(Path("classes"), Path("src2"))
399
400 # If the classes directory is not-empty, package classes in a DEX file.
401 # NB: some tests provide classes rather than java files.
402 if any(Path("classes").glob("*")) and need_dex:
403 make_dex(Path("classes"))
404
405 if Path("jasmin_classes").exists():
406 # Compile Jasmin classes as if they were part of the classes.dex file.
407 if need_dex:
408 make_dex(Path("jasmin_classes"))
409 make_dexmerge(Path("classes.dex"), Path("jasmin_classes.dex"))
410 else:
411 # Move jasmin classes into classes directory so that they are picked up
412 # with -cp classes.
413 Path("classes").mkdir(exist_ok=True)
414 copytree(Path("jasmin_classes"), Path("classes"), dirs_exist_ok=True)
415
416 if need_dex and make_smali(Path("smali_classes.dex"), Path("smali")):
417 # Merge smali files into classes.dex,
418 # this takes priority over any jasmin files.
419 make_dexmerge(Path("classes.dex"), Path("smali_classes.dex"))
420
421 # Compile Jasmin classes in jasmin-multidex as if they were part of
422 # the classes2.jar
David Srbeckyd272f2d2022-11-15 19:13:34 +0000423 if Path("jasmin-multidex").exists():
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000424 if need_dex:
425 make_dex(Path("jasmin_classes2"))
426 make_dexmerge(Path("classes2.dex"), Path("jasmin_classes2.dex"))
427 else:
428 # Move jasmin classes into classes2 directory so that
429 # they are picked up with -cp classes2.
430 Path("classes2").mkdir()
431 copytree(Path("jasmin_classes2"), Path("classes2"), dirs_exist_ok=True)
432 rmtree(Path("jasmin_classes2"))
433
434 if need_dex and make_smali(Path("smali_classes2.dex"), Path("smali-multidex")):
435 # Merge smali_classes2.dex into classes2.dex
436 make_dexmerge(Path("classes2.dex"), Path("smali_classes2.dex"))
437
438 make_java(Path("classes-ex"), Path("src-ex"))
439
440 make_java(Path("classes-ex"), Path("src-ex2"))
441
442 if Path("classes-ex").exists() and need_dex:
443 make_dex(Path("classes-ex"))
444
445 if need_dex and make_smali(Path("smali_classes-ex.dex"), Path("smali-ex")):
446 # Merge smali files into classes-ex.dex.
447 make_dexmerge(Path("classes-ex.dex"), Path("smali_classes-ex.dex"))
448
449 if Path("classes-ex.dex").exists():
450 # Apply hiddenapi on the dex files if the test has API list file(s).
David Srbeckyd272f2d2022-11-15 19:13:34 +0000451 make_hiddenapi(Path("classes-ex.dex"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000452
453 # quick shuffle so that the stored name is "classes.dex"
454 Path("classes.dex").rename(Path("classes-1.dex"))
455 Path("classes-ex.dex").rename(Path("classes.dex"))
David Srbeckyd272f2d2022-11-15 19:13:34 +0000456 zip(Path(self.test_name + "-ex.jar"), Path("classes.dex"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000457 Path("classes.dex").rename(Path("classes-ex.dex"))
458 Path("classes-1.dex").rename(Path("classes.dex"))
459
460 # Apply hiddenapi on the dex files if the test has API list file(s).
David Srbeckyd272f2d2022-11-15 19:13:34 +0000461 if need_dex:
462 if any(Path(".").glob("*-multidex")):
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000463 make_hiddenapi(Path("classes.dex"), Path("classes2.dex"))
464 else:
465 make_hiddenapi(Path("classes.dex"))
466
467 # Create a single dex jar with two dex files for multidex.
468 if need_dex:
469 if Path("classes2.dex").exists():
David Srbeckyd272f2d2022-11-15 19:13:34 +0000470 zip(Path(self.test_name + ".jar"), Path("classes.dex"), Path("classes2.dex"))
David Srbeckyaf0ad5a2022-11-15 18:28:35 +0000471 else:
David Srbeckyd272f2d2022-11-15 19:13:34 +0000472 zip(Path(self.test_name + ".jar"), Path("classes.dex"))
David Srbecky37274d52022-11-03 11:04:16 +0000473
474
475# If we build just individual shard, we want to split the work among all the cores,
476# but if the build system builds all shards, we don't want to overload the machine.
477# We don't know which situation we are in, so as simple work-around, we use a lock
478# file to allow only one shard to use multiprocessing at the same time.
479def use_multiprocessing(mode: str) -> bool:
David Srbeckyd26f9072022-12-09 16:40:40 +0000480 if "RBE_server_address" in os.environ:
481 return True
David Srbecky37274d52022-11-03 11:04:16 +0000482 global lock_file
David Srbecky36b520b2022-10-27 14:43:12 +0100483 lock_path = Path(environ["TMPDIR"]) / ("art-test-run-test-build-py-" + mode)
David Srbecky37274d52022-11-03 11:04:16 +0000484 lock_file = open(lock_path, "w")
485 try:
486 lockf(lock_file, LOCK_EX | LOCK_NB)
487 return True # We are the only instance of this script in the build system.
488 except BlockingIOError:
489 return False # Some other instance is already running.
490
491
492def main() -> None:
493 parser = ArgumentParser(description=__doc__)
David Srbecky6bd1cd12022-11-05 18:54:19 +0000494 parser.add_argument("--out", type=Path, help="Final zip file")
David Srbecky37274d52022-11-03 11:04:16 +0000495 parser.add_argument("--mode", choices=["host", "jvm", "target"])
David Srbecky6bd1cd12022-11-05 18:54:19 +0000496 parser.add_argument("--bootclasspath", type=Path)
497 parser.add_argument("--d8", type=Path)
498 parser.add_argument("--hiddenapi", type=Path)
499 parser.add_argument("--jasmin", type=Path)
500 parser.add_argument("--smali", type=Path)
501 parser.add_argument("--soong_zip", type=Path)
502 parser.add_argument("--zipalign", type=Path)
503 parser.add_argument("srcs", nargs="+", type=Path)
David Srbecky37274d52022-11-03 11:04:16 +0000504 args = parser.parse_args()
505
David Srbecky36b520b2022-10-27 14:43:12 +0100506 android_build_top = Path(getcwd()).absolute()
507 ziproot = args.out.absolute().parent / "zip"
508 srcdirs = set(s.parents[-4].absolute() for s in args.srcs)
David Srbecky37274d52022-11-03 11:04:16 +0000509
David Srbeckyc282bef2022-11-16 21:52:33 +0000510 # Special hidden-api shard: If the --hiddenapi flag is provided, build only
511 # hiddenapi tests. Otherwise exclude all hiddenapi tests from normal shards.
512 def filter_by_hiddenapi(srcdir: Path) -> bool:
513 return (args.hiddenapi != None) == ("hiddenapi" in srcdir.name)
514
David Srbecky36b520b2022-10-27 14:43:12 +0100515 # Initialize the test objects.
516 # We need to do this before we change the working directory below.
517 tests: List[BuildTestContext] = []
David Srbeckyc282bef2022-11-16 21:52:33 +0000518 for srcdir in filter(filter_by_hiddenapi, srcdirs):
David Srbecky36b520b2022-10-27 14:43:12 +0100519 dstdir = ziproot / args.mode / srcdir.name
520 copytree(srcdir, dstdir)
521 tests.append(BuildTestContext(args, android_build_top, dstdir))
522
523 # We can not change the working directory per each thread since they all run in parallel.
524 # Create invalid read-only directory to catch accidental use of current working directory.
525 with TemporaryDirectory("-do-not-use-cwd") as invalid_tmpdir:
526 os.chdir(invalid_tmpdir)
527 os.chmod(invalid_tmpdir, 0)
528 with ThreadPoolExecutor(cpu_count() if use_multiprocessing(args.mode) else 1) as pool:
529 jobs = {}
530 for ctx in tests:
David Srbeckyd272f2d2022-11-15 19:13:34 +0000531 jobs[ctx.test_name] = pool.submit(ctx.build)
David Srbecky36b520b2022-10-27 14:43:12 +0100532 for test_name, job in jobs.items():
533 try:
534 job.result()
535 except Exception as e:
536 raise Exception("Failed to build " + test_name) from e
David Srbecky37274d52022-11-03 11:04:16 +0000537
538 # Create the final zip file which contains the content of the temporary directory.
David Srbecky36b520b2022-10-27 14:43:12 +0100539 proc = run([android_build_top / args.soong_zip, "-o", android_build_top / args.out,
540 "-C", ziproot, "-D", ziproot], check=True)
David Srbecky37274d52022-11-03 11:04:16 +0000541
542
543if __name__ == "__main__":
544 main()