blob: 50d04d4e760ef9fbdfba27dd790f4108744685a5 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2015 The ChromiumOS Authors
David Pursell9476bf42015-03-30 13:34:27 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Alex Kleinaaddc932020-01-30 15:02:24 -07005"""Deploy packages onto a target device.
6
7Integration tests for this file can be found at cli/cros/tests/cros_vm_tests.py.
8See that file for more information.
9"""
David Pursell9476bf42015-03-30 13:34:27 -070010
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040011from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -070012
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070013import bz2
David Pursell9476bf42015-03-30 13:34:27 -070014import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070015import functools
David Pursell9476bf42015-03-30 13:34:27 -070016import json
Chris McDonald14ac61d2021-07-21 11:49:56 -060017import logging
David Pursell9476bf42015-03-30 13:34:27 -070018import os
Jae Hoon Kim2376e142022-09-03 00:18:58 +000019from pathlib import Path
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070020import tempfile
Tim Baine4a783b2023-04-21 20:05:51 +000021from typing import Dict, List, Set, Tuple
David Pursell9476bf42015-03-30 13:34:27 -070022
Ralph Nathane01ccf12015-04-16 10:40:32 -070023from chromite.cli import command
Mike Frysinger06a51c82021-04-06 11:39:17 -040024from chromite.lib import build_target_lib
Ram Chandrasekar56152ec2021-11-22 17:10:41 +000025from chromite.lib import constants
David Pursell9476bf42015-03-30 13:34:27 -070026from chromite.lib import cros_build_lib
Alex Klein18a60af2020-06-11 12:08:47 -060027from chromite.lib import dlc_lib
Ralph Nathane01ccf12015-04-16 10:40:32 -070028from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070029from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070030from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070031from chromite.lib import remote_access
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +090032from chromite.lib import workon_helper
Alex Klein18a60af2020-06-11 12:08:47 -060033from chromite.lib.parser import package_info
34
Chris McDonald14ac61d2021-07-21 11:49:56 -060035
David Pursell9476bf42015-03-30 13:34:27 -070036try:
Alex Klein1699fab2022-09-08 08:46:06 -060037 import portage
David Pursell9476bf42015-03-30 13:34:27 -070038except ImportError:
Alex Klein1699fab2022-09-08 08:46:06 -060039 if cros_build_lib.IsInsideChroot():
40 raise
David Pursell9476bf42015-03-30 13:34:27 -070041
42
Alex Klein1699fab2022-09-08 08:46:06 -060043_DEVICE_BASE_DIR = "/usr/local/tmp/cros-deploy"
David Pursell9476bf42015-03-30 13:34:27 -070044# This is defined in src/platform/dev/builder.py
Alex Klein1699fab2022-09-08 08:46:06 -060045_STRIPPED_PACKAGES_DIR = "stripped-packages"
David Pursell9476bf42015-03-30 13:34:27 -070046
47_MAX_UPDATES_NUM = 10
48_MAX_UPDATES_WARNING = (
Alex Klein1699fab2022-09-08 08:46:06 -060049 "You are about to update a large number of installed packages, which "
50 "might take a long time, fail midway, or leave the target in an "
51 "inconsistent state. It is highly recommended that you flash a new image "
52 "instead."
53)
David Pursell9476bf42015-03-30 13:34:27 -070054
Alex Klein1699fab2022-09-08 08:46:06 -060055_DLC_ID = "DLC_ID"
56_DLC_PACKAGE = "DLC_PACKAGE"
57_DLC_ENABLED = "DLC_ENABLED"
58_ENVIRONMENT_FILENAME = "environment.bz2"
59_DLC_INSTALL_ROOT = "/var/cache/dlc"
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070060
David Pursell9476bf42015-03-30 13:34:27 -070061
62class DeployError(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060063 """Thrown when an unrecoverable error is encountered during deploy."""
David Pursell9476bf42015-03-30 13:34:27 -070064
65
Ralph Nathane01ccf12015-04-16 10:40:32 -070066class BrilloDeployOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -060067 """ProgressBarOperation specific for brillo deploy."""
Ralph Nathane01ccf12015-04-16 10:40:32 -070068
Alex Klein1699fab2022-09-08 08:46:06 -060069 # These two variables are used to validate the output in the VM integration
70 # tests. Changes to the output must be reflected here.
71 MERGE_EVENTS = (
72 "Preparing local packages",
73 "NOTICE: Copying binpkgs",
74 "NOTICE: Installing",
75 "been installed.",
76 "Please restart any updated",
77 )
78 UNMERGE_EVENTS = (
79 "NOTICE: Unmerging",
80 "been uninstalled.",
81 "Please restart any updated",
82 )
Ralph Nathane01ccf12015-04-16 10:40:32 -070083
Tim Baine4a783b2023-04-21 20:05:51 +000084 def __init__(self, emerge: bool):
Alex Klein1699fab2022-09-08 08:46:06 -060085 """Construct BrilloDeployOperation object.
Ralph Nathane01ccf12015-04-16 10:40:32 -070086
Alex Klein1699fab2022-09-08 08:46:06 -060087 Args:
Tim Baine4a783b2023-04-21 20:05:51 +000088 emerge: True if emerge, False if unmerge.
Alex Klein1699fab2022-09-08 08:46:06 -060089 """
90 super().__init__()
91 if emerge:
92 self._events = self.MERGE_EVENTS
93 else:
94 self._events = self.UNMERGE_EVENTS
95 self._total = len(self._events)
96 self._completed = 0
97
98 def ParseOutput(self, output=None):
99 """Parse the output of brillo deploy to update a progress bar."""
100 stdout = self._stdout.read()
101 stderr = self._stderr.read()
102 output = stdout + stderr
103 for event in self._events:
104 self._completed += output.count(event)
105 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700106
107
David Pursell9476bf42015-03-30 13:34:27 -0700108class _InstallPackageScanner(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600109 """Finds packages that need to be installed on a target device.
David Pursell9476bf42015-03-30 13:34:27 -0700110
Alex Klein1699fab2022-09-08 08:46:06 -0600111 Scans the sysroot bintree, beginning with a user-provided list of packages,
112 to find all packages that need to be installed. If so instructed,
113 transitively scans forward (mandatory) and backward (optional) dependencies
114 as well. A package will be installed if missing on the target (mandatory
115 packages only), or it will be updated if its sysroot version and build time
116 are different from the target. Common usage:
David Pursell9476bf42015-03-30 13:34:27 -0700117
Alex Klein53cc3bf2022-10-13 08:50:01 -0600118 pkg_scanner = _InstallPackageScanner(sysroot)
119 pkgs = pkg_scanner.Run(...)
Alex Klein1699fab2022-09-08 08:46:06 -0600120 """
David Pursell9476bf42015-03-30 13:34:27 -0700121
Alex Klein1699fab2022-09-08 08:46:06 -0600122 class VartreeError(Exception):
123 """An error in the processing of the installed packages tree."""
David Pursell9476bf42015-03-30 13:34:27 -0700124
Alex Klein1699fab2022-09-08 08:46:06 -0600125 class BintreeError(Exception):
126 """An error in the processing of the source binpkgs tree."""
David Pursell9476bf42015-03-30 13:34:27 -0700127
Alex Klein1699fab2022-09-08 08:46:06 -0600128 class PkgInfo(object):
129 """A record containing package information."""
David Pursell9476bf42015-03-30 13:34:27 -0700130
Tim Baine4a783b2023-04-21 20:05:51 +0000131 __slots__ = (
132 "cpv",
133 "build_time",
134 "rdeps_raw",
135 "use",
136 "rdeps",
137 "rev_rdeps",
138 )
David Pursell9476bf42015-03-30 13:34:27 -0700139
Alex Klein1699fab2022-09-08 08:46:06 -0600140 def __init__(
Tim Baine4a783b2023-04-21 20:05:51 +0000141 self,
142 cpv: package_info.CPV,
143 build_time: int,
144 rdeps_raw: str,
145 use: str,
146 rdeps: set = None,
147 rev_rdeps: set = None,
Alex Klein1699fab2022-09-08 08:46:06 -0600148 ):
149 self.cpv = cpv
150 self.build_time = build_time
151 self.rdeps_raw = rdeps_raw
Tim Baine4a783b2023-04-21 20:05:51 +0000152 self.use = use
Alex Klein1699fab2022-09-08 08:46:06 -0600153 self.rdeps = set() if rdeps is None else rdeps
154 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
David Pursell9476bf42015-03-30 13:34:27 -0700155
Alex Klein1699fab2022-09-08 08:46:06 -0600156 # Python snippet for dumping vartree info on the target. Instantiate using
157 # _GetVartreeSnippet().
158 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700159import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700160import os
161import portage
162
163# Normalize the path to match what portage will index.
164target_root = os.path.normpath('%(root)s')
165if not target_root.endswith('/'):
166 target_root += '/'
167trees = portage.create_trees(target_root=target_root, config_root='/')
168vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700169pkg_info = []
170for cpv in vartree.dbapi.cpv_all():
Tim Baine4a783b2023-04-21 20:05:51 +0000171 slot, rdep_raw, build_time, use = vartree.dbapi.aux_get(
172 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME', 'USE'))
173 pkg_info.append((cpv, slot, rdep_raw, build_time, use))
David Pursell9476bf42015-03-30 13:34:27 -0700174
175print(json.dumps(pkg_info))
176"""
177
Tim Baine4a783b2023-04-21 20:05:51 +0000178 def __init__(self, sysroot: str):
Alex Klein1699fab2022-09-08 08:46:06 -0600179 self.sysroot = sysroot
Alex Klein975e86c2023-01-23 16:49:10 -0700180 # Members containing the sysroot (binpkg) and target (installed) package
181 # DB.
Alex Klein1699fab2022-09-08 08:46:06 -0600182 self.target_db = None
183 self.binpkgs_db = None
184 # Members for managing the dependency resolution work queue.
185 self.queue = None
186 self.seen = None
187 self.listed = None
David Pursell9476bf42015-03-30 13:34:27 -0700188
Alex Klein1699fab2022-09-08 08:46:06 -0600189 @staticmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000190 def _GetCP(cpv: package_info.CPV) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600191 """Returns the CP value for a given CPV string."""
192 attrs = package_info.SplitCPV(cpv, strict=False)
193 if not attrs.cp:
194 raise ValueError("Cannot get CP value for %s" % cpv)
195 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700196
Alex Klein1699fab2022-09-08 08:46:06 -0600197 @staticmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000198 def _InDB(cp: str, slot: str, db: Dict[str, Dict[str, PkgInfo]]) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -0600199 """Returns whether CP and slot are found in a database (if provided)."""
200 cp_slots = db.get(cp) if db else None
201 return cp_slots is not None and (not slot or slot in cp_slots)
David Pursell9476bf42015-03-30 13:34:27 -0700202
Alex Klein1699fab2022-09-08 08:46:06 -0600203 @staticmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000204 def _AtomStr(cp: str, slot: str) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600205 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
206 return "%s:%s" % (cp, slot) if slot else cp
David Pursell9476bf42015-03-30 13:34:27 -0700207
Alex Klein1699fab2022-09-08 08:46:06 -0600208 @classmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000209 def _GetVartreeSnippet(cls, root: str = "/") -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600210 """Returns a code snippet for dumping the vartree on the target.
David Pursell9476bf42015-03-30 13:34:27 -0700211
Alex Klein1699fab2022-09-08 08:46:06 -0600212 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600213 root: The installation root.
David Pursell9476bf42015-03-30 13:34:27 -0700214
Alex Klein1699fab2022-09-08 08:46:06 -0600215 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600216 The said code snippet (string) with parameters filled in.
Alex Klein1699fab2022-09-08 08:46:06 -0600217 """
218 return cls._GET_VARTREE % {"root": root}
David Pursell9476bf42015-03-30 13:34:27 -0700219
Alex Klein1699fab2022-09-08 08:46:06 -0600220 @classmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000221 def _StripDepAtom(
222 cls, dep_atom: str, installed_db: Dict[str, Dict[str, PkgInfo]] = None
223 ) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600224 """Strips a dependency atom and returns a (CP, slot) pair."""
225 # TODO(garnold) This is a gross simplification of ebuild dependency
226 # semantics, stripping and ignoring various qualifiers (versions, slots,
227 # USE flag, negation) and will likely need to be fixed. chromium:447366.
David Pursell9476bf42015-03-30 13:34:27 -0700228
Alex Klein1699fab2022-09-08 08:46:06 -0600229 # Ignore unversioned blockers, leaving them for the user to resolve.
230 if dep_atom[0] == "!" and dep_atom[1] not in "<=>~":
231 return None, None
David Pursell9476bf42015-03-30 13:34:27 -0700232
Alex Klein1699fab2022-09-08 08:46:06 -0600233 cp = dep_atom
234 slot = None
235 require_installed = False
David Pursell9476bf42015-03-30 13:34:27 -0700236
Alex Klein1699fab2022-09-08 08:46:06 -0600237 # Versioned blockers should be updated, but only if already installed.
Alex Klein975e86c2023-01-23 16:49:10 -0700238 # These are often used for forcing cascaded updates of multiple
239 # packages, so we're treating them as ordinary constraints with hopes
240 # that it'll lead to the desired result.
Alex Klein1699fab2022-09-08 08:46:06 -0600241 if cp.startswith("!"):
242 cp = cp.lstrip("!")
243 require_installed = True
David Pursell9476bf42015-03-30 13:34:27 -0700244
Alex Klein1699fab2022-09-08 08:46:06 -0600245 # Remove USE flags.
246 if "[" in cp:
247 cp = cp[: cp.index("[")] + cp[cp.index("]") + 1 :]
David Pursell9476bf42015-03-30 13:34:27 -0700248
Alex Klein1699fab2022-09-08 08:46:06 -0600249 # Separate the slot qualifier and strip off subslots.
250 if ":" in cp:
251 cp, slot = cp.split(":")
252 for delim in ("/", "="):
253 slot = slot.split(delim, 1)[0]
David Pursell9476bf42015-03-30 13:34:27 -0700254
Alex Klein1699fab2022-09-08 08:46:06 -0600255 # Strip version wildcards (right), comparators (left).
256 cp = cp.rstrip("*")
257 cp = cp.lstrip("<=>~")
David Pursell9476bf42015-03-30 13:34:27 -0700258
Alex Klein1699fab2022-09-08 08:46:06 -0600259 # Turn into CP form.
260 cp = cls._GetCP(cp)
David Pursell9476bf42015-03-30 13:34:27 -0700261
Alex Klein1699fab2022-09-08 08:46:06 -0600262 if require_installed and not cls._InDB(cp, None, installed_db):
263 return None, None
David Pursell9476bf42015-03-30 13:34:27 -0700264
Alex Klein1699fab2022-09-08 08:46:06 -0600265 return cp, slot
David Pursell9476bf42015-03-30 13:34:27 -0700266
Alex Klein1699fab2022-09-08 08:46:06 -0600267 @classmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000268 def _ProcessDepStr(
269 cls,
270 dep_str: str,
271 installed_db: Dict[str, Dict[str, PkgInfo]],
272 avail_db: Dict[str, Dict[str, PkgInfo]],
273 ) -> set:
Alex Klein1699fab2022-09-08 08:46:06 -0600274 """Resolves and returns a list of dependencies from a dependency string.
David Pursell9476bf42015-03-30 13:34:27 -0700275
Alex Klein1699fab2022-09-08 08:46:06 -0600276 This parses a dependency string and returns a list of package names and
Alex Klein975e86c2023-01-23 16:49:10 -0700277 slots. Other atom qualifiers (version, sub-slot, block) are ignored.
278 When resolving disjunctive deps, we include all choices that are fully
279 present in |installed_db|. If none is present, we choose an arbitrary
280 one that is available.
David Pursell9476bf42015-03-30 13:34:27 -0700281
Alex Klein1699fab2022-09-08 08:46:06 -0600282 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600283 dep_str: A raw dependency string.
284 installed_db: A database of installed packages.
285 avail_db: A database of packages available for installation.
David Pursell9476bf42015-03-30 13:34:27 -0700286
Alex Klein1699fab2022-09-08 08:46:06 -0600287 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600288 A list of pairs (CP, slot).
David Pursell9476bf42015-03-30 13:34:27 -0700289
Alex Klein1699fab2022-09-08 08:46:06 -0600290 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600291 ValueError: the dependencies string is malformed.
Alex Klein1699fab2022-09-08 08:46:06 -0600292 """
David Pursell9476bf42015-03-30 13:34:27 -0700293
Tim Baine4a783b2023-04-21 20:05:51 +0000294 def ProcessSubDeps(
295 dep_exp: Set[Tuple[str, str]], disjunct: bool
296 ) -> Set[Tuple[str, str]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600297 """Parses and processes a dependency (sub)expression."""
298 deps = set()
299 default_deps = set()
300 sub_disjunct = False
301 for dep_sub_exp in dep_exp:
302 sub_deps = set()
David Pursell9476bf42015-03-30 13:34:27 -0700303
Alex Klein1699fab2022-09-08 08:46:06 -0600304 if isinstance(dep_sub_exp, (list, tuple)):
305 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
306 sub_disjunct = False
307 elif sub_disjunct:
308 raise ValueError("Malformed disjunctive operation in deps")
309 elif dep_sub_exp == "||":
310 sub_disjunct = True
311 elif dep_sub_exp.endswith("?"):
312 raise ValueError("Dependencies contain a conditional")
313 else:
314 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
315 if cp:
316 sub_deps = set([(cp, slot)])
317 elif disjunct:
318 raise ValueError("Atom in disjunct ignored")
David Pursell9476bf42015-03-30 13:34:27 -0700319
Alex Klein1699fab2022-09-08 08:46:06 -0600320 # Handle sub-deps of a disjunctive expression.
321 if disjunct:
Alex Klein975e86c2023-01-23 16:49:10 -0700322 # Make the first available choice the default, for use in
323 # case that no option is installed.
Alex Klein1699fab2022-09-08 08:46:06 -0600324 if (
325 not default_deps
326 and avail_db is not None
327 and all(
328 cls._InDB(cp, slot, avail_db)
329 for cp, slot in sub_deps
330 )
331 ):
332 default_deps = sub_deps
David Pursell9476bf42015-03-30 13:34:27 -0700333
Alex Klein975e86c2023-01-23 16:49:10 -0700334 # If not all sub-deps are installed, then don't consider
335 # them.
Alex Klein1699fab2022-09-08 08:46:06 -0600336 if not all(
337 cls._InDB(cp, slot, installed_db)
338 for cp, slot in sub_deps
339 ):
340 sub_deps = set()
David Pursell9476bf42015-03-30 13:34:27 -0700341
Alex Klein1699fab2022-09-08 08:46:06 -0600342 deps.update(sub_deps)
David Pursell9476bf42015-03-30 13:34:27 -0700343
Alex Klein1699fab2022-09-08 08:46:06 -0600344 return deps or default_deps
David Pursell9476bf42015-03-30 13:34:27 -0700345
Alex Klein1699fab2022-09-08 08:46:06 -0600346 try:
347 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
348 except portage.exception.InvalidDependString as e:
349 raise ValueError("Invalid dep string: %s" % e)
350 except ValueError as e:
351 raise ValueError("%s: %s" % (e, dep_str))
David Pursell9476bf42015-03-30 13:34:27 -0700352
Alex Klein1699fab2022-09-08 08:46:06 -0600353 def _BuildDB(
Tim Baine4a783b2023-04-21 20:05:51 +0000354 self,
355 cpv_info: List[Tuple[Dict[str, package_info.CPV], str, str, int, str]],
356 process_rdeps: bool,
357 process_rev_rdeps: bool,
358 installed_db: Dict[str, Dict[str, PkgInfo]] = None,
359 ) -> Dict[str, Dict[str, PkgInfo]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600360 """Returns a database of packages given a list of CPV info.
David Pursell9476bf42015-03-30 13:34:27 -0700361
Alex Klein1699fab2022-09-08 08:46:06 -0600362 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600363 cpv_info: A list of tuples containing package CPV and attributes.
364 process_rdeps: Whether to populate forward dependencies.
365 process_rev_rdeps: Whether to populate reverse dependencies.
366 installed_db: A database of installed packages for filtering
367 disjunctive choices against; if None, using own built database.
David Pursell9476bf42015-03-30 13:34:27 -0700368
Alex Klein1699fab2022-09-08 08:46:06 -0600369 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600370 A map from CP values to another dictionary that maps slots
371 to package attribute tuples. Tuples contain a CPV value
372 (string), build time (string), runtime dependencies (set),
373 and reverse dependencies (set, empty if not populated).
David Pursell9476bf42015-03-30 13:34:27 -0700374
Alex Klein1699fab2022-09-08 08:46:06 -0600375 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600376 ValueError: If more than one CPV occupies a single slot.
Alex Klein1699fab2022-09-08 08:46:06 -0600377 """
378 db = {}
379 logging.debug("Populating package DB...")
Tim Baine4a783b2023-04-21 20:05:51 +0000380 for cpv, slot, rdeps_raw, build_time, use in cpv_info:
Alex Klein1699fab2022-09-08 08:46:06 -0600381 cp = self._GetCP(cpv)
382 cp_slots = db.setdefault(cp, dict())
383 if slot in cp_slots:
384 raise ValueError(
385 "More than one package found for %s"
386 % self._AtomStr(cp, slot)
387 )
388 logging.debug(
389 " %s -> %s, built %s, raw rdeps: %s",
390 self._AtomStr(cp, slot),
391 cpv,
392 build_time,
393 rdeps_raw,
394 )
Tim Baine4a783b2023-04-21 20:05:51 +0000395 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw, use)
David Pursell9476bf42015-03-30 13:34:27 -0700396
Alex Klein1699fab2022-09-08 08:46:06 -0600397 avail_db = db
398 if installed_db is None:
399 installed_db = db
400 avail_db = None
David Pursell9476bf42015-03-30 13:34:27 -0700401
Alex Klein1699fab2022-09-08 08:46:06 -0600402 # Add approximate forward dependencies.
David Pursell9476bf42015-03-30 13:34:27 -0700403 if process_rdeps:
Alex Klein1699fab2022-09-08 08:46:06 -0600404 logging.debug("Populating forward dependencies...")
405 for cp, cp_slots in db.items():
406 for slot, pkg_info in cp_slots.items():
407 pkg_info.rdeps.update(
408 self._ProcessDepStr(
409 pkg_info.rdeps_raw, installed_db, avail_db
410 )
411 )
412 logging.debug(
413 " %s (%s) processed rdeps: %s",
414 self._AtomStr(cp, slot),
415 pkg_info.cpv,
416 " ".join(
417 [
418 self._AtomStr(rdep_cp, rdep_slot)
419 for rdep_cp, rdep_slot in pkg_info.rdeps
420 ]
421 ),
422 )
423
424 # Add approximate reverse dependencies (optional).
David Pursell9476bf42015-03-30 13:34:27 -0700425 if process_rev_rdeps:
Alex Klein1699fab2022-09-08 08:46:06 -0600426 logging.debug("Populating reverse dependencies...")
427 for cp, cp_slots in db.items():
428 for slot, pkg_info in cp_slots.items():
429 for rdep_cp, rdep_slot in pkg_info.rdeps:
430 to_slots = db.get(rdep_cp)
431 if not to_slots:
432 continue
David Pursell9476bf42015-03-30 13:34:27 -0700433
Alex Klein1699fab2022-09-08 08:46:06 -0600434 for to_slot, to_pkg_info in to_slots.items():
435 if rdep_slot and to_slot != rdep_slot:
436 continue
437 logging.debug(
438 " %s (%s) added as rev rdep for %s (%s)",
439 self._AtomStr(cp, slot),
440 pkg_info.cpv,
441 self._AtomStr(rdep_cp, to_slot),
442 to_pkg_info.cpv,
443 )
444 to_pkg_info.rev_rdeps.add((cp, slot))
David Pursell9476bf42015-03-30 13:34:27 -0700445
Alex Klein1699fab2022-09-08 08:46:06 -0600446 return db
David Pursell9476bf42015-03-30 13:34:27 -0700447
Tim Baine4a783b2023-04-21 20:05:51 +0000448 def _InitTargetVarDB(
449 self,
450 device: remote_access.RemoteDevice,
451 root: str,
452 process_rdeps: bool,
453 process_rev_rdeps: bool,
454 ) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600455 """Initializes a dictionary of packages installed on |device|."""
456 get_vartree_script = self._GetVartreeSnippet(root)
457 try:
Mike Frysingerc0780a62022-08-29 04:41:56 -0400458 result = device.agent.RemoteSh(
Alex Klein1699fab2022-09-08 08:46:06 -0600459 ["python"], remote_sudo=True, input=get_vartree_script
460 )
461 except cros_build_lib.RunCommandError as e:
462 logging.error("Cannot get target vartree:\n%s", e.stderr)
463 raise
David Pursell9476bf42015-03-30 13:34:27 -0700464
Alex Klein1699fab2022-09-08 08:46:06 -0600465 try:
466 self.target_db = self._BuildDB(
467 json.loads(result.stdout), process_rdeps, process_rev_rdeps
468 )
469 except ValueError as e:
470 raise self.VartreeError(str(e))
David Pursell9476bf42015-03-30 13:34:27 -0700471
Tim Baine4a783b2023-04-21 20:05:51 +0000472 def _InitBinpkgDB(self, process_rdeps: bool) -> None:
Alex Klein975e86c2023-01-23 16:49:10 -0700473 """Initializes a dictionary of binpkgs for updating the target."""
Alex Klein1699fab2022-09-08 08:46:06 -0600474 # Get build root trees; portage indexes require a trailing '/'.
475 build_root = os.path.join(self.sysroot, "")
476 trees = portage.create_trees(
477 target_root=build_root, config_root=build_root
478 )
479 bintree = trees[build_root]["bintree"]
480 binpkgs_info = []
481 for cpv in bintree.dbapi.cpv_all():
Tim Baine4a783b2023-04-21 20:05:51 +0000482 slot, rdep_raw, build_time, use = bintree.dbapi.aux_get(
483 cpv, ["SLOT", "RDEPEND", "BUILD_TIME", "USE"]
Alex Klein1699fab2022-09-08 08:46:06 -0600484 )
Tim Baine4a783b2023-04-21 20:05:51 +0000485 binpkgs_info.append((cpv, slot, rdep_raw, build_time, use))
David Pursell9476bf42015-03-30 13:34:27 -0700486
Alex Klein1699fab2022-09-08 08:46:06 -0600487 try:
488 self.binpkgs_db = self._BuildDB(
489 binpkgs_info, process_rdeps, False, installed_db=self.target_db
490 )
491 except ValueError as e:
492 raise self.BintreeError(str(e))
David Pursell9476bf42015-03-30 13:34:27 -0700493
Tim Baine4a783b2023-04-21 20:05:51 +0000494 def _InitDepQueue(self) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600495 """Initializes the dependency work queue."""
496 self.queue = set()
497 self.seen = {}
498 self.listed = set()
David Pursell9476bf42015-03-30 13:34:27 -0700499
Tim Baine4a783b2023-04-21 20:05:51 +0000500 def _EnqDep(self, dep: str, listed: bool, optional: bool) -> bool:
Alex Klein975e86c2023-01-23 16:49:10 -0700501 """Enqueues a dependency if not seen before or if set non-optional."""
Alex Klein1699fab2022-09-08 08:46:06 -0600502 if dep in self.seen and (optional or not self.seen[dep]):
503 return False
David Pursell9476bf42015-03-30 13:34:27 -0700504
Alex Klein1699fab2022-09-08 08:46:06 -0600505 self.queue.add(dep)
506 self.seen[dep] = optional
507 if listed:
508 self.listed.add(dep)
509 return True
David Pursell9476bf42015-03-30 13:34:27 -0700510
Tim Baine4a783b2023-04-21 20:05:51 +0000511 def _DeqDep(self) -> Tuple[str, bool, bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600512 """Dequeues and returns a dependency, its listed and optional flags.
David Pursell9476bf42015-03-30 13:34:27 -0700513
Alex Klein975e86c2023-01-23 16:49:10 -0700514 This returns listed packages first, if any are present, to ensure that
515 we correctly mark them as such when they are first being processed.
Alex Klein1699fab2022-09-08 08:46:06 -0600516 """
517 if self.listed:
518 dep = self.listed.pop()
519 self.queue.remove(dep)
520 listed = True
521 else:
522 dep = self.queue.pop()
523 listed = False
David Pursell9476bf42015-03-30 13:34:27 -0700524
Alex Klein1699fab2022-09-08 08:46:06 -0600525 return dep, listed, self.seen[dep]
David Pursell9476bf42015-03-30 13:34:27 -0700526
Tim Baine4a783b2023-04-21 20:05:51 +0000527 def _FindPackageMatches(self, cpv_pattern: str) -> List[Tuple[str, str]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600528 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
David Pursell9476bf42015-03-30 13:34:27 -0700529
Alex Klein1699fab2022-09-08 08:46:06 -0600530 This is breaking |cpv_pattern| into its C, P and V components, each of
531 which may or may not be present or contain wildcards. It then scans the
Alex Klein975e86c2023-01-23 16:49:10 -0700532 binpkgs database to find all atoms that match these components,
533 returning a list of CP and slot qualifier. When the pattern does not
534 specify a version, or when a CP has only one slot in the binpkgs
535 database, we omit the slot qualifier in the result.
David Pursell9476bf42015-03-30 13:34:27 -0700536
Alex Klein1699fab2022-09-08 08:46:06 -0600537 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600538 cpv_pattern: A CPV pattern, potentially partial and/or having
539 wildcards.
David Pursell9476bf42015-03-30 13:34:27 -0700540
Alex Klein1699fab2022-09-08 08:46:06 -0600541 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600542 A list of (CPV, slot) pairs of packages in the binpkgs database that
543 match the pattern.
Alex Klein1699fab2022-09-08 08:46:06 -0600544 """
545 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
546 cp_pattern = os.path.join(attrs.category or "*", attrs.package or "*")
547 matches = []
548 for cp, cp_slots in self.binpkgs_db.items():
549 if not fnmatch.fnmatchcase(cp, cp_pattern):
550 continue
David Pursell9476bf42015-03-30 13:34:27 -0700551
Alex Klein975e86c2023-01-23 16:49:10 -0700552 # If no version attribute was given or there's only one slot, omit
553 # the slot qualifier.
Alex Klein1699fab2022-09-08 08:46:06 -0600554 if not attrs.version or len(cp_slots) == 1:
555 matches.append((cp, None))
556 else:
557 cpv_pattern = "%s-%s" % (cp, attrs.version)
558 for slot, pkg_info in cp_slots.items():
559 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
560 matches.append((cp, slot))
David Pursell9476bf42015-03-30 13:34:27 -0700561
Alex Klein1699fab2022-09-08 08:46:06 -0600562 return matches
David Pursell9476bf42015-03-30 13:34:27 -0700563
Tim Baine4a783b2023-04-21 20:05:51 +0000564 def _FindPackage(self, pkg: str) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600565 """Returns the (CP, slot) pair for a package matching |pkg|.
David Pursell9476bf42015-03-30 13:34:27 -0700566
Alex Klein1699fab2022-09-08 08:46:06 -0600567 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600568 pkg: Path to a binary package or a (partial) package CPV specifier.
David Pursell9476bf42015-03-30 13:34:27 -0700569
Alex Klein1699fab2022-09-08 08:46:06 -0600570 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600571 A (CP, slot) pair for the given package; slot may be None
572 (unspecified).
David Pursell9476bf42015-03-30 13:34:27 -0700573
Alex Klein1699fab2022-09-08 08:46:06 -0600574 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600575 ValueError: if |pkg| is not a binpkg file nor does it match
576 something that's in the bintree.
Alex Klein1699fab2022-09-08 08:46:06 -0600577 """
578 if pkg.endswith(".tbz2") and os.path.isfile(pkg):
579 package = os.path.basename(os.path.splitext(pkg)[0])
580 category = os.path.basename(os.path.dirname(pkg))
581 return self._GetCP(os.path.join(category, package)), None
David Pursell9476bf42015-03-30 13:34:27 -0700582
Alex Klein1699fab2022-09-08 08:46:06 -0600583 matches = self._FindPackageMatches(pkg)
584 if not matches:
585 raise ValueError("No package found for %s" % pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700586
Alex Klein1699fab2022-09-08 08:46:06 -0600587 idx = 0
588 if len(matches) > 1:
589 # Ask user to pick among multiple matches.
590 idx = cros_build_lib.GetChoice(
591 "Multiple matches found for %s: " % pkg,
592 ["%s:%s" % (cp, slot) if slot else cp for cp, slot in matches],
593 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700594
Alex Klein1699fab2022-09-08 08:46:06 -0600595 return matches[idx]
596
Tim Baine4a783b2023-04-21 20:05:51 +0000597 def _NeedsInstall(
598 self, cpv: str, slot: str, build_time: int, optional: bool
599 ) -> Tuple[bool, bool, bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600600 """Returns whether a package needs to be installed on the target.
601
602 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600603 cpv: Fully qualified CPV (string) of the package.
604 slot: Slot identifier (string).
605 build_time: The BUILT_TIME value (string) of the binpkg.
606 optional: Whether package is optional on the target.
Alex Klein1699fab2022-09-08 08:46:06 -0600607
608 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000609 A tuple (install, update, use_mismatch) indicating whether to
610 |install| the package, whether it is an |update| to an existing
611 package, and whether the package's USE flags mismatch the existing
612 package.
Alex Klein1699fab2022-09-08 08:46:06 -0600613
614 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600615 ValueError: if slot is not provided.
Alex Klein1699fab2022-09-08 08:46:06 -0600616 """
617 # If not checking installed packages, always install.
618 if not self.target_db:
Tim Baine4a783b2023-04-21 20:05:51 +0000619 return True, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600620
621 cp = self._GetCP(cpv)
622 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
623 if target_pkg_info is not None:
Tim Baine4a783b2023-04-21 20:05:51 +0000624 attrs = package_info.SplitCPV(cpv)
625 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
Alex Klein1699fab2022-09-08 08:46:06 -0600626
Tim Baine4a783b2023-04-21 20:05:51 +0000627 def _get_attr_mismatch(
628 attr_name: str, new_attr: any, target_attr: any
629 ) -> Tuple[str, str, str]:
630 """Check if the new and target packages differ for an attribute.
631
632 Args:
633 attr_name: The name of the attribute being checked (string).
634 new_attr: The value of the given attribute for the new
635 package (string).
636 target_attr: The value of the given attribute for the target
637 (existing) package (string).
638
639 Returns:
640 A tuple (attr_name, new_attr, target_attr) composed of the
641 args if there is a mismatch, or None if the values match.
642 """
643 mismatch = new_attr != target_attr
644 if mismatch:
645 return attr_name, new_attr, target_attr
646
647 update_info = _get_attr_mismatch(
648 "version", attrs.version, target_attrs.version
649 ) or _get_attr_mismatch(
650 "build time", build_time, target_pkg_info.build_time
651 )
652
653 if update_info:
654 attr_name, new_attr, target_attr = update_info
Alex Klein1699fab2022-09-08 08:46:06 -0600655 logging.debug(
Tim Baine4a783b2023-04-21 20:05:51 +0000656 "Updating %s: %s (%s) different on target (%s)",
657 cp,
658 attr_name,
659 new_attr,
660 target_attr,
Alex Klein1699fab2022-09-08 08:46:06 -0600661 )
Tim Baine4a783b2023-04-21 20:05:51 +0000662
663 binpkg_pkg_info = self.binpkgs_db.get(cp, dict()).get(slot)
664 use_mismatch = binpkg_pkg_info.use != target_pkg_info.use
665 if use_mismatch:
666 logging.warning(
667 "USE flags for package %s do not match (Existing='%s', "
668 "New='%s').",
669 cp,
670 target_pkg_info.use,
671 binpkg_pkg_info.use,
672 )
673 return True, True, use_mismatch
Alex Klein1699fab2022-09-08 08:46:06 -0600674
675 logging.debug(
676 "Not updating %s: already up-to-date (%s, built %s)",
677 cp,
678 target_pkg_info.cpv,
679 target_pkg_info.build_time,
680 )
Tim Baine4a783b2023-04-21 20:05:51 +0000681 return False, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600682
683 if optional:
684 logging.debug(
685 "Not installing %s: missing on target but optional", cp
686 )
Tim Baine4a783b2023-04-21 20:05:51 +0000687 return False, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600688
689 logging.debug(
690 "Installing %s: missing on target and non-optional (%s)", cp, cpv
691 )
Tim Baine4a783b2023-04-21 20:05:51 +0000692 return True, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600693
Tim Baine4a783b2023-04-21 20:05:51 +0000694 def _ProcessDeps(self, deps: List[str], reverse: bool) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600695 """Enqueues dependencies for processing.
696
697 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600698 deps: List of dependencies to enqueue.
699 reverse: Whether these are reverse dependencies.
Alex Klein1699fab2022-09-08 08:46:06 -0600700 """
701 if not deps:
702 return
703
704 logging.debug(
705 "Processing %d %s dep(s)...",
706 len(deps),
707 "reverse" if reverse else "forward",
708 )
709 num_already_seen = 0
710 for dep in deps:
711 if self._EnqDep(dep, False, reverse):
712 logging.debug(" Queued dep %s", dep)
713 else:
714 num_already_seen += 1
715
716 if num_already_seen:
717 logging.debug("%d dep(s) already seen", num_already_seen)
718
Tim Baine4a783b2023-04-21 20:05:51 +0000719 def _ComputeInstalls(
720 self, process_rdeps: bool, process_rev_rdeps: bool
721 ) -> Tuple[Dict[str, package_info.CPV], bool]:
Alex Klein975e86c2023-01-23 16:49:10 -0700722 """Returns a dict of packages that need to be installed on the target.
Alex Klein1699fab2022-09-08 08:46:06 -0600723
724 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600725 process_rdeps: Whether to trace forward dependencies.
726 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600727
728 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000729 A tuple (installs, warnings_shown) where |installs| is a dictionary
730 mapping CP values (string) to tuples containing a CPV (string), a
731 slot (string), a boolean indicating whether the package was
732 initially listed in the queue, and a boolean indicating whether this
733 is an update to an existing package, and |warnings_shown| is a
734 boolean indicating whether warnings were shown that might require a
735 prompt whether to continue.
Alex Klein1699fab2022-09-08 08:46:06 -0600736 """
737 installs = {}
Tim Baine4a783b2023-04-21 20:05:51 +0000738 warnings_shown = False
Alex Klein1699fab2022-09-08 08:46:06 -0600739 while self.queue:
740 dep, listed, optional = self._DeqDep()
741 cp, required_slot = dep
742 if cp in installs:
743 logging.debug("Already updating %s", cp)
744 continue
745
746 cp_slots = self.binpkgs_db.get(cp, dict())
747 logging.debug(
748 "Checking packages matching %s%s%s...",
749 cp,
750 " (slot: %s)" % required_slot if required_slot else "",
751 " (optional)" if optional else "",
752 )
753 num_processed = 0
754 for slot, pkg_info in cp_slots.items():
755 if required_slot and slot != required_slot:
756 continue
757
758 num_processed += 1
759 logging.debug(" Checking %s...", pkg_info.cpv)
760
Tim Baine4a783b2023-04-21 20:05:51 +0000761 install, update, use_mismatch = self._NeedsInstall(
Alex Klein1699fab2022-09-08 08:46:06 -0600762 pkg_info.cpv, slot, pkg_info.build_time, optional
763 )
764 if not install:
765 continue
766
767 installs[cp] = (pkg_info.cpv, slot, listed, update)
Tim Baine4a783b2023-04-21 20:05:51 +0000768 warnings_shown |= use_mismatch
Alex Klein1699fab2022-09-08 08:46:06 -0600769
770 # Add forward and backward runtime dependencies to queue.
771 if process_rdeps:
772 self._ProcessDeps(pkg_info.rdeps, False)
773 if process_rev_rdeps:
774 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
775 if target_pkg_info:
776 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
777
778 if num_processed == 0:
779 logging.warning(
780 "No qualified bintree package corresponding to %s", cp
781 )
782
Tim Baine4a783b2023-04-21 20:05:51 +0000783 return installs, warnings_shown
Alex Klein1699fab2022-09-08 08:46:06 -0600784
Tim Baine4a783b2023-04-21 20:05:51 +0000785 def _SortInstalls(self, installs: List[str]) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600786 """Returns a sorted list of packages to install.
787
788 Performs a topological sort based on dependencies found in the binary
789 package database.
790
791 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600792 installs: Dictionary of packages to install indexed by CP.
Alex Klein1699fab2022-09-08 08:46:06 -0600793
794 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600795 A list of package CPVs (string).
Alex Klein1699fab2022-09-08 08:46:06 -0600796
797 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600798 ValueError: If dependency graph contains a cycle.
Alex Klein1699fab2022-09-08 08:46:06 -0600799 """
800 not_visited = set(installs.keys())
801 curr_path = []
802 sorted_installs = []
803
Tim Baine4a783b2023-04-21 20:05:51 +0000804 def SortFrom(cp: str) -> None:
Alex Klein975e86c2023-01-23 16:49:10 -0700805 """Traverses deps recursively, emitting nodes in reverse order."""
Alex Klein1699fab2022-09-08 08:46:06 -0600806 cpv, slot, _, _ = installs[cp]
807 if cpv in curr_path:
808 raise ValueError(
809 "Dependencies contain a cycle: %s -> %s"
810 % (" -> ".join(curr_path[curr_path.index(cpv) :]), cpv)
811 )
812 curr_path.append(cpv)
813 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
814 if rdep_cp in not_visited:
815 not_visited.remove(rdep_cp)
816 SortFrom(rdep_cp)
817
818 sorted_installs.append(cpv)
819 curr_path.pop()
820
821 # So long as there's more packages, keep expanding dependency paths.
822 while not_visited:
823 SortFrom(not_visited.pop())
824
825 return sorted_installs
826
Tim Baine4a783b2023-04-21 20:05:51 +0000827 def _EnqListedPkg(self, pkg: str) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -0600828 """Finds and enqueues a listed package."""
829 cp, slot = self._FindPackage(pkg)
830 if cp not in self.binpkgs_db:
831 raise self.BintreeError(
832 "Package %s not found in binpkgs tree" % pkg
833 )
834 self._EnqDep((cp, slot), True, False)
835
Tim Baine4a783b2023-04-21 20:05:51 +0000836 def _EnqInstalledPkgs(self) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600837 """Enqueues all available binary packages that are already installed."""
838 for cp, cp_slots in self.binpkgs_db.items():
839 target_cp_slots = self.target_db.get(cp)
840 if target_cp_slots:
841 for slot in cp_slots.keys():
842 if slot in target_cp_slots:
843 self._EnqDep((cp, slot), True, False)
844
845 def Run(
846 self,
Tim Baine4a783b2023-04-21 20:05:51 +0000847 device: remote_access.RemoteDevice,
848 root: str,
849 listed_pkgs: List[str],
850 update: bool,
851 process_rdeps: bool,
852 process_rev_rdeps: bool,
853 ) -> Tuple[List[str], List[str], int, Dict[str, str], bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600854 """Computes the list of packages that need to be installed on a target.
855
856 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600857 device: Target handler object.
858 root: Package installation root.
859 listed_pkgs: Package names/files listed by the user.
860 update: Whether to read the target's installed package database.
861 process_rdeps: Whether to trace forward dependencies.
862 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600863
864 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000865 A tuple (sorted, listed, num_updates, install_attrs, warnings_shown)
866 where |sorted| is a list of package CPVs (string) to install on the
867 target in an order that satisfies their inter-dependencies, |listed|
Alex Klein53cc3bf2022-10-13 08:50:01 -0600868 the subset that was requested by the user, and |num_updates|
869 the number of packages being installed over preexisting
870 versions. Note that installation order should be reversed for
871 removal, |install_attrs| is a dictionary mapping a package
Tim Baine4a783b2023-04-21 20:05:51 +0000872 CPV (string) to some of its extracted environment attributes, and
873 |warnings_shown| is a boolean indicating whether warnings were shown
874 that might require a prompt whether to continue.
Alex Klein1699fab2022-09-08 08:46:06 -0600875 """
876 if process_rev_rdeps and not process_rdeps:
877 raise ValueError(
878 "Must processing forward deps when processing rev deps"
879 )
880 if process_rdeps and not update:
881 raise ValueError(
882 "Must check installed packages when processing deps"
883 )
884
885 if update:
886 logging.info("Initializing target intalled packages database...")
887 self._InitTargetVarDB(
888 device, root, process_rdeps, process_rev_rdeps
889 )
890
891 logging.info("Initializing binary packages database...")
892 self._InitBinpkgDB(process_rdeps)
893
894 logging.info("Finding listed package(s)...")
895 self._InitDepQueue()
896 for pkg in listed_pkgs:
897 if pkg == "@installed":
898 if not update:
899 raise ValueError(
Alex Klein975e86c2023-01-23 16:49:10 -0700900 "Must check installed packages when updating all of "
901 "them."
Alex Klein1699fab2022-09-08 08:46:06 -0600902 )
903 self._EnqInstalledPkgs()
904 else:
905 self._EnqListedPkg(pkg)
906
907 logging.info("Computing set of packages to install...")
Tim Baine4a783b2023-04-21 20:05:51 +0000908 installs, warnings_shown = self._ComputeInstalls(
909 process_rdeps, process_rev_rdeps
910 )
Alex Klein1699fab2022-09-08 08:46:06 -0600911
912 num_updates = 0
913 listed_installs = []
914 for cpv, _, listed, isupdate in installs.values():
915 if listed:
916 listed_installs.append(cpv)
917 if isupdate:
918 num_updates += 1
919
920 logging.info(
921 "Processed %d package(s), %d will be installed, %d are "
922 "updating existing packages",
923 len(self.seen),
924 len(installs),
925 num_updates,
926 )
927
928 sorted_installs = self._SortInstalls(installs)
929
930 install_attrs = {}
931 for pkg in sorted_installs:
932 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
933 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
934 install_attrs[pkg] = {}
935 if dlc_id and dlc_package:
936 install_attrs[pkg][_DLC_ID] = dlc_id
937
Tim Baine4a783b2023-04-21 20:05:51 +0000938 return (
939 sorted_installs,
940 listed_installs,
941 num_updates,
942 install_attrs,
943 warnings_shown,
944 )
David Pursell9476bf42015-03-30 13:34:27 -0700945
946
Tim Baine4a783b2023-04-21 20:05:51 +0000947def _Emerge(
948 device: remote_access.RemoteDevice,
949 pkg_paths: List[str],
950 root: str,
951 extra_args: List[str] = None,
952) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600953 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -0700954
Alex Klein1699fab2022-09-08 08:46:06 -0600955 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600956 device: A ChromiumOSDevice object.
957 pkg_paths: (Local) paths to binary packages.
958 root: Package installation root path.
959 extra_args: Extra arguments to pass to emerge.
David Pursell9476bf42015-03-30 13:34:27 -0700960
Alex Klein1699fab2022-09-08 08:46:06 -0600961 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600962 DeployError: Unrecoverable error during emerge.
Alex Klein1699fab2022-09-08 08:46:06 -0600963 """
Mike Frysinger63d35512021-01-26 23:16:13 -0500964
Alex Klein1699fab2022-09-08 08:46:06 -0600965 def path_to_name(pkg_path):
966 return os.path.basename(pkg_path)
Mike Frysinger63d35512021-01-26 23:16:13 -0500967
Alex Klein1699fab2022-09-08 08:46:06 -0600968 def path_to_category(pkg_path):
969 return os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700970
Alex Klein1699fab2022-09-08 08:46:06 -0600971 pkg_names = ", ".join(path_to_name(x) for x in pkg_paths)
David Pursell9476bf42015-03-30 13:34:27 -0700972
Alex Klein1699fab2022-09-08 08:46:06 -0600973 pkgroot = os.path.join(device.work_dir, "packages")
974 portage_tmpdir = os.path.join(device.work_dir, "portage-tmp")
Alex Klein975e86c2023-01-23 16:49:10 -0700975 # Clean out the dirs first if we had a previous emerge on the device so as
976 # to free up space for this emerge. The last emerge gets implicitly cleaned
977 # up when the device connection deletes its work_dir.
Alex Klein1699fab2022-09-08 08:46:06 -0600978 device.run(
979 f"cd {device.work_dir} && "
980 f"rm -rf packages portage-tmp && "
981 f"mkdir -p portage-tmp packages && "
982 f"cd packages && "
983 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
984 shell=True,
985 remote_sudo=True,
986 )
Mike Frysinger63d35512021-01-26 23:16:13 -0500987
Alex Klein1699fab2022-09-08 08:46:06 -0600988 logging.info("Use portage temp dir %s", portage_tmpdir)
David Pursell9476bf42015-03-30 13:34:27 -0700989
Mike Frysinger63d35512021-01-26 23:16:13 -0500990 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -0600991 logging.notice("Copying binpkgs to device.")
992 for pkg_path in pkg_paths:
993 pkg_name = path_to_name(pkg_path)
994 logging.info("Copying %s", pkg_name)
995 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
996 device.CopyToDevice(
997 pkg_path, pkg_dir, mode="rsync", remote_sudo=True, compress=False
998 )
999
1000 # This message is read by BrilloDeployOperation.
1001 logging.notice("Installing: %s", pkg_names)
1002
1003 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
1004 # chromeos-base packages will be skipped due to the configuration
1005 # in /etc/protage/make.profile/package.provided. However, there is
1006 # a known bug that /usr/local/etc/portage is not setup properly
1007 # (crbug.com/312041). This does not affect `cros deploy` because
1008 # we do not use the preset PKGDIR.
1009 extra_env = {
1010 "FEATURES": "-sandbox",
1011 "PKGDIR": pkgroot,
1012 "PORTAGE_CONFIGROOT": "/usr/local",
1013 "PORTAGE_TMPDIR": portage_tmpdir,
1014 "PORTDIR": device.work_dir,
1015 "CONFIG_PROTECT": "-*",
1016 }
1017
Alex Klein975e86c2023-01-23 16:49:10 -07001018 # --ignore-built-slot-operator-deps because we don't rebuild everything. It
1019 # can cause errors, but that's expected with cros deploy since it's just a
Alex Klein1699fab2022-09-08 08:46:06 -06001020 # best effort to prevent developers avoid rebuilding an image every time.
1021 cmd = [
1022 "emerge",
1023 "--usepkg",
1024 "--ignore-built-slot-operator-deps=y",
1025 "--root",
1026 root,
1027 ] + [os.path.join(pkgroot, *x.split("/")[-2:]) for x in pkg_paths]
1028 if extra_args:
1029 cmd.append(extra_args)
1030
1031 logging.warning(
1032 "Ignoring slot dependencies! This may break things! e.g. "
1033 "packages built against the old version may not be able to "
1034 "load the new .so. This is expected, and you will just need "
1035 "to build and flash a new image if you have problems."
1036 )
1037 try:
1038 result = device.run(
1039 cmd,
1040 extra_env=extra_env,
1041 remote_sudo=True,
1042 capture_output=True,
1043 debug_level=logging.INFO,
1044 )
1045
1046 pattern = (
1047 "A requested package will not be merged because "
1048 "it is listed in package.provided"
1049 )
1050 output = result.stderr.replace("\n", " ").replace("\r", "")
1051 if pattern in output:
1052 error = (
1053 "Package failed to emerge: %s\n"
1054 "Remove %s from /etc/portage/make.profile/"
1055 "package.provided/chromeos-base.packages\n"
1056 "(also see crbug.com/920140 for more context)\n"
1057 % (pattern, pkg_name)
1058 )
1059 cros_build_lib.Die(error)
1060 except Exception:
1061 logging.error("Failed to emerge packages %s", pkg_names)
1062 raise
1063 else:
1064 # This message is read by BrilloDeployOperation.
1065 logging.notice("Packages have been installed.")
David Pursell9476bf42015-03-30 13:34:27 -07001066
1067
Tim Baine4a783b2023-04-21 20:05:51 +00001068def _RestoreSELinuxContext(
1069 device: remote_access.RemoteDevice, pkgpath: str, root: str
1070) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001071 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +09001072
Alex Klein1699fab2022-09-08 08:46:06 -06001073 This reads the tarball from pkgpath, and calls restorecon on device to
Alex Klein975e86c2023-01-23 16:49:10 -07001074 restore SELinux context for files listed in the tarball, assuming those
1075 files are installed to /
Qijiang Fan8a945032019-04-25 20:53:29 +09001076
Alex Klein1699fab2022-09-08 08:46:06 -06001077 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001078 device: a ChromiumOSDevice object
1079 pkgpath: path to tarball
1080 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001081 """
1082 pkgroot = os.path.join(device.work_dir, "packages")
1083 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
1084 pkgpath_device = os.path.join(
1085 pkgroot, pkg_dirname, os.path.basename(pkgpath)
1086 )
1087 # Testing shows restorecon splits on newlines instead of spaces.
1088 device.run(
1089 [
1090 "cd",
1091 root,
1092 "&&",
1093 "tar",
1094 "tf",
1095 pkgpath_device,
1096 "|",
1097 "restorecon",
1098 "-i",
1099 "-f",
1100 "-",
1101 ],
1102 remote_sudo=True,
1103 )
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001104
1105
Tim Baine4a783b2023-04-21 20:05:51 +00001106def _GetPackagesByCPV(
1107 cpvs: List[package_info.CPV], strip: bool, sysroot: str
1108) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001109 """Returns paths to binary packages corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001110
Alex Klein1699fab2022-09-08 08:46:06 -06001111 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001112 cpvs: List of CPV components given by package_info.SplitCPV().
1113 strip: True to run strip_package.
1114 sysroot: Sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001115
Alex Klein1699fab2022-09-08 08:46:06 -06001116 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001117 List of paths corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001118
Alex Klein1699fab2022-09-08 08:46:06 -06001119 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001120 DeployError: If a package is missing.
Alex Klein1699fab2022-09-08 08:46:06 -06001121 """
1122 packages_dir = None
1123 if strip:
1124 try:
1125 cros_build_lib.run(
1126 [
Mike Frysinger5429f302023-03-27 15:48:52 -04001127 constants.CHROMITE_SCRIPTS_DIR / "strip_package",
Alex Klein1699fab2022-09-08 08:46:06 -06001128 "--sysroot",
1129 sysroot,
1130 ]
1131 + [cpv.cpf for cpv in cpvs]
1132 )
1133 packages_dir = _STRIPPED_PACKAGES_DIR
1134 except cros_build_lib.RunCommandError:
1135 logging.error(
1136 "Cannot strip packages %s", " ".join([str(cpv) for cpv in cpvs])
1137 )
1138 raise
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001139
Alex Klein1699fab2022-09-08 08:46:06 -06001140 paths = []
1141 for cpv in cpvs:
1142 path = portage_util.GetBinaryPackagePath(
1143 cpv.category,
1144 cpv.package,
1145 cpv.version,
1146 sysroot=sysroot,
1147 packages_dir=packages_dir,
1148 )
1149 if not path:
1150 raise DeployError("Missing package %s." % cpv)
1151 paths.append(path)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001152
Alex Klein1699fab2022-09-08 08:46:06 -06001153 return paths
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001154
1155
Tim Baine4a783b2023-04-21 20:05:51 +00001156def _GetPackagesPaths(pkgs: List[str], strip: bool, sysroot: str) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001157 """Returns paths to binary |pkgs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001158
Alex Klein1699fab2022-09-08 08:46:06 -06001159 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001160 pkgs: List of package CPVs string.
1161 strip: Whether or not to run strip_package for CPV packages.
1162 sysroot: The sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001163
Alex Klein1699fab2022-09-08 08:46:06 -06001164 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001165 List of paths corresponding to |pkgs|.
Alex Klein1699fab2022-09-08 08:46:06 -06001166 """
1167 cpvs = [package_info.SplitCPV(p) for p in pkgs]
1168 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001169
1170
Tim Baine4a783b2023-04-21 20:05:51 +00001171def _Unmerge(
1172 device: remote_access.RemoteDevice, pkgs: List[str], root: str
1173) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001174 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -07001175
Alex Klein1699fab2022-09-08 08:46:06 -06001176 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001177 device: A RemoteDevice object.
1178 pkgs: Package names.
1179 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001180 """
1181 pkg_names = ", ".join(os.path.basename(x) for x in pkgs)
Mike Frysinger22bb5502021-01-29 13:05:46 -05001182 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001183 logging.notice("Unmerging %s.", pkg_names)
1184 cmd = ["qmerge", "--yes"]
1185 # Check if qmerge is available on the device. If not, use emerge.
1186 if device.run(["qmerge", "--version"], check=False).returncode != 0:
1187 cmd = ["emerge"]
1188
1189 cmd += ["--unmerge", "--root", root]
1190 cmd.extend("f={x}" for x in pkgs)
1191 try:
1192 # Always showing the emerge output for clarity.
1193 device.run(
1194 cmd,
1195 capture_output=False,
1196 remote_sudo=True,
1197 debug_level=logging.INFO,
1198 )
1199 except Exception:
1200 logging.error("Failed to unmerge packages %s", pkg_names)
1201 raise
1202 else:
1203 # This message is read by BrilloDeployOperation.
1204 logging.notice("Packages have been uninstalled.")
David Pursell9476bf42015-03-30 13:34:27 -07001205
1206
Tim Baine4a783b2023-04-21 20:05:51 +00001207def _ConfirmDeploy(num_updates: int) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -06001208 """Returns whether we can continue deployment."""
1209 if num_updates > _MAX_UPDATES_NUM:
1210 logging.warning(_MAX_UPDATES_WARNING)
1211 return cros_build_lib.BooleanPrompt(default=False)
David Pursell9476bf42015-03-30 13:34:27 -07001212
Alex Klein1699fab2022-09-08 08:46:06 -06001213 return True
David Pursell9476bf42015-03-30 13:34:27 -07001214
1215
Tim Baine4a783b2023-04-21 20:05:51 +00001216def _ConfirmUpdateDespiteWarnings() -> bool:
1217 """Returns whether we can continue updating despite warnings."""
1218 logging.warning("Continue despite prior warnings?")
1219 return cros_build_lib.BooleanPrompt(default=False)
1220
1221
1222def _EmergePackages(
1223 pkgs: List[str],
1224 device: remote_access.RemoteDevice,
1225 strip: bool,
1226 sysroot: str,
1227 root: str,
1228 board: str,
1229 emerge_args: List[str],
1230) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001231 """Call _Emerge for each package in pkgs."""
Ben Pastene5f03b052019-08-12 18:03:24 -07001232 if device.IsSELinuxAvailable():
Alex Klein1699fab2022-09-08 08:46:06 -06001233 enforced = device.IsSELinuxEnforced()
1234 if enforced:
1235 device.run(["setenforce", "0"])
1236 else:
1237 enforced = False
Andrewc7e1c6b2020-02-27 16:03:53 -08001238
Alex Klein1699fab2022-09-08 08:46:06 -06001239 dlc_deployed = False
1240 # This message is read by BrilloDeployOperation.
1241 logging.info("Preparing local packages for transfer.")
1242 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
1243 # Install all the packages in one pass so inter-package blockers work.
1244 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
1245 logging.info("Updating SELinux settings & DLC images.")
1246 for pkg_path in pkg_paths:
1247 if device.IsSELinuxAvailable():
1248 _RestoreSELinuxContext(device, pkg_path, root)
Mike Frysinger5f4c2742021-02-08 14:37:23 -05001249
Alex Klein1699fab2022-09-08 08:46:06 -06001250 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
1251 if dlc_id and dlc_package:
1252 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
1253 dlc_deployed = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001254
Alex Klein1699fab2022-09-08 08:46:06 -06001255 if dlc_deployed:
1256 # Clean up empty directories created by emerging DLCs.
1257 device.run(
1258 [
1259 "test",
1260 "-d",
1261 "/build/rootfs",
1262 "&&",
1263 "rmdir",
1264 "--ignore-fail-on-non-empty",
1265 "/build/rootfs",
1266 "/build",
1267 ],
1268 check=False,
1269 )
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001270
Alex Klein1699fab2022-09-08 08:46:06 -06001271 if enforced:
1272 device.run(["setenforce", "1"])
1273
1274 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1275 # we installed new DLC images).
1276 if dlc_deployed:
1277 device.run(["restart", "dlcservice"])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001278
1279
Tim Baine4a783b2023-04-21 20:05:51 +00001280def _UnmergePackages(
1281 pkgs: List[str],
1282 device: remote_access.RemoteDevice,
1283 root: str,
1284 pkgs_attrs: Dict[str, List[str]],
1285) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -06001286 """Call _Unmege for each package in pkgs."""
1287 dlc_uninstalled = False
1288 _Unmerge(device, pkgs, root)
1289 logging.info("Cleaning up DLC images.")
1290 for pkg in pkgs:
1291 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1292 dlc_uninstalled = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001293
Alex Klein1699fab2022-09-08 08:46:06 -06001294 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1295 # uninstalled DLC images).
1296 if dlc_uninstalled:
1297 device.run(["restart", "dlcservice"])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001298
1299
Tim Baine4a783b2023-04-21 20:05:51 +00001300def _UninstallDLCImage(
1301 device: remote_access.RemoteDevice, pkg_attrs: Dict[str, List[str]]
1302):
Alex Klein1699fab2022-09-08 08:46:06 -06001303 """Uninstall a DLC image."""
1304 if _DLC_ID in pkg_attrs:
1305 dlc_id = pkg_attrs[_DLC_ID]
1306 logging.notice("Uninstalling DLC image for %s", dlc_id)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001307
Alex Klein1699fab2022-09-08 08:46:06 -06001308 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1309 return True
1310 else:
1311 logging.debug("DLC_ID not found in package")
1312 return False
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001313
1314
Tim Baine4a783b2023-04-21 20:05:51 +00001315def _DeployDLCImage(
1316 device: remote_access.RemoteDevice,
1317 sysroot: str,
1318 board: str,
1319 dlc_id: str,
1320 dlc_package: str,
1321):
Alex Klein1699fab2022-09-08 08:46:06 -06001322 """Deploy (install and mount) a DLC image.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001323
Alex Klein1699fab2022-09-08 08:46:06 -06001324 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001325 device: A device object.
1326 sysroot: The sysroot path.
1327 board: Board to use.
1328 dlc_id: The DLC ID.
1329 dlc_package: The DLC package name.
Alex Klein1699fab2022-09-08 08:46:06 -06001330 """
1331 # Requires `sudo_rm` because installations of files are running with sudo.
1332 with osutils.TempDir(sudo_rm=True) as tempdir:
1333 temp_rootfs = Path(tempdir)
1334 # Build the DLC image if the image is outdated or doesn't exist.
1335 dlc_lib.InstallDlcImages(
1336 sysroot=sysroot, rootfs=temp_rootfs, dlc_id=dlc_id, board=board
1337 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001338
Alex Klein1699fab2022-09-08 08:46:06 -06001339 logging.debug("Uninstall DLC %s if it is installed.", dlc_id)
1340 try:
1341 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1342 except cros_build_lib.RunCommandError as e:
1343 logging.info(
1344 "Failed to uninstall DLC:%s. Continue anyway.", e.stderr
1345 )
1346 except Exception:
1347 logging.error("Failed to uninstall DLC.")
1348 raise
Andrewc7e1c6b2020-02-27 16:03:53 -08001349
Alex Klein1699fab2022-09-08 08:46:06 -06001350 logging.notice("Deploy the DLC image for %s", dlc_id)
1351 dlc_img_path_src = os.path.join(
1352 sysroot,
1353 dlc_lib.DLC_BUILD_DIR,
1354 dlc_id,
1355 dlc_package,
1356 dlc_lib.DLC_IMAGE,
1357 )
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001358 if not os.path.exists(dlc_img_path_src):
1359 dlc_img_path_src = os.path.join(
1360 sysroot,
1361 dlc_lib.DLC_BUILD_DIR_SCALED,
1362 dlc_id,
1363 dlc_package,
1364 dlc_lib.DLC_IMAGE,
1365 )
1366
Alex Klein1699fab2022-09-08 08:46:06 -06001367 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1368 dlc_img_path_a = os.path.join(dlc_img_path, "dlc_a")
1369 dlc_img_path_b = os.path.join(dlc_img_path, "dlc_b")
1370 # Create directories for DLC images.
1371 device.run(["mkdir", "-p", dlc_img_path_a, dlc_img_path_b])
1372 # Copy images to the destination directories.
1373 device.CopyToDevice(
1374 dlc_img_path_src,
1375 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1376 mode="rsync",
1377 )
1378 device.run(
1379 [
1380 "cp",
1381 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1382 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE),
1383 ]
1384 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001385
Alex Klein1699fab2022-09-08 08:46:06 -06001386 # Set the proper perms and ownership so dlcservice can access the image.
1387 device.run(["chmod", "-R", "u+rwX,go+rX,go-w", _DLC_INSTALL_ROOT])
1388 device.run(["chown", "-R", "dlcservice:dlcservice", _DLC_INSTALL_ROOT])
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001389
Alex Klein1699fab2022-09-08 08:46:06 -06001390 # Copy metadata to device.
1391 dest_meta_dir = Path("/") / dlc_lib.DLC_META_DIR / dlc_id / dlc_package
1392 device.run(["mkdir", "-p", dest_meta_dir])
1393 src_meta_dir = os.path.join(
1394 sysroot,
1395 dlc_lib.DLC_BUILD_DIR,
1396 dlc_id,
1397 dlc_package,
1398 dlc_lib.DLC_TMP_META_DIR,
1399 )
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001400 if not os.path.exists(src_meta_dir):
1401 src_meta_dir = os.path.join(
1402 sysroot,
1403 dlc_lib.DLC_BUILD_DIR_SCALED,
1404 dlc_id,
1405 dlc_package,
1406 dlc_lib.DLC_TMP_META_DIR,
1407 )
Alex Klein1699fab2022-09-08 08:46:06 -06001408 device.CopyToDevice(
1409 src_meta_dir + "/",
1410 dest_meta_dir,
1411 mode="rsync",
1412 recursive=True,
1413 remote_sudo=True,
1414 )
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001415
Alex Klein1699fab2022-09-08 08:46:06 -06001416 # TODO(kimjae): Make this generic so it recomputes all the DLCs + copies
Alex Klein975e86c2023-01-23 16:49:10 -07001417 # over a fresh list of dm-verity digests instead of appending and
1418 # keeping the stale digests when developers are testing.
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001419
Alex Klein1699fab2022-09-08 08:46:06 -06001420 # Copy the LoadPin dm-verity digests to device.
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001421 _DeployDLCLoadPin(temp_rootfs, device)
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001422
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001423
1424def _DeployDLCLoadPin(
Tim Baine4a783b2023-04-21 20:05:51 +00001425 rootfs: os.PathLike, device: remote_access.RemoteDevice
1426) -> None:
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001427 """Deploy DLC LoadPin from temp rootfs to device.
1428
1429 Args:
1430 rootfs: Path to rootfs.
1431 device: A device object.
1432 """
1433 loadpin = dlc_lib.DLC_LOADPIN_TRUSTED_VERITY_DIGESTS
1434 dst_loadpin = Path("/") / dlc_lib.DLC_META_DIR / loadpin
1435 src_loadpin = rootfs / dlc_lib.DLC_META_DIR / loadpin
1436 if src_loadpin.exists():
1437 digests = set(osutils.ReadFile(src_loadpin).splitlines())
1438 digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1439 try:
1440 device_digests = set(device.CatFile(dst_loadpin).splitlines())
1441 device_digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1442 digests.update(device_digests)
1443 except remote_access.CatFileError:
1444 pass
1445
1446 with tempfile.NamedTemporaryFile(dir=rootfs) as f:
1447 osutils.WriteFile(f.name, dlc_lib.DLC_LOADPIN_FILE_HEADER + "\n")
1448 osutils.WriteFile(f.name, "\n".join(digests) + "\n", mode="a")
1449 device.CopyToDevice(
1450 f.name, dst_loadpin, mode="rsync", remote_sudo=True
1451 )
Andrew67b5fa72020-02-05 14:14:48 -08001452
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001453
Tim Baine4a783b2023-04-21 20:05:51 +00001454def _GetDLCInfo(
1455 device: remote_access.RemoteDevice, pkg_path: str, from_dut: bool
1456) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001457 """Returns information of a DLC given its package path.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001458
Alex Klein1699fab2022-09-08 08:46:06 -06001459 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001460 device: commandline.Device object; None to use the default device.
1461 pkg_path: path to the package.
1462 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1463 info from host.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001464
Alex Klein1699fab2022-09-08 08:46:06 -06001465 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001466 A tuple (dlc_id, dlc_package).
Alex Klein1699fab2022-09-08 08:46:06 -06001467 """
1468 environment_content = ""
1469 if from_dut:
1470 # On DUT, |pkg_path| is the directory which contains environment file.
1471 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1472 try:
1473 environment_data = device.CatFile(
1474 environment_path, max_size=None, encoding=None
1475 )
1476 except remote_access.CatFileError:
1477 # The package is not installed on DUT yet. Skip extracting info.
1478 return None, None
1479 else:
1480 # On host, pkg_path is tbz2 file which contains environment file.
1481 # Extract the metadata of the package file.
1482 data = portage.xpak.tbz2(pkg_path).get_data()
1483 environment_data = data[_ENVIRONMENT_FILENAME.encode("utf-8")]
1484
1485 # Extract the environment metadata.
1486 environment_content = bz2.decompress(environment_data)
1487
1488 with tempfile.NamedTemporaryFile() as f:
1489 # Dumps content into a file so we can use osutils.SourceEnvironment.
1490 path = os.path.realpath(f.name)
1491 osutils.WriteFile(path, environment_content, mode="wb")
1492 content = osutils.SourceEnvironment(
1493 path, (_DLC_ID, _DLC_PACKAGE, _DLC_ENABLED)
1494 )
1495
1496 dlc_enabled = content.get(_DLC_ENABLED)
1497 if dlc_enabled is not None and (
1498 dlc_enabled is False or str(dlc_enabled) == "false"
1499 ):
1500 logging.info("Installing DLC in rootfs.")
1501 return None, None
1502 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
1503
1504
1505def Deploy(
Tim Baine4a783b2023-04-21 20:05:51 +00001506 device: remote_access.RemoteDevice,
1507 packages: List[str],
1508 board: str = None,
1509 emerge: bool = True,
1510 update: bool = False,
1511 deep: bool = False,
1512 deep_rev: bool = False,
1513 clean_binpkg: bool = True,
1514 root: str = "/",
1515 strip: bool = True,
1516 emerge_args: List[str] = None,
1517 ssh_private_key: str = None,
1518 ping: bool = True,
1519 force: bool = False,
1520 dry_run: bool = False,
1521) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001522 """Deploys packages to a device.
1523
1524 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001525 device: commandline.Device object; None to use the default device.
1526 packages: List of packages (strings) to deploy to device.
1527 board: Board to use; None to automatically detect.
1528 emerge: True to emerge package, False to unmerge.
1529 update: Check installed version on device.
1530 deep: Install dependencies also. Implies |update|.
1531 deep_rev: Install reverse dependencies. Implies |deep|.
1532 clean_binpkg: Clean outdated binary packages.
1533 root: Package installation root path.
1534 strip: Run strip_package to filter out preset paths in the package.
1535 emerge_args: Extra arguments to pass to emerge.
1536 ssh_private_key: Path to an SSH private key file; None to use test keys.
1537 ping: True to ping the device before trying to connect.
1538 force: Ignore confidence checks and prompts.
1539 dry_run: Print deployment plan but do not deploy anything.
Alex Klein1699fab2022-09-08 08:46:06 -06001540
1541 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001542 ValueError: Invalid parameter or parameter combination.
1543 DeployError: Unrecoverable failure during deploy.
Alex Klein1699fab2022-09-08 08:46:06 -06001544 """
1545 if deep_rev:
1546 deep = True
1547 if deep:
1548 update = True
1549
1550 if not packages:
1551 raise DeployError("No packages provided, nothing to deploy.")
1552
1553 if update and not emerge:
1554 raise ValueError("Cannot update and unmerge.")
1555
1556 if device:
1557 hostname, username, port = device.hostname, device.username, device.port
1558 else:
1559 hostname, username, port = None, None, None
1560
1561 lsb_release = None
1562 sysroot = None
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001563 try:
Alex Klein1699fab2022-09-08 08:46:06 -06001564 # Somewhat confusing to clobber, but here we are.
1565 # pylint: disable=redefined-argument-from-local
1566 with remote_access.ChromiumOSDeviceHandler(
1567 hostname,
1568 port=port,
1569 username=username,
1570 private_key=ssh_private_key,
1571 base_dir=_DEVICE_BASE_DIR,
1572 ping=ping,
1573 ) as device:
1574 lsb_release = device.lsb_release
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001575
Alex Klein1699fab2022-09-08 08:46:06 -06001576 board = cros_build_lib.GetBoard(
1577 device_board=device.board, override_board=board
1578 )
1579 if not force and board != device.board:
1580 raise DeployError(
1581 "Device (%s) is incompatible with board %s. Use "
1582 "--force to deploy anyway." % (device.board, board)
1583 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001584
Alex Klein1699fab2022-09-08 08:46:06 -06001585 sysroot = build_target_lib.get_default_sysroot_path(board)
Andrew67b5fa72020-02-05 14:14:48 -08001586
Alex Klein975e86c2023-01-23 16:49:10 -07001587 # Don't bother trying to clean for unmerges. We won't use the local
1588 # db, and it just slows things down for the user.
Alex Klein1699fab2022-09-08 08:46:06 -06001589 if emerge and clean_binpkg:
1590 logging.notice(
1591 "Cleaning outdated binary packages from %s", sysroot
1592 )
1593 portage_util.CleanOutdatedBinaryPackages(sysroot)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001594
Alex Klein1699fab2022-09-08 08:46:06 -06001595 # Remount rootfs as writable if necessary.
1596 if not device.MountRootfsReadWrite():
1597 raise DeployError(
1598 "Cannot remount rootfs as read-write. Exiting."
1599 )
Ralph Nathane01ccf12015-04-16 10:40:32 -07001600
Alex Klein1699fab2022-09-08 08:46:06 -06001601 # Obtain list of packages to upgrade/remove.
1602 pkg_scanner = _InstallPackageScanner(sysroot)
Tim Baine4a783b2023-04-21 20:05:51 +00001603 (
1604 pkgs,
1605 listed,
1606 num_updates,
1607 pkgs_attrs,
1608 warnings_shown,
1609 ) = pkg_scanner.Run(device, root, packages, update, deep, deep_rev)
Alex Klein1699fab2022-09-08 08:46:06 -06001610 if emerge:
1611 action_str = "emerge"
1612 else:
1613 pkgs.reverse()
1614 action_str = "unmerge"
David Pursell9476bf42015-03-30 13:34:27 -07001615
Alex Klein1699fab2022-09-08 08:46:06 -06001616 if not pkgs:
1617 logging.notice("No packages to %s", action_str)
1618 return
David Pursell9476bf42015-03-30 13:34:27 -07001619
Alex Klein1699fab2022-09-08 08:46:06 -06001620 # Warn when the user installs & didn't `cros workon start`.
1621 if emerge:
1622 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(
1623 use_all=True
1624 )
1625 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1626 for package in listed:
1627 cp = package_info.SplitCPV(package).cp
1628 if cp in all_workon and cp not in worked_on_cps:
1629 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001630 "Are you intentionally deploying unmodified "
1631 "packages, or did you forget to run "
1632 "`cros workon --board=$BOARD start %s`?",
Alex Klein1699fab2022-09-08 08:46:06 -06001633 cp,
1634 )
David Pursell9476bf42015-03-30 13:34:27 -07001635
Alex Klein1699fab2022-09-08 08:46:06 -06001636 logging.notice("These are the packages to %s:", action_str)
1637 for i, pkg in enumerate(pkgs):
1638 logging.notice(
1639 "%s %d) %s", "*" if pkg in listed else " ", i + 1, pkg
1640 )
Gilad Arnolda0a98062015-07-07 08:34:27 -07001641
Alex Klein1699fab2022-09-08 08:46:06 -06001642 if dry_run or not _ConfirmDeploy(num_updates):
1643 return
David Pursell9476bf42015-03-30 13:34:27 -07001644
Tim Baine4a783b2023-04-21 20:05:51 +00001645 if (
1646 warnings_shown
1647 and not force
1648 and not _ConfirmUpdateDespiteWarnings()
1649 ):
1650 return
1651
Alex Klein1699fab2022-09-08 08:46:06 -06001652 # Select function (emerge or unmerge) and bind args.
1653 if emerge:
1654 func = functools.partial(
1655 _EmergePackages,
1656 pkgs,
1657 device,
1658 strip,
1659 sysroot,
1660 root,
1661 board,
1662 emerge_args,
1663 )
1664 else:
1665 func = functools.partial(
1666 _UnmergePackages, pkgs, device, root, pkgs_attrs
1667 )
David Pursell2e773382015-04-03 14:30:47 -07001668
Alex Klein1699fab2022-09-08 08:46:06 -06001669 # Call the function with the progress bar or with normal output.
1670 if command.UseProgressBar():
1671 op = BrilloDeployOperation(emerge)
1672 op.Run(func, log_level=logging.DEBUG)
1673 else:
1674 func()
David Pursell9476bf42015-03-30 13:34:27 -07001675
Alex Klein1699fab2022-09-08 08:46:06 -06001676 if device.IsSELinuxAvailable():
1677 if sum(x.count("selinux-policy") for x in pkgs):
1678 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001679 "Deploying SELinux policy will not take effect until "
1680 "reboot. SELinux policy is loaded by init. Also, "
1681 "changing the security contexts (labels) of a file "
1682 "will require building a new image and flashing the "
1683 "image onto the device."
Alex Klein1699fab2022-09-08 08:46:06 -06001684 )
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001685
Alex Klein1699fab2022-09-08 08:46:06 -06001686 # This message is read by BrilloDeployOperation.
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001687 logging.warning(
Alex Klein1699fab2022-09-08 08:46:06 -06001688 "Please restart any updated services on the device, "
1689 "or just reboot it."
1690 )
1691 except Exception:
1692 if lsb_release:
1693 lsb_entries = sorted(lsb_release.items())
1694 logging.info(
1695 "Following are the LSB version details of the device:\n%s",
1696 "\n".join("%s=%s" % (k, v) for k, v in lsb_entries),
1697 )
1698 raise