blob: 7745924757b54a3355b8af386b15b15f78a08bd7 [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
Jack Rosenthal2aff1af2023-07-13 18:34:28 -060020import re
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070021import tempfile
Tim Bain980db312023-04-26 17:29:00 +000022from typing import Dict, List, NamedTuple, Set, Tuple
David Pursell9476bf42015-03-30 13:34:27 -070023
Ralph Nathane01ccf12015-04-16 10:40:32 -070024from chromite.cli import command
Mike Frysinger06a51c82021-04-06 11:39:17 -040025from chromite.lib import build_target_lib
Ram Chandrasekar56152ec2021-11-22 17:10:41 +000026from chromite.lib import constants
David Pursell9476bf42015-03-30 13:34:27 -070027from chromite.lib import cros_build_lib
Alex Klein18a60af2020-06-11 12:08:47 -060028from chromite.lib import dlc_lib
Ralph Nathane01ccf12015-04-16 10:40:32 -070029from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070030from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070031from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070032from chromite.lib import remote_access
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +090033from chromite.lib import workon_helper
Alex Klein18a60af2020-06-11 12:08:47 -060034from chromite.lib.parser import package_info
35
Chris McDonald14ac61d2021-07-21 11:49:56 -060036
David Pursell9476bf42015-03-30 13:34:27 -070037try:
Alex Klein1699fab2022-09-08 08:46:06 -060038 import portage
David Pursell9476bf42015-03-30 13:34:27 -070039except ImportError:
Alex Klein1699fab2022-09-08 08:46:06 -060040 if cros_build_lib.IsInsideChroot():
41 raise
David Pursell9476bf42015-03-30 13:34:27 -070042
43
Alex Klein1699fab2022-09-08 08:46:06 -060044_DEVICE_BASE_DIR = "/usr/local/tmp/cros-deploy"
David Pursell9476bf42015-03-30 13:34:27 -070045# This is defined in src/platform/dev/builder.py
Alex Klein1699fab2022-09-08 08:46:06 -060046_STRIPPED_PACKAGES_DIR = "stripped-packages"
David Pursell9476bf42015-03-30 13:34:27 -070047
48_MAX_UPDATES_NUM = 10
49_MAX_UPDATES_WARNING = (
Alex Klein1699fab2022-09-08 08:46:06 -060050 "You are about to update a large number of installed packages, which "
51 "might take a long time, fail midway, or leave the target in an "
52 "inconsistent state. It is highly recommended that you flash a new image "
53 "instead."
54)
David Pursell9476bf42015-03-30 13:34:27 -070055
Alex Klein1699fab2022-09-08 08:46:06 -060056_DLC_ID = "DLC_ID"
57_DLC_PACKAGE = "DLC_PACKAGE"
58_DLC_ENABLED = "DLC_ENABLED"
59_ENVIRONMENT_FILENAME = "environment.bz2"
60_DLC_INSTALL_ROOT = "/var/cache/dlc"
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070061
David Pursell9476bf42015-03-30 13:34:27 -070062
Tim Bain980db312023-04-26 17:29:00 +000063class CpvInfo(NamedTuple):
64 """Holds a CPV and its associated information that we care about"""
65
66 cpv: Dict[str, package_info.CPV]
67 slot: str
68 rdep_raw: str
69 build_time: int
70 use: str
71
72
David Pursell9476bf42015-03-30 13:34:27 -070073class DeployError(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060074 """Thrown when an unrecoverable error is encountered during deploy."""
David Pursell9476bf42015-03-30 13:34:27 -070075
76
Ralph Nathane01ccf12015-04-16 10:40:32 -070077class BrilloDeployOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -060078 """ProgressBarOperation specific for brillo deploy."""
Ralph Nathane01ccf12015-04-16 10:40:32 -070079
Alex Klein1699fab2022-09-08 08:46:06 -060080 # These two variables are used to validate the output in the VM integration
81 # tests. Changes to the output must be reflected here.
82 MERGE_EVENTS = (
83 "Preparing local packages",
84 "NOTICE: Copying binpkgs",
85 "NOTICE: Installing",
86 "been installed.",
87 "Please restart any updated",
88 )
89 UNMERGE_EVENTS = (
90 "NOTICE: Unmerging",
91 "been uninstalled.",
92 "Please restart any updated",
93 )
Ralph Nathane01ccf12015-04-16 10:40:32 -070094
Tim Baine4a783b2023-04-21 20:05:51 +000095 def __init__(self, emerge: bool):
Alex Klein1699fab2022-09-08 08:46:06 -060096 """Construct BrilloDeployOperation object.
Ralph Nathane01ccf12015-04-16 10:40:32 -070097
Alex Klein1699fab2022-09-08 08:46:06 -060098 Args:
Tim Baine4a783b2023-04-21 20:05:51 +000099 emerge: True if emerge, False if unmerge.
Alex Klein1699fab2022-09-08 08:46:06 -0600100 """
101 super().__init__()
102 if emerge:
103 self._events = self.MERGE_EVENTS
104 else:
105 self._events = self.UNMERGE_EVENTS
106 self._total = len(self._events)
107 self._completed = 0
108
109 def ParseOutput(self, output=None):
110 """Parse the output of brillo deploy to update a progress bar."""
111 stdout = self._stdout.read()
112 stderr = self._stderr.read()
113 output = stdout + stderr
114 for event in self._events:
115 self._completed += output.count(event)
116 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700117
118
Alex Klein074f94f2023-06-22 10:32:06 -0600119class _InstallPackageScanner:
Alex Klein1699fab2022-09-08 08:46:06 -0600120 """Finds packages that need to be installed on a target device.
David Pursell9476bf42015-03-30 13:34:27 -0700121
Alex Klein1699fab2022-09-08 08:46:06 -0600122 Scans the sysroot bintree, beginning with a user-provided list of packages,
123 to find all packages that need to be installed. If so instructed,
124 transitively scans forward (mandatory) and backward (optional) dependencies
125 as well. A package will be installed if missing on the target (mandatory
126 packages only), or it will be updated if its sysroot version and build time
127 are different from the target. Common usage:
David Pursell9476bf42015-03-30 13:34:27 -0700128
Alex Klein53cc3bf2022-10-13 08:50:01 -0600129 pkg_scanner = _InstallPackageScanner(sysroot)
130 pkgs = pkg_scanner.Run(...)
Alex Klein1699fab2022-09-08 08:46:06 -0600131 """
David Pursell9476bf42015-03-30 13:34:27 -0700132
Alex Klein1699fab2022-09-08 08:46:06 -0600133 class VartreeError(Exception):
134 """An error in the processing of the installed packages tree."""
David Pursell9476bf42015-03-30 13:34:27 -0700135
Alex Klein1699fab2022-09-08 08:46:06 -0600136 class BintreeError(Exception):
137 """An error in the processing of the source binpkgs tree."""
David Pursell9476bf42015-03-30 13:34:27 -0700138
Alex Klein074f94f2023-06-22 10:32:06 -0600139 class PkgInfo:
Alex Klein1699fab2022-09-08 08:46:06 -0600140 """A record containing package information."""
David Pursell9476bf42015-03-30 13:34:27 -0700141
Tim Baine4a783b2023-04-21 20:05:51 +0000142 __slots__ = (
143 "cpv",
144 "build_time",
145 "rdeps_raw",
146 "use",
147 "rdeps",
148 "rev_rdeps",
149 )
David Pursell9476bf42015-03-30 13:34:27 -0700150
Alex Klein1699fab2022-09-08 08:46:06 -0600151 def __init__(
Tim Baine4a783b2023-04-21 20:05:51 +0000152 self,
153 cpv: package_info.CPV,
154 build_time: int,
155 rdeps_raw: str,
156 use: str,
157 rdeps: set = None,
158 rev_rdeps: set = None,
Alex Klein1699fab2022-09-08 08:46:06 -0600159 ):
160 self.cpv = cpv
161 self.build_time = build_time
162 self.rdeps_raw = rdeps_raw
Tim Baine4a783b2023-04-21 20:05:51 +0000163 self.use = use
Alex Klein1699fab2022-09-08 08:46:06 -0600164 self.rdeps = set() if rdeps is None else rdeps
165 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
David Pursell9476bf42015-03-30 13:34:27 -0700166
Alex Klein1699fab2022-09-08 08:46:06 -0600167 # Python snippet for dumping vartree info on the target. Instantiate using
168 # _GetVartreeSnippet().
169 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700170import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700171import os
172import portage
173
174# Normalize the path to match what portage will index.
175target_root = os.path.normpath('%(root)s')
176if not target_root.endswith('/'):
177 target_root += '/'
178trees = portage.create_trees(target_root=target_root, config_root='/')
179vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700180pkg_info = []
181for cpv in vartree.dbapi.cpv_all():
Tim Baine4a783b2023-04-21 20:05:51 +0000182 slot, rdep_raw, build_time, use = vartree.dbapi.aux_get(
183 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME', 'USE'))
184 pkg_info.append((cpv, slot, rdep_raw, build_time, use))
David Pursell9476bf42015-03-30 13:34:27 -0700185
186print(json.dumps(pkg_info))
187"""
188
Tim Baine4a783b2023-04-21 20:05:51 +0000189 def __init__(self, sysroot: str):
Alex Klein1699fab2022-09-08 08:46:06 -0600190 self.sysroot = sysroot
Alex Klein975e86c2023-01-23 16:49:10 -0700191 # Members containing the sysroot (binpkg) and target (installed) package
192 # DB.
Alex Klein1699fab2022-09-08 08:46:06 -0600193 self.target_db = None
194 self.binpkgs_db = None
195 # Members for managing the dependency resolution work queue.
196 self.queue = None
197 self.seen = None
198 self.listed = None
David Pursell9476bf42015-03-30 13:34:27 -0700199
Alex Klein1699fab2022-09-08 08:46:06 -0600200 @staticmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000201 def _GetCP(cpv: package_info.CPV) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600202 """Returns the CP value for a given CPV string."""
203 attrs = package_info.SplitCPV(cpv, strict=False)
204 if not attrs.cp:
205 raise ValueError("Cannot get CP value for %s" % cpv)
206 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700207
Alex Klein1699fab2022-09-08 08:46:06 -0600208 @staticmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000209 def _InDB(cp: str, slot: str, db: Dict[str, Dict[str, PkgInfo]]) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -0600210 """Returns whether CP and slot are found in a database (if provided)."""
211 cp_slots = db.get(cp) if db else None
212 return cp_slots is not None and (not slot or slot in cp_slots)
David Pursell9476bf42015-03-30 13:34:27 -0700213
Alex Klein1699fab2022-09-08 08:46:06 -0600214 @staticmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000215 def _AtomStr(cp: str, slot: str) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600216 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
217 return "%s:%s" % (cp, slot) if slot else cp
David Pursell9476bf42015-03-30 13:34:27 -0700218
Alex Klein1699fab2022-09-08 08:46:06 -0600219 @classmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000220 def _GetVartreeSnippet(cls, root: str = "/") -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600221 """Returns a code snippet for dumping the vartree on the target.
David Pursell9476bf42015-03-30 13:34:27 -0700222
Alex Klein1699fab2022-09-08 08:46:06 -0600223 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600224 root: The installation root.
David Pursell9476bf42015-03-30 13:34:27 -0700225
Alex Klein1699fab2022-09-08 08:46:06 -0600226 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600227 The said code snippet (string) with parameters filled in.
Alex Klein1699fab2022-09-08 08:46:06 -0600228 """
229 return cls._GET_VARTREE % {"root": root}
David Pursell9476bf42015-03-30 13:34:27 -0700230
Alex Klein1699fab2022-09-08 08:46:06 -0600231 @classmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000232 def _StripDepAtom(
233 cls, dep_atom: str, installed_db: Dict[str, Dict[str, PkgInfo]] = None
234 ) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600235 """Strips a dependency atom and returns a (CP, slot) pair."""
236 # TODO(garnold) This is a gross simplification of ebuild dependency
237 # semantics, stripping and ignoring various qualifiers (versions, slots,
238 # USE flag, negation) and will likely need to be fixed. chromium:447366.
David Pursell9476bf42015-03-30 13:34:27 -0700239
Alex Klein1699fab2022-09-08 08:46:06 -0600240 # Ignore unversioned blockers, leaving them for the user to resolve.
241 if dep_atom[0] == "!" and dep_atom[1] not in "<=>~":
242 return None, None
David Pursell9476bf42015-03-30 13:34:27 -0700243
Alex Klein1699fab2022-09-08 08:46:06 -0600244 cp = dep_atom
245 slot = None
246 require_installed = False
David Pursell9476bf42015-03-30 13:34:27 -0700247
Alex Klein1699fab2022-09-08 08:46:06 -0600248 # Versioned blockers should be updated, but only if already installed.
Alex Klein975e86c2023-01-23 16:49:10 -0700249 # These are often used for forcing cascaded updates of multiple
250 # packages, so we're treating them as ordinary constraints with hopes
251 # that it'll lead to the desired result.
Alex Klein1699fab2022-09-08 08:46:06 -0600252 if cp.startswith("!"):
253 cp = cp.lstrip("!")
254 require_installed = True
David Pursell9476bf42015-03-30 13:34:27 -0700255
Alex Klein1699fab2022-09-08 08:46:06 -0600256 # Remove USE flags.
257 if "[" in cp:
258 cp = cp[: cp.index("[")] + cp[cp.index("]") + 1 :]
David Pursell9476bf42015-03-30 13:34:27 -0700259
Matt Turner2fa91bc2023-08-11 18:39:35 +0000260 # Separate the slot qualifier and strip off subslot binding operator
Alex Klein1699fab2022-09-08 08:46:06 -0600261 if ":" in cp:
262 cp, slot = cp.split(":")
Matt Turner2fa91bc2023-08-11 18:39:35 +0000263 for delim in ("=",):
Alex Klein1699fab2022-09-08 08:46:06 -0600264 slot = slot.split(delim, 1)[0]
David Pursell9476bf42015-03-30 13:34:27 -0700265
Alex Klein1699fab2022-09-08 08:46:06 -0600266 # Strip version wildcards (right), comparators (left).
267 cp = cp.rstrip("*")
268 cp = cp.lstrip("<=>~")
David Pursell9476bf42015-03-30 13:34:27 -0700269
Alex Klein1699fab2022-09-08 08:46:06 -0600270 # Turn into CP form.
271 cp = cls._GetCP(cp)
David Pursell9476bf42015-03-30 13:34:27 -0700272
Alex Klein1699fab2022-09-08 08:46:06 -0600273 if require_installed and not cls._InDB(cp, None, installed_db):
274 return None, None
David Pursell9476bf42015-03-30 13:34:27 -0700275
Alex Klein1699fab2022-09-08 08:46:06 -0600276 return cp, slot
David Pursell9476bf42015-03-30 13:34:27 -0700277
Alex Klein1699fab2022-09-08 08:46:06 -0600278 @classmethod
Tim Baine4a783b2023-04-21 20:05:51 +0000279 def _ProcessDepStr(
280 cls,
281 dep_str: str,
282 installed_db: Dict[str, Dict[str, PkgInfo]],
283 avail_db: Dict[str, Dict[str, PkgInfo]],
284 ) -> set:
Alex Klein1699fab2022-09-08 08:46:06 -0600285 """Resolves and returns a list of dependencies from a dependency string.
David Pursell9476bf42015-03-30 13:34:27 -0700286
Alex Klein1699fab2022-09-08 08:46:06 -0600287 This parses a dependency string and returns a list of package names and
Alex Klein975e86c2023-01-23 16:49:10 -0700288 slots. Other atom qualifiers (version, sub-slot, block) are ignored.
289 When resolving disjunctive deps, we include all choices that are fully
290 present in |installed_db|. If none is present, we choose an arbitrary
291 one that is available.
David Pursell9476bf42015-03-30 13:34:27 -0700292
Alex Klein1699fab2022-09-08 08:46:06 -0600293 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600294 dep_str: A raw dependency string.
295 installed_db: A database of installed packages.
296 avail_db: A database of packages available for installation.
David Pursell9476bf42015-03-30 13:34:27 -0700297
Alex Klein1699fab2022-09-08 08:46:06 -0600298 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600299 A list of pairs (CP, slot).
David Pursell9476bf42015-03-30 13:34:27 -0700300
Alex Klein1699fab2022-09-08 08:46:06 -0600301 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600302 ValueError: the dependencies string is malformed.
Alex Klein1699fab2022-09-08 08:46:06 -0600303 """
David Pursell9476bf42015-03-30 13:34:27 -0700304
Tim Baine4a783b2023-04-21 20:05:51 +0000305 def ProcessSubDeps(
306 dep_exp: Set[Tuple[str, str]], disjunct: bool
307 ) -> Set[Tuple[str, str]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600308 """Parses and processes a dependency (sub)expression."""
309 deps = set()
310 default_deps = set()
311 sub_disjunct = False
312 for dep_sub_exp in dep_exp:
313 sub_deps = set()
David Pursell9476bf42015-03-30 13:34:27 -0700314
Alex Klein1699fab2022-09-08 08:46:06 -0600315 if isinstance(dep_sub_exp, (list, tuple)):
316 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
317 sub_disjunct = False
318 elif sub_disjunct:
319 raise ValueError("Malformed disjunctive operation in deps")
320 elif dep_sub_exp == "||":
321 sub_disjunct = True
322 elif dep_sub_exp.endswith("?"):
323 raise ValueError("Dependencies contain a conditional")
324 else:
325 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
326 if cp:
327 sub_deps = set([(cp, slot)])
328 elif disjunct:
329 raise ValueError("Atom in disjunct ignored")
David Pursell9476bf42015-03-30 13:34:27 -0700330
Alex Klein1699fab2022-09-08 08:46:06 -0600331 # Handle sub-deps of a disjunctive expression.
332 if disjunct:
Alex Klein975e86c2023-01-23 16:49:10 -0700333 # Make the first available choice the default, for use in
334 # case that no option is installed.
Alex Klein1699fab2022-09-08 08:46:06 -0600335 if (
336 not default_deps
337 and avail_db is not None
338 and all(
339 cls._InDB(cp, slot, avail_db)
340 for cp, slot in sub_deps
341 )
342 ):
343 default_deps = sub_deps
David Pursell9476bf42015-03-30 13:34:27 -0700344
Alex Klein975e86c2023-01-23 16:49:10 -0700345 # If not all sub-deps are installed, then don't consider
346 # them.
Alex Klein1699fab2022-09-08 08:46:06 -0600347 if not all(
348 cls._InDB(cp, slot, installed_db)
349 for cp, slot in sub_deps
350 ):
351 sub_deps = set()
David Pursell9476bf42015-03-30 13:34:27 -0700352
Alex Klein1699fab2022-09-08 08:46:06 -0600353 deps.update(sub_deps)
David Pursell9476bf42015-03-30 13:34:27 -0700354
Alex Klein1699fab2022-09-08 08:46:06 -0600355 return deps or default_deps
David Pursell9476bf42015-03-30 13:34:27 -0700356
Alex Klein1699fab2022-09-08 08:46:06 -0600357 try:
358 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
359 except portage.exception.InvalidDependString as e:
360 raise ValueError("Invalid dep string: %s" % e)
361 except ValueError as e:
362 raise ValueError("%s: %s" % (e, dep_str))
David Pursell9476bf42015-03-30 13:34:27 -0700363
Alex Klein1699fab2022-09-08 08:46:06 -0600364 def _BuildDB(
Tim Baine4a783b2023-04-21 20:05:51 +0000365 self,
Tim Bain980db312023-04-26 17:29:00 +0000366 cpv_info: List[CpvInfo],
Tim Baine4a783b2023-04-21 20:05:51 +0000367 process_rdeps: bool,
368 process_rev_rdeps: bool,
369 installed_db: Dict[str, Dict[str, PkgInfo]] = None,
370 ) -> Dict[str, Dict[str, PkgInfo]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600371 """Returns a database of packages given a list of CPV info.
David Pursell9476bf42015-03-30 13:34:27 -0700372
Alex Klein1699fab2022-09-08 08:46:06 -0600373 Args:
Tim Bain980db312023-04-26 17:29:00 +0000374 cpv_info: A list of CpvInfos containing package CPV and attributes.
Alex Klein53cc3bf2022-10-13 08:50:01 -0600375 process_rdeps: Whether to populate forward dependencies.
376 process_rev_rdeps: Whether to populate reverse dependencies.
377 installed_db: A database of installed packages for filtering
378 disjunctive choices against; if None, using own built database.
David Pursell9476bf42015-03-30 13:34:27 -0700379
Alex Klein1699fab2022-09-08 08:46:06 -0600380 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600381 A map from CP values to another dictionary that maps slots
382 to package attribute tuples. Tuples contain a CPV value
383 (string), build time (string), runtime dependencies (set),
384 and reverse dependencies (set, empty if not populated).
David Pursell9476bf42015-03-30 13:34:27 -0700385
Alex Klein1699fab2022-09-08 08:46:06 -0600386 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600387 ValueError: If more than one CPV occupies a single slot.
Alex Klein1699fab2022-09-08 08:46:06 -0600388 """
389 db = {}
390 logging.debug("Populating package DB...")
Tim Baine4a783b2023-04-21 20:05:51 +0000391 for cpv, slot, rdeps_raw, build_time, use in cpv_info:
Alex Klein1699fab2022-09-08 08:46:06 -0600392 cp = self._GetCP(cpv)
Trent Apted1e2e4f32023-05-05 03:50:20 +0000393 cp_slots = db.setdefault(cp, {})
Alex Klein1699fab2022-09-08 08:46:06 -0600394 if slot in cp_slots:
395 raise ValueError(
396 "More than one package found for %s"
397 % self._AtomStr(cp, slot)
398 )
399 logging.debug(
400 " %s -> %s, built %s, raw rdeps: %s",
401 self._AtomStr(cp, slot),
402 cpv,
403 build_time,
404 rdeps_raw,
405 )
Tim Baine4a783b2023-04-21 20:05:51 +0000406 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw, use)
David Pursell9476bf42015-03-30 13:34:27 -0700407
Alex Klein1699fab2022-09-08 08:46:06 -0600408 avail_db = db
409 if installed_db is None:
410 installed_db = db
411 avail_db = None
David Pursell9476bf42015-03-30 13:34:27 -0700412
Alex Klein1699fab2022-09-08 08:46:06 -0600413 # Add approximate forward dependencies.
David Pursell9476bf42015-03-30 13:34:27 -0700414 if process_rdeps:
Alex Klein1699fab2022-09-08 08:46:06 -0600415 logging.debug("Populating forward dependencies...")
416 for cp, cp_slots in db.items():
417 for slot, pkg_info in cp_slots.items():
418 pkg_info.rdeps.update(
419 self._ProcessDepStr(
420 pkg_info.rdeps_raw, installed_db, avail_db
421 )
422 )
423 logging.debug(
424 " %s (%s) processed rdeps: %s",
425 self._AtomStr(cp, slot),
426 pkg_info.cpv,
427 " ".join(
428 [
429 self._AtomStr(rdep_cp, rdep_slot)
430 for rdep_cp, rdep_slot in pkg_info.rdeps
431 ]
432 ),
433 )
434
435 # Add approximate reverse dependencies (optional).
David Pursell9476bf42015-03-30 13:34:27 -0700436 if process_rev_rdeps:
Alex Klein1699fab2022-09-08 08:46:06 -0600437 logging.debug("Populating reverse dependencies...")
438 for cp, cp_slots in db.items():
439 for slot, pkg_info in cp_slots.items():
440 for rdep_cp, rdep_slot in pkg_info.rdeps:
441 to_slots = db.get(rdep_cp)
442 if not to_slots:
443 continue
David Pursell9476bf42015-03-30 13:34:27 -0700444
Alex Klein1699fab2022-09-08 08:46:06 -0600445 for to_slot, to_pkg_info in to_slots.items():
446 if rdep_slot and to_slot != rdep_slot:
447 continue
448 logging.debug(
449 " %s (%s) added as rev rdep for %s (%s)",
450 self._AtomStr(cp, slot),
451 pkg_info.cpv,
452 self._AtomStr(rdep_cp, to_slot),
453 to_pkg_info.cpv,
454 )
455 to_pkg_info.rev_rdeps.add((cp, slot))
David Pursell9476bf42015-03-30 13:34:27 -0700456
Alex Klein1699fab2022-09-08 08:46:06 -0600457 return db
David Pursell9476bf42015-03-30 13:34:27 -0700458
Jack Rosenthal2aff1af2023-07-13 18:34:28 -0600459 def _get_portage_interpreter(
460 self, device: remote_access.RemoteDevice
461 ) -> str:
462 """Get the Python interpreter that should be used for Portage.
463
464 Args:
465 device: The device to find the interpreter on.
466
467 Returns:
468 The executable that should be used for Python.
469 """
470 result = device.agent.RemoteSh(
471 "ls -1 /usr/lib/python-exec/python*/emerge"
472 )
473 emerge_bins = [Path(x) for x in result.stdout.splitlines()]
474 if not emerge_bins:
475 raise self.VartreeError(
476 "No suitable Python interpreter found for Portage."
477 )
478
479 # If Portage is installed for multiple Python versions, prefer the
480 # interpreter with the highest version.
481 def _parse_version(name):
482 match = re.fullmatch(r"python(\d+)\.(\d+)", name)
483 if match:
484 return tuple(int(x) for x in match.groups())
485 return (0, 0)
486
487 return max((x.parent.name for x in emerge_bins), key=_parse_version)
488
Tim Baine4a783b2023-04-21 20:05:51 +0000489 def _InitTargetVarDB(
490 self,
491 device: remote_access.RemoteDevice,
492 root: str,
493 process_rdeps: bool,
494 process_rev_rdeps: bool,
495 ) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600496 """Initializes a dictionary of packages installed on |device|."""
497 get_vartree_script = self._GetVartreeSnippet(root)
Jack Rosenthal2aff1af2023-07-13 18:34:28 -0600498 python = self._get_portage_interpreter(device)
Alex Klein1699fab2022-09-08 08:46:06 -0600499 try:
Mike Frysingerc0780a62022-08-29 04:41:56 -0400500 result = device.agent.RemoteSh(
Jack Rosenthal2aff1af2023-07-13 18:34:28 -0600501 [python], remote_sudo=True, input=get_vartree_script
Alex Klein1699fab2022-09-08 08:46:06 -0600502 )
503 except cros_build_lib.RunCommandError as e:
504 logging.error("Cannot get target vartree:\n%s", e.stderr)
505 raise
David Pursell9476bf42015-03-30 13:34:27 -0700506
Alex Klein1699fab2022-09-08 08:46:06 -0600507 try:
508 self.target_db = self._BuildDB(
Tim Bain980db312023-04-26 17:29:00 +0000509 [CpvInfo(*cpv_info) for cpv_info in json.loads(result.stdout)],
510 process_rdeps,
511 process_rev_rdeps,
Alex Klein1699fab2022-09-08 08:46:06 -0600512 )
513 except ValueError as e:
514 raise self.VartreeError(str(e))
David Pursell9476bf42015-03-30 13:34:27 -0700515
Tim Baine4a783b2023-04-21 20:05:51 +0000516 def _InitBinpkgDB(self, process_rdeps: bool) -> None:
Alex Klein975e86c2023-01-23 16:49:10 -0700517 """Initializes a dictionary of binpkgs for updating the target."""
Alex Klein1699fab2022-09-08 08:46:06 -0600518 # Get build root trees; portage indexes require a trailing '/'.
519 build_root = os.path.join(self.sysroot, "")
520 trees = portage.create_trees(
521 target_root=build_root, config_root=build_root
522 )
523 bintree = trees[build_root]["bintree"]
524 binpkgs_info = []
525 for cpv in bintree.dbapi.cpv_all():
Tim Baine4a783b2023-04-21 20:05:51 +0000526 slot, rdep_raw, build_time, use = bintree.dbapi.aux_get(
527 cpv, ["SLOT", "RDEPEND", "BUILD_TIME", "USE"]
Alex Klein1699fab2022-09-08 08:46:06 -0600528 )
Tim Bain980db312023-04-26 17:29:00 +0000529 binpkgs_info.append(CpvInfo(cpv, slot, rdep_raw, build_time, use))
David Pursell9476bf42015-03-30 13:34:27 -0700530
Alex Klein1699fab2022-09-08 08:46:06 -0600531 try:
532 self.binpkgs_db = self._BuildDB(
533 binpkgs_info, process_rdeps, False, installed_db=self.target_db
534 )
535 except ValueError as e:
536 raise self.BintreeError(str(e))
David Pursell9476bf42015-03-30 13:34:27 -0700537
Tim Baine4a783b2023-04-21 20:05:51 +0000538 def _InitDepQueue(self) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600539 """Initializes the dependency work queue."""
540 self.queue = set()
541 self.seen = {}
542 self.listed = set()
David Pursell9476bf42015-03-30 13:34:27 -0700543
Tim Baine4a783b2023-04-21 20:05:51 +0000544 def _EnqDep(self, dep: str, listed: bool, optional: bool) -> bool:
Alex Klein975e86c2023-01-23 16:49:10 -0700545 """Enqueues a dependency if not seen before or if set non-optional."""
Alex Klein1699fab2022-09-08 08:46:06 -0600546 if dep in self.seen and (optional or not self.seen[dep]):
547 return False
David Pursell9476bf42015-03-30 13:34:27 -0700548
Alex Klein1699fab2022-09-08 08:46:06 -0600549 self.queue.add(dep)
550 self.seen[dep] = optional
551 if listed:
552 self.listed.add(dep)
553 return True
David Pursell9476bf42015-03-30 13:34:27 -0700554
Tim Baine4a783b2023-04-21 20:05:51 +0000555 def _DeqDep(self) -> Tuple[str, bool, bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600556 """Dequeues and returns a dependency, its listed and optional flags.
David Pursell9476bf42015-03-30 13:34:27 -0700557
Alex Klein975e86c2023-01-23 16:49:10 -0700558 This returns listed packages first, if any are present, to ensure that
559 we correctly mark them as such when they are first being processed.
Alex Klein1699fab2022-09-08 08:46:06 -0600560 """
561 if self.listed:
562 dep = self.listed.pop()
563 self.queue.remove(dep)
564 listed = True
565 else:
566 dep = self.queue.pop()
567 listed = False
David Pursell9476bf42015-03-30 13:34:27 -0700568
Alex Klein1699fab2022-09-08 08:46:06 -0600569 return dep, listed, self.seen[dep]
David Pursell9476bf42015-03-30 13:34:27 -0700570
Tim Baine4a783b2023-04-21 20:05:51 +0000571 def _FindPackageMatches(self, cpv_pattern: str) -> List[Tuple[str, str]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600572 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
David Pursell9476bf42015-03-30 13:34:27 -0700573
Alex Klein1699fab2022-09-08 08:46:06 -0600574 This is breaking |cpv_pattern| into its C, P and V components, each of
575 which may or may not be present or contain wildcards. It then scans the
Alex Klein975e86c2023-01-23 16:49:10 -0700576 binpkgs database to find all atoms that match these components,
577 returning a list of CP and slot qualifier. When the pattern does not
578 specify a version, or when a CP has only one slot in the binpkgs
579 database, we omit the slot qualifier in the result.
David Pursell9476bf42015-03-30 13:34:27 -0700580
Alex Klein1699fab2022-09-08 08:46:06 -0600581 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600582 cpv_pattern: A CPV pattern, potentially partial and/or having
583 wildcards.
David Pursell9476bf42015-03-30 13:34:27 -0700584
Alex Klein1699fab2022-09-08 08:46:06 -0600585 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600586 A list of (CPV, slot) pairs of packages in the binpkgs database that
587 match the pattern.
Alex Klein1699fab2022-09-08 08:46:06 -0600588 """
589 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
590 cp_pattern = os.path.join(attrs.category or "*", attrs.package or "*")
591 matches = []
592 for cp, cp_slots in self.binpkgs_db.items():
593 if not fnmatch.fnmatchcase(cp, cp_pattern):
594 continue
David Pursell9476bf42015-03-30 13:34:27 -0700595
Alex Klein975e86c2023-01-23 16:49:10 -0700596 # If no version attribute was given or there's only one slot, omit
597 # the slot qualifier.
Alex Klein1699fab2022-09-08 08:46:06 -0600598 if not attrs.version or len(cp_slots) == 1:
599 matches.append((cp, None))
600 else:
601 cpv_pattern = "%s-%s" % (cp, attrs.version)
602 for slot, pkg_info in cp_slots.items():
603 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
604 matches.append((cp, slot))
David Pursell9476bf42015-03-30 13:34:27 -0700605
Alex Klein1699fab2022-09-08 08:46:06 -0600606 return matches
David Pursell9476bf42015-03-30 13:34:27 -0700607
Tim Baine4a783b2023-04-21 20:05:51 +0000608 def _FindPackage(self, pkg: str) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600609 """Returns the (CP, slot) pair for a package matching |pkg|.
David Pursell9476bf42015-03-30 13:34:27 -0700610
Alex Klein1699fab2022-09-08 08:46:06 -0600611 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600612 pkg: Path to a binary package or a (partial) package CPV specifier.
David Pursell9476bf42015-03-30 13:34:27 -0700613
Alex Klein1699fab2022-09-08 08:46:06 -0600614 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600615 A (CP, slot) pair for the given package; slot may be None
616 (unspecified).
David Pursell9476bf42015-03-30 13:34:27 -0700617
Alex Klein1699fab2022-09-08 08:46:06 -0600618 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600619 ValueError: if |pkg| is not a binpkg file nor does it match
620 something that's in the bintree.
Alex Klein1699fab2022-09-08 08:46:06 -0600621 """
622 if pkg.endswith(".tbz2") and os.path.isfile(pkg):
623 package = os.path.basename(os.path.splitext(pkg)[0])
624 category = os.path.basename(os.path.dirname(pkg))
625 return self._GetCP(os.path.join(category, package)), None
David Pursell9476bf42015-03-30 13:34:27 -0700626
Alex Klein1699fab2022-09-08 08:46:06 -0600627 matches = self._FindPackageMatches(pkg)
628 if not matches:
629 raise ValueError("No package found for %s" % pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700630
Alex Klein1699fab2022-09-08 08:46:06 -0600631 idx = 0
632 if len(matches) > 1:
633 # Ask user to pick among multiple matches.
634 idx = cros_build_lib.GetChoice(
635 "Multiple matches found for %s: " % pkg,
636 ["%s:%s" % (cp, slot) if slot else cp for cp, slot in matches],
637 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700638
Alex Klein1699fab2022-09-08 08:46:06 -0600639 return matches[idx]
640
Tim Baine4a783b2023-04-21 20:05:51 +0000641 def _NeedsInstall(
642 self, cpv: str, slot: str, build_time: int, optional: bool
643 ) -> Tuple[bool, bool, bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600644 """Returns whether a package needs to be installed on the target.
645
646 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600647 cpv: Fully qualified CPV (string) of the package.
648 slot: Slot identifier (string).
649 build_time: The BUILT_TIME value (string) of the binpkg.
650 optional: Whether package is optional on the target.
Alex Klein1699fab2022-09-08 08:46:06 -0600651
652 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000653 A tuple (install, update, use_mismatch) indicating whether to
654 |install| the package, whether it is an |update| to an existing
655 package, and whether the package's USE flags mismatch the existing
656 package.
Alex Klein1699fab2022-09-08 08:46:06 -0600657
658 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600659 ValueError: if slot is not provided.
Alex Klein1699fab2022-09-08 08:46:06 -0600660 """
661 # If not checking installed packages, always install.
662 if not self.target_db:
Tim Baine4a783b2023-04-21 20:05:51 +0000663 return True, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600664
665 cp = self._GetCP(cpv)
Trent Apted1e2e4f32023-05-05 03:50:20 +0000666 target_pkg_info = self.target_db.get(cp, {}).get(slot)
Alex Klein1699fab2022-09-08 08:46:06 -0600667 if target_pkg_info is not None:
Tim Baine4a783b2023-04-21 20:05:51 +0000668 attrs = package_info.SplitCPV(cpv)
669 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
Alex Klein1699fab2022-09-08 08:46:06 -0600670
Tim Baine4a783b2023-04-21 20:05:51 +0000671 def _get_attr_mismatch(
672 attr_name: str, new_attr: any, target_attr: any
673 ) -> Tuple[str, str, str]:
674 """Check if the new and target packages differ for an attribute.
675
676 Args:
677 attr_name: The name of the attribute being checked (string).
678 new_attr: The value of the given attribute for the new
679 package (string).
680 target_attr: The value of the given attribute for the target
681 (existing) package (string).
682
683 Returns:
684 A tuple (attr_name, new_attr, target_attr) composed of the
685 args if there is a mismatch, or None if the values match.
686 """
687 mismatch = new_attr != target_attr
688 if mismatch:
689 return attr_name, new_attr, target_attr
690
691 update_info = _get_attr_mismatch(
692 "version", attrs.version, target_attrs.version
693 ) or _get_attr_mismatch(
694 "build time", build_time, target_pkg_info.build_time
695 )
696
697 if update_info:
698 attr_name, new_attr, target_attr = update_info
Alex Klein1699fab2022-09-08 08:46:06 -0600699 logging.debug(
Tim Baine4a783b2023-04-21 20:05:51 +0000700 "Updating %s: %s (%s) different on target (%s)",
701 cp,
702 attr_name,
703 new_attr,
704 target_attr,
Alex Klein1699fab2022-09-08 08:46:06 -0600705 )
Tim Baine4a783b2023-04-21 20:05:51 +0000706
Trent Apted1e2e4f32023-05-05 03:50:20 +0000707 binpkg_pkg_info = self.binpkgs_db.get(cp, {}).get(slot)
Tim Baine4a783b2023-04-21 20:05:51 +0000708 use_mismatch = binpkg_pkg_info.use != target_pkg_info.use
709 if use_mismatch:
710 logging.warning(
711 "USE flags for package %s do not match (Existing='%s', "
712 "New='%s').",
713 cp,
714 target_pkg_info.use,
715 binpkg_pkg_info.use,
716 )
717 return True, True, use_mismatch
Alex Klein1699fab2022-09-08 08:46:06 -0600718
719 logging.debug(
720 "Not updating %s: already up-to-date (%s, built %s)",
721 cp,
722 target_pkg_info.cpv,
723 target_pkg_info.build_time,
724 )
Tim Baine4a783b2023-04-21 20:05:51 +0000725 return False, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600726
727 if optional:
728 logging.debug(
729 "Not installing %s: missing on target but optional", cp
730 )
Tim Baine4a783b2023-04-21 20:05:51 +0000731 return False, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600732
733 logging.debug(
734 "Installing %s: missing on target and non-optional (%s)", cp, cpv
735 )
Tim Baine4a783b2023-04-21 20:05:51 +0000736 return True, False, False
Alex Klein1699fab2022-09-08 08:46:06 -0600737
Tim Baine4a783b2023-04-21 20:05:51 +0000738 def _ProcessDeps(self, deps: List[str], reverse: bool) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600739 """Enqueues dependencies for processing.
740
741 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600742 deps: List of dependencies to enqueue.
743 reverse: Whether these are reverse dependencies.
Alex Klein1699fab2022-09-08 08:46:06 -0600744 """
745 if not deps:
746 return
747
748 logging.debug(
749 "Processing %d %s dep(s)...",
750 len(deps),
751 "reverse" if reverse else "forward",
752 )
753 num_already_seen = 0
754 for dep in deps:
755 if self._EnqDep(dep, False, reverse):
756 logging.debug(" Queued dep %s", dep)
757 else:
758 num_already_seen += 1
759
760 if num_already_seen:
761 logging.debug("%d dep(s) already seen", num_already_seen)
762
Tim Baine4a783b2023-04-21 20:05:51 +0000763 def _ComputeInstalls(
764 self, process_rdeps: bool, process_rev_rdeps: bool
765 ) -> Tuple[Dict[str, package_info.CPV], bool]:
Alex Klein975e86c2023-01-23 16:49:10 -0700766 """Returns a dict of packages that need to be installed on the target.
Alex Klein1699fab2022-09-08 08:46:06 -0600767
768 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600769 process_rdeps: Whether to trace forward dependencies.
770 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600771
772 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000773 A tuple (installs, warnings_shown) where |installs| is a dictionary
774 mapping CP values (string) to tuples containing a CPV (string), a
775 slot (string), a boolean indicating whether the package was
776 initially listed in the queue, and a boolean indicating whether this
777 is an update to an existing package, and |warnings_shown| is a
778 boolean indicating whether warnings were shown that might require a
779 prompt whether to continue.
Alex Klein1699fab2022-09-08 08:46:06 -0600780 """
781 installs = {}
Tim Baine4a783b2023-04-21 20:05:51 +0000782 warnings_shown = False
Alex Klein1699fab2022-09-08 08:46:06 -0600783 while self.queue:
784 dep, listed, optional = self._DeqDep()
785 cp, required_slot = dep
786 if cp in installs:
787 logging.debug("Already updating %s", cp)
788 continue
789
Trent Apted1e2e4f32023-05-05 03:50:20 +0000790 cp_slots = self.binpkgs_db.get(cp, {})
Alex Klein1699fab2022-09-08 08:46:06 -0600791 logging.debug(
792 "Checking packages matching %s%s%s...",
793 cp,
794 " (slot: %s)" % required_slot if required_slot else "",
795 " (optional)" if optional else "",
796 )
797 num_processed = 0
798 for slot, pkg_info in cp_slots.items():
Matt Turner2fa91bc2023-08-11 18:39:35 +0000799 if required_slot and "/" not in slot:
800 logging.debug(
801 " Dropping subslot from required_slot (%s) "
802 "because package does not have a subslot (%s)",
803 required_slot,
804 slot,
805 )
806 required_slot = required_slot.split("/", 1)[0]
807
Matt Turnercd383d42023-08-09 21:33:50 +0000808 if not required_slot:
809 logging.debug(" Including because no required_slot")
810 elif slot == required_slot:
811 logging.debug(
812 " Including because slot (%s) == required_slot (%s)",
813 slot,
814 required_slot,
815 )
816 else:
817 logging.debug(
818 " Skipping because slot (%s) != required_slot (%s)",
819 slot,
820 required_slot,
821 )
Alex Klein1699fab2022-09-08 08:46:06 -0600822 continue
823
824 num_processed += 1
825 logging.debug(" Checking %s...", pkg_info.cpv)
826
Tim Baine4a783b2023-04-21 20:05:51 +0000827 install, update, use_mismatch = self._NeedsInstall(
Alex Klein1699fab2022-09-08 08:46:06 -0600828 pkg_info.cpv, slot, pkg_info.build_time, optional
829 )
830 if not install:
831 continue
832
833 installs[cp] = (pkg_info.cpv, slot, listed, update)
Tim Baine4a783b2023-04-21 20:05:51 +0000834 warnings_shown |= use_mismatch
Alex Klein1699fab2022-09-08 08:46:06 -0600835
836 # Add forward and backward runtime dependencies to queue.
837 if process_rdeps:
838 self._ProcessDeps(pkg_info.rdeps, False)
839 if process_rev_rdeps:
Trent Apted1e2e4f32023-05-05 03:50:20 +0000840 target_pkg_info = self.target_db.get(cp, {}).get(slot)
Alex Klein1699fab2022-09-08 08:46:06 -0600841 if target_pkg_info:
842 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
843
844 if num_processed == 0:
845 logging.warning(
846 "No qualified bintree package corresponding to %s", cp
847 )
848
Tim Baine4a783b2023-04-21 20:05:51 +0000849 return installs, warnings_shown
Alex Klein1699fab2022-09-08 08:46:06 -0600850
Tim Baine4a783b2023-04-21 20:05:51 +0000851 def _SortInstalls(self, installs: List[str]) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600852 """Returns a sorted list of packages to install.
853
854 Performs a topological sort based on dependencies found in the binary
855 package database.
856
857 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600858 installs: Dictionary of packages to install indexed by CP.
Alex Klein1699fab2022-09-08 08:46:06 -0600859
860 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600861 A list of package CPVs (string).
Alex Klein1699fab2022-09-08 08:46:06 -0600862
863 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600864 ValueError: If dependency graph contains a cycle.
Alex Klein1699fab2022-09-08 08:46:06 -0600865 """
866 not_visited = set(installs.keys())
867 curr_path = []
868 sorted_installs = []
869
Tim Baine4a783b2023-04-21 20:05:51 +0000870 def SortFrom(cp: str) -> None:
Alex Klein975e86c2023-01-23 16:49:10 -0700871 """Traverses deps recursively, emitting nodes in reverse order."""
Alex Klein1699fab2022-09-08 08:46:06 -0600872 cpv, slot, _, _ = installs[cp]
873 if cpv in curr_path:
874 raise ValueError(
875 "Dependencies contain a cycle: %s -> %s"
876 % (" -> ".join(curr_path[curr_path.index(cpv) :]), cpv)
877 )
878 curr_path.append(cpv)
879 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
880 if rdep_cp in not_visited:
881 not_visited.remove(rdep_cp)
882 SortFrom(rdep_cp)
883
884 sorted_installs.append(cpv)
885 curr_path.pop()
886
887 # So long as there's more packages, keep expanding dependency paths.
888 while not_visited:
889 SortFrom(not_visited.pop())
890
891 return sorted_installs
892
Tim Baine4a783b2023-04-21 20:05:51 +0000893 def _EnqListedPkg(self, pkg: str) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -0600894 """Finds and enqueues a listed package."""
895 cp, slot = self._FindPackage(pkg)
896 if cp not in self.binpkgs_db:
897 raise self.BintreeError(
898 "Package %s not found in binpkgs tree" % pkg
899 )
900 self._EnqDep((cp, slot), True, False)
901
Tim Baine4a783b2023-04-21 20:05:51 +0000902 def _EnqInstalledPkgs(self) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600903 """Enqueues all available binary packages that are already installed."""
904 for cp, cp_slots in self.binpkgs_db.items():
905 target_cp_slots = self.target_db.get(cp)
906 if target_cp_slots:
907 for slot in cp_slots.keys():
908 if slot in target_cp_slots:
909 self._EnqDep((cp, slot), True, False)
910
911 def Run(
912 self,
Tim Baine4a783b2023-04-21 20:05:51 +0000913 device: remote_access.RemoteDevice,
914 root: str,
915 listed_pkgs: List[str],
916 update: bool,
917 process_rdeps: bool,
918 process_rev_rdeps: bool,
919 ) -> Tuple[List[str], List[str], int, Dict[str, str], bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600920 """Computes the list of packages that need to be installed on a target.
921
922 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600923 device: Target handler object.
924 root: Package installation root.
925 listed_pkgs: Package names/files listed by the user.
926 update: Whether to read the target's installed package database.
927 process_rdeps: Whether to trace forward dependencies.
928 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600929
930 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000931 A tuple (sorted, listed, num_updates, install_attrs, warnings_shown)
932 where |sorted| is a list of package CPVs (string) to install on the
933 target in an order that satisfies their inter-dependencies, |listed|
Alex Klein53cc3bf2022-10-13 08:50:01 -0600934 the subset that was requested by the user, and |num_updates|
935 the number of packages being installed over preexisting
936 versions. Note that installation order should be reversed for
937 removal, |install_attrs| is a dictionary mapping a package
Tim Baine4a783b2023-04-21 20:05:51 +0000938 CPV (string) to some of its extracted environment attributes, and
939 |warnings_shown| is a boolean indicating whether warnings were shown
940 that might require a prompt whether to continue.
Alex Klein1699fab2022-09-08 08:46:06 -0600941 """
942 if process_rev_rdeps and not process_rdeps:
943 raise ValueError(
944 "Must processing forward deps when processing rev deps"
945 )
946 if process_rdeps and not update:
947 raise ValueError(
948 "Must check installed packages when processing deps"
949 )
950
951 if update:
952 logging.info("Initializing target intalled packages database...")
953 self._InitTargetVarDB(
954 device, root, process_rdeps, process_rev_rdeps
955 )
956
957 logging.info("Initializing binary packages database...")
958 self._InitBinpkgDB(process_rdeps)
959
960 logging.info("Finding listed package(s)...")
961 self._InitDepQueue()
962 for pkg in listed_pkgs:
963 if pkg == "@installed":
964 if not update:
965 raise ValueError(
Alex Klein975e86c2023-01-23 16:49:10 -0700966 "Must check installed packages when updating all of "
967 "them."
Alex Klein1699fab2022-09-08 08:46:06 -0600968 )
969 self._EnqInstalledPkgs()
970 else:
971 self._EnqListedPkg(pkg)
972
973 logging.info("Computing set of packages to install...")
Tim Baine4a783b2023-04-21 20:05:51 +0000974 installs, warnings_shown = self._ComputeInstalls(
975 process_rdeps, process_rev_rdeps
976 )
Alex Klein1699fab2022-09-08 08:46:06 -0600977
978 num_updates = 0
979 listed_installs = []
980 for cpv, _, listed, isupdate in installs.values():
981 if listed:
982 listed_installs.append(cpv)
983 if isupdate:
984 num_updates += 1
985
986 logging.info(
987 "Processed %d package(s), %d will be installed, %d are "
988 "updating existing packages",
989 len(self.seen),
990 len(installs),
991 num_updates,
992 )
993
994 sorted_installs = self._SortInstalls(installs)
995
996 install_attrs = {}
997 for pkg in sorted_installs:
998 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
999 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
1000 install_attrs[pkg] = {}
1001 if dlc_id and dlc_package:
1002 install_attrs[pkg][_DLC_ID] = dlc_id
1003
Tim Baine4a783b2023-04-21 20:05:51 +00001004 return (
1005 sorted_installs,
1006 listed_installs,
1007 num_updates,
1008 install_attrs,
1009 warnings_shown,
1010 )
David Pursell9476bf42015-03-30 13:34:27 -07001011
1012
Tim Baine4a783b2023-04-21 20:05:51 +00001013def _Emerge(
1014 device: remote_access.RemoteDevice,
1015 pkg_paths: List[str],
1016 root: str,
1017 extra_args: List[str] = None,
1018) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -06001019 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -07001020
Alex Klein1699fab2022-09-08 08:46:06 -06001021 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001022 device: A ChromiumOSDevice object.
Mike Frysinger9fba8d02023-05-15 15:04:07 -04001023 pkg_paths: Local paths to binary packages.
Alex Klein53cc3bf2022-10-13 08:50:01 -06001024 root: Package installation root path.
1025 extra_args: Extra arguments to pass to emerge.
David Pursell9476bf42015-03-30 13:34:27 -07001026
Alex Klein1699fab2022-09-08 08:46:06 -06001027 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001028 DeployError: Unrecoverable error during emerge.
Alex Klein1699fab2022-09-08 08:46:06 -06001029 """
Mike Frysinger63d35512021-01-26 23:16:13 -05001030
Alex Klein1699fab2022-09-08 08:46:06 -06001031 def path_to_name(pkg_path):
1032 return os.path.basename(pkg_path)
Mike Frysinger63d35512021-01-26 23:16:13 -05001033
Alex Klein1699fab2022-09-08 08:46:06 -06001034 def path_to_category(pkg_path):
1035 return os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -07001036
Alex Klein1699fab2022-09-08 08:46:06 -06001037 pkg_names = ", ".join(path_to_name(x) for x in pkg_paths)
David Pursell9476bf42015-03-30 13:34:27 -07001038
Alex Klein1699fab2022-09-08 08:46:06 -06001039 pkgroot = os.path.join(device.work_dir, "packages")
1040 portage_tmpdir = os.path.join(device.work_dir, "portage-tmp")
Alex Klein975e86c2023-01-23 16:49:10 -07001041 # Clean out the dirs first if we had a previous emerge on the device so as
1042 # to free up space for this emerge. The last emerge gets implicitly cleaned
1043 # up when the device connection deletes its work_dir.
Alex Klein1699fab2022-09-08 08:46:06 -06001044 device.run(
1045 f"cd {device.work_dir} && "
1046 f"rm -rf packages portage-tmp && "
1047 f"mkdir -p portage-tmp packages && "
1048 f"cd packages && "
1049 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
1050 shell=True,
1051 remote_sudo=True,
1052 )
Mike Frysinger63d35512021-01-26 23:16:13 -05001053
Alex Klein1699fab2022-09-08 08:46:06 -06001054 logging.info("Use portage temp dir %s", portage_tmpdir)
David Pursell9476bf42015-03-30 13:34:27 -07001055
Mike Frysinger63d35512021-01-26 23:16:13 -05001056 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001057 logging.notice("Copying binpkgs to device.")
1058 for pkg_path in pkg_paths:
1059 pkg_name = path_to_name(pkg_path)
1060 logging.info("Copying %s", pkg_name)
1061 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
1062 device.CopyToDevice(
1063 pkg_path, pkg_dir, mode="rsync", remote_sudo=True, compress=False
1064 )
1065
1066 # This message is read by BrilloDeployOperation.
1067 logging.notice("Installing: %s", pkg_names)
1068
1069 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
1070 # chromeos-base packages will be skipped due to the configuration
1071 # in /etc/protage/make.profile/package.provided. However, there is
1072 # a known bug that /usr/local/etc/portage is not setup properly
1073 # (crbug.com/312041). This does not affect `cros deploy` because
1074 # we do not use the preset PKGDIR.
1075 extra_env = {
1076 "FEATURES": "-sandbox",
1077 "PKGDIR": pkgroot,
1078 "PORTAGE_CONFIGROOT": "/usr/local",
1079 "PORTAGE_TMPDIR": portage_tmpdir,
1080 "PORTDIR": device.work_dir,
1081 "CONFIG_PROTECT": "-*",
1082 }
1083
Alex Klein975e86c2023-01-23 16:49:10 -07001084 # --ignore-built-slot-operator-deps because we don't rebuild everything. It
1085 # can cause errors, but that's expected with cros deploy since it's just a
Alex Klein1699fab2022-09-08 08:46:06 -06001086 # best effort to prevent developers avoid rebuilding an image every time.
1087 cmd = [
1088 "emerge",
1089 "--usepkg",
1090 "--ignore-built-slot-operator-deps=y",
1091 "--root",
1092 root,
1093 ] + [os.path.join(pkgroot, *x.split("/")[-2:]) for x in pkg_paths]
1094 if extra_args:
1095 cmd.append(extra_args)
1096
1097 logging.warning(
1098 "Ignoring slot dependencies! This may break things! e.g. "
1099 "packages built against the old version may not be able to "
1100 "load the new .so. This is expected, and you will just need "
1101 "to build and flash a new image if you have problems."
1102 )
1103 try:
1104 result = device.run(
1105 cmd,
1106 extra_env=extra_env,
1107 remote_sudo=True,
1108 capture_output=True,
1109 debug_level=logging.INFO,
1110 )
1111
1112 pattern = (
1113 "A requested package will not be merged because "
1114 "it is listed in package.provided"
1115 )
1116 output = result.stderr.replace("\n", " ").replace("\r", "")
1117 if pattern in output:
1118 error = (
1119 "Package failed to emerge: %s\n"
1120 "Remove %s from /etc/portage/make.profile/"
1121 "package.provided/chromeos-base.packages\n"
1122 "(also see crbug.com/920140 for more context)\n"
1123 % (pattern, pkg_name)
1124 )
1125 cros_build_lib.Die(error)
1126 except Exception:
1127 logging.error("Failed to emerge packages %s", pkg_names)
1128 raise
1129 else:
1130 # This message is read by BrilloDeployOperation.
1131 logging.notice("Packages have been installed.")
David Pursell9476bf42015-03-30 13:34:27 -07001132
1133
Tim Baine4a783b2023-04-21 20:05:51 +00001134def _RestoreSELinuxContext(
1135 device: remote_access.RemoteDevice, pkgpath: str, root: str
1136) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001137 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +09001138
Alex Klein1699fab2022-09-08 08:46:06 -06001139 This reads the tarball from pkgpath, and calls restorecon on device to
Alex Klein975e86c2023-01-23 16:49:10 -07001140 restore SELinux context for files listed in the tarball, assuming those
1141 files are installed to /
Qijiang Fan8a945032019-04-25 20:53:29 +09001142
Alex Klein1699fab2022-09-08 08:46:06 -06001143 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001144 device: a ChromiumOSDevice object
1145 pkgpath: path to tarball
1146 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001147 """
1148 pkgroot = os.path.join(device.work_dir, "packages")
1149 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
1150 pkgpath_device = os.path.join(
1151 pkgroot, pkg_dirname, os.path.basename(pkgpath)
1152 )
1153 # Testing shows restorecon splits on newlines instead of spaces.
1154 device.run(
1155 [
1156 "cd",
1157 root,
1158 "&&",
1159 "tar",
1160 "tf",
1161 pkgpath_device,
1162 "|",
1163 "restorecon",
1164 "-i",
1165 "-f",
1166 "-",
1167 ],
1168 remote_sudo=True,
1169 )
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001170
1171
Tim Baine4a783b2023-04-21 20:05:51 +00001172def _GetPackagesByCPV(
1173 cpvs: List[package_info.CPV], strip: bool, sysroot: str
1174) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001175 """Returns paths to binary packages corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001176
Alex Klein1699fab2022-09-08 08:46:06 -06001177 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001178 cpvs: List of CPV components given by package_info.SplitCPV().
1179 strip: True to run strip_package.
1180 sysroot: Sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001181
Alex Klein1699fab2022-09-08 08:46:06 -06001182 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001183 List of paths corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001184
Alex Klein1699fab2022-09-08 08:46:06 -06001185 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001186 DeployError: If a package is missing.
Alex Klein1699fab2022-09-08 08:46:06 -06001187 """
1188 packages_dir = None
1189 if strip:
1190 try:
1191 cros_build_lib.run(
1192 [
Mike Frysinger5429f302023-03-27 15:48:52 -04001193 constants.CHROMITE_SCRIPTS_DIR / "strip_package",
Alex Klein1699fab2022-09-08 08:46:06 -06001194 "--sysroot",
1195 sysroot,
1196 ]
1197 + [cpv.cpf for cpv in cpvs]
1198 )
1199 packages_dir = _STRIPPED_PACKAGES_DIR
1200 except cros_build_lib.RunCommandError:
1201 logging.error(
1202 "Cannot strip packages %s", " ".join([str(cpv) for cpv in cpvs])
1203 )
1204 raise
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001205
Alex Klein1699fab2022-09-08 08:46:06 -06001206 paths = []
1207 for cpv in cpvs:
1208 path = portage_util.GetBinaryPackagePath(
1209 cpv.category,
1210 cpv.package,
1211 cpv.version,
1212 sysroot=sysroot,
1213 packages_dir=packages_dir,
1214 )
1215 if not path:
1216 raise DeployError("Missing package %s." % cpv)
1217 paths.append(path)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001218
Alex Klein1699fab2022-09-08 08:46:06 -06001219 return paths
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001220
1221
Tim Baine4a783b2023-04-21 20:05:51 +00001222def _GetPackagesPaths(pkgs: List[str], strip: bool, sysroot: str) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001223 """Returns paths to binary |pkgs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001224
Alex Klein1699fab2022-09-08 08:46:06 -06001225 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001226 pkgs: List of package CPVs string.
1227 strip: Whether or not to run strip_package for CPV packages.
1228 sysroot: The sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001229
Alex Klein1699fab2022-09-08 08:46:06 -06001230 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001231 List of paths corresponding to |pkgs|.
Alex Klein1699fab2022-09-08 08:46:06 -06001232 """
1233 cpvs = [package_info.SplitCPV(p) for p in pkgs]
1234 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001235
1236
Tim Baine4a783b2023-04-21 20:05:51 +00001237def _Unmerge(
1238 device: remote_access.RemoteDevice, pkgs: List[str], root: str
1239) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001240 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -07001241
Alex Klein1699fab2022-09-08 08:46:06 -06001242 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001243 device: A RemoteDevice object.
1244 pkgs: Package names.
1245 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001246 """
1247 pkg_names = ", ".join(os.path.basename(x) for x in pkgs)
Mike Frysinger22bb5502021-01-29 13:05:46 -05001248 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001249 logging.notice("Unmerging %s.", pkg_names)
1250 cmd = ["qmerge", "--yes"]
1251 # Check if qmerge is available on the device. If not, use emerge.
1252 if device.run(["qmerge", "--version"], check=False).returncode != 0:
1253 cmd = ["emerge"]
1254
1255 cmd += ["--unmerge", "--root", root]
1256 cmd.extend("f={x}" for x in pkgs)
1257 try:
1258 # Always showing the emerge output for clarity.
1259 device.run(
1260 cmd,
1261 capture_output=False,
1262 remote_sudo=True,
1263 debug_level=logging.INFO,
1264 )
1265 except Exception:
1266 logging.error("Failed to unmerge packages %s", pkg_names)
1267 raise
1268 else:
1269 # This message is read by BrilloDeployOperation.
1270 logging.notice("Packages have been uninstalled.")
David Pursell9476bf42015-03-30 13:34:27 -07001271
1272
Tim Baine4a783b2023-04-21 20:05:51 +00001273def _ConfirmDeploy(num_updates: int) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -06001274 """Returns whether we can continue deployment."""
1275 if num_updates > _MAX_UPDATES_NUM:
1276 logging.warning(_MAX_UPDATES_WARNING)
1277 return cros_build_lib.BooleanPrompt(default=False)
David Pursell9476bf42015-03-30 13:34:27 -07001278
Alex Klein1699fab2022-09-08 08:46:06 -06001279 return True
David Pursell9476bf42015-03-30 13:34:27 -07001280
1281
Tim Baine4a783b2023-04-21 20:05:51 +00001282def _ConfirmUpdateDespiteWarnings() -> bool:
1283 """Returns whether we can continue updating despite warnings."""
1284 logging.warning("Continue despite prior warnings?")
1285 return cros_build_lib.BooleanPrompt(default=False)
1286
1287
1288def _EmergePackages(
1289 pkgs: List[str],
1290 device: remote_access.RemoteDevice,
1291 strip: bool,
1292 sysroot: str,
1293 root: str,
1294 board: str,
1295 emerge_args: List[str],
1296) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001297 """Call _Emerge for each package in pkgs."""
Ben Pastene5f03b052019-08-12 18:03:24 -07001298 if device.IsSELinuxAvailable():
Alex Klein1699fab2022-09-08 08:46:06 -06001299 enforced = device.IsSELinuxEnforced()
1300 if enforced:
1301 device.run(["setenforce", "0"])
1302 else:
1303 enforced = False
Andrewc7e1c6b2020-02-27 16:03:53 -08001304
Alex Klein1699fab2022-09-08 08:46:06 -06001305 dlc_deployed = False
1306 # This message is read by BrilloDeployOperation.
1307 logging.info("Preparing local packages for transfer.")
1308 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
1309 # Install all the packages in one pass so inter-package blockers work.
1310 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
1311 logging.info("Updating SELinux settings & DLC images.")
1312 for pkg_path in pkg_paths:
1313 if device.IsSELinuxAvailable():
1314 _RestoreSELinuxContext(device, pkg_path, root)
Mike Frysinger5f4c2742021-02-08 14:37:23 -05001315
Alex Klein1699fab2022-09-08 08:46:06 -06001316 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
1317 if dlc_id and dlc_package:
1318 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
1319 dlc_deployed = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001320
Alex Klein1699fab2022-09-08 08:46:06 -06001321 if dlc_deployed:
1322 # Clean up empty directories created by emerging DLCs.
1323 device.run(
1324 [
1325 "test",
1326 "-d",
1327 "/build/rootfs",
1328 "&&",
1329 "rmdir",
1330 "--ignore-fail-on-non-empty",
1331 "/build/rootfs",
1332 "/build",
1333 ],
1334 check=False,
1335 )
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001336
Alex Klein1699fab2022-09-08 08:46:06 -06001337 if enforced:
1338 device.run(["setenforce", "1"])
1339
1340 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1341 # we installed new DLC images).
1342 if dlc_deployed:
1343 device.run(["restart", "dlcservice"])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001344
1345
Tim Baine4a783b2023-04-21 20:05:51 +00001346def _UnmergePackages(
1347 pkgs: List[str],
1348 device: remote_access.RemoteDevice,
1349 root: str,
1350 pkgs_attrs: Dict[str, List[str]],
1351) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -06001352 """Call _Unmege for each package in pkgs."""
1353 dlc_uninstalled = False
1354 _Unmerge(device, pkgs, root)
1355 logging.info("Cleaning up DLC images.")
1356 for pkg in pkgs:
1357 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1358 dlc_uninstalled = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001359
Alex Klein1699fab2022-09-08 08:46:06 -06001360 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1361 # uninstalled DLC images).
1362 if dlc_uninstalled:
1363 device.run(["restart", "dlcservice"])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001364
1365
Tim Baine4a783b2023-04-21 20:05:51 +00001366def _UninstallDLCImage(
1367 device: remote_access.RemoteDevice, pkg_attrs: Dict[str, List[str]]
1368):
Alex Klein1699fab2022-09-08 08:46:06 -06001369 """Uninstall a DLC image."""
1370 if _DLC_ID in pkg_attrs:
1371 dlc_id = pkg_attrs[_DLC_ID]
1372 logging.notice("Uninstalling DLC image for %s", dlc_id)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001373
Alex Klein1699fab2022-09-08 08:46:06 -06001374 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1375 return True
1376 else:
1377 logging.debug("DLC_ID not found in package")
1378 return False
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001379
1380
Tim Baine4a783b2023-04-21 20:05:51 +00001381def _DeployDLCImage(
1382 device: remote_access.RemoteDevice,
1383 sysroot: str,
1384 board: str,
1385 dlc_id: str,
1386 dlc_package: str,
1387):
Alex Klein1699fab2022-09-08 08:46:06 -06001388 """Deploy (install and mount) a DLC image.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001389
Alex Klein1699fab2022-09-08 08:46:06 -06001390 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001391 device: A device object.
1392 sysroot: The sysroot path.
1393 board: Board to use.
1394 dlc_id: The DLC ID.
1395 dlc_package: The DLC package name.
Alex Klein1699fab2022-09-08 08:46:06 -06001396 """
1397 # Requires `sudo_rm` because installations of files are running with sudo.
1398 with osutils.TempDir(sudo_rm=True) as tempdir:
1399 temp_rootfs = Path(tempdir)
1400 # Build the DLC image if the image is outdated or doesn't exist.
1401 dlc_lib.InstallDlcImages(
1402 sysroot=sysroot, rootfs=temp_rootfs, dlc_id=dlc_id, board=board
1403 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001404
Alex Klein1699fab2022-09-08 08:46:06 -06001405 logging.debug("Uninstall DLC %s if it is installed.", dlc_id)
1406 try:
1407 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1408 except cros_build_lib.RunCommandError as e:
1409 logging.info(
1410 "Failed to uninstall DLC:%s. Continue anyway.", e.stderr
1411 )
1412 except Exception:
1413 logging.error("Failed to uninstall DLC.")
1414 raise
Andrewc7e1c6b2020-02-27 16:03:53 -08001415
Yuanpeng Niaba30342023-07-11 14:24:09 -07001416 src_dlc_dir = os.path.join(
Alex Klein1699fab2022-09-08 08:46:06 -06001417 sysroot,
1418 dlc_lib.DLC_BUILD_DIR,
1419 dlc_id,
Alex Klein1699fab2022-09-08 08:46:06 -06001420 )
Yuanpeng Niaba30342023-07-11 14:24:09 -07001421 if not os.path.exists(src_dlc_dir):
1422 src_dlc_dir = os.path.join(
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001423 sysroot,
1424 dlc_lib.DLC_BUILD_DIR_SCALED,
1425 dlc_id,
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001426 )
1427
Yuanpeng Niaba30342023-07-11 14:24:09 -07001428 # Deploy the metadata entry to compressed metadata on device.
1429 logging.notice("Setting the DLC metadata for %s", dlc_id)
1430 metadata = dlc_lib.DlcMetadata.LoadSrcMetadata(src_dlc_dir)
1431 device.run(
1432 [dlc_lib.DLC_METADATA_UTIL, "--set", f"--id={dlc_id}"],
1433 input=json.dumps(metadata),
1434 check=False,
1435 )
1436
1437 logging.notice("Deploy the DLC image for %s", dlc_id)
1438 dlc_img_path_src = os.path.join(
1439 src_dlc_dir,
1440 dlc_package,
1441 dlc_lib.DLC_IMAGE,
1442 )
1443
Alex Klein1699fab2022-09-08 08:46:06 -06001444 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1445 dlc_img_path_a = os.path.join(dlc_img_path, "dlc_a")
1446 dlc_img_path_b = os.path.join(dlc_img_path, "dlc_b")
1447 # Create directories for DLC images.
Mike Frysinger445f6bf2023-06-27 11:43:33 -04001448 device.mkdir([dlc_img_path_a, dlc_img_path_b])
Alex Klein1699fab2022-09-08 08:46:06 -06001449 # Copy images to the destination directories.
1450 device.CopyToDevice(
1451 dlc_img_path_src,
1452 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1453 mode="rsync",
1454 )
1455 device.run(
1456 [
1457 "cp",
1458 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1459 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE),
1460 ]
1461 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001462
Alex Klein1699fab2022-09-08 08:46:06 -06001463 # Set the proper perms and ownership so dlcservice can access the image.
1464 device.run(["chmod", "-R", "u+rwX,go+rX,go-w", _DLC_INSTALL_ROOT])
1465 device.run(["chown", "-R", "dlcservice:dlcservice", _DLC_INSTALL_ROOT])
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001466
Alex Klein1699fab2022-09-08 08:46:06 -06001467 # Copy metadata to device.
Yuanpeng Niaba30342023-07-11 14:24:09 -07001468 # TODO(b/290961240): To be removed once the transition to compressed
1469 # metadata is complete.
Alex Klein1699fab2022-09-08 08:46:06 -06001470 dest_meta_dir = Path("/") / dlc_lib.DLC_META_DIR / dlc_id / dlc_package
Mike Frysinger445f6bf2023-06-27 11:43:33 -04001471 device.mkdir(dest_meta_dir)
Alex Klein1699fab2022-09-08 08:46:06 -06001472 src_meta_dir = os.path.join(
Yuanpeng Niaba30342023-07-11 14:24:09 -07001473 src_dlc_dir,
Alex Klein1699fab2022-09-08 08:46:06 -06001474 dlc_package,
1475 dlc_lib.DLC_TMP_META_DIR,
1476 )
1477 device.CopyToDevice(
1478 src_meta_dir + "/",
1479 dest_meta_dir,
1480 mode="rsync",
1481 recursive=True,
1482 remote_sudo=True,
1483 )
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001484
Alex Klein1699fab2022-09-08 08:46:06 -06001485 # TODO(kimjae): Make this generic so it recomputes all the DLCs + copies
Alex Klein975e86c2023-01-23 16:49:10 -07001486 # over a fresh list of dm-verity digests instead of appending and
1487 # keeping the stale digests when developers are testing.
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001488
Alex Klein1699fab2022-09-08 08:46:06 -06001489 # Copy the LoadPin dm-verity digests to device.
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001490 _DeployDLCLoadPin(temp_rootfs, device)
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001491
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001492
1493def _DeployDLCLoadPin(
Tim Baine4a783b2023-04-21 20:05:51 +00001494 rootfs: os.PathLike, device: remote_access.RemoteDevice
1495) -> None:
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001496 """Deploy DLC LoadPin from temp rootfs to device.
1497
1498 Args:
1499 rootfs: Path to rootfs.
1500 device: A device object.
1501 """
1502 loadpin = dlc_lib.DLC_LOADPIN_TRUSTED_VERITY_DIGESTS
1503 dst_loadpin = Path("/") / dlc_lib.DLC_META_DIR / loadpin
1504 src_loadpin = rootfs / dlc_lib.DLC_META_DIR / loadpin
1505 if src_loadpin.exists():
1506 digests = set(osutils.ReadFile(src_loadpin).splitlines())
1507 digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1508 try:
1509 device_digests = set(device.CatFile(dst_loadpin).splitlines())
1510 device_digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1511 digests.update(device_digests)
1512 except remote_access.CatFileError:
1513 pass
1514
1515 with tempfile.NamedTemporaryFile(dir=rootfs) as f:
1516 osutils.WriteFile(f.name, dlc_lib.DLC_LOADPIN_FILE_HEADER + "\n")
1517 osutils.WriteFile(f.name, "\n".join(digests) + "\n", mode="a")
1518 device.CopyToDevice(
1519 f.name, dst_loadpin, mode="rsync", remote_sudo=True
1520 )
Andrew67b5fa72020-02-05 14:14:48 -08001521
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001522
Tim Baine4a783b2023-04-21 20:05:51 +00001523def _GetDLCInfo(
1524 device: remote_access.RemoteDevice, pkg_path: str, from_dut: bool
1525) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001526 """Returns information of a DLC given its package path.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001527
Alex Klein1699fab2022-09-08 08:46:06 -06001528 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001529 device: commandline.Device object; None to use the default device.
1530 pkg_path: path to the package.
1531 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1532 info from host.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001533
Alex Klein1699fab2022-09-08 08:46:06 -06001534 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001535 A tuple (dlc_id, dlc_package).
Alex Klein1699fab2022-09-08 08:46:06 -06001536 """
1537 environment_content = ""
1538 if from_dut:
1539 # On DUT, |pkg_path| is the directory which contains environment file.
1540 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1541 try:
1542 environment_data = device.CatFile(
1543 environment_path, max_size=None, encoding=None
1544 )
1545 except remote_access.CatFileError:
1546 # The package is not installed on DUT yet. Skip extracting info.
1547 return None, None
1548 else:
1549 # On host, pkg_path is tbz2 file which contains environment file.
1550 # Extract the metadata of the package file.
1551 data = portage.xpak.tbz2(pkg_path).get_data()
1552 environment_data = data[_ENVIRONMENT_FILENAME.encode("utf-8")]
1553
1554 # Extract the environment metadata.
1555 environment_content = bz2.decompress(environment_data)
1556
1557 with tempfile.NamedTemporaryFile() as f:
1558 # Dumps content into a file so we can use osutils.SourceEnvironment.
1559 path = os.path.realpath(f.name)
1560 osutils.WriteFile(path, environment_content, mode="wb")
1561 content = osutils.SourceEnvironment(
1562 path, (_DLC_ID, _DLC_PACKAGE, _DLC_ENABLED)
1563 )
1564
1565 dlc_enabled = content.get(_DLC_ENABLED)
1566 if dlc_enabled is not None and (
1567 dlc_enabled is False or str(dlc_enabled) == "false"
1568 ):
1569 logging.info("Installing DLC in rootfs.")
1570 return None, None
1571 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
1572
1573
1574def Deploy(
Tim Baine4a783b2023-04-21 20:05:51 +00001575 device: remote_access.RemoteDevice,
1576 packages: List[str],
1577 board: str = None,
1578 emerge: bool = True,
1579 update: bool = False,
1580 deep: bool = False,
1581 deep_rev: bool = False,
1582 clean_binpkg: bool = True,
1583 root: str = "/",
1584 strip: bool = True,
1585 emerge_args: List[str] = None,
1586 ssh_private_key: str = None,
1587 ping: bool = True,
1588 force: bool = False,
1589 dry_run: bool = False,
1590) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001591 """Deploys packages to a device.
1592
1593 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001594 device: commandline.Device object; None to use the default device.
1595 packages: List of packages (strings) to deploy to device.
1596 board: Board to use; None to automatically detect.
1597 emerge: True to emerge package, False to unmerge.
1598 update: Check installed version on device.
1599 deep: Install dependencies also. Implies |update|.
1600 deep_rev: Install reverse dependencies. Implies |deep|.
1601 clean_binpkg: Clean outdated binary packages.
1602 root: Package installation root path.
1603 strip: Run strip_package to filter out preset paths in the package.
1604 emerge_args: Extra arguments to pass to emerge.
1605 ssh_private_key: Path to an SSH private key file; None to use test keys.
1606 ping: True to ping the device before trying to connect.
1607 force: Ignore confidence checks and prompts.
1608 dry_run: Print deployment plan but do not deploy anything.
Alex Klein1699fab2022-09-08 08:46:06 -06001609
1610 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001611 ValueError: Invalid parameter or parameter combination.
1612 DeployError: Unrecoverable failure during deploy.
Alex Klein1699fab2022-09-08 08:46:06 -06001613 """
1614 if deep_rev:
1615 deep = True
1616 if deep:
1617 update = True
1618
1619 if not packages:
1620 raise DeployError("No packages provided, nothing to deploy.")
1621
1622 if update and not emerge:
1623 raise ValueError("Cannot update and unmerge.")
1624
1625 if device:
1626 hostname, username, port = device.hostname, device.username, device.port
1627 else:
1628 hostname, username, port = None, None, None
1629
1630 lsb_release = None
1631 sysroot = None
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001632 try:
Alex Klein1699fab2022-09-08 08:46:06 -06001633 # Somewhat confusing to clobber, but here we are.
1634 # pylint: disable=redefined-argument-from-local
1635 with remote_access.ChromiumOSDeviceHandler(
1636 hostname,
1637 port=port,
1638 username=username,
1639 private_key=ssh_private_key,
1640 base_dir=_DEVICE_BASE_DIR,
1641 ping=ping,
1642 ) as device:
1643 lsb_release = device.lsb_release
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001644
Alex Klein1699fab2022-09-08 08:46:06 -06001645 board = cros_build_lib.GetBoard(
1646 device_board=device.board, override_board=board
1647 )
1648 if not force and board != device.board:
1649 raise DeployError(
1650 "Device (%s) is incompatible with board %s. Use "
1651 "--force to deploy anyway." % (device.board, board)
1652 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001653
Alex Klein1699fab2022-09-08 08:46:06 -06001654 sysroot = build_target_lib.get_default_sysroot_path(board)
Andrew67b5fa72020-02-05 14:14:48 -08001655
Alex Klein975e86c2023-01-23 16:49:10 -07001656 # Don't bother trying to clean for unmerges. We won't use the local
1657 # db, and it just slows things down for the user.
Alex Klein1699fab2022-09-08 08:46:06 -06001658 if emerge and clean_binpkg:
1659 logging.notice(
1660 "Cleaning outdated binary packages from %s", sysroot
1661 )
1662 portage_util.CleanOutdatedBinaryPackages(sysroot)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001663
Alex Klein1699fab2022-09-08 08:46:06 -06001664 # Remount rootfs as writable if necessary.
1665 if not device.MountRootfsReadWrite():
1666 raise DeployError(
1667 "Cannot remount rootfs as read-write. Exiting."
1668 )
Ralph Nathane01ccf12015-04-16 10:40:32 -07001669
Alex Klein1699fab2022-09-08 08:46:06 -06001670 # Obtain list of packages to upgrade/remove.
1671 pkg_scanner = _InstallPackageScanner(sysroot)
Tim Baine4a783b2023-04-21 20:05:51 +00001672 (
1673 pkgs,
1674 listed,
1675 num_updates,
1676 pkgs_attrs,
1677 warnings_shown,
1678 ) = pkg_scanner.Run(device, root, packages, update, deep, deep_rev)
Alex Klein1699fab2022-09-08 08:46:06 -06001679 if emerge:
1680 action_str = "emerge"
1681 else:
1682 pkgs.reverse()
1683 action_str = "unmerge"
David Pursell9476bf42015-03-30 13:34:27 -07001684
Alex Klein1699fab2022-09-08 08:46:06 -06001685 if not pkgs:
1686 logging.notice("No packages to %s", action_str)
1687 return
David Pursell9476bf42015-03-30 13:34:27 -07001688
Alex Klein1699fab2022-09-08 08:46:06 -06001689 # Warn when the user installs & didn't `cros workon start`.
1690 if emerge:
1691 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(
1692 use_all=True
1693 )
1694 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1695 for package in listed:
1696 cp = package_info.SplitCPV(package).cp
1697 if cp in all_workon and cp not in worked_on_cps:
1698 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001699 "Are you intentionally deploying unmodified "
1700 "packages, or did you forget to run "
1701 "`cros workon --board=$BOARD start %s`?",
Alex Klein1699fab2022-09-08 08:46:06 -06001702 cp,
1703 )
David Pursell9476bf42015-03-30 13:34:27 -07001704
Alex Klein1699fab2022-09-08 08:46:06 -06001705 logging.notice("These are the packages to %s:", action_str)
1706 for i, pkg in enumerate(pkgs):
1707 logging.notice(
1708 "%s %d) %s", "*" if pkg in listed else " ", i + 1, pkg
1709 )
Gilad Arnolda0a98062015-07-07 08:34:27 -07001710
Alex Klein1699fab2022-09-08 08:46:06 -06001711 if dry_run or not _ConfirmDeploy(num_updates):
1712 return
David Pursell9476bf42015-03-30 13:34:27 -07001713
Tim Baine4a783b2023-04-21 20:05:51 +00001714 if (
1715 warnings_shown
1716 and not force
1717 and not _ConfirmUpdateDespiteWarnings()
1718 ):
1719 return
1720
Alex Klein1699fab2022-09-08 08:46:06 -06001721 # Select function (emerge or unmerge) and bind args.
1722 if emerge:
1723 func = functools.partial(
1724 _EmergePackages,
1725 pkgs,
1726 device,
1727 strip,
1728 sysroot,
1729 root,
1730 board,
1731 emerge_args,
1732 )
1733 else:
1734 func = functools.partial(
1735 _UnmergePackages, pkgs, device, root, pkgs_attrs
1736 )
David Pursell2e773382015-04-03 14:30:47 -07001737
Alex Klein1699fab2022-09-08 08:46:06 -06001738 # Call the function with the progress bar or with normal output.
1739 if command.UseProgressBar():
1740 op = BrilloDeployOperation(emerge)
1741 op.Run(func, log_level=logging.DEBUG)
1742 else:
1743 func()
David Pursell9476bf42015-03-30 13:34:27 -07001744
Alex Klein1699fab2022-09-08 08:46:06 -06001745 if device.IsSELinuxAvailable():
1746 if sum(x.count("selinux-policy") for x in pkgs):
1747 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001748 "Deploying SELinux policy will not take effect until "
1749 "reboot. SELinux policy is loaded by init. Also, "
1750 "changing the security contexts (labels) of a file "
1751 "will require building a new image and flashing the "
1752 "image onto the device."
Alex Klein1699fab2022-09-08 08:46:06 -06001753 )
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001754
Alex Klein1699fab2022-09-08 08:46:06 -06001755 # This message is read by BrilloDeployOperation.
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001756 logging.warning(
Alex Klein1699fab2022-09-08 08:46:06 -06001757 "Please restart any updated services on the device, "
1758 "or just reboot it."
1759 )
1760 except Exception:
1761 if lsb_release:
1762 lsb_entries = sorted(lsb_release.items())
1763 logging.info(
1764 "Following are the LSB version details of the device:\n%s",
1765 "\n".join("%s=%s" % (k, v) for k, v in lsb_entries),
1766 )
1767 raise