blob: 08a5403df3553185c47431e414035b5b6541bf75 [file] [log] [blame]
Orion Hodson15826e52021-04-22 18:58:57 +01001#!/usr/bin/env -S python3 -B
Martin Stjernholmee6d2352021-02-07 21:32:27 +00002#
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"""Downloads ART Module prebuilts and creates CLs to update them in git."""
18
19import argparse
20import collections
21import os
22import re
23import 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
38PACKAGE_PATH = "packages/modules/ArtPrebuilt"
39
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
48SCRIPT_PATH = PACKAGE_PATH + "/update-art-module-prebuilts.py"
49
50
51InstallEntry = collections.namedtuple("InstallEntry", [
52 # Artifact path in the build, passed to fetch_target
53 "source_path",
54 # Local install path
55 "install_path",
Martin Stjernholm2a980db2021-02-07 21:49:39 +000056 # True if this is a module SDK, to be skipped by --skip-module-sdk.
57 "module_sdk",
Martin Stjernholmee6d2352021-02-07 21:32:27 +000058 # True if the entry is a zip file that should be unzipped to install_path
59 "install_unzipped",
60])
61
62
63def install_apex_entries(apex_name):
64 res = []
65 for arch in ARCHES:
66 res.append(InstallEntry(
67 os.path.join(arch, apex_name + ".apex"),
68 os.path.join(PACKAGE_PATH, apex_name + "-" + arch + ".apex"),
Martin Stjernholm2a980db2021-02-07 21:49:39 +000069 module_sdk=False,
Martin Stjernholmee6d2352021-02-07 21:32:27 +000070 install_unzipped=False))
71 return res
72
73
74def install_sdk_entries(mainline_sdk_name, sdk_dir):
75 return [InstallEntry(
76 os.path.join("mainline-sdks",
77 mainline_sdk_name + "-" + SDK_VERSION + ".zip"),
78 os.path.join(SDK_PATH, SDK_VERSION, sdk_dir),
Martin Stjernholm2a980db2021-02-07 21:49:39 +000079 module_sdk=True,
Martin Stjernholmee6d2352021-02-07 21:32:27 +000080 install_unzipped=True)]
81
82
83install_entries = (
84 install_apex_entries("com.android.art") +
85 install_apex_entries("com.android.art.debug") +
86 install_sdk_entries("art-module-sdk", "sdk") +
87 install_sdk_entries("art-module-host-exports", "host-exports") +
88 install_sdk_entries("art-module-test-exports", "test-exports")
89)
90
91
92def rewrite_bp_for_art_module_source_build(bp_path):
93 """Rewrites an Android.bp file to conditionally prefer prebuilts."""
94 print("Rewriting {} for SOONG_CONFIG_art_module_source_build use."
95 .format(bp_path))
96 bp_file = open(bp_path, "r+")
97
98 # TODO(b/174997203): Remove this when we have a proper way to control prefer
99 # flags in Mainline modules.
100
101 header_lines = []
102 for line in bp_file:
103 line = line.rstrip("\n")
104 if not line.startswith("//"):
105 break
106 header_lines.append(line)
107
108 art_module_types = set()
109
110 content_lines = []
111 for line in bp_file:
112 line = line.rstrip("\n")
113 module_header = re.match("([a-z0-9_]+) +{$", line)
114 if not module_header:
115 content_lines.append(line)
116 else:
117 # Iterate over one Soong module.
118 module_start = line
119 soong_config_clause = False
120 module_content = []
121
122 for module_line in bp_file:
123 module_line = module_line.rstrip("\n")
124 if module_line == "}":
125 break
126 if module_line == " prefer: false,":
127 module_content.extend([
128 (" // Do not prefer prebuilt if "
129 "SOONG_CONFIG_art_module_source_build is true."),
130 " prefer: true,",
131 " soong_config_variables: {",
132 " source_build: {",
133 " prefer: false,",
134 " },",
135 " },"])
136 soong_config_clause = True
137 else:
138 module_content.append(module_line)
139
140 if soong_config_clause:
141 module_type = "art_prebuilt_" + module_header.group(1)
142 module_start = module_type + " {"
143 art_module_types.add(module_type)
144
145 content_lines.append(module_start)
146 content_lines.extend(module_content)
147 content_lines.append("}")
148
149 header_lines.extend(
150 ["",
151 "// Soong config variable stanza added by {}.".format(SCRIPT_PATH),
152 "soong_config_module_type_import {",
153 " from: \"prebuilts/module_sdk/art/SoongConfig.bp\",",
154 " module_types: ["] +
155 [" \"{}\",".format(art_module)
156 for art_module in sorted(art_module_types)] +
157 [" ],",
158 "}",
159 ""])
160
161 bp_file.seek(0)
162 bp_file.truncate()
163 bp_file.write("\n".join(header_lines + content_lines))
164 bp_file.close()
165
166
167def check_call(cmd, **kwargs):
168 """Proxy for subprocess.check_call with logging."""
169 msg = " ".join(cmd) if isinstance(cmd, list) else cmd
170 if "cwd" in kwargs:
171 msg = "In " + kwargs["cwd"] + ": " + msg
172 print(msg)
173 subprocess.check_call(cmd, **kwargs)
174
175
176def fetch_artifact(branch, target, build, fetch_pattern, local_dir):
177 """Fetches artifact from the build server."""
178 fetch_artifact_path = "/google/data/ro/projects/android/fetch_artifact"
179 cmd = [fetch_artifact_path, "--branch", branch, "--target", target,
180 "--bid", build, fetch_pattern]
181 check_call(cmd, cwd=local_dir)
182
183
184def start_branch(branch_name, git_dirs):
185 """Creates a new repo branch in the given projects."""
186 check_call(["repo", "start", branch_name] + git_dirs)
187 # In case the branch already exists we reset it to upstream, to get a clean
188 # update CL.
189 for git_dir in git_dirs:
190 check_call(["git", "reset", "--hard", "@{upstream}"], cwd=git_dir)
191
192
193def upload_branch(git_root, branch_name):
194 """Uploads the CLs in the given branch in the given project."""
195 # Set the branch as topic to bundle with the CLs in other git projects (if
196 # any).
197 check_call(["repo", "upload", "-t", "--br=" + branch_name, git_root])
198
199
200def remove_files(git_root, subpaths, stage_removals):
201 """Removes files in the work tree, optionally staging them in git."""
202 if stage_removals:
203 check_call(["git", "rm", "-qrf", "--ignore-unmatch"] + subpaths, cwd=git_root)
204 # Need a plain rm afterwards even if git rm was executed, because git won't
205 # remove directories if they have non-git files in them.
206 check_call(["rm", "-rf"] + subpaths, cwd=git_root)
207
208
Martin Stjernholm90784f32021-02-18 13:25:13 +0000209def commit(git_root, prebuilt_descr, branch, target, build, add_paths, bug_number):
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000210 """Commits the new prebuilts."""
211 check_call(["git", "add"] + add_paths, cwd=git_root)
212
213 if build:
214 message = (
215 "Update {prebuilt_descr} prebuilts to build {build}.\n\n"
Martin Stjernholm90784f32021-02-18 13:25:13 +0000216 "Taken from branch {branch}, target {target}."
217 .format(prebuilt_descr=prebuilt_descr, branch=branch, target=target,
218 build=build))
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000219 else:
220 message = (
221 "DO NOT SUBMIT: Update {prebuilt_descr} prebuilts from local build."
222 .format(prebuilt_descr=prebuilt_descr))
223 message += ("\n\nCL prepared by {}."
224 "\n\nTest: Presubmits".format(SCRIPT_PATH))
Martin Stjernholm90784f32021-02-18 13:25:13 +0000225 if bug_number:
226 message += ("\nBug: {}".format(bug_number))
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000227 msg_fd, msg_path = tempfile.mkstemp()
228 with os.fdopen(msg_fd, "w") as f:
229 f.write(message)
230
231 # Do a diff first to skip the commit without error if there are no changes to
232 # commit.
233 check_call("git diff-index --quiet --cached HEAD -- || "
234 "git commit -F " + msg_path, shell=True, cwd=git_root)
235 os.unlink(msg_path)
236
237
238def install_entry(build, local_dist, entry):
239 """Installs one file specified by entry."""
240
241 install_dir, install_file = os.path.split(entry.install_path)
242 if install_dir and not os.path.exists(install_dir):
243 os.makedirs(install_dir)
244
245 if build:
246 fetch_artifact(BRANCH, TARGET, build, entry.source_path, install_dir)
247 else:
248 check_call(["cp", os.path.join(local_dist, entry.source_path), install_dir])
249 source_file = os.path.basename(entry.source_path)
250
251 if entry.install_unzipped:
252 check_call(["mkdir", install_file], cwd=install_dir)
253 # Add -DD to not extract timestamps that may confuse the build system.
254 check_call(["unzip", "-DD", source_file, "-d", install_file],
255 cwd=install_dir)
256 check_call(["rm", source_file], cwd=install_dir)
257
258 elif source_file != install_file:
259 check_call(["mv", source_file, install_file], cwd=install_dir)
260
261
262def install_paths_per_git_root(roots, paths):
263 """Partitions the given paths into subpaths within the given roots.
264
265 Args:
266 roots: List of root paths.
267 paths: List of paths relative to the same directory as the root paths.
268
269 Returns:
270 A dict mapping each root to the subpaths under it. It's an error if some
271 path doesn't go into any root.
272 """
273 res = collections.defaultdict(list)
274 for path in paths:
275 found = False
276 for root in roots:
277 if path.startswith(root + "/"):
278 res[root].append(path[len(root) + 1:])
279 found = True
280 break
281 if not found:
282 sys.exit("Install path {} is not in any of the git roots: {}"
283 .format(path, " ".join(roots)))
284 return res
285
286
287def get_args():
288 """Parses and returns command line arguments."""
289 parser = argparse.ArgumentParser(
290 epilog="Either --build or --local-dist is required.")
291
292 parser.add_argument("--build", metavar="NUMBER",
293 help="Build number to fetch from branch {}, target {}"
294 .format(BRANCH, TARGET))
295 parser.add_argument("--local-dist", metavar="PATH",
296 help="Take prebuilts from this local dist dir instead of "
297 "using fetch_artifact")
Martin Stjernholm2a980db2021-02-07 21:49:39 +0000298 parser.add_argument("--skip-apex", action="store_true",
299 help="Do not fetch .apex files.")
300 parser.add_argument("--skip-module-sdk", action="store_true",
301 help="Do not fetch and unpack sdk and module_export zips.")
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000302 parser.add_argument("--skip-cls", action="store_true",
303 help="Do not create branches or git commits")
Martin Stjernholm90784f32021-02-18 13:25:13 +0000304 parser.add_argument("--bug", metavar="NUMBER",
305 help="Add a 'Bug' line with this number to commit "
306 "messages.")
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000307 parser.add_argument("--upload", action="store_true",
308 help="Upload the CLs to Gerrit")
309
310 args = parser.parse_args()
311 if ((not args.build and not args.local_dist) or
312 (args.build and args.local_dist)):
313 sys.exit(parser.format_help())
314 return args
315
316
317def main():
318 """Program entry point."""
319 args = get_args()
320
321 if any(path for path in GIT_PROJECT_ROOTS if not os.path.exists(path)):
322 sys.exit("This script must be run in the root of the Android build tree.")
323
Martin Stjernholm2a980db2021-02-07 21:49:39 +0000324 entries = install_entries
325 if args.skip_apex:
326 entries = [entry for entry in entries if entry.module_sdk]
327 if args.skip_module_sdk:
328 entries = [entry for entry in entries if not entry.module_sdk]
329 if not entries:
330 sys.exit("Both APEXes and SDKs skipped - nothing to do.")
331
332 install_paths = [entry.install_path for entry in entries]
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000333 install_paths_per_root = install_paths_per_git_root(
334 GIT_PROJECT_ROOTS, install_paths)
335
336 branch_name = PREBUILT_DESCR.lower().replace(" ", "-") + "-update"
337 if args.build:
338 branch_name += "-" + args.build
339
340 if not args.skip_cls:
Orion Hodson15826e52021-04-22 18:58:57 +0100341 git_paths = list(install_paths_per_root.keys())
342 start_branch(branch_name, git_paths)
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000343
344 for git_root, subpaths in install_paths_per_root.items():
345 remove_files(git_root, subpaths, not args.skip_cls)
Martin Stjernholm2a980db2021-02-07 21:49:39 +0000346 for entry in entries:
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000347 install_entry(args.build, args.local_dist, entry)
348
349 # Postprocess the Android.bp files in the SDK snapshot to control prefer flags
350 # on the prebuilts through SOONG_CONFIG_art_module_source_build.
351 # TODO(b/174997203): Replace this with a better way to control prefer flags on
352 # Mainline module prebuilts.
Martin Stjernholm2a980db2021-02-07 21:49:39 +0000353 for entry in entries:
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000354 if entry.install_unzipped:
355 bp_path = os.path.join(entry.install_path, "Android.bp")
356 if os.path.exists(bp_path):
357 rewrite_bp_for_art_module_source_build(bp_path)
358
359 if not args.skip_cls:
360 for git_root, subpaths in install_paths_per_root.items():
Martin Stjernholm90784f32021-02-18 13:25:13 +0000361 commit(git_root, PREBUILT_DESCR, BRANCH, TARGET, args.build, subpaths,
362 args.bug)
Martin Stjernholmee6d2352021-02-07 21:32:27 +0000363
364 if args.upload:
365 # Don't upload all projects in a single repo upload call, because that
366 # makes it pop up an interactive editor.
367 for git_root in install_paths_per_root:
368 upload_branch(git_root, branch_name)
369
370
371if __name__ == "__main__":
372 main()