blob: 90166db1edf600b35940b3a7d47cdce910b0f9d5 [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
Alex Klein1699fab2022-09-08 08:46:06 -0600260 # Separate the slot qualifier and strip off subslots.
261 if ":" in cp:
262 cp, slot = cp.split(":")
263 for delim in ("/", "="):
264 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():
799 if required_slot and slot != required_slot:
800 continue
801
802 num_processed += 1
803 logging.debug(" Checking %s...", pkg_info.cpv)
804
Tim Baine4a783b2023-04-21 20:05:51 +0000805 install, update, use_mismatch = self._NeedsInstall(
Alex Klein1699fab2022-09-08 08:46:06 -0600806 pkg_info.cpv, slot, pkg_info.build_time, optional
807 )
808 if not install:
809 continue
810
811 installs[cp] = (pkg_info.cpv, slot, listed, update)
Tim Baine4a783b2023-04-21 20:05:51 +0000812 warnings_shown |= use_mismatch
Alex Klein1699fab2022-09-08 08:46:06 -0600813
814 # Add forward and backward runtime dependencies to queue.
815 if process_rdeps:
816 self._ProcessDeps(pkg_info.rdeps, False)
817 if process_rev_rdeps:
Trent Apted1e2e4f32023-05-05 03:50:20 +0000818 target_pkg_info = self.target_db.get(cp, {}).get(slot)
Alex Klein1699fab2022-09-08 08:46:06 -0600819 if target_pkg_info:
820 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
821
822 if num_processed == 0:
823 logging.warning(
824 "No qualified bintree package corresponding to %s", cp
825 )
826
Tim Baine4a783b2023-04-21 20:05:51 +0000827 return installs, warnings_shown
Alex Klein1699fab2022-09-08 08:46:06 -0600828
Tim Baine4a783b2023-04-21 20:05:51 +0000829 def _SortInstalls(self, installs: List[str]) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600830 """Returns a sorted list of packages to install.
831
832 Performs a topological sort based on dependencies found in the binary
833 package database.
834
835 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600836 installs: Dictionary of packages to install indexed by CP.
Alex Klein1699fab2022-09-08 08:46:06 -0600837
838 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600839 A list of package CPVs (string).
Alex Klein1699fab2022-09-08 08:46:06 -0600840
841 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600842 ValueError: If dependency graph contains a cycle.
Alex Klein1699fab2022-09-08 08:46:06 -0600843 """
844 not_visited = set(installs.keys())
845 curr_path = []
846 sorted_installs = []
847
Tim Baine4a783b2023-04-21 20:05:51 +0000848 def SortFrom(cp: str) -> None:
Alex Klein975e86c2023-01-23 16:49:10 -0700849 """Traverses deps recursively, emitting nodes in reverse order."""
Alex Klein1699fab2022-09-08 08:46:06 -0600850 cpv, slot, _, _ = installs[cp]
851 if cpv in curr_path:
852 raise ValueError(
853 "Dependencies contain a cycle: %s -> %s"
854 % (" -> ".join(curr_path[curr_path.index(cpv) :]), cpv)
855 )
856 curr_path.append(cpv)
857 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
858 if rdep_cp in not_visited:
859 not_visited.remove(rdep_cp)
860 SortFrom(rdep_cp)
861
862 sorted_installs.append(cpv)
863 curr_path.pop()
864
865 # So long as there's more packages, keep expanding dependency paths.
866 while not_visited:
867 SortFrom(not_visited.pop())
868
869 return sorted_installs
870
Tim Baine4a783b2023-04-21 20:05:51 +0000871 def _EnqListedPkg(self, pkg: str) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -0600872 """Finds and enqueues a listed package."""
873 cp, slot = self._FindPackage(pkg)
874 if cp not in self.binpkgs_db:
875 raise self.BintreeError(
876 "Package %s not found in binpkgs tree" % pkg
877 )
878 self._EnqDep((cp, slot), True, False)
879
Tim Baine4a783b2023-04-21 20:05:51 +0000880 def _EnqInstalledPkgs(self) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600881 """Enqueues all available binary packages that are already installed."""
882 for cp, cp_slots in self.binpkgs_db.items():
883 target_cp_slots = self.target_db.get(cp)
884 if target_cp_slots:
885 for slot in cp_slots.keys():
886 if slot in target_cp_slots:
887 self._EnqDep((cp, slot), True, False)
888
889 def Run(
890 self,
Tim Baine4a783b2023-04-21 20:05:51 +0000891 device: remote_access.RemoteDevice,
892 root: str,
893 listed_pkgs: List[str],
894 update: bool,
895 process_rdeps: bool,
896 process_rev_rdeps: bool,
897 ) -> Tuple[List[str], List[str], int, Dict[str, str], bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600898 """Computes the list of packages that need to be installed on a target.
899
900 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600901 device: Target handler object.
902 root: Package installation root.
903 listed_pkgs: Package names/files listed by the user.
904 update: Whether to read the target's installed package database.
905 process_rdeps: Whether to trace forward dependencies.
906 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600907
908 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000909 A tuple (sorted, listed, num_updates, install_attrs, warnings_shown)
910 where |sorted| is a list of package CPVs (string) to install on the
911 target in an order that satisfies their inter-dependencies, |listed|
Alex Klein53cc3bf2022-10-13 08:50:01 -0600912 the subset that was requested by the user, and |num_updates|
913 the number of packages being installed over preexisting
914 versions. Note that installation order should be reversed for
915 removal, |install_attrs| is a dictionary mapping a package
Tim Baine4a783b2023-04-21 20:05:51 +0000916 CPV (string) to some of its extracted environment attributes, and
917 |warnings_shown| is a boolean indicating whether warnings were shown
918 that might require a prompt whether to continue.
Alex Klein1699fab2022-09-08 08:46:06 -0600919 """
920 if process_rev_rdeps and not process_rdeps:
921 raise ValueError(
922 "Must processing forward deps when processing rev deps"
923 )
924 if process_rdeps and not update:
925 raise ValueError(
926 "Must check installed packages when processing deps"
927 )
928
929 if update:
930 logging.info("Initializing target intalled packages database...")
931 self._InitTargetVarDB(
932 device, root, process_rdeps, process_rev_rdeps
933 )
934
935 logging.info("Initializing binary packages database...")
936 self._InitBinpkgDB(process_rdeps)
937
938 logging.info("Finding listed package(s)...")
939 self._InitDepQueue()
940 for pkg in listed_pkgs:
941 if pkg == "@installed":
942 if not update:
943 raise ValueError(
Alex Klein975e86c2023-01-23 16:49:10 -0700944 "Must check installed packages when updating all of "
945 "them."
Alex Klein1699fab2022-09-08 08:46:06 -0600946 )
947 self._EnqInstalledPkgs()
948 else:
949 self._EnqListedPkg(pkg)
950
951 logging.info("Computing set of packages to install...")
Tim Baine4a783b2023-04-21 20:05:51 +0000952 installs, warnings_shown = self._ComputeInstalls(
953 process_rdeps, process_rev_rdeps
954 )
Alex Klein1699fab2022-09-08 08:46:06 -0600955
956 num_updates = 0
957 listed_installs = []
958 for cpv, _, listed, isupdate in installs.values():
959 if listed:
960 listed_installs.append(cpv)
961 if isupdate:
962 num_updates += 1
963
964 logging.info(
965 "Processed %d package(s), %d will be installed, %d are "
966 "updating existing packages",
967 len(self.seen),
968 len(installs),
969 num_updates,
970 )
971
972 sorted_installs = self._SortInstalls(installs)
973
974 install_attrs = {}
975 for pkg in sorted_installs:
976 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
977 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
978 install_attrs[pkg] = {}
979 if dlc_id and dlc_package:
980 install_attrs[pkg][_DLC_ID] = dlc_id
981
Tim Baine4a783b2023-04-21 20:05:51 +0000982 return (
983 sorted_installs,
984 listed_installs,
985 num_updates,
986 install_attrs,
987 warnings_shown,
988 )
David Pursell9476bf42015-03-30 13:34:27 -0700989
990
Tim Baine4a783b2023-04-21 20:05:51 +0000991def _Emerge(
992 device: remote_access.RemoteDevice,
993 pkg_paths: List[str],
994 root: str,
995 extra_args: List[str] = None,
996) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -0600997 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -0700998
Alex Klein1699fab2022-09-08 08:46:06 -0600999 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001000 device: A ChromiumOSDevice object.
Mike Frysinger9fba8d02023-05-15 15:04:07 -04001001 pkg_paths: Local paths to binary packages.
Alex Klein53cc3bf2022-10-13 08:50:01 -06001002 root: Package installation root path.
1003 extra_args: Extra arguments to pass to emerge.
David Pursell9476bf42015-03-30 13:34:27 -07001004
Alex Klein1699fab2022-09-08 08:46:06 -06001005 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001006 DeployError: Unrecoverable error during emerge.
Alex Klein1699fab2022-09-08 08:46:06 -06001007 """
Mike Frysinger63d35512021-01-26 23:16:13 -05001008
Alex Klein1699fab2022-09-08 08:46:06 -06001009 def path_to_name(pkg_path):
1010 return os.path.basename(pkg_path)
Mike Frysinger63d35512021-01-26 23:16:13 -05001011
Alex Klein1699fab2022-09-08 08:46:06 -06001012 def path_to_category(pkg_path):
1013 return os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -07001014
Alex Klein1699fab2022-09-08 08:46:06 -06001015 pkg_names = ", ".join(path_to_name(x) for x in pkg_paths)
David Pursell9476bf42015-03-30 13:34:27 -07001016
Alex Klein1699fab2022-09-08 08:46:06 -06001017 pkgroot = os.path.join(device.work_dir, "packages")
1018 portage_tmpdir = os.path.join(device.work_dir, "portage-tmp")
Alex Klein975e86c2023-01-23 16:49:10 -07001019 # Clean out the dirs first if we had a previous emerge on the device so as
1020 # to free up space for this emerge. The last emerge gets implicitly cleaned
1021 # up when the device connection deletes its work_dir.
Alex Klein1699fab2022-09-08 08:46:06 -06001022 device.run(
1023 f"cd {device.work_dir} && "
1024 f"rm -rf packages portage-tmp && "
1025 f"mkdir -p portage-tmp packages && "
1026 f"cd packages && "
1027 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
1028 shell=True,
1029 remote_sudo=True,
1030 )
Mike Frysinger63d35512021-01-26 23:16:13 -05001031
Alex Klein1699fab2022-09-08 08:46:06 -06001032 logging.info("Use portage temp dir %s", portage_tmpdir)
David Pursell9476bf42015-03-30 13:34:27 -07001033
Mike Frysinger63d35512021-01-26 23:16:13 -05001034 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001035 logging.notice("Copying binpkgs to device.")
1036 for pkg_path in pkg_paths:
1037 pkg_name = path_to_name(pkg_path)
1038 logging.info("Copying %s", pkg_name)
1039 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
1040 device.CopyToDevice(
1041 pkg_path, pkg_dir, mode="rsync", remote_sudo=True, compress=False
1042 )
1043
1044 # This message is read by BrilloDeployOperation.
1045 logging.notice("Installing: %s", pkg_names)
1046
1047 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
1048 # chromeos-base packages will be skipped due to the configuration
1049 # in /etc/protage/make.profile/package.provided. However, there is
1050 # a known bug that /usr/local/etc/portage is not setup properly
1051 # (crbug.com/312041). This does not affect `cros deploy` because
1052 # we do not use the preset PKGDIR.
1053 extra_env = {
1054 "FEATURES": "-sandbox",
1055 "PKGDIR": pkgroot,
1056 "PORTAGE_CONFIGROOT": "/usr/local",
1057 "PORTAGE_TMPDIR": portage_tmpdir,
1058 "PORTDIR": device.work_dir,
1059 "CONFIG_PROTECT": "-*",
1060 }
1061
Alex Klein975e86c2023-01-23 16:49:10 -07001062 # --ignore-built-slot-operator-deps because we don't rebuild everything. It
1063 # can cause errors, but that's expected with cros deploy since it's just a
Alex Klein1699fab2022-09-08 08:46:06 -06001064 # best effort to prevent developers avoid rebuilding an image every time.
1065 cmd = [
1066 "emerge",
1067 "--usepkg",
1068 "--ignore-built-slot-operator-deps=y",
1069 "--root",
1070 root,
1071 ] + [os.path.join(pkgroot, *x.split("/")[-2:]) for x in pkg_paths]
1072 if extra_args:
1073 cmd.append(extra_args)
1074
1075 logging.warning(
1076 "Ignoring slot dependencies! This may break things! e.g. "
1077 "packages built against the old version may not be able to "
1078 "load the new .so. This is expected, and you will just need "
1079 "to build and flash a new image if you have problems."
1080 )
1081 try:
1082 result = device.run(
1083 cmd,
1084 extra_env=extra_env,
1085 remote_sudo=True,
1086 capture_output=True,
1087 debug_level=logging.INFO,
1088 )
1089
1090 pattern = (
1091 "A requested package will not be merged because "
1092 "it is listed in package.provided"
1093 )
1094 output = result.stderr.replace("\n", " ").replace("\r", "")
1095 if pattern in output:
1096 error = (
1097 "Package failed to emerge: %s\n"
1098 "Remove %s from /etc/portage/make.profile/"
1099 "package.provided/chromeos-base.packages\n"
1100 "(also see crbug.com/920140 for more context)\n"
1101 % (pattern, pkg_name)
1102 )
1103 cros_build_lib.Die(error)
1104 except Exception:
1105 logging.error("Failed to emerge packages %s", pkg_names)
1106 raise
1107 else:
1108 # This message is read by BrilloDeployOperation.
1109 logging.notice("Packages have been installed.")
David Pursell9476bf42015-03-30 13:34:27 -07001110
1111
Tim Baine4a783b2023-04-21 20:05:51 +00001112def _RestoreSELinuxContext(
1113 device: remote_access.RemoteDevice, pkgpath: str, root: str
1114) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001115 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +09001116
Alex Klein1699fab2022-09-08 08:46:06 -06001117 This reads the tarball from pkgpath, and calls restorecon on device to
Alex Klein975e86c2023-01-23 16:49:10 -07001118 restore SELinux context for files listed in the tarball, assuming those
1119 files are installed to /
Qijiang Fan8a945032019-04-25 20:53:29 +09001120
Alex Klein1699fab2022-09-08 08:46:06 -06001121 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001122 device: a ChromiumOSDevice object
1123 pkgpath: path to tarball
1124 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001125 """
1126 pkgroot = os.path.join(device.work_dir, "packages")
1127 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
1128 pkgpath_device = os.path.join(
1129 pkgroot, pkg_dirname, os.path.basename(pkgpath)
1130 )
1131 # Testing shows restorecon splits on newlines instead of spaces.
1132 device.run(
1133 [
1134 "cd",
1135 root,
1136 "&&",
1137 "tar",
1138 "tf",
1139 pkgpath_device,
1140 "|",
1141 "restorecon",
1142 "-i",
1143 "-f",
1144 "-",
1145 ],
1146 remote_sudo=True,
1147 )
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001148
1149
Tim Baine4a783b2023-04-21 20:05:51 +00001150def _GetPackagesByCPV(
1151 cpvs: List[package_info.CPV], strip: bool, sysroot: str
1152) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001153 """Returns paths to binary packages corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001154
Alex Klein1699fab2022-09-08 08:46:06 -06001155 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001156 cpvs: List of CPV components given by package_info.SplitCPV().
1157 strip: True to run strip_package.
1158 sysroot: Sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001159
Alex Klein1699fab2022-09-08 08:46:06 -06001160 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001161 List of paths corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001162
Alex Klein1699fab2022-09-08 08:46:06 -06001163 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001164 DeployError: If a package is missing.
Alex Klein1699fab2022-09-08 08:46:06 -06001165 """
1166 packages_dir = None
1167 if strip:
1168 try:
1169 cros_build_lib.run(
1170 [
Mike Frysinger5429f302023-03-27 15:48:52 -04001171 constants.CHROMITE_SCRIPTS_DIR / "strip_package",
Alex Klein1699fab2022-09-08 08:46:06 -06001172 "--sysroot",
1173 sysroot,
1174 ]
1175 + [cpv.cpf for cpv in cpvs]
1176 )
1177 packages_dir = _STRIPPED_PACKAGES_DIR
1178 except cros_build_lib.RunCommandError:
1179 logging.error(
1180 "Cannot strip packages %s", " ".join([str(cpv) for cpv in cpvs])
1181 )
1182 raise
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001183
Alex Klein1699fab2022-09-08 08:46:06 -06001184 paths = []
1185 for cpv in cpvs:
1186 path = portage_util.GetBinaryPackagePath(
1187 cpv.category,
1188 cpv.package,
1189 cpv.version,
1190 sysroot=sysroot,
1191 packages_dir=packages_dir,
1192 )
1193 if not path:
1194 raise DeployError("Missing package %s." % cpv)
1195 paths.append(path)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001196
Alex Klein1699fab2022-09-08 08:46:06 -06001197 return paths
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001198
1199
Tim Baine4a783b2023-04-21 20:05:51 +00001200def _GetPackagesPaths(pkgs: List[str], strip: bool, sysroot: str) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001201 """Returns paths to binary |pkgs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001202
Alex Klein1699fab2022-09-08 08:46:06 -06001203 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001204 pkgs: List of package CPVs string.
1205 strip: Whether or not to run strip_package for CPV packages.
1206 sysroot: The sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001207
Alex Klein1699fab2022-09-08 08:46:06 -06001208 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001209 List of paths corresponding to |pkgs|.
Alex Klein1699fab2022-09-08 08:46:06 -06001210 """
1211 cpvs = [package_info.SplitCPV(p) for p in pkgs]
1212 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001213
1214
Tim Baine4a783b2023-04-21 20:05:51 +00001215def _Unmerge(
1216 device: remote_access.RemoteDevice, pkgs: List[str], root: str
1217) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001218 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -07001219
Alex Klein1699fab2022-09-08 08:46:06 -06001220 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001221 device: A RemoteDevice object.
1222 pkgs: Package names.
1223 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001224 """
1225 pkg_names = ", ".join(os.path.basename(x) for x in pkgs)
Mike Frysinger22bb5502021-01-29 13:05:46 -05001226 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001227 logging.notice("Unmerging %s.", pkg_names)
1228 cmd = ["qmerge", "--yes"]
1229 # Check if qmerge is available on the device. If not, use emerge.
1230 if device.run(["qmerge", "--version"], check=False).returncode != 0:
1231 cmd = ["emerge"]
1232
1233 cmd += ["--unmerge", "--root", root]
1234 cmd.extend("f={x}" for x in pkgs)
1235 try:
1236 # Always showing the emerge output for clarity.
1237 device.run(
1238 cmd,
1239 capture_output=False,
1240 remote_sudo=True,
1241 debug_level=logging.INFO,
1242 )
1243 except Exception:
1244 logging.error("Failed to unmerge packages %s", pkg_names)
1245 raise
1246 else:
1247 # This message is read by BrilloDeployOperation.
1248 logging.notice("Packages have been uninstalled.")
David Pursell9476bf42015-03-30 13:34:27 -07001249
1250
Tim Baine4a783b2023-04-21 20:05:51 +00001251def _ConfirmDeploy(num_updates: int) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -06001252 """Returns whether we can continue deployment."""
1253 if num_updates > _MAX_UPDATES_NUM:
1254 logging.warning(_MAX_UPDATES_WARNING)
1255 return cros_build_lib.BooleanPrompt(default=False)
David Pursell9476bf42015-03-30 13:34:27 -07001256
Alex Klein1699fab2022-09-08 08:46:06 -06001257 return True
David Pursell9476bf42015-03-30 13:34:27 -07001258
1259
Tim Baine4a783b2023-04-21 20:05:51 +00001260def _ConfirmUpdateDespiteWarnings() -> bool:
1261 """Returns whether we can continue updating despite warnings."""
1262 logging.warning("Continue despite prior warnings?")
1263 return cros_build_lib.BooleanPrompt(default=False)
1264
1265
1266def _EmergePackages(
1267 pkgs: List[str],
1268 device: remote_access.RemoteDevice,
1269 strip: bool,
1270 sysroot: str,
1271 root: str,
1272 board: str,
1273 emerge_args: List[str],
1274) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001275 """Call _Emerge for each package in pkgs."""
Ben Pastene5f03b052019-08-12 18:03:24 -07001276 if device.IsSELinuxAvailable():
Alex Klein1699fab2022-09-08 08:46:06 -06001277 enforced = device.IsSELinuxEnforced()
1278 if enforced:
1279 device.run(["setenforce", "0"])
1280 else:
1281 enforced = False
Andrewc7e1c6b2020-02-27 16:03:53 -08001282
Alex Klein1699fab2022-09-08 08:46:06 -06001283 dlc_deployed = False
1284 # This message is read by BrilloDeployOperation.
1285 logging.info("Preparing local packages for transfer.")
1286 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
1287 # Install all the packages in one pass so inter-package blockers work.
1288 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
1289 logging.info("Updating SELinux settings & DLC images.")
1290 for pkg_path in pkg_paths:
1291 if device.IsSELinuxAvailable():
1292 _RestoreSELinuxContext(device, pkg_path, root)
Mike Frysinger5f4c2742021-02-08 14:37:23 -05001293
Alex Klein1699fab2022-09-08 08:46:06 -06001294 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
1295 if dlc_id and dlc_package:
1296 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
1297 dlc_deployed = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001298
Alex Klein1699fab2022-09-08 08:46:06 -06001299 if dlc_deployed:
1300 # Clean up empty directories created by emerging DLCs.
1301 device.run(
1302 [
1303 "test",
1304 "-d",
1305 "/build/rootfs",
1306 "&&",
1307 "rmdir",
1308 "--ignore-fail-on-non-empty",
1309 "/build/rootfs",
1310 "/build",
1311 ],
1312 check=False,
1313 )
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001314
Alex Klein1699fab2022-09-08 08:46:06 -06001315 if enforced:
1316 device.run(["setenforce", "1"])
1317
1318 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1319 # we installed new DLC images).
1320 if dlc_deployed:
1321 device.run(["restart", "dlcservice"])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001322
1323
Tim Baine4a783b2023-04-21 20:05:51 +00001324def _UnmergePackages(
1325 pkgs: List[str],
1326 device: remote_access.RemoteDevice,
1327 root: str,
1328 pkgs_attrs: Dict[str, List[str]],
1329) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -06001330 """Call _Unmege for each package in pkgs."""
1331 dlc_uninstalled = False
1332 _Unmerge(device, pkgs, root)
1333 logging.info("Cleaning up DLC images.")
1334 for pkg in pkgs:
1335 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1336 dlc_uninstalled = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001337
Alex Klein1699fab2022-09-08 08:46:06 -06001338 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1339 # uninstalled DLC images).
1340 if dlc_uninstalled:
1341 device.run(["restart", "dlcservice"])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001342
1343
Tim Baine4a783b2023-04-21 20:05:51 +00001344def _UninstallDLCImage(
1345 device: remote_access.RemoteDevice, pkg_attrs: Dict[str, List[str]]
1346):
Alex Klein1699fab2022-09-08 08:46:06 -06001347 """Uninstall a DLC image."""
1348 if _DLC_ID in pkg_attrs:
1349 dlc_id = pkg_attrs[_DLC_ID]
1350 logging.notice("Uninstalling DLC image for %s", dlc_id)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001351
Alex Klein1699fab2022-09-08 08:46:06 -06001352 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1353 return True
1354 else:
1355 logging.debug("DLC_ID not found in package")
1356 return False
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001357
1358
Tim Baine4a783b2023-04-21 20:05:51 +00001359def _DeployDLCImage(
1360 device: remote_access.RemoteDevice,
1361 sysroot: str,
1362 board: str,
1363 dlc_id: str,
1364 dlc_package: str,
1365):
Alex Klein1699fab2022-09-08 08:46:06 -06001366 """Deploy (install and mount) a DLC image.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001367
Alex Klein1699fab2022-09-08 08:46:06 -06001368 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001369 device: A device object.
1370 sysroot: The sysroot path.
1371 board: Board to use.
1372 dlc_id: The DLC ID.
1373 dlc_package: The DLC package name.
Alex Klein1699fab2022-09-08 08:46:06 -06001374 """
1375 # Requires `sudo_rm` because installations of files are running with sudo.
1376 with osutils.TempDir(sudo_rm=True) as tempdir:
1377 temp_rootfs = Path(tempdir)
1378 # Build the DLC image if the image is outdated or doesn't exist.
1379 dlc_lib.InstallDlcImages(
1380 sysroot=sysroot, rootfs=temp_rootfs, dlc_id=dlc_id, board=board
1381 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001382
Alex Klein1699fab2022-09-08 08:46:06 -06001383 logging.debug("Uninstall DLC %s if it is installed.", dlc_id)
1384 try:
1385 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1386 except cros_build_lib.RunCommandError as e:
1387 logging.info(
1388 "Failed to uninstall DLC:%s. Continue anyway.", e.stderr
1389 )
1390 except Exception:
1391 logging.error("Failed to uninstall DLC.")
1392 raise
Andrewc7e1c6b2020-02-27 16:03:53 -08001393
Yuanpeng Niaba30342023-07-11 14:24:09 -07001394 src_dlc_dir = os.path.join(
Alex Klein1699fab2022-09-08 08:46:06 -06001395 sysroot,
1396 dlc_lib.DLC_BUILD_DIR,
1397 dlc_id,
Alex Klein1699fab2022-09-08 08:46:06 -06001398 )
Yuanpeng Niaba30342023-07-11 14:24:09 -07001399 if not os.path.exists(src_dlc_dir):
1400 src_dlc_dir = os.path.join(
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001401 sysroot,
1402 dlc_lib.DLC_BUILD_DIR_SCALED,
1403 dlc_id,
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001404 )
1405
Yuanpeng Niaba30342023-07-11 14:24:09 -07001406 # Deploy the metadata entry to compressed metadata on device.
1407 logging.notice("Setting the DLC metadata for %s", dlc_id)
1408 metadata = dlc_lib.DlcMetadata.LoadSrcMetadata(src_dlc_dir)
1409 device.run(
1410 [dlc_lib.DLC_METADATA_UTIL, "--set", f"--id={dlc_id}"],
1411 input=json.dumps(metadata),
1412 check=False,
1413 )
1414
1415 logging.notice("Deploy the DLC image for %s", dlc_id)
1416 dlc_img_path_src = os.path.join(
1417 src_dlc_dir,
1418 dlc_package,
1419 dlc_lib.DLC_IMAGE,
1420 )
1421
Alex Klein1699fab2022-09-08 08:46:06 -06001422 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1423 dlc_img_path_a = os.path.join(dlc_img_path, "dlc_a")
1424 dlc_img_path_b = os.path.join(dlc_img_path, "dlc_b")
1425 # Create directories for DLC images.
Mike Frysinger445f6bf2023-06-27 11:43:33 -04001426 device.mkdir([dlc_img_path_a, dlc_img_path_b])
Alex Klein1699fab2022-09-08 08:46:06 -06001427 # Copy images to the destination directories.
1428 device.CopyToDevice(
1429 dlc_img_path_src,
1430 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1431 mode="rsync",
1432 )
1433 device.run(
1434 [
1435 "cp",
1436 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1437 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE),
1438 ]
1439 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001440
Alex Klein1699fab2022-09-08 08:46:06 -06001441 # Set the proper perms and ownership so dlcservice can access the image.
1442 device.run(["chmod", "-R", "u+rwX,go+rX,go-w", _DLC_INSTALL_ROOT])
1443 device.run(["chown", "-R", "dlcservice:dlcservice", _DLC_INSTALL_ROOT])
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001444
Alex Klein1699fab2022-09-08 08:46:06 -06001445 # Copy metadata to device.
Yuanpeng Niaba30342023-07-11 14:24:09 -07001446 # TODO(b/290961240): To be removed once the transition to compressed
1447 # metadata is complete.
Alex Klein1699fab2022-09-08 08:46:06 -06001448 dest_meta_dir = Path("/") / dlc_lib.DLC_META_DIR / dlc_id / dlc_package
Mike Frysinger445f6bf2023-06-27 11:43:33 -04001449 device.mkdir(dest_meta_dir)
Alex Klein1699fab2022-09-08 08:46:06 -06001450 src_meta_dir = os.path.join(
Yuanpeng Niaba30342023-07-11 14:24:09 -07001451 src_dlc_dir,
Alex Klein1699fab2022-09-08 08:46:06 -06001452 dlc_package,
1453 dlc_lib.DLC_TMP_META_DIR,
1454 )
1455 device.CopyToDevice(
1456 src_meta_dir + "/",
1457 dest_meta_dir,
1458 mode="rsync",
1459 recursive=True,
1460 remote_sudo=True,
1461 )
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001462
Alex Klein1699fab2022-09-08 08:46:06 -06001463 # TODO(kimjae): Make this generic so it recomputes all the DLCs + copies
Alex Klein975e86c2023-01-23 16:49:10 -07001464 # over a fresh list of dm-verity digests instead of appending and
1465 # keeping the stale digests when developers are testing.
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001466
Alex Klein1699fab2022-09-08 08:46:06 -06001467 # Copy the LoadPin dm-verity digests to device.
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001468 _DeployDLCLoadPin(temp_rootfs, device)
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001469
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001470
1471def _DeployDLCLoadPin(
Tim Baine4a783b2023-04-21 20:05:51 +00001472 rootfs: os.PathLike, device: remote_access.RemoteDevice
1473) -> None:
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001474 """Deploy DLC LoadPin from temp rootfs to device.
1475
1476 Args:
1477 rootfs: Path to rootfs.
1478 device: A device object.
1479 """
1480 loadpin = dlc_lib.DLC_LOADPIN_TRUSTED_VERITY_DIGESTS
1481 dst_loadpin = Path("/") / dlc_lib.DLC_META_DIR / loadpin
1482 src_loadpin = rootfs / dlc_lib.DLC_META_DIR / loadpin
1483 if src_loadpin.exists():
1484 digests = set(osutils.ReadFile(src_loadpin).splitlines())
1485 digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1486 try:
1487 device_digests = set(device.CatFile(dst_loadpin).splitlines())
1488 device_digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1489 digests.update(device_digests)
1490 except remote_access.CatFileError:
1491 pass
1492
1493 with tempfile.NamedTemporaryFile(dir=rootfs) as f:
1494 osutils.WriteFile(f.name, dlc_lib.DLC_LOADPIN_FILE_HEADER + "\n")
1495 osutils.WriteFile(f.name, "\n".join(digests) + "\n", mode="a")
1496 device.CopyToDevice(
1497 f.name, dst_loadpin, mode="rsync", remote_sudo=True
1498 )
Andrew67b5fa72020-02-05 14:14:48 -08001499
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001500
Tim Baine4a783b2023-04-21 20:05:51 +00001501def _GetDLCInfo(
1502 device: remote_access.RemoteDevice, pkg_path: str, from_dut: bool
1503) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001504 """Returns information of a DLC given its package path.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001505
Alex Klein1699fab2022-09-08 08:46:06 -06001506 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001507 device: commandline.Device object; None to use the default device.
1508 pkg_path: path to the package.
1509 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1510 info from host.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001511
Alex Klein1699fab2022-09-08 08:46:06 -06001512 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001513 A tuple (dlc_id, dlc_package).
Alex Klein1699fab2022-09-08 08:46:06 -06001514 """
1515 environment_content = ""
1516 if from_dut:
1517 # On DUT, |pkg_path| is the directory which contains environment file.
1518 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1519 try:
1520 environment_data = device.CatFile(
1521 environment_path, max_size=None, encoding=None
1522 )
1523 except remote_access.CatFileError:
1524 # The package is not installed on DUT yet. Skip extracting info.
1525 return None, None
1526 else:
1527 # On host, pkg_path is tbz2 file which contains environment file.
1528 # Extract the metadata of the package file.
1529 data = portage.xpak.tbz2(pkg_path).get_data()
1530 environment_data = data[_ENVIRONMENT_FILENAME.encode("utf-8")]
1531
1532 # Extract the environment metadata.
1533 environment_content = bz2.decompress(environment_data)
1534
1535 with tempfile.NamedTemporaryFile() as f:
1536 # Dumps content into a file so we can use osutils.SourceEnvironment.
1537 path = os.path.realpath(f.name)
1538 osutils.WriteFile(path, environment_content, mode="wb")
1539 content = osutils.SourceEnvironment(
1540 path, (_DLC_ID, _DLC_PACKAGE, _DLC_ENABLED)
1541 )
1542
1543 dlc_enabled = content.get(_DLC_ENABLED)
1544 if dlc_enabled is not None and (
1545 dlc_enabled is False or str(dlc_enabled) == "false"
1546 ):
1547 logging.info("Installing DLC in rootfs.")
1548 return None, None
1549 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
1550
1551
1552def Deploy(
Tim Baine4a783b2023-04-21 20:05:51 +00001553 device: remote_access.RemoteDevice,
1554 packages: List[str],
1555 board: str = None,
1556 emerge: bool = True,
1557 update: bool = False,
1558 deep: bool = False,
1559 deep_rev: bool = False,
1560 clean_binpkg: bool = True,
1561 root: str = "/",
1562 strip: bool = True,
1563 emerge_args: List[str] = None,
1564 ssh_private_key: str = None,
1565 ping: bool = True,
1566 force: bool = False,
1567 dry_run: bool = False,
1568) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001569 """Deploys packages to a device.
1570
1571 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001572 device: commandline.Device object; None to use the default device.
1573 packages: List of packages (strings) to deploy to device.
1574 board: Board to use; None to automatically detect.
1575 emerge: True to emerge package, False to unmerge.
1576 update: Check installed version on device.
1577 deep: Install dependencies also. Implies |update|.
1578 deep_rev: Install reverse dependencies. Implies |deep|.
1579 clean_binpkg: Clean outdated binary packages.
1580 root: Package installation root path.
1581 strip: Run strip_package to filter out preset paths in the package.
1582 emerge_args: Extra arguments to pass to emerge.
1583 ssh_private_key: Path to an SSH private key file; None to use test keys.
1584 ping: True to ping the device before trying to connect.
1585 force: Ignore confidence checks and prompts.
1586 dry_run: Print deployment plan but do not deploy anything.
Alex Klein1699fab2022-09-08 08:46:06 -06001587
1588 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001589 ValueError: Invalid parameter or parameter combination.
1590 DeployError: Unrecoverable failure during deploy.
Alex Klein1699fab2022-09-08 08:46:06 -06001591 """
1592 if deep_rev:
1593 deep = True
1594 if deep:
1595 update = True
1596
1597 if not packages:
1598 raise DeployError("No packages provided, nothing to deploy.")
1599
1600 if update and not emerge:
1601 raise ValueError("Cannot update and unmerge.")
1602
1603 if device:
1604 hostname, username, port = device.hostname, device.username, device.port
1605 else:
1606 hostname, username, port = None, None, None
1607
1608 lsb_release = None
1609 sysroot = None
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001610 try:
Alex Klein1699fab2022-09-08 08:46:06 -06001611 # Somewhat confusing to clobber, but here we are.
1612 # pylint: disable=redefined-argument-from-local
1613 with remote_access.ChromiumOSDeviceHandler(
1614 hostname,
1615 port=port,
1616 username=username,
1617 private_key=ssh_private_key,
1618 base_dir=_DEVICE_BASE_DIR,
1619 ping=ping,
1620 ) as device:
1621 lsb_release = device.lsb_release
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001622
Alex Klein1699fab2022-09-08 08:46:06 -06001623 board = cros_build_lib.GetBoard(
1624 device_board=device.board, override_board=board
1625 )
1626 if not force and board != device.board:
1627 raise DeployError(
1628 "Device (%s) is incompatible with board %s. Use "
1629 "--force to deploy anyway." % (device.board, board)
1630 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001631
Alex Klein1699fab2022-09-08 08:46:06 -06001632 sysroot = build_target_lib.get_default_sysroot_path(board)
Andrew67b5fa72020-02-05 14:14:48 -08001633
Alex Klein975e86c2023-01-23 16:49:10 -07001634 # Don't bother trying to clean for unmerges. We won't use the local
1635 # db, and it just slows things down for the user.
Alex Klein1699fab2022-09-08 08:46:06 -06001636 if emerge and clean_binpkg:
1637 logging.notice(
1638 "Cleaning outdated binary packages from %s", sysroot
1639 )
1640 portage_util.CleanOutdatedBinaryPackages(sysroot)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001641
Alex Klein1699fab2022-09-08 08:46:06 -06001642 # Remount rootfs as writable if necessary.
1643 if not device.MountRootfsReadWrite():
1644 raise DeployError(
1645 "Cannot remount rootfs as read-write. Exiting."
1646 )
Ralph Nathane01ccf12015-04-16 10:40:32 -07001647
Alex Klein1699fab2022-09-08 08:46:06 -06001648 # Obtain list of packages to upgrade/remove.
1649 pkg_scanner = _InstallPackageScanner(sysroot)
Tim Baine4a783b2023-04-21 20:05:51 +00001650 (
1651 pkgs,
1652 listed,
1653 num_updates,
1654 pkgs_attrs,
1655 warnings_shown,
1656 ) = pkg_scanner.Run(device, root, packages, update, deep, deep_rev)
Alex Klein1699fab2022-09-08 08:46:06 -06001657 if emerge:
1658 action_str = "emerge"
1659 else:
1660 pkgs.reverse()
1661 action_str = "unmerge"
David Pursell9476bf42015-03-30 13:34:27 -07001662
Alex Klein1699fab2022-09-08 08:46:06 -06001663 if not pkgs:
1664 logging.notice("No packages to %s", action_str)
1665 return
David Pursell9476bf42015-03-30 13:34:27 -07001666
Alex Klein1699fab2022-09-08 08:46:06 -06001667 # Warn when the user installs & didn't `cros workon start`.
1668 if emerge:
1669 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(
1670 use_all=True
1671 )
1672 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1673 for package in listed:
1674 cp = package_info.SplitCPV(package).cp
1675 if cp in all_workon and cp not in worked_on_cps:
1676 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001677 "Are you intentionally deploying unmodified "
1678 "packages, or did you forget to run "
1679 "`cros workon --board=$BOARD start %s`?",
Alex Klein1699fab2022-09-08 08:46:06 -06001680 cp,
1681 )
David Pursell9476bf42015-03-30 13:34:27 -07001682
Alex Klein1699fab2022-09-08 08:46:06 -06001683 logging.notice("These are the packages to %s:", action_str)
1684 for i, pkg in enumerate(pkgs):
1685 logging.notice(
1686 "%s %d) %s", "*" if pkg in listed else " ", i + 1, pkg
1687 )
Gilad Arnolda0a98062015-07-07 08:34:27 -07001688
Alex Klein1699fab2022-09-08 08:46:06 -06001689 if dry_run or not _ConfirmDeploy(num_updates):
1690 return
David Pursell9476bf42015-03-30 13:34:27 -07001691
Tim Baine4a783b2023-04-21 20:05:51 +00001692 if (
1693 warnings_shown
1694 and not force
1695 and not _ConfirmUpdateDespiteWarnings()
1696 ):
1697 return
1698
Alex Klein1699fab2022-09-08 08:46:06 -06001699 # Select function (emerge or unmerge) and bind args.
1700 if emerge:
1701 func = functools.partial(
1702 _EmergePackages,
1703 pkgs,
1704 device,
1705 strip,
1706 sysroot,
1707 root,
1708 board,
1709 emerge_args,
1710 )
1711 else:
1712 func = functools.partial(
1713 _UnmergePackages, pkgs, device, root, pkgs_attrs
1714 )
David Pursell2e773382015-04-03 14:30:47 -07001715
Alex Klein1699fab2022-09-08 08:46:06 -06001716 # Call the function with the progress bar or with normal output.
1717 if command.UseProgressBar():
1718 op = BrilloDeployOperation(emerge)
1719 op.Run(func, log_level=logging.DEBUG)
1720 else:
1721 func()
David Pursell9476bf42015-03-30 13:34:27 -07001722
Alex Klein1699fab2022-09-08 08:46:06 -06001723 if device.IsSELinuxAvailable():
1724 if sum(x.count("selinux-policy") for x in pkgs):
1725 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001726 "Deploying SELinux policy will not take effect until "
1727 "reboot. SELinux policy is loaded by init. Also, "
1728 "changing the security contexts (labels) of a file "
1729 "will require building a new image and flashing the "
1730 "image onto the device."
Alex Klein1699fab2022-09-08 08:46:06 -06001731 )
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001732
Alex Klein1699fab2022-09-08 08:46:06 -06001733 # This message is read by BrilloDeployOperation.
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001734 logging.warning(
Alex Klein1699fab2022-09-08 08:46:06 -06001735 "Please restart any updated services on the device, "
1736 "or just reboot it."
1737 )
1738 except Exception:
1739 if lsb_release:
1740 lsb_entries = sorted(lsb_release.items())
1741 logging.info(
1742 "Following are the LSB version details of the device:\n%s",
1743 "\n".join("%s=%s" % (k, v) for k, v in lsb_entries),
1744 )
1745 raise