blob: 3a41c779c6f5c0a4080b64d0321f4c2d41f7f7b7 [file] [log] [blame]
Martin Stjernholmdf105da2020-11-27 02:28:03 +00001#!/usr/bin/env -S python -B
2#
3# Copyright (C) 2020 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"""Downloads ART Module prebuilts and creates CLs to update them in git."""
18
19import argparse
20import collections
21import os
Martin Stjernholm10bb9c52020-12-01 22:53:51 +000022import re
Martin Stjernholmdf105da2020-11-27 02:28:03 +000023import subprocess
24import sys
25import tempfile
26
27
28# Prebuilt description used in commit message
29PREBUILT_DESCR = "ART Module"
30
31# fetch_artifact branch and target
32BRANCH = "aosp-master-art"
33TARGET = "aosp_art_module"
34
35ARCHES = ["arm", "arm64", "x86", "x86_64"]
36
37# Where to install the APEX packages
Martin Stjernholm99459f32020-12-11 22:54:28 +000038PACKAGE_PATH = "packages/modules/ArtPrebuilt"
Martin Stjernholmdf105da2020-11-27 02:28:03 +000039
40# Where to install the SDKs and module exports
41SDK_PATH = "prebuilts/module_sdk/art"
42
43SDK_VERSION = "current"
44
45# Paths to git projects to prepare CLs in
46GIT_PROJECT_ROOTS = [PACKAGE_PATH, SDK_PATH]
47
Martin Stjernholm402361b2020-12-01 20:33:22 +000048SCRIPT_PATH = "art/build/update-art-module-prebuilts.py"
49
Martin Stjernholmdf105da2020-11-27 02:28:03 +000050
51InstallEntry = collections.namedtuple("InstallEntry", [
52 # Artifact path in the build, passed to fetch_target
53 "source_path",
54 # Local install path
55 "install_path",
56 # True if the entry is a zip file that should be unzipped to install_path
57 "install_unzipped",
58])
59
60
61def install_apex_entries(apex_name):
62 res = []
63 for arch in ARCHES:
64 res.append(InstallEntry(
65 os.path.join(arch, apex_name + ".apex"),
66 os.path.join(PACKAGE_PATH, apex_name + "-" + arch + ".apex"),
67 install_unzipped=False))
68 return res
69
70
71def install_sdk_entries(mainline_sdk_name, sdk_dir):
72 return [InstallEntry(
73 os.path.join("mainline-sdks",
74 mainline_sdk_name + "-" + SDK_VERSION + ".zip"),
75 os.path.join(SDK_PATH, SDK_VERSION, sdk_dir),
76 install_unzipped=True)]
77
78
79install_entries = (
80 install_apex_entries("com.android.art") +
81 install_apex_entries("com.android.art.debug") +
82 install_sdk_entries("art-module-sdk", "sdk") +
83 install_sdk_entries("art-module-host-exports", "host-exports") +
84 install_sdk_entries("art-module-test-exports", "test-exports")
85)
86
87
Martin Stjernholm10bb9c52020-12-01 22:53:51 +000088def rewrite_bp_for_art_module_source_build(bp_path):
89 """Rewrites an Android.bp file to conditionally prefer prebuilts."""
90 print("Rewriting {} for SOONG_CONFIG_art_module_source_build use."
91 .format(bp_path))
92 bp_file = open(bp_path, "r+")
93
94 # TODO(b/174997203): Remove this when we have a proper way to control prefer
95 # flags in Mainline modules.
96
97 header_lines = []
98 for line in bp_file:
99 line = line.rstrip("\n")
100 if not line.startswith("//"):
101 break
102 header_lines.append(line)
103
104 art_module_types = set()
105
106 content_lines = []
107 for line in bp_file:
108 line = line.rstrip("\n")
109 module_header = re.match("([a-z0-9_]+) +{$", line)
110 if not module_header:
111 content_lines.append(line)
112 else:
113 # Iterate over one Soong module.
114 module_start = line
115 soong_config_clause = False
116 module_content = []
117
118 for module_line in bp_file:
119 module_line = module_line.rstrip("\n")
120 if module_line == "}":
121 break
122 if module_line == " prefer: false,":
123 module_content.extend([
124 (" // Do not prefer prebuilt if "
125 "SOONG_CONFIG_art_module_source_build is true."),
Martin Stjernholm20cadbf2020-12-15 02:32:03 +0000126 " prefer: true,",
Martin Stjernholm10bb9c52020-12-01 22:53:51 +0000127 " soong_config_variables: {",
128 " source_build: {",
129 " prefer: false,",
130 " },",
131 " },"])
132 soong_config_clause = True
133 else:
134 module_content.append(module_line)
135
136 if soong_config_clause:
137 module_type = "art_prebuilt_" + module_header.group(1)
138 module_start = module_type + " {"
139 art_module_types.add(module_type)
140
141 content_lines.append(module_start)
142 content_lines.extend(module_content)
143 content_lines.append("}")
144
145 header_lines.extend(
146 ["",
147 "// Soong config variable stanza added by {}.".format(SCRIPT_PATH),
148 "soong_config_module_type_import {",
149 " from: \"prebuilts/module_sdk/art/SoongConfig.bp\",",
150 " module_types: ["] +
151 [" \"{}\",".format(art_module)
152 for art_module in sorted(art_module_types)] +
153 [" ],",
154 "}",
Martin Stjernholm10bb9c52020-12-01 22:53:51 +0000155 ""])
156
157 bp_file.seek(0)
158 bp_file.truncate()
159 bp_file.write("\n".join(header_lines + content_lines))
160 bp_file.close()
161
162
Martin Stjernholmdf105da2020-11-27 02:28:03 +0000163def check_call(cmd, **kwargs):
164 """Proxy for subprocess.check_call with logging."""
165 msg = " ".join(cmd) if isinstance(cmd, list) else cmd
166 if "cwd" in kwargs:
167 msg = "In " + kwargs["cwd"] + ": " + msg
168 print(msg)
169 subprocess.check_call(cmd, **kwargs)
170
171
172def fetch_artifact(branch, target, build, fetch_pattern, local_dir):
173 """Fetches artifact from the build server."""
174 fetch_artifact_path = "/google/data/ro/projects/android/fetch_artifact"
175 cmd = [fetch_artifact_path, "--branch", branch, "--target", target,
176 "--bid", build, fetch_pattern]
177 check_call(cmd, cwd=local_dir)
178
179
180def start_branch(branch_name, git_dirs):
181 """Creates a new repo branch in the given projects."""
182 check_call(["repo", "start", branch_name] + git_dirs)
Martin Stjernholm402361b2020-12-01 20:33:22 +0000183 # In case the branch already exists we reset it to upstream, to get a clean
Martin Stjernholmdf105da2020-11-27 02:28:03 +0000184 # update CL.
185 for git_dir in git_dirs:
186 check_call(["git", "reset", "--hard", "@{upstream}"], cwd=git_dir)
187
188
189def upload_branch(git_root, branch_name):
190 """Uploads the CLs in the given branch in the given project."""
191 # Set the branch as topic to bundle with the CLs in other git projects (if
192 # any).
193 check_call(["repo", "upload", "-t", "--br=" + branch_name, git_root])
194
195
196def remove_files(git_root, subpaths):
Martin Stjernholm402361b2020-12-01 20:33:22 +0000197 """Removes files in the work tree, and stages the removals in git."""
Martin Stjernholmdf105da2020-11-27 02:28:03 +0000198 check_call(["git", "rm", "-qrf", "--ignore-unmatch"] + subpaths, cwd=git_root)
199 # Need a plain rm afterwards because git won't remove directories if they have
200 # non-git files in them.
201 check_call(["rm", "-rf"] + subpaths, cwd=git_root)
202
203
204def commit(git_root, prebuilt_descr, branch, build, add_paths):
205 """Commits the new prebuilts."""
206 check_call(["git", "add"] + add_paths, cwd=git_root)
207
208 if build:
209 message = (
210 "Update {prebuilt_descr} prebuilts to build {build}.\n\n"
211 "Taken from branch {branch}."
212 .format(prebuilt_descr=prebuilt_descr, branch=branch, build=build))
213 else:
214 message = (
Martin Stjernholm402361b2020-12-01 20:33:22 +0000215 "DO NOT SUBMIT: Update {prebuilt_descr} prebuilts from local build."
Martin Stjernholmdf105da2020-11-27 02:28:03 +0000216 .format(prebuilt_descr=prebuilt_descr))
Martin Stjernholm402361b2020-12-01 20:33:22 +0000217 message += ("\n\nCL prepared by {}."
218 "\n\nTest: Presubmits".format(SCRIPT_PATH))
Martin Stjernholmdf105da2020-11-27 02:28:03 +0000219 msg_fd, msg_path = tempfile.mkstemp()
220 with os.fdopen(msg_fd, "w") as f:
221 f.write(message)
222
223 # Do a diff first to skip the commit without error if there are no changes to
224 # commit.
225 check_call("git diff-index --quiet --cached HEAD -- || "
226 "git commit -F " + msg_path, shell=True, cwd=git_root)
227 os.unlink(msg_path)
228
229
230def install_entry(build, local_dist, entry):
231 """Installs one file specified by entry."""
232
233 install_dir, install_file = os.path.split(entry.install_path)
234 if install_dir and not os.path.exists(install_dir):
235 os.makedirs(install_dir)
236
237 if build:
238 fetch_artifact(BRANCH, TARGET, build, entry.source_path, install_dir)
239 else:
240 check_call(["cp", os.path.join(local_dist, entry.source_path), install_dir])
241 source_file = os.path.basename(entry.source_path)
242
243 if entry.install_unzipped:
244 check_call(["mkdir", install_file], cwd=install_dir)
245 # Add -DD to not extract timestamps that may confuse the build system.
246 check_call(["unzip", "-DD", source_file, "-d", install_file],
247 cwd=install_dir)
248 check_call(["rm", source_file], cwd=install_dir)
249
250 elif source_file != install_file:
251 check_call(["mv", source_file, install_file], cwd=install_dir)
252
253
254def install_paths_per_git_root(roots, paths):
255 """Partitions the given paths into subpaths within the given roots.
256
257 Args:
258 roots: List of root paths.
259 paths: List of paths relative to the same directory as the root paths.
260
261 Returns:
262 A dict mapping each root to the subpaths under it. It's an error if some
263 path doesn't go into any root.
264 """
265 res = collections.defaultdict(list)
266 for path in paths:
267 found = False
268 for root in roots:
269 if path.startswith(root + "/"):
270 res[root].append(path[len(root) + 1:])
271 found = True
272 break
273 if not found:
274 sys.exit("Install path {} is not in any of the git roots: {}"
275 .format(path, " ".join(roots)))
276 return res
277
278
279def get_args():
280 """Parses and returns command line arguments."""
281 parser = argparse.ArgumentParser(
282 epilog="Either --build or --local-dist is required.")
283
284 parser.add_argument("--build", metavar="NUMBER",
285 help="Build number to fetch from branch {}, target {}"
286 .format(BRANCH, TARGET))
287 parser.add_argument("--local-dist", metavar="PATH",
288 help="Take prebuilts from this local dist dir instead of "
289 "using fetch_artifact")
290 parser.add_argument("--skip-cls", action="store_true",
291 help="Do not create branches or git commits")
292 parser.add_argument("--upload", action="store_true",
293 help="Upload the CLs to Gerrit")
294
295 args = parser.parse_args()
296 if ((not args.build and not args.local_dist) or
297 (args.build and args.local_dist)):
298 sys.exit(parser.format_help())
299 return args
300
301
302def main():
303 """Program entry point."""
304 args = get_args()
305
306 if any(path for path in GIT_PROJECT_ROOTS if not os.path.exists(path)):
Martin Stjernholm402361b2020-12-01 20:33:22 +0000307 sys.exit("This script must be run in the root of the Android build tree.")
Martin Stjernholmdf105da2020-11-27 02:28:03 +0000308
309 install_paths = [entry.install_path for entry in install_entries]
310 install_paths_per_root = install_paths_per_git_root(
311 GIT_PROJECT_ROOTS, install_paths)
312
313 branch_name = PREBUILT_DESCR.lower().replace(" ", "-") + "-update"
314 if args.build:
315 branch_name += "-" + args.build
316
317 if not args.skip_cls:
318 start_branch(branch_name, install_paths_per_root.keys())
319
320 for git_root, subpaths in install_paths_per_root.items():
321 remove_files(git_root, subpaths)
322 for entry in install_entries:
323 install_entry(args.build, args.local_dist, entry)
324
Martin Stjernholm10bb9c52020-12-01 22:53:51 +0000325 # Postprocess the Android.bp files in the SDK snapshot to control prefer flags
326 # on the prebuilts through SOONG_CONFIG_art_module_source_build.
327 # TODO(b/174997203): Replace this with a better way to control prefer flags on
328 # Mainline module prebuilts.
329 for entry in install_entries:
330 if entry.install_unzipped:
331 bp_path = os.path.join(entry.install_path, "Android.bp")
332 if os.path.exists(bp_path):
333 rewrite_bp_for_art_module_source_build(bp_path)
334
Martin Stjernholmdf105da2020-11-27 02:28:03 +0000335 if not args.skip_cls:
336 for git_root, subpaths in install_paths_per_root.items():
337 commit(git_root, PREBUILT_DESCR, BRANCH, args.build, subpaths)
338
339 if args.upload:
340 # Don't upload all projects in a single repo upload call, because that
341 # makes it pop up an interactive editor.
342 for git_root in install_paths_per_root:
343 upload_branch(git_root, branch_name)
344
345
346if __name__ == "__main__":
347 main()