blob: 253b8822480b1cae05d05824f478a7c2e0a1d8b1 [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 Bain980db312023-04-26 17:29:00 +000021from typing import Dict, List, NamedTuple, 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
Tim Bain980db312023-04-26 17:29:00 +000062class CpvInfo(NamedTuple):
63 """Holds a CPV and its associated information that we care about"""
64
65 cpv: Dict[str, package_info.CPV]
66 slot: str
67 rdep_raw: str
68 build_time: int
69 use: str
70
71
David Pursell9476bf42015-03-30 13:34:27 -070072class DeployError(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060073 """Thrown when an unrecoverable error is encountered during deploy."""
David Pursell9476bf42015-03-30 13:34:27 -070074
75
Ralph Nathane01ccf12015-04-16 10:40:32 -070076class BrilloDeployOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -060077 """ProgressBarOperation specific for brillo deploy."""
Ralph Nathane01ccf12015-04-16 10:40:32 -070078
Alex Klein1699fab2022-09-08 08:46:06 -060079 # These two variables are used to validate the output in the VM integration
80 # tests. Changes to the output must be reflected here.
81 MERGE_EVENTS = (
82 "Preparing local packages",
83 "NOTICE: Copying binpkgs",
84 "NOTICE: Installing",
85 "been installed.",
86 "Please restart any updated",
87 )
88 UNMERGE_EVENTS = (
89 "NOTICE: Unmerging",
90 "been uninstalled.",
91 "Please restart any updated",
92 )
Ralph Nathane01ccf12015-04-16 10:40:32 -070093
Tim Baine4a783b2023-04-21 20:05:51 +000094 def __init__(self, emerge: bool):
Alex Klein1699fab2022-09-08 08:46:06 -060095 """Construct BrilloDeployOperation object.
Ralph Nathane01ccf12015-04-16 10:40:32 -070096
Alex Klein1699fab2022-09-08 08:46:06 -060097 Args:
Tim Baine4a783b2023-04-21 20:05:51 +000098 emerge: True if emerge, False if unmerge.
Alex Klein1699fab2022-09-08 08:46:06 -060099 """
100 super().__init__()
101 if emerge:
102 self._events = self.MERGE_EVENTS
103 else:
104 self._events = self.UNMERGE_EVENTS
105 self._total = len(self._events)
106 self._completed = 0
107
108 def ParseOutput(self, output=None):
109 """Parse the output of brillo deploy to update a progress bar."""
110 stdout = self._stdout.read()
111 stderr = self._stderr.read()
112 output = stdout + stderr
113 for event in self._events:
114 self._completed += output.count(event)
115 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700116
117
Alex Klein074f94f2023-06-22 10:32:06 -0600118class _InstallPackageScanner:
Alex Klein1699fab2022-09-08 08:46:06 -0600119 """Finds packages that need to be installed on a target device.
David Pursell9476bf42015-03-30 13:34:27 -0700120
Alex Klein1699fab2022-09-08 08:46:06 -0600121 Scans the sysroot bintree, beginning with a user-provided list of packages,
122 to find all packages that need to be installed. If so instructed,
123 transitively scans forward (mandatory) and backward (optional) dependencies
124 as well. A package will be installed if missing on the target (mandatory
125 packages only), or it will be updated if its sysroot version and build time
126 are different from the target. Common usage:
David Pursell9476bf42015-03-30 13:34:27 -0700127
Alex Klein53cc3bf2022-10-13 08:50:01 -0600128 pkg_scanner = _InstallPackageScanner(sysroot)
129 pkgs = pkg_scanner.Run(...)
Alex Klein1699fab2022-09-08 08:46:06 -0600130 """
David Pursell9476bf42015-03-30 13:34:27 -0700131
Alex Klein1699fab2022-09-08 08:46:06 -0600132 class VartreeError(Exception):
133 """An error in the processing of the installed packages tree."""
David Pursell9476bf42015-03-30 13:34:27 -0700134
Alex Klein1699fab2022-09-08 08:46:06 -0600135 class BintreeError(Exception):
136 """An error in the processing of the source binpkgs tree."""
David Pursell9476bf42015-03-30 13:34:27 -0700137
Alex Klein074f94f2023-06-22 10:32:06 -0600138 class PkgInfo:
Alex Klein1699fab2022-09-08 08:46:06 -0600139 """A record containing package information."""
David Pursell9476bf42015-03-30 13:34:27 -0700140
Tim Baine4a783b2023-04-21 20:05:51 +0000141 __slots__ = (
142 "cpv",
143 "build_time",
144 "rdeps_raw",
145 "use",
146 "rdeps",
147 "rev_rdeps",
148 )
David Pursell9476bf42015-03-30 13:34:27 -0700149
Alex Klein1699fab2022-09-08 08:46:06 -0600150 def __init__(
Tim Baine4a783b2023-04-21 20:05:51 +0000151 self,
152 cpv: package_info.CPV,
153 build_time: int,
154 rdeps_raw: str,
155 use: str,
156 rdeps: set = None,
157 rev_rdeps: set = None,
Alex Klein1699fab2022-09-08 08:46:06 -0600158 ):
159 self.cpv = cpv
160 self.build_time = build_time
161 self.rdeps_raw = rdeps_raw
Tim Baine4a783b2023-04-21 20:05:51 +0000162 self.use = use
Alex Klein1699fab2022-09-08 08:46:06 -0600163 self.rdeps = set() if rdeps is None else rdeps
164 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
David Pursell9476bf42015-03-30 13:34:27 -0700165
Alex Klein1699fab2022-09-08 08:46:06 -0600166 # Python snippet for dumping vartree info on the target. Instantiate using
167 # _GetVartreeSnippet().
168 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700169import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700170import os
171import portage
172
173# Normalize the path to match what portage will index.
174target_root = os.path.normpath('%(root)s')
175if not target_root.endswith('/'):
176 target_root += '/'
177trees = portage.create_trees(target_root=target_root, config_root='/')
178vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700179pkg_info = []
180for cpv in vartree.dbapi.cpv_all():
Tim Baine4a783b2023-04-21 20:05:51 +0000181 slot, rdep_raw, build_time, use = vartree.dbapi.aux_get(
182 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME', 'USE'))
183 pkg_info.append((cpv, slot, rdep_raw, build_time, use))
David Pursell9476bf42015-03-30 13:34:27 -0700184
185print(json.dumps(pkg_info))
186"""
187
Tim Baine4a783b2023-04-21 20:05:51 +0000188 def __init__(self, sysroot: str):
Alex Klein1699fab2022-09-08 08:46:06 -0600189 self.sysroot = sysroot
Alex Klein975e86c2023-01-23 16:49:10 -0700190 # Members containing the sysroot (binpkg) and target (installed) package
191 # DB.
Alex Klein1699fab2022-09-08 08:46:06 -0600192 self.target_db = None
193 self.binpkgs_db = None
194 # Members for managing the dependency resolution work queue.
195 self.queue = None
196 self.seen = None
197 self.listed = None
David Pursell9476bf42015-03-30 13:34:27 -0700198
Alex Klein1699fab2022-09-08 08:46:06 -0600199 @staticmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000200 def _GetCP(cpv: package_info.CPV) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600201 """Returns the CP value for a given CPV string."""
202 attrs = package_info.SplitCPV(cpv, strict=False)
203 if not attrs.cp:
204 raise ValueError("Cannot get CP value for %s" % cpv)
205 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700206
Alex Klein1699fab2022-09-08 08:46:06 -0600207 @staticmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000208 def _InDB(cp: str, slot: str, db: Dict[str, Dict[str, PkgInfo]]) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -0600209 """Returns whether CP and slot are found in a database (if provided)."""
210 cp_slots = db.get(cp) if db else None
211 return cp_slots is not None and (not slot or slot in cp_slots)
David Pursell9476bf42015-03-30 13:34:27 -0700212
Alex Klein1699fab2022-09-08 08:46:06 -0600213 @staticmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000214 def _AtomStr(cp: str, slot: str) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600215 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
216 return "%s:%s" % (cp, slot) if slot else cp
David Pursell9476bf42015-03-30 13:34:27 -0700217
Alex Klein1699fab2022-09-08 08:46:06 -0600218 @classmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000219 def _GetVartreeSnippet(cls, root: str = "/") -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600220 """Returns a code snippet for dumping the vartree on the target.
David Pursell9476bf42015-03-30 13:34:27 -0700221
Alex Klein1699fab2022-09-08 08:46:06 -0600222 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600223 root: The installation root.
David Pursell9476bf42015-03-30 13:34:27 -0700224
Alex Klein1699fab2022-09-08 08:46:06 -0600225 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600226 The said code snippet (string) with parameters filled in.
Alex Klein1699fab2022-09-08 08:46:06 -0600227 """
228 return cls._GET_VARTREE % {"root": root}
David Pursell9476bf42015-03-30 13:34:27 -0700229
Alex Klein1699fab2022-09-08 08:46:06 -0600230 @classmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000231 def _StripDepAtom(
232 cls, dep_atom: str, installed_db: Dict[str, Dict[str, PkgInfo]] = None
233 ) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600234 """Strips a dependency atom and returns a (CP, slot) pair."""
235 # TODO(garnold) This is a gross simplification of ebuild dependency
236 # semantics, stripping and ignoring various qualifiers (versions, slots,
237 # USE flag, negation) and will likely need to be fixed. chromium:447366.
David Pursell9476bf42015-03-30 13:34:27 -0700238
Alex Klein1699fab2022-09-08 08:46:06 -0600239 # Ignore unversioned blockers, leaving them for the user to resolve.
240 if dep_atom[0] == "!" and dep_atom[1] not in "<=>~":
241 return None, None
David Pursell9476bf42015-03-30 13:34:27 -0700242
Alex Klein1699fab2022-09-08 08:46:06 -0600243 cp = dep_atom
244 slot = None
245 require_installed = False
David Pursell9476bf42015-03-30 13:34:27 -0700246
Alex Klein1699fab2022-09-08 08:46:06 -0600247 # Versioned blockers should be updated, but only if already installed.
Alex Klein975e86c2023-01-23 16:49:10 -0700248 # These are often used for forcing cascaded updates of multiple
249 # packages, so we're treating them as ordinary constraints with hopes
250 # that it'll lead to the desired result.
Alex Klein1699fab2022-09-08 08:46:06 -0600251 if cp.startswith("!"):
252 cp = cp.lstrip("!")
253 require_installed = True
David Pursell9476bf42015-03-30 13:34:27 -0700254
Alex Klein1699fab2022-09-08 08:46:06 -0600255 # Remove USE flags.
256 if "[" in cp:
257 cp = cp[: cp.index("[")] + cp[cp.index("]") + 1 :]
David Pursell9476bf42015-03-30 13:34:27 -0700258
Alex Klein1699fab2022-09-08 08:46:06 -0600259 # Separate the slot qualifier and strip off subslots.
260 if ":" in cp:
261 cp, slot = cp.split(":")
262 for delim in ("/", "="):
263 slot = slot.split(delim, 1)[0]
David Pursell9476bf42015-03-30 13:34:27 -0700264
Alex Klein1699fab2022-09-08 08:46:06 -0600265 # Strip version wildcards (right), comparators (left).
266 cp = cp.rstrip("*")
267 cp = cp.lstrip("<=>~")
David Pursell9476bf42015-03-30 13:34:27 -0700268
Alex Klein1699fab2022-09-08 08:46:06 -0600269 # Turn into CP form.
270 cp = cls._GetCP(cp)
David Pursell9476bf42015-03-30 13:34:27 -0700271
Alex Klein1699fab2022-09-08 08:46:06 -0600272 if require_installed and not cls._InDB(cp, None, installed_db):
273 return None, None
David Pursell9476bf42015-03-30 13:34:27 -0700274
Alex Klein1699fab2022-09-08 08:46:06 -0600275 return cp, slot
David Pursell9476bf42015-03-30 13:34:27 -0700276
Alex Klein1699fab2022-09-08 08:46:06 -0600277 @classmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000278 def _ProcessDepStr(
279 cls,
280 dep_str: str,
281 installed_db: Dict[str, Dict[str, PkgInfo]],
282 avail_db: Dict[str, Dict[str, PkgInfo]],
283 ) -> set:
Alex Klein1699fab2022-09-08 08:46:06 -0600284 """Resolves and returns a list of dependencies from a dependency string.
David Pursell9476bf42015-03-30 13:34:27 -0700285
Alex Klein1699fab2022-09-08 08:46:06 -0600286 This parses a dependency string and returns a list of package names and
Alex Klein975e86c2023-01-23 16:49:10 -0700287 slots. Other atom qualifiers (version, sub-slot, block) are ignored.
288 When resolving disjunctive deps, we include all choices that are fully
289 present in |installed_db|. If none is present, we choose an arbitrary
290 one that is available.
David Pursell9476bf42015-03-30 13:34:27 -0700291
Alex Klein1699fab2022-09-08 08:46:06 -0600292 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600293 dep_str: A raw dependency string.
294 installed_db: A database of installed packages.
295 avail_db: A database of packages available for installation.
David Pursell9476bf42015-03-30 13:34:27 -0700296
Alex Klein1699fab2022-09-08 08:46:06 -0600297 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600298 A list of pairs (CP, slot).
David Pursell9476bf42015-03-30 13:34:27 -0700299
Alex Klein1699fab2022-09-08 08:46:06 -0600300 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600301 ValueError: the dependencies string is malformed.
Alex Klein1699fab2022-09-08 08:46:06 -0600302 """
David Pursell9476bf42015-03-30 13:34:27 -0700303
Tim Baine4a783b2023-04-21 20:05:51 +0000304 def ProcessSubDeps(
305 dep_exp: Set[Tuple[str, str]], disjunct: bool
306 ) -> Set[Tuple[str, str]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600307 """Parses and processes a dependency (sub)expression."""
308 deps = set()
309 default_deps = set()
310 sub_disjunct = False
311 for dep_sub_exp in dep_exp:
312 sub_deps = set()
David Pursell9476bf42015-03-30 13:34:27 -0700313
Alex Klein1699fab2022-09-08 08:46:06 -0600314 if isinstance(dep_sub_exp, (list, tuple)):
315 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
316 sub_disjunct = False
317 elif sub_disjunct:
318 raise ValueError("Malformed disjunctive operation in deps")
319 elif dep_sub_exp == "||":
320 sub_disjunct = True
321 elif dep_sub_exp.endswith("?"):
322 raise ValueError("Dependencies contain a conditional")
323 else:
324 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
325 if cp:
326 sub_deps = set([(cp, slot)])
327 elif disjunct:
328 raise ValueError("Atom in disjunct ignored")
David Pursell9476bf42015-03-30 13:34:27 -0700329
Alex Klein1699fab2022-09-08 08:46:06 -0600330 # Handle sub-deps of a disjunctive expression.
331 if disjunct:
Alex Klein975e86c2023-01-23 16:49:10 -0700332 # Make the first available choice the default, for use in
333 # case that no option is installed.
Alex Klein1699fab2022-09-08 08:46:06 -0600334 if (
335 not default_deps
336 and avail_db is not None
337 and all(
338 cls._InDB(cp, slot, avail_db)
339 for cp, slot in sub_deps
340 )
341 ):
342 default_deps = sub_deps
David Pursell9476bf42015-03-30 13:34:27 -0700343
Alex Klein975e86c2023-01-23 16:49:10 -0700344 # If not all sub-deps are installed, then don't consider
345 # them.
Alex Klein1699fab2022-09-08 08:46:06 -0600346 if not all(
347 cls._InDB(cp, slot, installed_db)
348 for cp, slot in sub_deps
349 ):
350 sub_deps = set()
David Pursell9476bf42015-03-30 13:34:27 -0700351
Alex Klein1699fab2022-09-08 08:46:06 -0600352 deps.update(sub_deps)
David Pursell9476bf42015-03-30 13:34:27 -0700353
Alex Klein1699fab2022-09-08 08:46:06 -0600354 return deps or default_deps
David Pursell9476bf42015-03-30 13:34:27 -0700355
Alex Klein1699fab2022-09-08 08:46:06 -0600356 try:
357 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
358 except portage.exception.InvalidDependString as e:
359 raise ValueError("Invalid dep string: %s" % e)
360 except ValueError as e:
361 raise ValueError("%s: %s" % (e, dep_str))
David Pursell9476bf42015-03-30 13:34:27 -0700362
Alex Klein1699fab2022-09-08 08:46:06 -0600363 def _BuildDB(
Tim Baine4a783b2023-04-21 20:05:51 +0000364 self,
Tim Bain980db312023-04-26 17:29:00 +0000365 cpv_info: List[CpvInfo],
Tim Baine4a783b2023-04-21 20:05:51 +0000366 process_rdeps: bool,
367 process_rev_rdeps: bool,
368 installed_db: Dict[str, Dict[str, PkgInfo]] = None,
369 ) -> Dict[str, Dict[str, PkgInfo]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600370 """Returns a database of packages given a list of CPV info.
David Pursell9476bf42015-03-30 13:34:27 -0700371
Alex Klein1699fab2022-09-08 08:46:06 -0600372 Args:
Tim Bain980db312023-04-26 17:29:00 +0000373 cpv_info: A list of CpvInfos containing package CPV and attributes.
Alex Klein53cc3bf2022-10-13 08:50:01 -0600374 process_rdeps: Whether to populate forward dependencies.
375 process_rev_rdeps: Whether to populate reverse dependencies.
376 installed_db: A database of installed packages for filtering
377 disjunctive choices against; if None, using own built database.
David Pursell9476bf42015-03-30 13:34:27 -0700378
Alex Klein1699fab2022-09-08 08:46:06 -0600379 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600380 A map from CP values to another dictionary that maps slots
381 to package attribute tuples. Tuples contain a CPV value
382 (string), build time (string), runtime dependencies (set),
383 and reverse dependencies (set, empty if not populated).
David Pursell9476bf42015-03-30 13:34:27 -0700384
Alex Klein1699fab2022-09-08 08:46:06 -0600385 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600386 ValueError: If more than one CPV occupies a single slot.
Alex Klein1699fab2022-09-08 08:46:06 -0600387 """
388 db = {}
389 logging.debug("Populating package DB...")
Tim Baine4a783b2023-04-21 20:05:51 +0000390 for cpv, slot, rdeps_raw, build_time, use in cpv_info:
Alex Klein1699fab2022-09-08 08:46:06 -0600391 cp = self._GetCP(cpv)
Trent Apted1e2e4f32023-05-05 03:50:20 +0000392 cp_slots = db.setdefault(cp, {})
Alex Klein1699fab2022-09-08 08:46:06 -0600393 if slot in cp_slots:
394 raise ValueError(
395 "More than one package found for %s"
396 % self._AtomStr(cp, slot)
397 )
398 logging.debug(
399 " %s -> %s, built %s, raw rdeps: %s",
400 self._AtomStr(cp, slot),
401 cpv,
402 build_time,
403 rdeps_raw,
404 )
Tim Baine4a783b2023-04-21 20:05:51 +0000405 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw, use)
David Pursell9476bf42015-03-30 13:34:27 -0700406
Alex Klein1699fab2022-09-08 08:46:06 -0600407 avail_db = db
408 if installed_db is None:
409 installed_db = db
410 avail_db = None
David Pursell9476bf42015-03-30 13:34:27 -0700411
Alex Klein1699fab2022-09-08 08:46:06 -0600412 # Add approximate forward dependencies.
David Pursell9476bf42015-03-30 13:34:27 -0700413 if process_rdeps:
Alex Klein1699fab2022-09-08 08:46:06 -0600414 logging.debug("Populating forward dependencies...")
415 for cp, cp_slots in db.items():
416 for slot, pkg_info in cp_slots.items():
417 pkg_info.rdeps.update(
418 self._ProcessDepStr(
419 pkg_info.rdeps_raw, installed_db, avail_db
420 )
421 )
422 logging.debug(
423 " %s (%s) processed rdeps: %s",
424 self._AtomStr(cp, slot),
425 pkg_info.cpv,
426 " ".join(
427 [
428 self._AtomStr(rdep_cp, rdep_slot)
429 for rdep_cp, rdep_slot in pkg_info.rdeps
430 ]
431 ),
432 )
433
434 # Add approximate reverse dependencies (optional).
David Pursell9476bf42015-03-30 13:34:27 -0700435 if process_rev_rdeps:
Alex Klein1699fab2022-09-08 08:46:06 -0600436 logging.debug("Populating reverse dependencies...")
437 for cp, cp_slots in db.items():
438 for slot, pkg_info in cp_slots.items():
439 for rdep_cp, rdep_slot in pkg_info.rdeps:
440 to_slots = db.get(rdep_cp)
441 if not to_slots:
442 continue
David Pursell9476bf42015-03-30 13:34:27 -0700443
Alex Klein1699fab2022-09-08 08:46:06 -0600444 for to_slot, to_pkg_info in to_slots.items():
445 if rdep_slot and to_slot != rdep_slot:
446 continue
447 logging.debug(
448 " %s (%s) added as rev rdep for %s (%s)",
449 self._AtomStr(cp, slot),
450 pkg_info.cpv,
451 self._AtomStr(rdep_cp, to_slot),
452 to_pkg_info.cpv,
453 )
454 to_pkg_info.rev_rdeps.add((cp, slot))
David Pursell9476bf42015-03-30 13:34:27 -0700455
Alex Klein1699fab2022-09-08 08:46:06 -0600456 return db
David Pursell9476bf42015-03-30 13:34:27 -0700457
Tim Baine4a783b2023-04-21 20:05:51 +0000458 def _InitTargetVarDB(
459 self,
460 device: remote_access.RemoteDevice,
461 root: str,
462 process_rdeps: bool,
463 process_rev_rdeps: bool,
464 ) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600465 """Initializes a dictionary of packages installed on |device|."""
466 get_vartree_script = self._GetVartreeSnippet(root)
467 try:
Mike Frysingerc0780a62022-08-29 04:41:56 -0400468 result = device.agent.RemoteSh(
Alex Klein1699fab2022-09-08 08:46:06 -0600469 ["python"], remote_sudo=True, input=get_vartree_script
470 )
471 except cros_build_lib.RunCommandError as e:
472 logging.error("Cannot get target vartree:\n%s", e.stderr)
473 raise
David Pursell9476bf42015-03-30 13:34:27 -0700474
Alex Klein1699fab2022-09-08 08:46:06 -0600475 try:
476 self.target_db = self._BuildDB(
Tim Bain980db312023-04-26 17:29:00 +0000477 [CpvInfo(*cpv_info) for cpv_info in json.loads(result.stdout)],
478 process_rdeps,
479 process_rev_rdeps,
Alex Klein1699fab2022-09-08 08:46:06 -0600480 )
481 except ValueError as e:
482 raise self.VartreeError(str(e))
David Pursell9476bf42015-03-30 13:34:27 -0700483
Tim Baine4a783b2023-04-21 20:05:51 +0000484 def _InitBinpkgDB(self, process_rdeps: bool) -> None:
Alex Klein975e86c2023-01-23 16:49:10 -0700485 """Initializes a dictionary of binpkgs for updating the target."""
Alex Klein1699fab2022-09-08 08:46:06 -0600486 # Get build root trees; portage indexes require a trailing '/'.
487 build_root = os.path.join(self.sysroot, "")
488 trees = portage.create_trees(
489 target_root=build_root, config_root=build_root
490 )
491 bintree = trees[build_root]["bintree"]
492 binpkgs_info = []
493 for cpv in bintree.dbapi.cpv_all():
Tim Baine4a783b2023-04-21 20:05:51 +0000494 slot, rdep_raw, build_time, use = bintree.dbapi.aux_get(
495 cpv, ["SLOT", "RDEPEND", "BUILD_TIME", "USE"]
Alex Klein1699fab2022-09-08 08:46:06 -0600496 )
Tim Bain980db312023-04-26 17:29:00 +0000497 binpkgs_info.append(CpvInfo(cpv, slot, rdep_raw, build_time, use))
David Pursell9476bf42015-03-30 13:34:27 -0700498
Alex Klein1699fab2022-09-08 08:46:06 -0600499 try:
500 self.binpkgs_db = self._BuildDB(
501 binpkgs_info, process_rdeps, False, installed_db=self.target_db
502 )
503 except ValueError as e:
504 raise self.BintreeError(str(e))
David Pursell9476bf42015-03-30 13:34:27 -0700505
Tim Baine4a783b2023-04-21 20:05:51 +0000506 def _InitDepQueue(self) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600507 """Initializes the dependency work queue."""
508 self.queue = set()
509 self.seen = {}
510 self.listed = set()
David Pursell9476bf42015-03-30 13:34:27 -0700511
Tim Baine4a783b2023-04-21 20:05:51 +0000512 def _EnqDep(self, dep: str, listed: bool, optional: bool) -> bool:
Alex Klein975e86c2023-01-23 16:49:10 -0700513 """Enqueues a dependency if not seen before or if set non-optional."""
Alex Klein1699fab2022-09-08 08:46:06 -0600514 if dep in self.seen and (optional or not self.seen[dep]):
515 return False
David Pursell9476bf42015-03-30 13:34:27 -0700516
Alex Klein1699fab2022-09-08 08:46:06 -0600517 self.queue.add(dep)
518 self.seen[dep] = optional
519 if listed:
520 self.listed.add(dep)
521 return True
David Pursell9476bf42015-03-30 13:34:27 -0700522
Tim Baine4a783b2023-04-21 20:05:51 +0000523 def _DeqDep(self) -> Tuple[str, bool, bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600524 """Dequeues and returns a dependency, its listed and optional flags.
David Pursell9476bf42015-03-30 13:34:27 -0700525
Alex Klein975e86c2023-01-23 16:49:10 -0700526 This returns listed packages first, if any are present, to ensure that
527 we correctly mark them as such when they are first being processed.
Alex Klein1699fab2022-09-08 08:46:06 -0600528 """
529 if self.listed:
530 dep = self.listed.pop()
531 self.queue.remove(dep)
532 listed = True
533 else:
534 dep = self.queue.pop()
535 listed = False
David Pursell9476bf42015-03-30 13:34:27 -0700536
Alex Klein1699fab2022-09-08 08:46:06 -0600537 return dep, listed, self.seen[dep]
David Pursell9476bf42015-03-30 13:34:27 -0700538
Tim Baine4a783b2023-04-21 20:05:51 +0000539 def _FindPackageMatches(self, cpv_pattern: str) -> List[Tuple[str, str]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600540 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
David Pursell9476bf42015-03-30 13:34:27 -0700541
Alex Klein1699fab2022-09-08 08:46:06 -0600542 This is breaking |cpv_pattern| into its C, P and V components, each of
543 which may or may not be present or contain wildcards. It then scans the
Alex Klein975e86c2023-01-23 16:49:10 -0700544 binpkgs database to find all atoms that match these components,
545 returning a list of CP and slot qualifier. When the pattern does not
546 specify a version, or when a CP has only one slot in the binpkgs
547 database, we omit the slot qualifier in the result.
David Pursell9476bf42015-03-30 13:34:27 -0700548
Alex Klein1699fab2022-09-08 08:46:06 -0600549 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600550 cpv_pattern: A CPV pattern, potentially partial and/or having
551 wildcards.
David Pursell9476bf42015-03-30 13:34:27 -0700552
Alex Klein1699fab2022-09-08 08:46:06 -0600553 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600554 A list of (CPV, slot) pairs of packages in the binpkgs database that
555 match the pattern.
Alex Klein1699fab2022-09-08 08:46:06 -0600556 """
557 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
558 cp_pattern = os.path.join(attrs.category or "*", attrs.package or "*")
559 matches = []
560 for cp, cp_slots in self.binpkgs_db.items():
561 if not fnmatch.fnmatchcase(cp, cp_pattern):
562 continue
David Pursell9476bf42015-03-30 13:34:27 -0700563
Alex Klein975e86c2023-01-23 16:49:10 -0700564 # If no version attribute was given or there's only one slot, omit
565 # the slot qualifier.
Alex Klein1699fab2022-09-08 08:46:06 -0600566 if not attrs.version or len(cp_slots) == 1:
567 matches.append((cp, None))
568 else:
569 cpv_pattern = "%s-%s" % (cp, attrs.version)
570 for slot, pkg_info in cp_slots.items():
571 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
572 matches.append((cp, slot))
David Pursell9476bf42015-03-30 13:34:27 -0700573
Alex Klein1699fab2022-09-08 08:46:06 -0600574 return matches
David Pursell9476bf42015-03-30 13:34:27 -0700575
Tim Baine4a783b2023-04-21 20:05:51 +0000576 def _FindPackage(self, pkg: str) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600577 """Returns the (CP, slot) pair for a package matching |pkg|.
David Pursell9476bf42015-03-30 13:34:27 -0700578
Alex Klein1699fab2022-09-08 08:46:06 -0600579 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600580 pkg: Path to a binary package or a (partial) package CPV specifier.
David Pursell9476bf42015-03-30 13:34:27 -0700581
Alex Klein1699fab2022-09-08 08:46:06 -0600582 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600583 A (CP, slot) pair for the given package; slot may be None
584 (unspecified).
David Pursell9476bf42015-03-30 13:34:27 -0700585
Alex Klein1699fab2022-09-08 08:46:06 -0600586 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600587 ValueError: if |pkg| is not a binpkg file nor does it match
588 something that's in the bintree.
Alex Klein1699fab2022-09-08 08:46:06 -0600589 """
590 if pkg.endswith(".tbz2") and os.path.isfile(pkg):
591 package = os.path.basename(os.path.splitext(pkg)[0])
592 category = os.path.basename(os.path.dirname(pkg))
593 return self._GetCP(os.path.join(category, package)), None
David Pursell9476bf42015-03-30 13:34:27 -0700594
Alex Klein1699fab2022-09-08 08:46:06 -0600595 matches = self._FindPackageMatches(pkg)
596 if not matches:
597 raise ValueError("No package found for %s" % pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700598
Alex Klein1699fab2022-09-08 08:46:06 -0600599 idx = 0
600 if len(matches) > 1:
601 # Ask user to pick among multiple matches.
602 idx = cros_build_lib.GetChoice(
603 "Multiple matches found for %s: " % pkg,
604 ["%s:%s" % (cp, slot) if slot else cp for cp, slot in matches],
605 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700606
Alex Klein1699fab2022-09-08 08:46:06 -0600607 return matches[idx]
608
Tim Baine4a783b2023-04-21 20:05:51 +0000609 def _NeedsInstall(
610 self, cpv: str, slot: str, build_time: int, optional: bool
611 ) -> Tuple[bool, bool, bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600612 """Returns whether a package needs to be installed on the target.
613
614 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600615 cpv: Fully qualified CPV (string) of the package.
616 slot: Slot identifier (string).
617 build_time: The BUILT_TIME value (string) of the binpkg.
618 optional: Whether package is optional on the target.
Alex Klein1699fab2022-09-08 08:46:06 -0600619
620 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000621 A tuple (install, update, use_mismatch) indicating whether to
622 |install| the package, whether it is an |update| to an existing
623 package, and whether the package's USE flags mismatch the existing
624 package.
Alex Klein1699fab2022-09-08 08:46:06 -0600625
626 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600627 ValueError: if slot is not provided.
Alex Klein1699fab2022-09-08 08:46:06 -0600628 """
629 # If not checking installed packages, always install.
630 if not self.target_db:
Tim Baine4a783b2023-04-21 20:05:51 +0000631 return True, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600632
633 cp = self._GetCP(cpv)
Trent Apted1e2e4f32023-05-05 03:50:20 +0000634 target_pkg_info = self.target_db.get(cp, {}).get(slot)
Alex Klein1699fab2022-09-08 08:46:06 -0600635 if target_pkg_info is not None:
Tim Baine4a783b2023-04-21 20:05:51 +0000636 attrs = package_info.SplitCPV(cpv)
637 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
Alex Klein1699fab2022-09-08 08:46:06 -0600638
Tim Baine4a783b2023-04-21 20:05:51 +0000639 def _get_attr_mismatch(
640 attr_name: str, new_attr: any, target_attr: any
641 ) -> Tuple[str, str, str]:
642 """Check if the new and target packages differ for an attribute.
643
644 Args:
645 attr_name: The name of the attribute being checked (string).
646 new_attr: The value of the given attribute for the new
647 package (string).
648 target_attr: The value of the given attribute for the target
649 (existing) package (string).
650
651 Returns:
652 A tuple (attr_name, new_attr, target_attr) composed of the
653 args if there is a mismatch, or None if the values match.
654 """
655 mismatch = new_attr != target_attr
656 if mismatch:
657 return attr_name, new_attr, target_attr
658
659 update_info = _get_attr_mismatch(
660 "version", attrs.version, target_attrs.version
661 ) or _get_attr_mismatch(
662 "build time", build_time, target_pkg_info.build_time
663 )
664
665 if update_info:
666 attr_name, new_attr, target_attr = update_info
Alex Klein1699fab2022-09-08 08:46:06 -0600667 logging.debug(
Tim Baine4a783b2023-04-21 20:05:51 +0000668 "Updating %s: %s (%s) different on target (%s)",
669 cp,
670 attr_name,
671 new_attr,
672 target_attr,
Alex Klein1699fab2022-09-08 08:46:06 -0600673 )
Tim Baine4a783b2023-04-21 20:05:51 +0000674
Trent Apted1e2e4f32023-05-05 03:50:20 +0000675 binpkg_pkg_info = self.binpkgs_db.get(cp, {}).get(slot)
Tim Baine4a783b2023-04-21 20:05:51 +0000676 use_mismatch = binpkg_pkg_info.use != target_pkg_info.use
677 if use_mismatch:
678 logging.warning(
679 "USE flags for package %s do not match (Existing='%s', "
680 "New='%s').",
681 cp,
682 target_pkg_info.use,
683 binpkg_pkg_info.use,
684 )
685 return True, True, use_mismatch
Alex Klein1699fab2022-09-08 08:46:06 -0600686
687 logging.debug(
688 "Not updating %s: already up-to-date (%s, built %s)",
689 cp,
690 target_pkg_info.cpv,
691 target_pkg_info.build_time,
692 )
Tim Baine4a783b2023-04-21 20:05:51 +0000693 return False, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600694
695 if optional:
696 logging.debug(
697 "Not installing %s: missing on target but optional", cp
698 )
Tim Baine4a783b2023-04-21 20:05:51 +0000699 return False, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600700
701 logging.debug(
702 "Installing %s: missing on target and non-optional (%s)", cp, cpv
703 )
Tim Baine4a783b2023-04-21 20:05:51 +0000704 return True, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600705
Tim Baine4a783b2023-04-21 20:05:51 +0000706 def _ProcessDeps(self, deps: List[str], reverse: bool) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600707 """Enqueues dependencies for processing.
708
709 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600710 deps: List of dependencies to enqueue.
711 reverse: Whether these are reverse dependencies.
Alex Klein1699fab2022-09-08 08:46:06 -0600712 """
713 if not deps:
714 return
715
716 logging.debug(
717 "Processing %d %s dep(s)...",
718 len(deps),
719 "reverse" if reverse else "forward",
720 )
721 num_already_seen = 0
722 for dep in deps:
723 if self._EnqDep(dep, False, reverse):
724 logging.debug(" Queued dep %s", dep)
725 else:
726 num_already_seen += 1
727
728 if num_already_seen:
729 logging.debug("%d dep(s) already seen", num_already_seen)
730
Tim Baine4a783b2023-04-21 20:05:51 +0000731 def _ComputeInstalls(
732 self, process_rdeps: bool, process_rev_rdeps: bool
733 ) -> Tuple[Dict[str, package_info.CPV], bool]:
Alex Klein975e86c2023-01-23 16:49:10 -0700734 """Returns a dict of packages that need to be installed on the target.
Alex Klein1699fab2022-09-08 08:46:06 -0600735
736 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600737 process_rdeps: Whether to trace forward dependencies.
738 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600739
740 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000741 A tuple (installs, warnings_shown) where |installs| is a dictionary
742 mapping CP values (string) to tuples containing a CPV (string), a
743 slot (string), a boolean indicating whether the package was
744 initially listed in the queue, and a boolean indicating whether this
745 is an update to an existing package, and |warnings_shown| is a
746 boolean indicating whether warnings were shown that might require a
747 prompt whether to continue.
Alex Klein1699fab2022-09-08 08:46:06 -0600748 """
749 installs = {}
Tim Baine4a783b2023-04-21 20:05:51 +0000750 warnings_shown = False
Alex Klein1699fab2022-09-08 08:46:06 -0600751 while self.queue:
752 dep, listed, optional = self._DeqDep()
753 cp, required_slot = dep
754 if cp in installs:
755 logging.debug("Already updating %s", cp)
756 continue
757
Trent Apted1e2e4f32023-05-05 03:50:20 +0000758 cp_slots = self.binpkgs_db.get(cp, {})
Alex Klein1699fab2022-09-08 08:46:06 -0600759 logging.debug(
760 "Checking packages matching %s%s%s...",
761 cp,
762 " (slot: %s)" % required_slot if required_slot else "",
763 " (optional)" if optional else "",
764 )
765 num_processed = 0
766 for slot, pkg_info in cp_slots.items():
767 if required_slot and slot != required_slot:
768 continue
769
770 num_processed += 1
771 logging.debug(" Checking %s...", pkg_info.cpv)
772
Tim Baine4a783b2023-04-21 20:05:51 +0000773 install, update, use_mismatch = self._NeedsInstall(
Alex Klein1699fab2022-09-08 08:46:06 -0600774 pkg_info.cpv, slot, pkg_info.build_time, optional
775 )
776 if not install:
777 continue
778
779 installs[cp] = (pkg_info.cpv, slot, listed, update)
Tim Baine4a783b2023-04-21 20:05:51 +0000780 warnings_shown |= use_mismatch
Alex Klein1699fab2022-09-08 08:46:06 -0600781
782 # Add forward and backward runtime dependencies to queue.
783 if process_rdeps:
784 self._ProcessDeps(pkg_info.rdeps, False)
785 if process_rev_rdeps:
Trent Apted1e2e4f32023-05-05 03:50:20 +0000786 target_pkg_info = self.target_db.get(cp, {}).get(slot)
Alex Klein1699fab2022-09-08 08:46:06 -0600787 if target_pkg_info:
788 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
789
790 if num_processed == 0:
791 logging.warning(
792 "No qualified bintree package corresponding to %s", cp
793 )
794
Tim Baine4a783b2023-04-21 20:05:51 +0000795 return installs, warnings_shown
Alex Klein1699fab2022-09-08 08:46:06 -0600796
Tim Baine4a783b2023-04-21 20:05:51 +0000797 def _SortInstalls(self, installs: List[str]) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600798 """Returns a sorted list of packages to install.
799
800 Performs a topological sort based on dependencies found in the binary
801 package database.
802
803 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600804 installs: Dictionary of packages to install indexed by CP.
Alex Klein1699fab2022-09-08 08:46:06 -0600805
806 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600807 A list of package CPVs (string).
Alex Klein1699fab2022-09-08 08:46:06 -0600808
809 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600810 ValueError: If dependency graph contains a cycle.
Alex Klein1699fab2022-09-08 08:46:06 -0600811 """
812 not_visited = set(installs.keys())
813 curr_path = []
814 sorted_installs = []
815
Tim Baine4a783b2023-04-21 20:05:51 +0000816 def SortFrom(cp: str) -> None:
Alex Klein975e86c2023-01-23 16:49:10 -0700817 """Traverses deps recursively, emitting nodes in reverse order."""
Alex Klein1699fab2022-09-08 08:46:06 -0600818 cpv, slot, _, _ = installs[cp]
819 if cpv in curr_path:
820 raise ValueError(
821 "Dependencies contain a cycle: %s -> %s"
822 % (" -> ".join(curr_path[curr_path.index(cpv) :]), cpv)
823 )
824 curr_path.append(cpv)
825 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
826 if rdep_cp in not_visited:
827 not_visited.remove(rdep_cp)
828 SortFrom(rdep_cp)
829
830 sorted_installs.append(cpv)
831 curr_path.pop()
832
833 # So long as there's more packages, keep expanding dependency paths.
834 while not_visited:
835 SortFrom(not_visited.pop())
836
837 return sorted_installs
838
Tim Baine4a783b2023-04-21 20:05:51 +0000839 def _EnqListedPkg(self, pkg: str) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -0600840 """Finds and enqueues a listed package."""
841 cp, slot = self._FindPackage(pkg)
842 if cp not in self.binpkgs_db:
843 raise self.BintreeError(
844 "Package %s not found in binpkgs tree" % pkg
845 )
846 self._EnqDep((cp, slot), True, False)
847
Tim Baine4a783b2023-04-21 20:05:51 +0000848 def _EnqInstalledPkgs(self) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600849 """Enqueues all available binary packages that are already installed."""
850 for cp, cp_slots in self.binpkgs_db.items():
851 target_cp_slots = self.target_db.get(cp)
852 if target_cp_slots:
853 for slot in cp_slots.keys():
854 if slot in target_cp_slots:
855 self._EnqDep((cp, slot), True, False)
856
857 def Run(
858 self,
Tim Baine4a783b2023-04-21 20:05:51 +0000859 device: remote_access.RemoteDevice,
860 root: str,
861 listed_pkgs: List[str],
862 update: bool,
863 process_rdeps: bool,
864 process_rev_rdeps: bool,
865 ) -> Tuple[List[str], List[str], int, Dict[str, str], bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600866 """Computes the list of packages that need to be installed on a target.
867
868 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600869 device: Target handler object.
870 root: Package installation root.
871 listed_pkgs: Package names/files listed by the user.
872 update: Whether to read the target's installed package database.
873 process_rdeps: Whether to trace forward dependencies.
874 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600875
876 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000877 A tuple (sorted, listed, num_updates, install_attrs, warnings_shown)
878 where |sorted| is a list of package CPVs (string) to install on the
879 target in an order that satisfies their inter-dependencies, |listed|
Alex Klein53cc3bf2022-10-13 08:50:01 -0600880 the subset that was requested by the user, and |num_updates|
881 the number of packages being installed over preexisting
882 versions. Note that installation order should be reversed for
883 removal, |install_attrs| is a dictionary mapping a package
Tim Baine4a783b2023-04-21 20:05:51 +0000884 CPV (string) to some of its extracted environment attributes, and
885 |warnings_shown| is a boolean indicating whether warnings were shown
886 that might require a prompt whether to continue.
Alex Klein1699fab2022-09-08 08:46:06 -0600887 """
888 if process_rev_rdeps and not process_rdeps:
889 raise ValueError(
890 "Must processing forward deps when processing rev deps"
891 )
892 if process_rdeps and not update:
893 raise ValueError(
894 "Must check installed packages when processing deps"
895 )
896
897 if update:
898 logging.info("Initializing target intalled packages database...")
899 self._InitTargetVarDB(
900 device, root, process_rdeps, process_rev_rdeps
901 )
902
903 logging.info("Initializing binary packages database...")
904 self._InitBinpkgDB(process_rdeps)
905
906 logging.info("Finding listed package(s)...")
907 self._InitDepQueue()
908 for pkg in listed_pkgs:
909 if pkg == "@installed":
910 if not update:
911 raise ValueError(
Alex Klein975e86c2023-01-23 16:49:10 -0700912 "Must check installed packages when updating all of "
913 "them."
Alex Klein1699fab2022-09-08 08:46:06 -0600914 )
915 self._EnqInstalledPkgs()
916 else:
917 self._EnqListedPkg(pkg)
918
919 logging.info("Computing set of packages to install...")
Tim Baine4a783b2023-04-21 20:05:51 +0000920 installs, warnings_shown = self._ComputeInstalls(
921 process_rdeps, process_rev_rdeps
922 )
Alex Klein1699fab2022-09-08 08:46:06 -0600923
924 num_updates = 0
925 listed_installs = []
926 for cpv, _, listed, isupdate in installs.values():
927 if listed:
928 listed_installs.append(cpv)
929 if isupdate:
930 num_updates += 1
931
932 logging.info(
933 "Processed %d package(s), %d will be installed, %d are "
934 "updating existing packages",
935 len(self.seen),
936 len(installs),
937 num_updates,
938 )
939
940 sorted_installs = self._SortInstalls(installs)
941
942 install_attrs = {}
943 for pkg in sorted_installs:
944 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
945 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
946 install_attrs[pkg] = {}
947 if dlc_id and dlc_package:
948 install_attrs[pkg][_DLC_ID] = dlc_id
949
Tim Baine4a783b2023-04-21 20:05:51 +0000950 return (
951 sorted_installs,
952 listed_installs,
953 num_updates,
954 install_attrs,
955 warnings_shown,
956 )
David Pursell9476bf42015-03-30 13:34:27 -0700957
958
Tim Baine4a783b2023-04-21 20:05:51 +0000959def _Emerge(
960 device: remote_access.RemoteDevice,
961 pkg_paths: List[str],
962 root: str,
963 extra_args: List[str] = None,
964) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600965 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -0700966
Alex Klein1699fab2022-09-08 08:46:06 -0600967 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600968 device: A ChromiumOSDevice object.
Mike Frysinger9fba8d02023-05-15 15:04:07 -0400969 pkg_paths: Local paths to binary packages.
Alex Klein53cc3bf2022-10-13 08:50:01 -0600970 root: Package installation root path.
971 extra_args: Extra arguments to pass to emerge.
David Pursell9476bf42015-03-30 13:34:27 -0700972
Alex Klein1699fab2022-09-08 08:46:06 -0600973 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600974 DeployError: Unrecoverable error during emerge.
Alex Klein1699fab2022-09-08 08:46:06 -0600975 """
Mike Frysinger63d35512021-01-26 23:16:13 -0500976
Alex Klein1699fab2022-09-08 08:46:06 -0600977 def path_to_name(pkg_path):
978 return os.path.basename(pkg_path)
Mike Frysinger63d35512021-01-26 23:16:13 -0500979
Alex Klein1699fab2022-09-08 08:46:06 -0600980 def path_to_category(pkg_path):
981 return os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700982
Alex Klein1699fab2022-09-08 08:46:06 -0600983 pkg_names = ", ".join(path_to_name(x) for x in pkg_paths)
David Pursell9476bf42015-03-30 13:34:27 -0700984
Alex Klein1699fab2022-09-08 08:46:06 -0600985 pkgroot = os.path.join(device.work_dir, "packages")
986 portage_tmpdir = os.path.join(device.work_dir, "portage-tmp")
Alex Klein975e86c2023-01-23 16:49:10 -0700987 # Clean out the dirs first if we had a previous emerge on the device so as
988 # to free up space for this emerge. The last emerge gets implicitly cleaned
989 # up when the device connection deletes its work_dir.
Alex Klein1699fab2022-09-08 08:46:06 -0600990 device.run(
991 f"cd {device.work_dir} && "
992 f"rm -rf packages portage-tmp && "
993 f"mkdir -p portage-tmp packages && "
994 f"cd packages && "
995 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
996 shell=True,
997 remote_sudo=True,
998 )
Mike Frysinger63d35512021-01-26 23:16:13 -0500999
Alex Klein1699fab2022-09-08 08:46:06 -06001000 logging.info("Use portage temp dir %s", portage_tmpdir)
David Pursell9476bf42015-03-30 13:34:27 -07001001
Mike Frysinger63d35512021-01-26 23:16:13 -05001002 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001003 logging.notice("Copying binpkgs to device.")
1004 for pkg_path in pkg_paths:
1005 pkg_name = path_to_name(pkg_path)
1006 logging.info("Copying %s", pkg_name)
1007 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
1008 device.CopyToDevice(
1009 pkg_path, pkg_dir, mode="rsync", remote_sudo=True, compress=False
1010 )
1011
1012 # This message is read by BrilloDeployOperation.
1013 logging.notice("Installing: %s", pkg_names)
1014
1015 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
1016 # chromeos-base packages will be skipped due to the configuration
1017 # in /etc/protage/make.profile/package.provided. However, there is
1018 # a known bug that /usr/local/etc/portage is not setup properly
1019 # (crbug.com/312041). This does not affect `cros deploy` because
1020 # we do not use the preset PKGDIR.
1021 extra_env = {
1022 "FEATURES": "-sandbox",
1023 "PKGDIR": pkgroot,
1024 "PORTAGE_CONFIGROOT": "/usr/local",
1025 "PORTAGE_TMPDIR": portage_tmpdir,
1026 "PORTDIR": device.work_dir,
1027 "CONFIG_PROTECT": "-*",
1028 }
1029
Alex Klein975e86c2023-01-23 16:49:10 -07001030 # --ignore-built-slot-operator-deps because we don't rebuild everything. It
1031 # can cause errors, but that's expected with cros deploy since it's just a
Alex Klein1699fab2022-09-08 08:46:06 -06001032 # best effort to prevent developers avoid rebuilding an image every time.
1033 cmd = [
1034 "emerge",
1035 "--usepkg",
1036 "--ignore-built-slot-operator-deps=y",
1037 "--root",
1038 root,
1039 ] + [os.path.join(pkgroot, *x.split("/")[-2:]) for x in pkg_paths]
1040 if extra_args:
1041 cmd.append(extra_args)
1042
1043 logging.warning(
1044 "Ignoring slot dependencies! This may break things! e.g. "
1045 "packages built against the old version may not be able to "
1046 "load the new .so. This is expected, and you will just need "
1047 "to build and flash a new image if you have problems."
1048 )
1049 try:
1050 result = device.run(
1051 cmd,
1052 extra_env=extra_env,
1053 remote_sudo=True,
1054 capture_output=True,
1055 debug_level=logging.INFO,
1056 )
1057
1058 pattern = (
1059 "A requested package will not be merged because "
1060 "it is listed in package.provided"
1061 )
1062 output = result.stderr.replace("\n", " ").replace("\r", "")
1063 if pattern in output:
1064 error = (
1065 "Package failed to emerge: %s\n"
1066 "Remove %s from /etc/portage/make.profile/"
1067 "package.provided/chromeos-base.packages\n"
1068 "(also see crbug.com/920140 for more context)\n"
1069 % (pattern, pkg_name)
1070 )
1071 cros_build_lib.Die(error)
1072 except Exception:
1073 logging.error("Failed to emerge packages %s", pkg_names)
1074 raise
1075 else:
1076 # This message is read by BrilloDeployOperation.
1077 logging.notice("Packages have been installed.")
David Pursell9476bf42015-03-30 13:34:27 -07001078
1079
Tim Baine4a783b2023-04-21 20:05:51 +00001080def _RestoreSELinuxContext(
1081 device: remote_access.RemoteDevice, pkgpath: str, root: str
1082) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001083 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +09001084
Alex Klein1699fab2022-09-08 08:46:06 -06001085 This reads the tarball from pkgpath, and calls restorecon on device to
Alex Klein975e86c2023-01-23 16:49:10 -07001086 restore SELinux context for files listed in the tarball, assuming those
1087 files are installed to /
Qijiang Fan8a945032019-04-25 20:53:29 +09001088
Alex Klein1699fab2022-09-08 08:46:06 -06001089 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001090 device: a ChromiumOSDevice object
1091 pkgpath: path to tarball
1092 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001093 """
1094 pkgroot = os.path.join(device.work_dir, "packages")
1095 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
1096 pkgpath_device = os.path.join(
1097 pkgroot, pkg_dirname, os.path.basename(pkgpath)
1098 )
1099 # Testing shows restorecon splits on newlines instead of spaces.
1100 device.run(
1101 [
1102 "cd",
1103 root,
1104 "&&",
1105 "tar",
1106 "tf",
1107 pkgpath_device,
1108 "|",
1109 "restorecon",
1110 "-i",
1111 "-f",
1112 "-",
1113 ],
1114 remote_sudo=True,
1115 )
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001116
1117
Tim Baine4a783b2023-04-21 20:05:51 +00001118def _GetPackagesByCPV(
1119 cpvs: List[package_info.CPV], strip: bool, sysroot: str
1120) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001121 """Returns paths to binary packages corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001122
Alex Klein1699fab2022-09-08 08:46:06 -06001123 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001124 cpvs: List of CPV components given by package_info.SplitCPV().
1125 strip: True to run strip_package.
1126 sysroot: Sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001127
Alex Klein1699fab2022-09-08 08:46:06 -06001128 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001129 List of paths corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001130
Alex Klein1699fab2022-09-08 08:46:06 -06001131 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001132 DeployError: If a package is missing.
Alex Klein1699fab2022-09-08 08:46:06 -06001133 """
1134 packages_dir = None
1135 if strip:
1136 try:
1137 cros_build_lib.run(
1138 [
Mike Frysinger5429f302023-03-27 15:48:52 -04001139 constants.CHROMITE_SCRIPTS_DIR / "strip_package",
Alex Klein1699fab2022-09-08 08:46:06 -06001140 "--sysroot",
1141 sysroot,
1142 ]
1143 + [cpv.cpf for cpv in cpvs]
1144 )
1145 packages_dir = _STRIPPED_PACKAGES_DIR
1146 except cros_build_lib.RunCommandError:
1147 logging.error(
1148 "Cannot strip packages %s", " ".join([str(cpv) for cpv in cpvs])
1149 )
1150 raise
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001151
Alex Klein1699fab2022-09-08 08:46:06 -06001152 paths = []
1153 for cpv in cpvs:
1154 path = portage_util.GetBinaryPackagePath(
1155 cpv.category,
1156 cpv.package,
1157 cpv.version,
1158 sysroot=sysroot,
1159 packages_dir=packages_dir,
1160 )
1161 if not path:
1162 raise DeployError("Missing package %s." % cpv)
1163 paths.append(path)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001164
Alex Klein1699fab2022-09-08 08:46:06 -06001165 return paths
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001166
1167
Tim Baine4a783b2023-04-21 20:05:51 +00001168def _GetPackagesPaths(pkgs: List[str], strip: bool, sysroot: str) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001169 """Returns paths to binary |pkgs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001170
Alex Klein1699fab2022-09-08 08:46:06 -06001171 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001172 pkgs: List of package CPVs string.
1173 strip: Whether or not to run strip_package for CPV packages.
1174 sysroot: The sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001175
Alex Klein1699fab2022-09-08 08:46:06 -06001176 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001177 List of paths corresponding to |pkgs|.
Alex Klein1699fab2022-09-08 08:46:06 -06001178 """
1179 cpvs = [package_info.SplitCPV(p) for p in pkgs]
1180 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001181
1182
Tim Baine4a783b2023-04-21 20:05:51 +00001183def _Unmerge(
1184 device: remote_access.RemoteDevice, pkgs: List[str], root: str
1185) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001186 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -07001187
Alex Klein1699fab2022-09-08 08:46:06 -06001188 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001189 device: A RemoteDevice object.
1190 pkgs: Package names.
1191 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001192 """
1193 pkg_names = ", ".join(os.path.basename(x) for x in pkgs)
Mike Frysinger22bb5502021-01-29 13:05:46 -05001194 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001195 logging.notice("Unmerging %s.", pkg_names)
1196 cmd = ["qmerge", "--yes"]
1197 # Check if qmerge is available on the device. If not, use emerge.
1198 if device.run(["qmerge", "--version"], check=False).returncode != 0:
1199 cmd = ["emerge"]
1200
1201 cmd += ["--unmerge", "--root", root]
1202 cmd.extend("f={x}" for x in pkgs)
1203 try:
1204 # Always showing the emerge output for clarity.
1205 device.run(
1206 cmd,
1207 capture_output=False,
1208 remote_sudo=True,
1209 debug_level=logging.INFO,
1210 )
1211 except Exception:
1212 logging.error("Failed to unmerge packages %s", pkg_names)
1213 raise
1214 else:
1215 # This message is read by BrilloDeployOperation.
1216 logging.notice("Packages have been uninstalled.")
David Pursell9476bf42015-03-30 13:34:27 -07001217
1218
Tim Baine4a783b2023-04-21 20:05:51 +00001219def _ConfirmDeploy(num_updates: int) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -06001220 """Returns whether we can continue deployment."""
1221 if num_updates > _MAX_UPDATES_NUM:
1222 logging.warning(_MAX_UPDATES_WARNING)
1223 return cros_build_lib.BooleanPrompt(default=False)
David Pursell9476bf42015-03-30 13:34:27 -07001224
Alex Klein1699fab2022-09-08 08:46:06 -06001225 return True
David Pursell9476bf42015-03-30 13:34:27 -07001226
1227
Tim Baine4a783b2023-04-21 20:05:51 +00001228def _ConfirmUpdateDespiteWarnings() -> bool:
1229 """Returns whether we can continue updating despite warnings."""
1230 logging.warning("Continue despite prior warnings?")
1231 return cros_build_lib.BooleanPrompt(default=False)
1232
1233
1234def _EmergePackages(
1235 pkgs: List[str],
1236 device: remote_access.RemoteDevice,
1237 strip: bool,
1238 sysroot: str,
1239 root: str,
1240 board: str,
1241 emerge_args: List[str],
1242) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001243 """Call _Emerge for each package in pkgs."""
Ben Pastene5f03b052019-08-12 18:03:24 -07001244 if device.IsSELinuxAvailable():
Alex Klein1699fab2022-09-08 08:46:06 -06001245 enforced = device.IsSELinuxEnforced()
1246 if enforced:
1247 device.run(["setenforce", "0"])
1248 else:
1249 enforced = False
Andrewc7e1c6b2020-02-27 16:03:53 -08001250
Alex Klein1699fab2022-09-08 08:46:06 -06001251 dlc_deployed = False
1252 # This message is read by BrilloDeployOperation.
1253 logging.info("Preparing local packages for transfer.")
1254 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
1255 # Install all the packages in one pass so inter-package blockers work.
1256 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
1257 logging.info("Updating SELinux settings & DLC images.")
1258 for pkg_path in pkg_paths:
1259 if device.IsSELinuxAvailable():
1260 _RestoreSELinuxContext(device, pkg_path, root)
Mike Frysinger5f4c2742021-02-08 14:37:23 -05001261
Alex Klein1699fab2022-09-08 08:46:06 -06001262 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
1263 if dlc_id and dlc_package:
1264 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
1265 dlc_deployed = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001266
Alex Klein1699fab2022-09-08 08:46:06 -06001267 if dlc_deployed:
1268 # Clean up empty directories created by emerging DLCs.
1269 device.run(
1270 [
1271 "test",
1272 "-d",
1273 "/build/rootfs",
1274 "&&",
1275 "rmdir",
1276 "--ignore-fail-on-non-empty",
1277 "/build/rootfs",
1278 "/build",
1279 ],
1280 check=False,
1281 )
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001282
Alex Klein1699fab2022-09-08 08:46:06 -06001283 if enforced:
1284 device.run(["setenforce", "1"])
1285
1286 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1287 # we installed new DLC images).
1288 if dlc_deployed:
1289 device.run(["restart", "dlcservice"])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001290
1291
Tim Baine4a783b2023-04-21 20:05:51 +00001292def _UnmergePackages(
1293 pkgs: List[str],
1294 device: remote_access.RemoteDevice,
1295 root: str,
1296 pkgs_attrs: Dict[str, List[str]],
1297) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -06001298 """Call _Unmege for each package in pkgs."""
1299 dlc_uninstalled = False
1300 _Unmerge(device, pkgs, root)
1301 logging.info("Cleaning up DLC images.")
1302 for pkg in pkgs:
1303 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1304 dlc_uninstalled = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001305
Alex Klein1699fab2022-09-08 08:46:06 -06001306 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1307 # uninstalled DLC images).
1308 if dlc_uninstalled:
1309 device.run(["restart", "dlcservice"])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001310
1311
Tim Baine4a783b2023-04-21 20:05:51 +00001312def _UninstallDLCImage(
1313 device: remote_access.RemoteDevice, pkg_attrs: Dict[str, List[str]]
1314):
Alex Klein1699fab2022-09-08 08:46:06 -06001315 """Uninstall a DLC image."""
1316 if _DLC_ID in pkg_attrs:
1317 dlc_id = pkg_attrs[_DLC_ID]
1318 logging.notice("Uninstalling DLC image for %s", dlc_id)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001319
Alex Klein1699fab2022-09-08 08:46:06 -06001320 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1321 return True
1322 else:
1323 logging.debug("DLC_ID not found in package")
1324 return False
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001325
1326
Tim Baine4a783b2023-04-21 20:05:51 +00001327def _DeployDLCImage(
1328 device: remote_access.RemoteDevice,
1329 sysroot: str,
1330 board: str,
1331 dlc_id: str,
1332 dlc_package: str,
1333):
Alex Klein1699fab2022-09-08 08:46:06 -06001334 """Deploy (install and mount) a DLC image.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001335
Alex Klein1699fab2022-09-08 08:46:06 -06001336 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001337 device: A device object.
1338 sysroot: The sysroot path.
1339 board: Board to use.
1340 dlc_id: The DLC ID.
1341 dlc_package: The DLC package name.
Alex Klein1699fab2022-09-08 08:46:06 -06001342 """
1343 # Requires `sudo_rm` because installations of files are running with sudo.
1344 with osutils.TempDir(sudo_rm=True) as tempdir:
1345 temp_rootfs = Path(tempdir)
1346 # Build the DLC image if the image is outdated or doesn't exist.
1347 dlc_lib.InstallDlcImages(
1348 sysroot=sysroot, rootfs=temp_rootfs, dlc_id=dlc_id, board=board
1349 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001350
Alex Klein1699fab2022-09-08 08:46:06 -06001351 logging.debug("Uninstall DLC %s if it is installed.", dlc_id)
1352 try:
1353 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1354 except cros_build_lib.RunCommandError as e:
1355 logging.info(
1356 "Failed to uninstall DLC:%s. Continue anyway.", e.stderr
1357 )
1358 except Exception:
1359 logging.error("Failed to uninstall DLC.")
1360 raise
Andrewc7e1c6b2020-02-27 16:03:53 -08001361
Yuanpeng Niaba30342023-07-11 14:24:09 -07001362 src_dlc_dir = os.path.join(
Alex Klein1699fab2022-09-08 08:46:06 -06001363 sysroot,
1364 dlc_lib.DLC_BUILD_DIR,
1365 dlc_id,
Alex Klein1699fab2022-09-08 08:46:06 -06001366 )
Yuanpeng Niaba30342023-07-11 14:24:09 -07001367 if not os.path.exists(src_dlc_dir):
1368 src_dlc_dir = os.path.join(
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001369 sysroot,
1370 dlc_lib.DLC_BUILD_DIR_SCALED,
1371 dlc_id,
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001372 )
1373
Yuanpeng Niaba30342023-07-11 14:24:09 -07001374 # Deploy the metadata entry to compressed metadata on device.
1375 logging.notice("Setting the DLC metadata for %s", dlc_id)
1376 metadata = dlc_lib.DlcMetadata.LoadSrcMetadata(src_dlc_dir)
1377 device.run(
1378 [dlc_lib.DLC_METADATA_UTIL, "--set", f"--id={dlc_id}"],
1379 input=json.dumps(metadata),
1380 check=False,
1381 )
1382
1383 logging.notice("Deploy the DLC image for %s", dlc_id)
1384 dlc_img_path_src = os.path.join(
1385 src_dlc_dir,
1386 dlc_package,
1387 dlc_lib.DLC_IMAGE,
1388 )
1389
Alex Klein1699fab2022-09-08 08:46:06 -06001390 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1391 dlc_img_path_a = os.path.join(dlc_img_path, "dlc_a")
1392 dlc_img_path_b = os.path.join(dlc_img_path, "dlc_b")
1393 # Create directories for DLC images.
Mike Frysinger445f6bf2023-06-27 11:43:33 -04001394 device.mkdir([dlc_img_path_a, dlc_img_path_b])
Alex Klein1699fab2022-09-08 08:46:06 -06001395 # Copy images to the destination directories.
1396 device.CopyToDevice(
1397 dlc_img_path_src,
1398 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1399 mode="rsync",
1400 )
1401 device.run(
1402 [
1403 "cp",
1404 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1405 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE),
1406 ]
1407 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001408
Alex Klein1699fab2022-09-08 08:46:06 -06001409 # Set the proper perms and ownership so dlcservice can access the image.
1410 device.run(["chmod", "-R", "u+rwX,go+rX,go-w", _DLC_INSTALL_ROOT])
1411 device.run(["chown", "-R", "dlcservice:dlcservice", _DLC_INSTALL_ROOT])
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001412
Alex Klein1699fab2022-09-08 08:46:06 -06001413 # Copy metadata to device.
Yuanpeng Niaba30342023-07-11 14:24:09 -07001414 # TODO(b/290961240): To be removed once the transition to compressed
1415 # metadata is complete.
Alex Klein1699fab2022-09-08 08:46:06 -06001416 dest_meta_dir = Path("/") / dlc_lib.DLC_META_DIR / dlc_id / dlc_package
Mike Frysinger445f6bf2023-06-27 11:43:33 -04001417 device.mkdir(dest_meta_dir)
Alex Klein1699fab2022-09-08 08:46:06 -06001418 src_meta_dir = os.path.join(
Yuanpeng Niaba30342023-07-11 14:24:09 -07001419 src_dlc_dir,
Alex Klein1699fab2022-09-08 08:46:06 -06001420 dlc_package,
1421 dlc_lib.DLC_TMP_META_DIR,
1422 )
1423 device.CopyToDevice(
1424 src_meta_dir + "/",
1425 dest_meta_dir,
1426 mode="rsync",
1427 recursive=True,
1428 remote_sudo=True,
1429 )
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001430
Alex Klein1699fab2022-09-08 08:46:06 -06001431 # TODO(kimjae): Make this generic so it recomputes all the DLCs + copies
Alex Klein975e86c2023-01-23 16:49:10 -07001432 # over a fresh list of dm-verity digests instead of appending and
1433 # keeping the stale digests when developers are testing.
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001434
Alex Klein1699fab2022-09-08 08:46:06 -06001435 # Copy the LoadPin dm-verity digests to device.
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001436 _DeployDLCLoadPin(temp_rootfs, device)
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001437
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001438
1439def _DeployDLCLoadPin(
Tim Baine4a783b2023-04-21 20:05:51 +00001440 rootfs: os.PathLike, device: remote_access.RemoteDevice
1441) -> None:
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001442 """Deploy DLC LoadPin from temp rootfs to device.
1443
1444 Args:
1445 rootfs: Path to rootfs.
1446 device: A device object.
1447 """
1448 loadpin = dlc_lib.DLC_LOADPIN_TRUSTED_VERITY_DIGESTS
1449 dst_loadpin = Path("/") / dlc_lib.DLC_META_DIR / loadpin
1450 src_loadpin = rootfs / dlc_lib.DLC_META_DIR / loadpin
1451 if src_loadpin.exists():
1452 digests = set(osutils.ReadFile(src_loadpin).splitlines())
1453 digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1454 try:
1455 device_digests = set(device.CatFile(dst_loadpin).splitlines())
1456 device_digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1457 digests.update(device_digests)
1458 except remote_access.CatFileError:
1459 pass
1460
1461 with tempfile.NamedTemporaryFile(dir=rootfs) as f:
1462 osutils.WriteFile(f.name, dlc_lib.DLC_LOADPIN_FILE_HEADER + "\n")
1463 osutils.WriteFile(f.name, "\n".join(digests) + "\n", mode="a")
1464 device.CopyToDevice(
1465 f.name, dst_loadpin, mode="rsync", remote_sudo=True
1466 )
Andrew67b5fa72020-02-05 14:14:48 -08001467
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001468
Tim Baine4a783b2023-04-21 20:05:51 +00001469def _GetDLCInfo(
1470 device: remote_access.RemoteDevice, pkg_path: str, from_dut: bool
1471) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001472 """Returns information of a DLC given its package path.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001473
Alex Klein1699fab2022-09-08 08:46:06 -06001474 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001475 device: commandline.Device object; None to use the default device.
1476 pkg_path: path to the package.
1477 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1478 info from host.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001479
Alex Klein1699fab2022-09-08 08:46:06 -06001480 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001481 A tuple (dlc_id, dlc_package).
Alex Klein1699fab2022-09-08 08:46:06 -06001482 """
1483 environment_content = ""
1484 if from_dut:
1485 # On DUT, |pkg_path| is the directory which contains environment file.
1486 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1487 try:
1488 environment_data = device.CatFile(
1489 environment_path, max_size=None, encoding=None
1490 )
1491 except remote_access.CatFileError:
1492 # The package is not installed on DUT yet. Skip extracting info.
1493 return None, None
1494 else:
1495 # On host, pkg_path is tbz2 file which contains environment file.
1496 # Extract the metadata of the package file.
1497 data = portage.xpak.tbz2(pkg_path).get_data()
1498 environment_data = data[_ENVIRONMENT_FILENAME.encode("utf-8")]
1499
1500 # Extract the environment metadata.
1501 environment_content = bz2.decompress(environment_data)
1502
1503 with tempfile.NamedTemporaryFile() as f:
1504 # Dumps content into a file so we can use osutils.SourceEnvironment.
1505 path = os.path.realpath(f.name)
1506 osutils.WriteFile(path, environment_content, mode="wb")
1507 content = osutils.SourceEnvironment(
1508 path, (_DLC_ID, _DLC_PACKAGE, _DLC_ENABLED)
1509 )
1510
1511 dlc_enabled = content.get(_DLC_ENABLED)
1512 if dlc_enabled is not None and (
1513 dlc_enabled is False or str(dlc_enabled) == "false"
1514 ):
1515 logging.info("Installing DLC in rootfs.")
1516 return None, None
1517 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
1518
1519
1520def Deploy(
Tim Baine4a783b2023-04-21 20:05:51 +00001521 device: remote_access.RemoteDevice,
1522 packages: List[str],
1523 board: str = None,
1524 emerge: bool = True,
1525 update: bool = False,
1526 deep: bool = False,
1527 deep_rev: bool = False,
1528 clean_binpkg: bool = True,
1529 root: str = "/",
1530 strip: bool = True,
1531 emerge_args: List[str] = None,
1532 ssh_private_key: str = None,
1533 ping: bool = True,
1534 force: bool = False,
1535 dry_run: bool = False,
1536) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001537 """Deploys packages to a device.
1538
1539 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001540 device: commandline.Device object; None to use the default device.
1541 packages: List of packages (strings) to deploy to device.
1542 board: Board to use; None to automatically detect.
1543 emerge: True to emerge package, False to unmerge.
1544 update: Check installed version on device.
1545 deep: Install dependencies also. Implies |update|.
1546 deep_rev: Install reverse dependencies. Implies |deep|.
1547 clean_binpkg: Clean outdated binary packages.
1548 root: Package installation root path.
1549 strip: Run strip_package to filter out preset paths in the package.
1550 emerge_args: Extra arguments to pass to emerge.
1551 ssh_private_key: Path to an SSH private key file; None to use test keys.
1552 ping: True to ping the device before trying to connect.
1553 force: Ignore confidence checks and prompts.
1554 dry_run: Print deployment plan but do not deploy anything.
Alex Klein1699fab2022-09-08 08:46:06 -06001555
1556 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001557 ValueError: Invalid parameter or parameter combination.
1558 DeployError: Unrecoverable failure during deploy.
Alex Klein1699fab2022-09-08 08:46:06 -06001559 """
1560 if deep_rev:
1561 deep = True
1562 if deep:
1563 update = True
1564
1565 if not packages:
1566 raise DeployError("No packages provided, nothing to deploy.")
1567
1568 if update and not emerge:
1569 raise ValueError("Cannot update and unmerge.")
1570
1571 if device:
1572 hostname, username, port = device.hostname, device.username, device.port
1573 else:
1574 hostname, username, port = None, None, None
1575
1576 lsb_release = None
1577 sysroot = None
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001578 try:
Alex Klein1699fab2022-09-08 08:46:06 -06001579 # Somewhat confusing to clobber, but here we are.
1580 # pylint: disable=redefined-argument-from-local
1581 with remote_access.ChromiumOSDeviceHandler(
1582 hostname,
1583 port=port,
1584 username=username,
1585 private_key=ssh_private_key,
1586 base_dir=_DEVICE_BASE_DIR,
1587 ping=ping,
1588 ) as device:
1589 lsb_release = device.lsb_release
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001590
Alex Klein1699fab2022-09-08 08:46:06 -06001591 board = cros_build_lib.GetBoard(
1592 device_board=device.board, override_board=board
1593 )
1594 if not force and board != device.board:
1595 raise DeployError(
1596 "Device (%s) is incompatible with board %s. Use "
1597 "--force to deploy anyway." % (device.board, board)
1598 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001599
Alex Klein1699fab2022-09-08 08:46:06 -06001600 sysroot = build_target_lib.get_default_sysroot_path(board)
Andrew67b5fa72020-02-05 14:14:48 -08001601
Alex Klein975e86c2023-01-23 16:49:10 -07001602 # Don't bother trying to clean for unmerges. We won't use the local
1603 # db, and it just slows things down for the user.
Alex Klein1699fab2022-09-08 08:46:06 -06001604 if emerge and clean_binpkg:
1605 logging.notice(
1606 "Cleaning outdated binary packages from %s", sysroot
1607 )
1608 portage_util.CleanOutdatedBinaryPackages(sysroot)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001609
Alex Klein1699fab2022-09-08 08:46:06 -06001610 # Remount rootfs as writable if necessary.
1611 if not device.MountRootfsReadWrite():
1612 raise DeployError(
1613 "Cannot remount rootfs as read-write. Exiting."
1614 )
Ralph Nathane01ccf12015-04-16 10:40:32 -07001615
Alex Klein1699fab2022-09-08 08:46:06 -06001616 # Obtain list of packages to upgrade/remove.
1617 pkg_scanner = _InstallPackageScanner(sysroot)
Tim Baine4a783b2023-04-21 20:05:51 +00001618 (
1619 pkgs,
1620 listed,
1621 num_updates,
1622 pkgs_attrs,
1623 warnings_shown,
1624 ) = pkg_scanner.Run(device, root, packages, update, deep, deep_rev)
Alex Klein1699fab2022-09-08 08:46:06 -06001625 if emerge:
1626 action_str = "emerge"
1627 else:
1628 pkgs.reverse()
1629 action_str = "unmerge"
David Pursell9476bf42015-03-30 13:34:27 -07001630
Alex Klein1699fab2022-09-08 08:46:06 -06001631 if not pkgs:
1632 logging.notice("No packages to %s", action_str)
1633 return
David Pursell9476bf42015-03-30 13:34:27 -07001634
Alex Klein1699fab2022-09-08 08:46:06 -06001635 # Warn when the user installs & didn't `cros workon start`.
1636 if emerge:
1637 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(
1638 use_all=True
1639 )
1640 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1641 for package in listed:
1642 cp = package_info.SplitCPV(package).cp
1643 if cp in all_workon and cp not in worked_on_cps:
1644 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001645 "Are you intentionally deploying unmodified "
1646 "packages, or did you forget to run "
1647 "`cros workon --board=$BOARD start %s`?",
Alex Klein1699fab2022-09-08 08:46:06 -06001648 cp,
1649 )
David Pursell9476bf42015-03-30 13:34:27 -07001650
Alex Klein1699fab2022-09-08 08:46:06 -06001651 logging.notice("These are the packages to %s:", action_str)
1652 for i, pkg in enumerate(pkgs):
1653 logging.notice(
1654 "%s %d) %s", "*" if pkg in listed else " ", i + 1, pkg
1655 )
Gilad Arnolda0a98062015-07-07 08:34:27 -07001656
Alex Klein1699fab2022-09-08 08:46:06 -06001657 if dry_run or not _ConfirmDeploy(num_updates):
1658 return
David Pursell9476bf42015-03-30 13:34:27 -07001659
Tim Baine4a783b2023-04-21 20:05:51 +00001660 if (
1661 warnings_shown
1662 and not force
1663 and not _ConfirmUpdateDespiteWarnings()
1664 ):
1665 return
1666
Alex Klein1699fab2022-09-08 08:46:06 -06001667 # Select function (emerge or unmerge) and bind args.
1668 if emerge:
1669 func = functools.partial(
1670 _EmergePackages,
1671 pkgs,
1672 device,
1673 strip,
1674 sysroot,
1675 root,
1676 board,
1677 emerge_args,
1678 )
1679 else:
1680 func = functools.partial(
1681 _UnmergePackages, pkgs, device, root, pkgs_attrs
1682 )
David Pursell2e773382015-04-03 14:30:47 -07001683
Alex Klein1699fab2022-09-08 08:46:06 -06001684 # Call the function with the progress bar or with normal output.
1685 if command.UseProgressBar():
1686 op = BrilloDeployOperation(emerge)
1687 op.Run(func, log_level=logging.DEBUG)
1688 else:
1689 func()
David Pursell9476bf42015-03-30 13:34:27 -07001690
Alex Klein1699fab2022-09-08 08:46:06 -06001691 if device.IsSELinuxAvailable():
1692 if sum(x.count("selinux-policy") for x in pkgs):
1693 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001694 "Deploying SELinux policy will not take effect until "
1695 "reboot. SELinux policy is loaded by init. Also, "
1696 "changing the security contexts (labels) of a file "
1697 "will require building a new image and flashing the "
1698 "image onto the device."
Alex Klein1699fab2022-09-08 08:46:06 -06001699 )
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001700
Alex Klein1699fab2022-09-08 08:46:06 -06001701 # This message is read by BrilloDeployOperation.
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001702 logging.warning(
Alex Klein1699fab2022-09-08 08:46:06 -06001703 "Please restart any updated services on the device, "
1704 "or just reboot it."
1705 )
1706 except Exception:
1707 if lsb_release:
1708 lsb_entries = sorted(lsb_release.items())
1709 logging.info(
1710 "Following are the LSB version details of the device:\n%s",
1711 "\n".join("%s=%s" % (k, v) for k, v in lsb_entries),
1712 )
1713 raise