blob: 092c97a77a6d8c648453eae52d862080264a01c0 [file] [log] [blame]
Trent Apted7d2777b2023-06-29 13:35:03 +10001# Copyright 2023 The ChromiumOS Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""build_sdk_subtools rebuilds binary packages exported by the subtools builder.
6
7The build_sdk_subtools process takes (copies) an amd64-host base SDK, compiles
8and installs additional packages needed by subtools, then creates relocatable
9binary subtool bundles that can be consumed by other build hosts and developer
10machines.
11
12If build_sdk_subtools has already been invoked for the provided chroot, all
13non-toolchain packages in the subtools deptree that have updated revisions or
14changed USE flags will be rebuilt, along with reverse dependencies.
15
16Packages (e.g. an ebuild) provide manifests that describes how files, once
17installed, are to be bundled and exported.
18
19If packages are specified in the command line, only consider the deptree from
20those specific packages rather than all of virtual/target-sdk-subtools.
21"""
22
23import argparse
24import logging
25import os
26from pathlib import Path
27import shutil
28import sys
29from typing import Dict, List, Optional, Protocol, Union
30
31from chromite.lib import build_target_lib
32from chromite.lib import commandline
33from chromite.lib import constants
34from chromite.lib import cros_build_lib
35from chromite.lib import cros_sdk_lib
36from chromite.lib import osutils
37from chromite.lib import portage_util
Trent Apted16007b82023-07-06 14:51:57 +100038from chromite.lib import subtool_lib
Trent Apted7d2777b2023-06-29 13:35:03 +100039from chromite.lib import sysroot_lib
40from chromite.service import sysroot
41
42
43assert sys.version_info >= (3, 8), "build_sdk_subtools uses Python 3.8 features"
44
45
46# Affects where building occurs (e.g. /build/amd64-subtools-host) if not
47# overridden by --output-dir. Note this will be a chroot.
48SUBTOOLS_OUTPUT_DIR = "amd64-subtools-host"
49
50# Version file that identifies a chroot setup as a subtools chroot.
51SUBTOOLS_CHROOT_VERSION_FILE = Path("/etc/cros_subtools_chroot_version")
52
53# Packages that the subtools builder should never rebuild. This is a superset of
54# sysroot._CRITICAL_SDK_PACKAGES.
55EXCLUDE_PACKAGES = (
56 "dev-embedded/hps-sdk",
57 "dev-lang/rust",
58 "dev-lang/go",
59 "sys-libs/glibc",
60 "sys-devel/gcc",
61 "sys-devel/binutils",
Trent Apted16007b82023-07-06 14:51:57 +100062 "sys-kernel/linux-headers",
Trent Apted7d2777b2023-06-29 13:35:03 +100063)
64
Trent Apted16007b82023-07-06 14:51:57 +100065# Path in subtools chroot that holds export package manifests.
66SUBTOOLS_EXPORTS_CONFIG_DIR = Path("/etc/cros/sdk-packages.d")
67
68# Path where subtools will be bundled.
69SUBTOOLS_BUNDLE_WORK_DIR = Path("/var/tmp/cros-subtools")
70
Trent Apted7d2777b2023-06-29 13:35:03 +100071# Flag passed to subprocesses in chroots that might not yet be set up as a
72# subtools chroot.
73_RELAUNCH_FOR_SETUP_FLAG = "--relaunch-for-setup"
74
Trent Apted16007b82023-07-06 14:51:57 +100075# Used to populate a test manifest in /etc/cros/standalone-packages.d/.
76# Ebuilds will later be updated to provide these files instead.
77_TEST_PACKAGE = SUBTOOLS_EXPORTS_CONFIG_DIR / "shellcheck.textproto"
78_TEST_PACKAGE_CONTENTS = """\
79# proto-file: chromiumos/build/api/subtools.proto
80# proto-message: chromiumos.build.api.SubtoolPackage
81name: "shellcheck"
82type: EXPORT_CIPD
83max_files: 2
84paths: [{
85 input: "/usr/bin/shellcheck"
86},{
87 input: "/usr/share/doc/*/LICENSE.gz"
88 ebuild_filter: "dev-util/shellcheck"
89}]
90"""
91
Trent Apted7d2777b2023-06-29 13:35:03 +100092
93class Options(Protocol):
94 """Protocol to formalize commandline arguments."""
95
96 build_run_config: sysroot.BuildPackagesRunConfig
97 clean: bool
98 setup_chroot: bool
99 update_packages: bool
100 relaunch_for_setup: bool
101 output_dir: Path
102 packages: List[str]
103 jobs: int
104
105 def Freeze(self) -> None:
106 pass
107
108
109def get_parser() -> commandline.ArgumentParser:
110 """Returns the cmdline argparser, populates the options and descriptions."""
111 parser = commandline.ArgumentParser(description=__doc__)
112
Trent Apted9b8b13d2023-08-04 16:49:04 +1000113 parser.add_bool_argument(
114 "--clean",
Trent Apted7d2777b2023-06-29 13:35:03 +1000115 False,
116 "Remove the subtools chroot and re-extract the SDK.",
117 "Re-use an existing subtools chroot.",
118 )
119
Trent Apted9b8b13d2023-08-04 16:49:04 +1000120 parser.add_bool_argument(
121 "--setup-chroot",
Trent Apted7d2777b2023-06-29 13:35:03 +1000122 True,
123 "Look for a newer base SDK and set it up as a subtools SDK.",
124 "Don't look for a newer base SDK and assume the chroot is setup.",
125 )
126
Trent Apted9b8b13d2023-08-04 16:49:04 +1000127 parser.add_bool_argument(
128 "--update-packages",
Trent Apted7d2777b2023-06-29 13:35:03 +1000129 True,
130 "Update and install packages before looking for things to export.",
131 "Only export packages already installed in the subtools SDK.",
132 )
133
134 parser.add_argument(
135 "--output-dir",
136 type=osutils.ExpandPath,
137 metavar="PATH",
138 help=f"Extract SDK and build in chroot (e.g. {SUBTOOLS_OUTPUT_DIR}).",
139 )
140
141 parser.add_argument(
142 "packages",
143 nargs="*",
144 default=["virtual/target-sdk-subtools"],
145 help="Packages to build before looking for export candidates.",
146 )
147
148 parser.add_argument(
149 "--jobs",
150 "-j",
151 type=int,
152 default=os.cpu_count(),
153 help="Number of packages to build in parallel. (Default: %(default)s)",
154 )
155
156 parser.add_argument(
157 _RELAUNCH_FOR_SETUP_FLAG,
158 action="store_true",
159 default=False,
160 help=argparse.SUPPRESS,
161 )
162
163 # TODO(b/277992359): Consider possibly relevant flags from build_packages:
164 # * --rebuild_revdeps=no: don't rebuild reverse dependencies.
165 # * --skip-toolchain-update? Likely no - the SDK is our toolchain.
166 # * --withdebugsymbols
167 # * --rebuild=no "Automatically rebuild dependencies"
168 # * --backtrack
169 # * --bazel "Use Bazel to build packages"
170
171 return parser
172
173
174def parse_args(argv: Optional[List[str]]) -> Options:
175 """Parse and validate CLI arguments."""
176
177 parser = get_parser()
178 opts: Options = parser.parse_args(argv)
179
180 # Although `BuildPackages` is not used, sharing a config allows better
181 # sharing of subcommands and concepts.
182 opts.build_run_config = sysroot.BuildPackagesRunConfig(
183 packages=opts.packages,
184 jobs=opts.jobs,
185 usepkg=False,
186 clean_build=False,
187 eclean=False,
188 rebuild_dep=False,
189 )
190 opts.Freeze()
191 return opts
192
193
194def _is_inside_subtools_chroot() -> bool:
195 """Returns True if we are inside subtools chroot."""
196 return SUBTOOLS_CHROOT_VERSION_FILE.exists()
197
198
199def _assert_inside_subtools_chroot() -> None:
200 """Die if not _is_inside_subtools_chroot()."""
201 if not _is_inside_subtools_chroot():
202 cros_build_lib.Die("Not in subtools SDK")
203
204
205def _setup_base_sdk(
206 build_target: build_target_lib.BuildTarget,
207 setup_chroot: bool,
208) -> None:
209 """SetupBoard workalike that converts a regular SDK into a subtools chroot.
210
211 Runs inside the /build/amd64-subtools-host subtools SDK chroot.
212 """
213 cros_build_lib.AssertInsideChroot()
214 cros_build_lib.AssertRootUser()
215
216 # "Convert" the SDK into a subtools SDK.
217 if not _is_inside_subtools_chroot():
218 # Copy the sentinel file that chromite uses to indicate the chroot's
219 # duality. The file is copied (not moved) so that other chromite tooling
220 # continues to work.
221 shutil.copy(
222 cros_sdk_lib.CHROOT_VERSION_FILE, SUBTOOLS_CHROOT_VERSION_FILE
223 )
224
225 if setup_chroot:
Trent Apted7d2777b2023-06-29 13:35:03 +1000226 logging.info("Setting up subtools SDK in %s.", build_target.root)
Trent Apted16007b82023-07-06 14:51:57 +1000227 osutils.SafeMakedirs(SUBTOOLS_EXPORTS_CONFIG_DIR)
228 _TEST_PACKAGE.write_text(_TEST_PACKAGE_CONTENTS, encoding="utf-8")
Trent Apted7d2777b2023-06-29 13:35:03 +1000229
230
231def _run_system_emerge(
232 emerge_cmd: List[Union[str, Path]],
233 extra_env: Dict[str, str],
234 use_goma: bool,
235 use_remoteexec: bool,
236 reason: str,
237) -> None:
238 """Runs an emerge command, updating the live system."""
239 extra_env = extra_env.copy()
240 with osutils.TempDir() as tempdir:
241 extra_env[constants.CROS_METRICS_DIR_ENVVAR] = tempdir
242 with sysroot.RemoteExecution(use_goma, use_remoteexec):
243 logging.info("Merging %s now.", reason)
244 try:
245 # TODO(b/277992359): Bazel.
246 cros_build_lib.sudo_run(
247 emerge_cmd,
248 preserve_env=True,
249 extra_env=extra_env,
250 )
251 logging.info("Merging %s complete.", reason)
252 except cros_build_lib.RunCommandError as e:
253 failed_pkgs = portage_util.ParseDieHookStatusFile(tempdir)
254 logging.error("Merging %s failed on %s", reason, failed_pkgs)
255 raise sysroot_lib.PackageInstallError(
256 f"Merging {reason} failed",
257 e.result,
258 exception=e,
259 packages=failed_pkgs,
260 )
261
262
263def _build_sdk_packages(config: sysroot.BuildPackagesRunConfig) -> None:
264 """The BuildPackages workalike for installing into the staging SDK."""
265 _assert_inside_subtools_chroot()
266 cros_build_lib.AssertNonRootUser()
267
268 try:
269 # sysroot.BuildPackages can't (yet?) be used here, because it _only_
270 # supports cross-compilation. SDK package management is currently all
271 # handled by src/scripts/sdk_lib/make_chroot.sh (b/191307774).
272
273 emerge = [constants.CHROMITE_BIN_DIR / "parallel_emerge"]
274 extra_env = config.GetExtraEnv()
275 emerge_flags = config.GetEmergeFlags()
276 exclude_pkgs = " ".join(EXCLUDE_PACKAGES)
277 emerge_flags.extend(
278 [
279 f"--useoldpkg-atoms={exclude_pkgs}",
280 f"--rebuild-exclude={exclude_pkgs}",
281 ]
282 )
283 cmd = emerge + emerge_flags + config.GetPackages()
284 _run_system_emerge(
285 cmd,
286 extra_env,
287 config.use_goma,
288 config.use_remoteexec,
289 reason="subtools builder SDK packages",
290 )
291
292 except sysroot_lib.PackageInstallError as e:
293 cros_build_lib.Die(e)
294
295
Trent Apted16007b82023-07-06 14:51:57 +1000296def _run_inside_subtools_chroot(opts: Options) -> None:
297 """Steps that build_sdk_subtools performs once it is in its chroot."""
298 _assert_inside_subtools_chroot()
299
300 if opts.update_packages:
301 _build_sdk_packages(opts.build_run_config)
302
303 subtools = subtool_lib.InstalledSubtools(
304 config_dir=SUBTOOLS_EXPORTS_CONFIG_DIR,
305 work_root=SUBTOOLS_BUNDLE_WORK_DIR,
306 )
307 subtools.bundle_all()
308 subtools.export_all()
309
310
Trent Apted7d2777b2023-06-29 13:35:03 +1000311def main(argv: Optional[List[str]] = None) -> Optional[int]:
312 opts = parse_args(argv)
313 return build_sdk_subtools(opts, argv if argv else [])
314
315
316def build_sdk_subtools(opts: Options, argv: List[str]) -> int:
317 """Executes SDK subtools builder steps according to `opts`."""
318 # BuildTarget needs a str, but opts.output_dir is osutils.ExpandPath.
319 custom_output_dir = str(opts.output_dir) if opts.output_dir else None
320 build_target = build_target_lib.BuildTarget(
321 name=SUBTOOLS_OUTPUT_DIR, build_root=custom_output_dir
322 )
323
324 # If the process is in the subtools chroot, we must assume it's already set
325 # up (we are in it). So start building.
326 if _is_inside_subtools_chroot() and not opts.relaunch_for_setup:
Trent Apted16007b82023-07-06 14:51:57 +1000327 _run_inside_subtools_chroot(opts)
Trent Apted7d2777b2023-06-29 13:35:03 +1000328 return 0
329
330 # Otherwise, we have the option to set it up. Then restart inside it. The
331 # setup runs `cros_sdk` to get a base SDK, creates an SDK subprocess to set
332 # it up as a subtools SDK, then restarts inside the subtools SDK.
333 if cros_build_lib.IsInsideChroot():
334 if opts.relaunch_for_setup:
335 # This is the subprocess of the not-in-chroot path used to convert
336 # the base SDK to a subtools SDK (within the chroot).
337 _setup_base_sdk(build_target, opts.setup_chroot)
338 return 0
339 else:
340 cros_build_lib.Die(
341 "build_sdk_subtools must be run outside the chroot."
342 )
343
344 logging.info("Initializing subtools builder in %s", build_target.root)
345 subtools_chroot = os.path.join(
346 constants.DEFAULT_CHROOT_PATH, build_target.root.lstrip("/")
347 )
348 chroot_args = ["--chroot", subtools_chroot]
349
350 if opts.setup_chroot:
351 # Get an SDK. TODO(b/277992359):
352 # - Fetch an SDK version pinned by pupr rather than the default.
353 # - Should this use cros_sdk_lib directly?
354
355 # Pass "--skip-chroot-upgrade": the SDK should initially be used
356 # "as-is", but later steps may upgrade packages in the subtools deptree.
357 cros_sdk_args = ["--create", "--skip-chroot-upgrade"]
358 cros_sdk_args += ["--delete"] if opts.clean else []
359 cros_sdk = cros_build_lib.run(
360 ["cros_sdk"] + chroot_args + cros_sdk_args,
361 check=False,
362 cwd=constants.SOURCE_ROOT,
363 )
364 if cros_sdk.returncode != 0:
365 return cros_sdk.returncode
366
367 # Invoke `_setup_base_sdk()` inside the SDK.
368 setup_base_sdk = cros_build_lib.sudo_run(
369 ["build_sdk_subtools"] + argv + [_RELAUNCH_FOR_SETUP_FLAG],
370 check=False,
371 enter_chroot=True,
372 chroot_args=chroot_args,
373 cwd=constants.SOURCE_ROOT,
374 )
375 if setup_base_sdk.returncode != 0:
376 return setup_base_sdk.returncode
377
378 raise commandline.ChrootRequiredError(
379 ["build_sdk_subtools"] + argv, chroot_args=chroot_args
380 )