blob: e4dc3113c47e4c4a144a8f2a1a13ba276010feda [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
38from chromite.lib import sysroot_lib
39from chromite.service import sysroot
40
41
42assert sys.version_info >= (3, 8), "build_sdk_subtools uses Python 3.8 features"
43
44
45# Affects where building occurs (e.g. /build/amd64-subtools-host) if not
46# overridden by --output-dir. Note this will be a chroot.
47SUBTOOLS_OUTPUT_DIR = "amd64-subtools-host"
48
49# Version file that identifies a chroot setup as a subtools chroot.
50SUBTOOLS_CHROOT_VERSION_FILE = Path("/etc/cros_subtools_chroot_version")
51
52# Packages that the subtools builder should never rebuild. This is a superset of
53# sysroot._CRITICAL_SDK_PACKAGES.
54EXCLUDE_PACKAGES = (
55 "dev-embedded/hps-sdk",
56 "dev-lang/rust",
57 "dev-lang/go",
58 "sys-libs/glibc",
59 "sys-devel/gcc",
60 "sys-devel/binutils",
61)
62
63# Flag passed to subprocesses in chroots that might not yet be set up as a
64# subtools chroot.
65_RELAUNCH_FOR_SETUP_FLAG = "--relaunch-for-setup"
66
67
68class Options(Protocol):
69 """Protocol to formalize commandline arguments."""
70
71 build_run_config: sysroot.BuildPackagesRunConfig
72 clean: bool
73 setup_chroot: bool
74 update_packages: bool
75 relaunch_for_setup: bool
76 output_dir: Path
77 packages: List[str]
78 jobs: int
79
80 def Freeze(self) -> None:
81 pass
82
83
84def get_parser() -> commandline.ArgumentParser:
85 """Returns the cmdline argparser, populates the options and descriptions."""
86 parser = commandline.ArgumentParser(description=__doc__)
87
88 def add_conforming_bool(flag: str, default: bool, desc: str, no_desc: str):
89 """Adds a boolean argument conforming to chromite recommendations.
90
91 See go/chromite-git/+/HEAD/docs/cli-guidelines.md#Boolean-Options.
92 """
93 desc += " (DEFAULT)" if default else ""
94 no_desc += " (DEFAULT)" if not default else ""
95 dest = flag.replace("-", "_")
96 parser.add_argument(
97 f"--{flag}", action="store_true", default=default, help=desc
98 )
99 parser.add_argument(
100 f"--no-{flag}", action="store_false", dest=dest, help=no_desc
101 )
102
103 add_conforming_bool(
104 "clean",
105 False,
106 "Remove the subtools chroot and re-extract the SDK.",
107 "Re-use an existing subtools chroot.",
108 )
109
110 add_conforming_bool(
111 "setup-chroot",
112 True,
113 "Look for a newer base SDK and set it up as a subtools SDK.",
114 "Don't look for a newer base SDK and assume the chroot is setup.",
115 )
116
117 add_conforming_bool(
118 "update-packages",
119 True,
120 "Update and install packages before looking for things to export.",
121 "Only export packages already installed in the subtools SDK.",
122 )
123
124 parser.add_argument(
125 "--output-dir",
126 type=osutils.ExpandPath,
127 metavar="PATH",
128 help=f"Extract SDK and build in chroot (e.g. {SUBTOOLS_OUTPUT_DIR}).",
129 )
130
131 parser.add_argument(
132 "packages",
133 nargs="*",
134 default=["virtual/target-sdk-subtools"],
135 help="Packages to build before looking for export candidates.",
136 )
137
138 parser.add_argument(
139 "--jobs",
140 "-j",
141 type=int,
142 default=os.cpu_count(),
143 help="Number of packages to build in parallel. (Default: %(default)s)",
144 )
145
146 parser.add_argument(
147 _RELAUNCH_FOR_SETUP_FLAG,
148 action="store_true",
149 default=False,
150 help=argparse.SUPPRESS,
151 )
152
153 # TODO(b/277992359): Consider possibly relevant flags from build_packages:
154 # * --rebuild_revdeps=no: don't rebuild reverse dependencies.
155 # * --skip-toolchain-update? Likely no - the SDK is our toolchain.
156 # * --withdebugsymbols
157 # * --rebuild=no "Automatically rebuild dependencies"
158 # * --backtrack
159 # * --bazel "Use Bazel to build packages"
160
161 return parser
162
163
164def parse_args(argv: Optional[List[str]]) -> Options:
165 """Parse and validate CLI arguments."""
166
167 parser = get_parser()
168 opts: Options = parser.parse_args(argv)
169
170 # Although `BuildPackages` is not used, sharing a config allows better
171 # sharing of subcommands and concepts.
172 opts.build_run_config = sysroot.BuildPackagesRunConfig(
173 packages=opts.packages,
174 jobs=opts.jobs,
175 usepkg=False,
176 clean_build=False,
177 eclean=False,
178 rebuild_dep=False,
179 )
180 opts.Freeze()
181 return opts
182
183
184def _is_inside_subtools_chroot() -> bool:
185 """Returns True if we are inside subtools chroot."""
186 return SUBTOOLS_CHROOT_VERSION_FILE.exists()
187
188
189def _assert_inside_subtools_chroot() -> None:
190 """Die if not _is_inside_subtools_chroot()."""
191 if not _is_inside_subtools_chroot():
192 cros_build_lib.Die("Not in subtools SDK")
193
194
195def _setup_base_sdk(
196 build_target: build_target_lib.BuildTarget,
197 setup_chroot: bool,
198) -> None:
199 """SetupBoard workalike that converts a regular SDK into a subtools chroot.
200
201 Runs inside the /build/amd64-subtools-host subtools SDK chroot.
202 """
203 cros_build_lib.AssertInsideChroot()
204 cros_build_lib.AssertRootUser()
205
206 # "Convert" the SDK into a subtools SDK.
207 if not _is_inside_subtools_chroot():
208 # Copy the sentinel file that chromite uses to indicate the chroot's
209 # duality. The file is copied (not moved) so that other chromite tooling
210 # continues to work.
211 shutil.copy(
212 cros_sdk_lib.CHROOT_VERSION_FILE, SUBTOOLS_CHROOT_VERSION_FILE
213 )
214
215 if setup_chroot:
216 # TODO(b/277992359): Additional setup here, e.g., packages, base layout.
217 logging.info("Setting up subtools SDK in %s.", build_target.root)
218
219
220def _run_system_emerge(
221 emerge_cmd: List[Union[str, Path]],
222 extra_env: Dict[str, str],
223 use_goma: bool,
224 use_remoteexec: bool,
225 reason: str,
226) -> None:
227 """Runs an emerge command, updating the live system."""
228 extra_env = extra_env.copy()
229 with osutils.TempDir() as tempdir:
230 extra_env[constants.CROS_METRICS_DIR_ENVVAR] = tempdir
231 with sysroot.RemoteExecution(use_goma, use_remoteexec):
232 logging.info("Merging %s now.", reason)
233 try:
234 # TODO(b/277992359): Bazel.
235 cros_build_lib.sudo_run(
236 emerge_cmd,
237 preserve_env=True,
238 extra_env=extra_env,
239 )
240 logging.info("Merging %s complete.", reason)
241 except cros_build_lib.RunCommandError as e:
242 failed_pkgs = portage_util.ParseDieHookStatusFile(tempdir)
243 logging.error("Merging %s failed on %s", reason, failed_pkgs)
244 raise sysroot_lib.PackageInstallError(
245 f"Merging {reason} failed",
246 e.result,
247 exception=e,
248 packages=failed_pkgs,
249 )
250
251
252def _build_sdk_packages(config: sysroot.BuildPackagesRunConfig) -> None:
253 """The BuildPackages workalike for installing into the staging SDK."""
254 _assert_inside_subtools_chroot()
255 cros_build_lib.AssertNonRootUser()
256
257 try:
258 # sysroot.BuildPackages can't (yet?) be used here, because it _only_
259 # supports cross-compilation. SDK package management is currently all
260 # handled by src/scripts/sdk_lib/make_chroot.sh (b/191307774).
261
262 emerge = [constants.CHROMITE_BIN_DIR / "parallel_emerge"]
263 extra_env = config.GetExtraEnv()
264 emerge_flags = config.GetEmergeFlags()
265 exclude_pkgs = " ".join(EXCLUDE_PACKAGES)
266 emerge_flags.extend(
267 [
268 f"--useoldpkg-atoms={exclude_pkgs}",
269 f"--rebuild-exclude={exclude_pkgs}",
270 ]
271 )
272 cmd = emerge + emerge_flags + config.GetPackages()
273 _run_system_emerge(
274 cmd,
275 extra_env,
276 config.use_goma,
277 config.use_remoteexec,
278 reason="subtools builder SDK packages",
279 )
280
281 except sysroot_lib.PackageInstallError as e:
282 cros_build_lib.Die(e)
283
284
285def main(argv: Optional[List[str]] = None) -> Optional[int]:
286 opts = parse_args(argv)
287 return build_sdk_subtools(opts, argv if argv else [])
288
289
290def build_sdk_subtools(opts: Options, argv: List[str]) -> int:
291 """Executes SDK subtools builder steps according to `opts`."""
292 # BuildTarget needs a str, but opts.output_dir is osutils.ExpandPath.
293 custom_output_dir = str(opts.output_dir) if opts.output_dir else None
294 build_target = build_target_lib.BuildTarget(
295 name=SUBTOOLS_OUTPUT_DIR, build_root=custom_output_dir
296 )
297
298 # If the process is in the subtools chroot, we must assume it's already set
299 # up (we are in it). So start building.
300 if _is_inside_subtools_chroot() and not opts.relaunch_for_setup:
301 if opts.update_packages:
302 _build_sdk_packages(opts.build_run_config)
303 return 0
304
305 # Otherwise, we have the option to set it up. Then restart inside it. The
306 # setup runs `cros_sdk` to get a base SDK, creates an SDK subprocess to set
307 # it up as a subtools SDK, then restarts inside the subtools SDK.
308 if cros_build_lib.IsInsideChroot():
309 if opts.relaunch_for_setup:
310 # This is the subprocess of the not-in-chroot path used to convert
311 # the base SDK to a subtools SDK (within the chroot).
312 _setup_base_sdk(build_target, opts.setup_chroot)
313 return 0
314 else:
315 cros_build_lib.Die(
316 "build_sdk_subtools must be run outside the chroot."
317 )
318
319 logging.info("Initializing subtools builder in %s", build_target.root)
320 subtools_chroot = os.path.join(
321 constants.DEFAULT_CHROOT_PATH, build_target.root.lstrip("/")
322 )
323 chroot_args = ["--chroot", subtools_chroot]
324
325 if opts.setup_chroot:
326 # Get an SDK. TODO(b/277992359):
327 # - Fetch an SDK version pinned by pupr rather than the default.
328 # - Should this use cros_sdk_lib directly?
329
330 # Pass "--skip-chroot-upgrade": the SDK should initially be used
331 # "as-is", but later steps may upgrade packages in the subtools deptree.
332 cros_sdk_args = ["--create", "--skip-chroot-upgrade"]
333 cros_sdk_args += ["--delete"] if opts.clean else []
334 cros_sdk = cros_build_lib.run(
335 ["cros_sdk"] + chroot_args + cros_sdk_args,
336 check=False,
337 cwd=constants.SOURCE_ROOT,
338 )
339 if cros_sdk.returncode != 0:
340 return cros_sdk.returncode
341
342 # Invoke `_setup_base_sdk()` inside the SDK.
343 setup_base_sdk = cros_build_lib.sudo_run(
344 ["build_sdk_subtools"] + argv + [_RELAUNCH_FOR_SETUP_FLAG],
345 check=False,
346 enter_chroot=True,
347 chroot_args=chroot_args,
348 cwd=constants.SOURCE_ROOT,
349 )
350 if setup_base_sdk.returncode != 0:
351 return setup_base_sdk.returncode
352
353 raise commandline.ChrootRequiredError(
354 ["build_sdk_subtools"] + argv, chroot_args=chroot_args
355 )