blob: c8d777bd75b29f3732f31f1c9ae289df0d0adf64 [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():
Matt Turnercd383d42023-08-09 21:33:50 +0000799 if not required_slot:
800 logging.debug(" Including because no required_slot")
801 elif slot == required_slot:
802 logging.debug(
803 " Including because slot (%s) == required_slot (%s)",
804 slot,
805 required_slot,
806 )
807 else:
808 logging.debug(
809 " Skipping because slot (%s) != required_slot (%s)",
810 slot,
811 required_slot,
812 )
Alex Klein1699fab2022-09-08 08:46:06 -0600813 continue
814
815 num_processed += 1
816 logging.debug(" Checking %s...", pkg_info.cpv)
817
Tim Baine4a783b2023-04-21 20:05:51 +0000818 install, update, use_mismatch = self._NeedsInstall(
Alex Klein1699fab2022-09-08 08:46:06 -0600819 pkg_info.cpv, slot, pkg_info.build_time, optional
820 )
821 if not install:
822 continue
823
824 installs[cp] = (pkg_info.cpv, slot, listed, update)
Tim Baine4a783b2023-04-21 20:05:51 +0000825 warnings_shown |= use_mismatch
Alex Klein1699fab2022-09-08 08:46:06 -0600826
827 # Add forward and backward runtime dependencies to queue.
828 if process_rdeps:
829 self._ProcessDeps(pkg_info.rdeps, False)
830 if process_rev_rdeps:
Trent Apted1e2e4f32023-05-05 03:50:20 +0000831 target_pkg_info = self.target_db.get(cp, {}).get(slot)
Alex Klein1699fab2022-09-08 08:46:06 -0600832 if target_pkg_info:
833 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
834
835 if num_processed == 0:
836 logging.warning(
837 "No qualified bintree package corresponding to %s", cp
838 )
839
Tim Baine4a783b2023-04-21 20:05:51 +0000840 return installs, warnings_shown
Alex Klein1699fab2022-09-08 08:46:06 -0600841
Tim Baine4a783b2023-04-21 20:05:51 +0000842 def _SortInstalls(self, installs: List[str]) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -0600843 """Returns a sorted list of packages to install.
844
845 Performs a topological sort based on dependencies found in the binary
846 package database.
847
848 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600849 installs: Dictionary of packages to install indexed by CP.
Alex Klein1699fab2022-09-08 08:46:06 -0600850
851 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600852 A list of package CPVs (string).
Alex Klein1699fab2022-09-08 08:46:06 -0600853
854 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600855 ValueError: If dependency graph contains a cycle.
Alex Klein1699fab2022-09-08 08:46:06 -0600856 """
857 not_visited = set(installs.keys())
858 curr_path = []
859 sorted_installs = []
860
Tim Baine4a783b2023-04-21 20:05:51 +0000861 def SortFrom(cp: str) -> None:
Alex Klein975e86c2023-01-23 16:49:10 -0700862 """Traverses deps recursively, emitting nodes in reverse order."""
Alex Klein1699fab2022-09-08 08:46:06 -0600863 cpv, slot, _, _ = installs[cp]
864 if cpv in curr_path:
865 raise ValueError(
866 "Dependencies contain a cycle: %s -> %s"
867 % (" -> ".join(curr_path[curr_path.index(cpv) :]), cpv)
868 )
869 curr_path.append(cpv)
870 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
871 if rdep_cp in not_visited:
872 not_visited.remove(rdep_cp)
873 SortFrom(rdep_cp)
874
875 sorted_installs.append(cpv)
876 curr_path.pop()
877
878 # So long as there's more packages, keep expanding dependency paths.
879 while not_visited:
880 SortFrom(not_visited.pop())
881
882 return sorted_installs
883
Tim Baine4a783b2023-04-21 20:05:51 +0000884 def _EnqListedPkg(self, pkg: str) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -0600885 """Finds and enqueues a listed package."""
886 cp, slot = self._FindPackage(pkg)
887 if cp not in self.binpkgs_db:
888 raise self.BintreeError(
889 "Package %s not found in binpkgs tree" % pkg
890 )
891 self._EnqDep((cp, slot), True, False)
892
Tim Baine4a783b2023-04-21 20:05:51 +0000893 def _EnqInstalledPkgs(self) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600894 """Enqueues all available binary packages that are already installed."""
895 for cp, cp_slots in self.binpkgs_db.items():
896 target_cp_slots = self.target_db.get(cp)
897 if target_cp_slots:
898 for slot in cp_slots.keys():
899 if slot in target_cp_slots:
900 self._EnqDep((cp, slot), True, False)
901
902 def Run(
903 self,
Tim Baine4a783b2023-04-21 20:05:51 +0000904 device: remote_access.RemoteDevice,
905 root: str,
906 listed_pkgs: List[str],
907 update: bool,
908 process_rdeps: bool,
909 process_rev_rdeps: bool,
910 ) -> Tuple[List[str], List[str], int, Dict[str, str], bool]:
Alex Klein1699fab2022-09-08 08:46:06 -0600911 """Computes the list of packages that need to be installed on a target.
912
913 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600914 device: Target handler object.
915 root: Package installation root.
916 listed_pkgs: Package names/files listed by the user.
917 update: Whether to read the target's installed package database.
918 process_rdeps: Whether to trace forward dependencies.
919 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600920
921 Returns:
Tim Baine4a783b2023-04-21 20:05:51 +0000922 A tuple (sorted, listed, num_updates, install_attrs, warnings_shown)
923 where |sorted| is a list of package CPVs (string) to install on the
924 target in an order that satisfies their inter-dependencies, |listed|
Alex Klein53cc3bf2022-10-13 08:50:01 -0600925 the subset that was requested by the user, and |num_updates|
926 the number of packages being installed over preexisting
927 versions. Note that installation order should be reversed for
928 removal, |install_attrs| is a dictionary mapping a package
Tim Baine4a783b2023-04-21 20:05:51 +0000929 CPV (string) to some of its extracted environment attributes, and
930 |warnings_shown| is a boolean indicating whether warnings were shown
931 that might require a prompt whether to continue.
Alex Klein1699fab2022-09-08 08:46:06 -0600932 """
933 if process_rev_rdeps and not process_rdeps:
934 raise ValueError(
935 "Must processing forward deps when processing rev deps"
936 )
937 if process_rdeps and not update:
938 raise ValueError(
939 "Must check installed packages when processing deps"
940 )
941
942 if update:
943 logging.info("Initializing target intalled packages database...")
944 self._InitTargetVarDB(
945 device, root, process_rdeps, process_rev_rdeps
946 )
947
948 logging.info("Initializing binary packages database...")
949 self._InitBinpkgDB(process_rdeps)
950
951 logging.info("Finding listed package(s)...")
952 self._InitDepQueue()
953 for pkg in listed_pkgs:
954 if pkg == "@installed":
955 if not update:
956 raise ValueError(
Alex Klein975e86c2023-01-23 16:49:10 -0700957 "Must check installed packages when updating all of "
958 "them."
Alex Klein1699fab2022-09-08 08:46:06 -0600959 )
960 self._EnqInstalledPkgs()
961 else:
962 self._EnqListedPkg(pkg)
963
964 logging.info("Computing set of packages to install...")
Tim Baine4a783b2023-04-21 20:05:51 +0000965 installs, warnings_shown = self._ComputeInstalls(
966 process_rdeps, process_rev_rdeps
967 )
Alex Klein1699fab2022-09-08 08:46:06 -0600968
969 num_updates = 0
970 listed_installs = []
971 for cpv, _, listed, isupdate in installs.values():
972 if listed:
973 listed_installs.append(cpv)
974 if isupdate:
975 num_updates += 1
976
977 logging.info(
978 "Processed %d package(s), %d will be installed, %d are "
979 "updating existing packages",
980 len(self.seen),
981 len(installs),
982 num_updates,
983 )
984
985 sorted_installs = self._SortInstalls(installs)
986
987 install_attrs = {}
988 for pkg in sorted_installs:
989 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
990 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
991 install_attrs[pkg] = {}
992 if dlc_id and dlc_package:
993 install_attrs[pkg][_DLC_ID] = dlc_id
994
Tim Baine4a783b2023-04-21 20:05:51 +0000995 return (
996 sorted_installs,
997 listed_installs,
998 num_updates,
999 install_attrs,
1000 warnings_shown,
1001 )
David Pursell9476bf42015-03-30 13:34:27 -07001002
1003
Tim Baine4a783b2023-04-21 20:05:51 +00001004def _Emerge(
1005 device: remote_access.RemoteDevice,
1006 pkg_paths: List[str],
1007 root: str,
1008 extra_args: List[str] = None,
1009) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -06001010 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -07001011
Alex Klein1699fab2022-09-08 08:46:06 -06001012 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001013 device: A ChromiumOSDevice object.
Mike Frysinger9fba8d02023-05-15 15:04:07 -04001014 pkg_paths: Local paths to binary packages.
Alex Klein53cc3bf2022-10-13 08:50:01 -06001015 root: Package installation root path.
1016 extra_args: Extra arguments to pass to emerge.
David Pursell9476bf42015-03-30 13:34:27 -07001017
Alex Klein1699fab2022-09-08 08:46:06 -06001018 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001019 DeployError: Unrecoverable error during emerge.
Alex Klein1699fab2022-09-08 08:46:06 -06001020 """
Mike Frysinger63d35512021-01-26 23:16:13 -05001021
Alex Klein1699fab2022-09-08 08:46:06 -06001022 def path_to_name(pkg_path):
1023 return os.path.basename(pkg_path)
Mike Frysinger63d35512021-01-26 23:16:13 -05001024
Alex Klein1699fab2022-09-08 08:46:06 -06001025 def path_to_category(pkg_path):
1026 return os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -07001027
Alex Klein1699fab2022-09-08 08:46:06 -06001028 pkg_names = ", ".join(path_to_name(x) for x in pkg_paths)
David Pursell9476bf42015-03-30 13:34:27 -07001029
Alex Klein1699fab2022-09-08 08:46:06 -06001030 pkgroot = os.path.join(device.work_dir, "packages")
1031 portage_tmpdir = os.path.join(device.work_dir, "portage-tmp")
Alex Klein975e86c2023-01-23 16:49:10 -07001032 # Clean out the dirs first if we had a previous emerge on the device so as
1033 # to free up space for this emerge. The last emerge gets implicitly cleaned
1034 # up when the device connection deletes its work_dir.
Alex Klein1699fab2022-09-08 08:46:06 -06001035 device.run(
1036 f"cd {device.work_dir} && "
1037 f"rm -rf packages portage-tmp && "
1038 f"mkdir -p portage-tmp packages && "
1039 f"cd packages && "
1040 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
1041 shell=True,
1042 remote_sudo=True,
1043 )
Mike Frysinger63d35512021-01-26 23:16:13 -05001044
Alex Klein1699fab2022-09-08 08:46:06 -06001045 logging.info("Use portage temp dir %s", portage_tmpdir)
David Pursell9476bf42015-03-30 13:34:27 -07001046
Mike Frysinger63d35512021-01-26 23:16:13 -05001047 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001048 logging.notice("Copying binpkgs to device.")
1049 for pkg_path in pkg_paths:
1050 pkg_name = path_to_name(pkg_path)
1051 logging.info("Copying %s", pkg_name)
1052 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
1053 device.CopyToDevice(
1054 pkg_path, pkg_dir, mode="rsync", remote_sudo=True, compress=False
1055 )
1056
1057 # This message is read by BrilloDeployOperation.
1058 logging.notice("Installing: %s", pkg_names)
1059
1060 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
1061 # chromeos-base packages will be skipped due to the configuration
1062 # in /etc/protage/make.profile/package.provided. However, there is
1063 # a known bug that /usr/local/etc/portage is not setup properly
1064 # (crbug.com/312041). This does not affect `cros deploy` because
1065 # we do not use the preset PKGDIR.
1066 extra_env = {
1067 "FEATURES": "-sandbox",
1068 "PKGDIR": pkgroot,
1069 "PORTAGE_CONFIGROOT": "/usr/local",
1070 "PORTAGE_TMPDIR": portage_tmpdir,
1071 "PORTDIR": device.work_dir,
1072 "CONFIG_PROTECT": "-*",
1073 }
1074
Alex Klein975e86c2023-01-23 16:49:10 -07001075 # --ignore-built-slot-operator-deps because we don't rebuild everything. It
1076 # can cause errors, but that's expected with cros deploy since it's just a
Alex Klein1699fab2022-09-08 08:46:06 -06001077 # best effort to prevent developers avoid rebuilding an image every time.
1078 cmd = [
1079 "emerge",
1080 "--usepkg",
1081 "--ignore-built-slot-operator-deps=y",
1082 "--root",
1083 root,
1084 ] + [os.path.join(pkgroot, *x.split("/")[-2:]) for x in pkg_paths]
1085 if extra_args:
1086 cmd.append(extra_args)
1087
1088 logging.warning(
1089 "Ignoring slot dependencies! This may break things! e.g. "
1090 "packages built against the old version may not be able to "
1091 "load the new .so. This is expected, and you will just need "
1092 "to build and flash a new image if you have problems."
1093 )
1094 try:
1095 result = device.run(
1096 cmd,
1097 extra_env=extra_env,
1098 remote_sudo=True,
1099 capture_output=True,
1100 debug_level=logging.INFO,
1101 )
1102
1103 pattern = (
1104 "A requested package will not be merged because "
1105 "it is listed in package.provided"
1106 )
1107 output = result.stderr.replace("\n", " ").replace("\r", "")
1108 if pattern in output:
1109 error = (
1110 "Package failed to emerge: %s\n"
1111 "Remove %s from /etc/portage/make.profile/"
1112 "package.provided/chromeos-base.packages\n"
1113 "(also see crbug.com/920140 for more context)\n"
1114 % (pattern, pkg_name)
1115 )
1116 cros_build_lib.Die(error)
1117 except Exception:
1118 logging.error("Failed to emerge packages %s", pkg_names)
1119 raise
1120 else:
1121 # This message is read by BrilloDeployOperation.
1122 logging.notice("Packages have been installed.")
David Pursell9476bf42015-03-30 13:34:27 -07001123
1124
Tim Baine4a783b2023-04-21 20:05:51 +00001125def _RestoreSELinuxContext(
1126 device: remote_access.RemoteDevice, pkgpath: str, root: str
1127) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001128 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +09001129
Alex Klein1699fab2022-09-08 08:46:06 -06001130 This reads the tarball from pkgpath, and calls restorecon on device to
Alex Klein975e86c2023-01-23 16:49:10 -07001131 restore SELinux context for files listed in the tarball, assuming those
1132 files are installed to /
Qijiang Fan8a945032019-04-25 20:53:29 +09001133
Alex Klein1699fab2022-09-08 08:46:06 -06001134 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001135 device: a ChromiumOSDevice object
1136 pkgpath: path to tarball
1137 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001138 """
1139 pkgroot = os.path.join(device.work_dir, "packages")
1140 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
1141 pkgpath_device = os.path.join(
1142 pkgroot, pkg_dirname, os.path.basename(pkgpath)
1143 )
1144 # Testing shows restorecon splits on newlines instead of spaces.
1145 device.run(
1146 [
1147 "cd",
1148 root,
1149 "&&",
1150 "tar",
1151 "tf",
1152 pkgpath_device,
1153 "|",
1154 "restorecon",
1155 "-i",
1156 "-f",
1157 "-",
1158 ],
1159 remote_sudo=True,
1160 )
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001161
1162
Tim Baine4a783b2023-04-21 20:05:51 +00001163def _GetPackagesByCPV(
1164 cpvs: List[package_info.CPV], strip: bool, sysroot: str
1165) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001166 """Returns paths to binary packages corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001167
Alex Klein1699fab2022-09-08 08:46:06 -06001168 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001169 cpvs: List of CPV components given by package_info.SplitCPV().
1170 strip: True to run strip_package.
1171 sysroot: Sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001172
Alex Klein1699fab2022-09-08 08:46:06 -06001173 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001174 List of paths corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001175
Alex Klein1699fab2022-09-08 08:46:06 -06001176 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001177 DeployError: If a package is missing.
Alex Klein1699fab2022-09-08 08:46:06 -06001178 """
1179 packages_dir = None
1180 if strip:
1181 try:
1182 cros_build_lib.run(
1183 [
Mike Frysinger5429f302023-03-27 15:48:52 -04001184 constants.CHROMITE_SCRIPTS_DIR / "strip_package",
Alex Klein1699fab2022-09-08 08:46:06 -06001185 "--sysroot",
1186 sysroot,
1187 ]
1188 + [cpv.cpf for cpv in cpvs]
1189 )
1190 packages_dir = _STRIPPED_PACKAGES_DIR
1191 except cros_build_lib.RunCommandError:
1192 logging.error(
1193 "Cannot strip packages %s", " ".join([str(cpv) for cpv in cpvs])
1194 )
1195 raise
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001196
Alex Klein1699fab2022-09-08 08:46:06 -06001197 paths = []
1198 for cpv in cpvs:
1199 path = portage_util.GetBinaryPackagePath(
1200 cpv.category,
1201 cpv.package,
1202 cpv.version,
1203 sysroot=sysroot,
1204 packages_dir=packages_dir,
1205 )
1206 if not path:
1207 raise DeployError("Missing package %s." % cpv)
1208 paths.append(path)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001209
Alex Klein1699fab2022-09-08 08:46:06 -06001210 return paths
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001211
1212
Tim Baine4a783b2023-04-21 20:05:51 +00001213def _GetPackagesPaths(pkgs: List[str], strip: bool, sysroot: str) -> List[str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001214 """Returns paths to binary |pkgs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001215
Alex Klein1699fab2022-09-08 08:46:06 -06001216 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001217 pkgs: List of package CPVs string.
1218 strip: Whether or not to run strip_package for CPV packages.
1219 sysroot: The sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001220
Alex Klein1699fab2022-09-08 08:46:06 -06001221 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001222 List of paths corresponding to |pkgs|.
Alex Klein1699fab2022-09-08 08:46:06 -06001223 """
1224 cpvs = [package_info.SplitCPV(p) for p in pkgs]
1225 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001226
1227
Tim Baine4a783b2023-04-21 20:05:51 +00001228def _Unmerge(
1229 device: remote_access.RemoteDevice, pkgs: List[str], root: str
1230) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001231 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -07001232
Alex Klein1699fab2022-09-08 08:46:06 -06001233 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001234 device: A RemoteDevice object.
1235 pkgs: Package names.
1236 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001237 """
1238 pkg_names = ", ".join(os.path.basename(x) for x in pkgs)
Mike Frysinger22bb5502021-01-29 13:05:46 -05001239 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001240 logging.notice("Unmerging %s.", pkg_names)
1241 cmd = ["qmerge", "--yes"]
1242 # Check if qmerge is available on the device. If not, use emerge.
1243 if device.run(["qmerge", "--version"], check=False).returncode != 0:
1244 cmd = ["emerge"]
1245
1246 cmd += ["--unmerge", "--root", root]
1247 cmd.extend("f={x}" for x in pkgs)
1248 try:
1249 # Always showing the emerge output for clarity.
1250 device.run(
1251 cmd,
1252 capture_output=False,
1253 remote_sudo=True,
1254 debug_level=logging.INFO,
1255 )
1256 except Exception:
1257 logging.error("Failed to unmerge packages %s", pkg_names)
1258 raise
1259 else:
1260 # This message is read by BrilloDeployOperation.
1261 logging.notice("Packages have been uninstalled.")
David Pursell9476bf42015-03-30 13:34:27 -07001262
1263
Tim Baine4a783b2023-04-21 20:05:51 +00001264def _ConfirmDeploy(num_updates: int) -> bool:
Alex Klein1699fab2022-09-08 08:46:06 -06001265 """Returns whether we can continue deployment."""
1266 if num_updates > _MAX_UPDATES_NUM:
1267 logging.warning(_MAX_UPDATES_WARNING)
1268 return cros_build_lib.BooleanPrompt(default=False)
David Pursell9476bf42015-03-30 13:34:27 -07001269
Alex Klein1699fab2022-09-08 08:46:06 -06001270 return True
David Pursell9476bf42015-03-30 13:34:27 -07001271
1272
Tim Baine4a783b2023-04-21 20:05:51 +00001273def _ConfirmUpdateDespiteWarnings() -> bool:
1274 """Returns whether we can continue updating despite warnings."""
1275 logging.warning("Continue despite prior warnings?")
1276 return cros_build_lib.BooleanPrompt(default=False)
1277
1278
1279def _EmergePackages(
1280 pkgs: List[str],
1281 device: remote_access.RemoteDevice,
1282 strip: bool,
1283 sysroot: str,
1284 root: str,
1285 board: str,
1286 emerge_args: List[str],
1287) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001288 """Call _Emerge for each package in pkgs."""
Ben Pastene5f03b052019-08-12 18:03:24 -07001289 if device.IsSELinuxAvailable():
Alex Klein1699fab2022-09-08 08:46:06 -06001290 enforced = device.IsSELinuxEnforced()
1291 if enforced:
1292 device.run(["setenforce", "0"])
1293 else:
1294 enforced = False
Andrewc7e1c6b2020-02-27 16:03:53 -08001295
Alex Klein1699fab2022-09-08 08:46:06 -06001296 dlc_deployed = False
1297 # This message is read by BrilloDeployOperation.
1298 logging.info("Preparing local packages for transfer.")
1299 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
1300 # Install all the packages in one pass so inter-package blockers work.
1301 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
1302 logging.info("Updating SELinux settings & DLC images.")
1303 for pkg_path in pkg_paths:
1304 if device.IsSELinuxAvailable():
1305 _RestoreSELinuxContext(device, pkg_path, root)
Mike Frysinger5f4c2742021-02-08 14:37:23 -05001306
Alex Klein1699fab2022-09-08 08:46:06 -06001307 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
1308 if dlc_id and dlc_package:
1309 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
1310 dlc_deployed = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001311
Alex Klein1699fab2022-09-08 08:46:06 -06001312 if dlc_deployed:
1313 # Clean up empty directories created by emerging DLCs.
1314 device.run(
1315 [
1316 "test",
1317 "-d",
1318 "/build/rootfs",
1319 "&&",
1320 "rmdir",
1321 "--ignore-fail-on-non-empty",
1322 "/build/rootfs",
1323 "/build",
1324 ],
1325 check=False,
1326 )
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001327
Alex Klein1699fab2022-09-08 08:46:06 -06001328 if enforced:
1329 device.run(["setenforce", "1"])
1330
1331 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1332 # we installed new DLC images).
1333 if dlc_deployed:
1334 device.run(["restart", "dlcservice"])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001335
1336
Tim Baine4a783b2023-04-21 20:05:51 +00001337def _UnmergePackages(
1338 pkgs: List[str],
1339 device: remote_access.RemoteDevice,
1340 root: str,
1341 pkgs_attrs: Dict[str, List[str]],
1342) -> str:
Alex Klein1699fab2022-09-08 08:46:06 -06001343 """Call _Unmege for each package in pkgs."""
1344 dlc_uninstalled = False
1345 _Unmerge(device, pkgs, root)
1346 logging.info("Cleaning up DLC images.")
1347 for pkg in pkgs:
1348 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1349 dlc_uninstalled = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001350
Alex Klein1699fab2022-09-08 08:46:06 -06001351 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1352 # uninstalled DLC images).
1353 if dlc_uninstalled:
1354 device.run(["restart", "dlcservice"])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001355
1356
Tim Baine4a783b2023-04-21 20:05:51 +00001357def _UninstallDLCImage(
1358 device: remote_access.RemoteDevice, pkg_attrs: Dict[str, List[str]]
1359):
Alex Klein1699fab2022-09-08 08:46:06 -06001360 """Uninstall a DLC image."""
1361 if _DLC_ID in pkg_attrs:
1362 dlc_id = pkg_attrs[_DLC_ID]
1363 logging.notice("Uninstalling DLC image for %s", dlc_id)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001364
Alex Klein1699fab2022-09-08 08:46:06 -06001365 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1366 return True
1367 else:
1368 logging.debug("DLC_ID not found in package")
1369 return False
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001370
1371
Tim Baine4a783b2023-04-21 20:05:51 +00001372def _DeployDLCImage(
1373 device: remote_access.RemoteDevice,
1374 sysroot: str,
1375 board: str,
1376 dlc_id: str,
1377 dlc_package: str,
1378):
Alex Klein1699fab2022-09-08 08:46:06 -06001379 """Deploy (install and mount) a DLC image.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001380
Alex Klein1699fab2022-09-08 08:46:06 -06001381 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001382 device: A device object.
1383 sysroot: The sysroot path.
1384 board: Board to use.
1385 dlc_id: The DLC ID.
1386 dlc_package: The DLC package name.
Alex Klein1699fab2022-09-08 08:46:06 -06001387 """
1388 # Requires `sudo_rm` because installations of files are running with sudo.
1389 with osutils.TempDir(sudo_rm=True) as tempdir:
1390 temp_rootfs = Path(tempdir)
1391 # Build the DLC image if the image is outdated or doesn't exist.
1392 dlc_lib.InstallDlcImages(
1393 sysroot=sysroot, rootfs=temp_rootfs, dlc_id=dlc_id, board=board
1394 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001395
Alex Klein1699fab2022-09-08 08:46:06 -06001396 logging.debug("Uninstall DLC %s if it is installed.", dlc_id)
1397 try:
1398 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1399 except cros_build_lib.RunCommandError as e:
1400 logging.info(
1401 "Failed to uninstall DLC:%s. Continue anyway.", e.stderr
1402 )
1403 except Exception:
1404 logging.error("Failed to uninstall DLC.")
1405 raise
Andrewc7e1c6b2020-02-27 16:03:53 -08001406
Yuanpeng Niaba30342023-07-11 14:24:09 -07001407 src_dlc_dir = os.path.join(
Alex Klein1699fab2022-09-08 08:46:06 -06001408 sysroot,
1409 dlc_lib.DLC_BUILD_DIR,
1410 dlc_id,
Alex Klein1699fab2022-09-08 08:46:06 -06001411 )
Yuanpeng Niaba30342023-07-11 14:24:09 -07001412 if not os.path.exists(src_dlc_dir):
1413 src_dlc_dir = os.path.join(
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001414 sysroot,
1415 dlc_lib.DLC_BUILD_DIR_SCALED,
1416 dlc_id,
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001417 )
1418
Yuanpeng Niaba30342023-07-11 14:24:09 -07001419 # Deploy the metadata entry to compressed metadata on device.
1420 logging.notice("Setting the DLC metadata for %s", dlc_id)
1421 metadata = dlc_lib.DlcMetadata.LoadSrcMetadata(src_dlc_dir)
1422 device.run(
1423 [dlc_lib.DLC_METADATA_UTIL, "--set", f"--id={dlc_id}"],
1424 input=json.dumps(metadata),
1425 check=False,
1426 )
1427
1428 logging.notice("Deploy the DLC image for %s", dlc_id)
1429 dlc_img_path_src = os.path.join(
1430 src_dlc_dir,
1431 dlc_package,
1432 dlc_lib.DLC_IMAGE,
1433 )
1434
Alex Klein1699fab2022-09-08 08:46:06 -06001435 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1436 dlc_img_path_a = os.path.join(dlc_img_path, "dlc_a")
1437 dlc_img_path_b = os.path.join(dlc_img_path, "dlc_b")
1438 # Create directories for DLC images.
Mike Frysinger445f6bf2023-06-27 11:43:33 -04001439 device.mkdir([dlc_img_path_a, dlc_img_path_b])
Alex Klein1699fab2022-09-08 08:46:06 -06001440 # Copy images to the destination directories.
1441 device.CopyToDevice(
1442 dlc_img_path_src,
1443 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1444 mode="rsync",
1445 )
1446 device.run(
1447 [
1448 "cp",
1449 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1450 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE),
1451 ]
1452 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001453
Alex Klein1699fab2022-09-08 08:46:06 -06001454 # Set the proper perms and ownership so dlcservice can access the image.
1455 device.run(["chmod", "-R", "u+rwX,go+rX,go-w", _DLC_INSTALL_ROOT])
1456 device.run(["chown", "-R", "dlcservice:dlcservice", _DLC_INSTALL_ROOT])
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001457
Alex Klein1699fab2022-09-08 08:46:06 -06001458 # Copy metadata to device.
Yuanpeng Niaba30342023-07-11 14:24:09 -07001459 # TODO(b/290961240): To be removed once the transition to compressed
1460 # metadata is complete.
Alex Klein1699fab2022-09-08 08:46:06 -06001461 dest_meta_dir = Path("/") / dlc_lib.DLC_META_DIR / dlc_id / dlc_package
Mike Frysinger445f6bf2023-06-27 11:43:33 -04001462 device.mkdir(dest_meta_dir)
Alex Klein1699fab2022-09-08 08:46:06 -06001463 src_meta_dir = os.path.join(
Yuanpeng Niaba30342023-07-11 14:24:09 -07001464 src_dlc_dir,
Alex Klein1699fab2022-09-08 08:46:06 -06001465 dlc_package,
1466 dlc_lib.DLC_TMP_META_DIR,
1467 )
1468 device.CopyToDevice(
1469 src_meta_dir + "/",
1470 dest_meta_dir,
1471 mode="rsync",
1472 recursive=True,
1473 remote_sudo=True,
1474 )
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001475
Alex Klein1699fab2022-09-08 08:46:06 -06001476 # TODO(kimjae): Make this generic so it recomputes all the DLCs + copies
Alex Klein975e86c2023-01-23 16:49:10 -07001477 # over a fresh list of dm-verity digests instead of appending and
1478 # keeping the stale digests when developers are testing.
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001479
Alex Klein1699fab2022-09-08 08:46:06 -06001480 # Copy the LoadPin dm-verity digests to device.
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001481 _DeployDLCLoadPin(temp_rootfs, device)
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001482
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001483
1484def _DeployDLCLoadPin(
Tim Baine4a783b2023-04-21 20:05:51 +00001485 rootfs: os.PathLike, device: remote_access.RemoteDevice
1486) -> None:
Jae Hoon Kimdf220852023-04-14 19:20:13 +00001487 """Deploy DLC LoadPin from temp rootfs to device.
1488
1489 Args:
1490 rootfs: Path to rootfs.
1491 device: A device object.
1492 """
1493 loadpin = dlc_lib.DLC_LOADPIN_TRUSTED_VERITY_DIGESTS
1494 dst_loadpin = Path("/") / dlc_lib.DLC_META_DIR / loadpin
1495 src_loadpin = rootfs / dlc_lib.DLC_META_DIR / loadpin
1496 if src_loadpin.exists():
1497 digests = set(osutils.ReadFile(src_loadpin).splitlines())
1498 digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1499 try:
1500 device_digests = set(device.CatFile(dst_loadpin).splitlines())
1501 device_digests.discard(dlc_lib.DLC_LOADPIN_FILE_HEADER)
1502 digests.update(device_digests)
1503 except remote_access.CatFileError:
1504 pass
1505
1506 with tempfile.NamedTemporaryFile(dir=rootfs) as f:
1507 osutils.WriteFile(f.name, dlc_lib.DLC_LOADPIN_FILE_HEADER + "\n")
1508 osutils.WriteFile(f.name, "\n".join(digests) + "\n", mode="a")
1509 device.CopyToDevice(
1510 f.name, dst_loadpin, mode="rsync", remote_sudo=True
1511 )
Andrew67b5fa72020-02-05 14:14:48 -08001512
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001513
Tim Baine4a783b2023-04-21 20:05:51 +00001514def _GetDLCInfo(
1515 device: remote_access.RemoteDevice, pkg_path: str, from_dut: bool
1516) -> Tuple[str, str]:
Alex Klein1699fab2022-09-08 08:46:06 -06001517 """Returns information of a DLC given its package path.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001518
Alex Klein1699fab2022-09-08 08:46:06 -06001519 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001520 device: commandline.Device object; None to use the default device.
1521 pkg_path: path to the package.
1522 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1523 info from host.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001524
Alex Klein1699fab2022-09-08 08:46:06 -06001525 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001526 A tuple (dlc_id, dlc_package).
Alex Klein1699fab2022-09-08 08:46:06 -06001527 """
1528 environment_content = ""
1529 if from_dut:
1530 # On DUT, |pkg_path| is the directory which contains environment file.
1531 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1532 try:
1533 environment_data = device.CatFile(
1534 environment_path, max_size=None, encoding=None
1535 )
1536 except remote_access.CatFileError:
1537 # The package is not installed on DUT yet. Skip extracting info.
1538 return None, None
1539 else:
1540 # On host, pkg_path is tbz2 file which contains environment file.
1541 # Extract the metadata of the package file.
1542 data = portage.xpak.tbz2(pkg_path).get_data()
1543 environment_data = data[_ENVIRONMENT_FILENAME.encode("utf-8")]
1544
1545 # Extract the environment metadata.
1546 environment_content = bz2.decompress(environment_data)
1547
1548 with tempfile.NamedTemporaryFile() as f:
1549 # Dumps content into a file so we can use osutils.SourceEnvironment.
1550 path = os.path.realpath(f.name)
1551 osutils.WriteFile(path, environment_content, mode="wb")
1552 content = osutils.SourceEnvironment(
1553 path, (_DLC_ID, _DLC_PACKAGE, _DLC_ENABLED)
1554 )
1555
1556 dlc_enabled = content.get(_DLC_ENABLED)
1557 if dlc_enabled is not None and (
1558 dlc_enabled is False or str(dlc_enabled) == "false"
1559 ):
1560 logging.info("Installing DLC in rootfs.")
1561 return None, None
1562 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
1563
1564
1565def Deploy(
Tim Baine4a783b2023-04-21 20:05:51 +00001566 device: remote_access.RemoteDevice,
1567 packages: List[str],
1568 board: str = None,
1569 emerge: bool = True,
1570 update: bool = False,
1571 deep: bool = False,
1572 deep_rev: bool = False,
1573 clean_binpkg: bool = True,
1574 root: str = "/",
1575 strip: bool = True,
1576 emerge_args: List[str] = None,
1577 ssh_private_key: str = None,
1578 ping: bool = True,
1579 force: bool = False,
1580 dry_run: bool = False,
1581) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -06001582 """Deploys packages to a device.
1583
1584 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001585 device: commandline.Device object; None to use the default device.
1586 packages: List of packages (strings) to deploy to device.
1587 board: Board to use; None to automatically detect.
1588 emerge: True to emerge package, False to unmerge.
1589 update: Check installed version on device.
1590 deep: Install dependencies also. Implies |update|.
1591 deep_rev: Install reverse dependencies. Implies |deep|.
1592 clean_binpkg: Clean outdated binary packages.
1593 root: Package installation root path.
1594 strip: Run strip_package to filter out preset paths in the package.
1595 emerge_args: Extra arguments to pass to emerge.
1596 ssh_private_key: Path to an SSH private key file; None to use test keys.
1597 ping: True to ping the device before trying to connect.
1598 force: Ignore confidence checks and prompts.
1599 dry_run: Print deployment plan but do not deploy anything.
Alex Klein1699fab2022-09-08 08:46:06 -06001600
1601 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001602 ValueError: Invalid parameter or parameter combination.
1603 DeployError: Unrecoverable failure during deploy.
Alex Klein1699fab2022-09-08 08:46:06 -06001604 """
1605 if deep_rev:
1606 deep = True
1607 if deep:
1608 update = True
1609
1610 if not packages:
1611 raise DeployError("No packages provided, nothing to deploy.")
1612
1613 if update and not emerge:
1614 raise ValueError("Cannot update and unmerge.")
1615
1616 if device:
1617 hostname, username, port = device.hostname, device.username, device.port
1618 else:
1619 hostname, username, port = None, None, None
1620
1621 lsb_release = None
1622 sysroot = None
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001623 try:
Alex Klein1699fab2022-09-08 08:46:06 -06001624 # Somewhat confusing to clobber, but here we are.
1625 # pylint: disable=redefined-argument-from-local
1626 with remote_access.ChromiumOSDeviceHandler(
1627 hostname,
1628 port=port,
1629 username=username,
1630 private_key=ssh_private_key,
1631 base_dir=_DEVICE_BASE_DIR,
1632 ping=ping,
1633 ) as device:
1634 lsb_release = device.lsb_release
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001635
Alex Klein1699fab2022-09-08 08:46:06 -06001636 board = cros_build_lib.GetBoard(
1637 device_board=device.board, override_board=board
1638 )
1639 if not force and board != device.board:
1640 raise DeployError(
1641 "Device (%s) is incompatible with board %s. Use "
1642 "--force to deploy anyway." % (device.board, board)
1643 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001644
Alex Klein1699fab2022-09-08 08:46:06 -06001645 sysroot = build_target_lib.get_default_sysroot_path(board)
Andrew67b5fa72020-02-05 14:14:48 -08001646
Alex Klein975e86c2023-01-23 16:49:10 -07001647 # Don't bother trying to clean for unmerges. We won't use the local
1648 # db, and it just slows things down for the user.
Alex Klein1699fab2022-09-08 08:46:06 -06001649 if emerge and clean_binpkg:
1650 logging.notice(
1651 "Cleaning outdated binary packages from %s", sysroot
1652 )
1653 portage_util.CleanOutdatedBinaryPackages(sysroot)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001654
Alex Klein1699fab2022-09-08 08:46:06 -06001655 # Remount rootfs as writable if necessary.
1656 if not device.MountRootfsReadWrite():
1657 raise DeployError(
1658 "Cannot remount rootfs as read-write. Exiting."
1659 )
Ralph Nathane01ccf12015-04-16 10:40:32 -07001660
Alex Klein1699fab2022-09-08 08:46:06 -06001661 # Obtain list of packages to upgrade/remove.
1662 pkg_scanner = _InstallPackageScanner(sysroot)
Tim Baine4a783b2023-04-21 20:05:51 +00001663 (
1664 pkgs,
1665 listed,
1666 num_updates,
1667 pkgs_attrs,
1668 warnings_shown,
1669 ) = pkg_scanner.Run(device, root, packages, update, deep, deep_rev)
Alex Klein1699fab2022-09-08 08:46:06 -06001670 if emerge:
1671 action_str = "emerge"
1672 else:
1673 pkgs.reverse()
1674 action_str = "unmerge"
David Pursell9476bf42015-03-30 13:34:27 -07001675
Alex Klein1699fab2022-09-08 08:46:06 -06001676 if not pkgs:
1677 logging.notice("No packages to %s", action_str)
1678 return
David Pursell9476bf42015-03-30 13:34:27 -07001679
Alex Klein1699fab2022-09-08 08:46:06 -06001680 # Warn when the user installs & didn't `cros workon start`.
1681 if emerge:
1682 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(
1683 use_all=True
1684 )
1685 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1686 for package in listed:
1687 cp = package_info.SplitCPV(package).cp
1688 if cp in all_workon and cp not in worked_on_cps:
1689 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001690 "Are you intentionally deploying unmodified "
1691 "packages, or did you forget to run "
1692 "`cros workon --board=$BOARD start %s`?",
Alex Klein1699fab2022-09-08 08:46:06 -06001693 cp,
1694 )
David Pursell9476bf42015-03-30 13:34:27 -07001695
Alex Klein1699fab2022-09-08 08:46:06 -06001696 logging.notice("These are the packages to %s:", action_str)
1697 for i, pkg in enumerate(pkgs):
1698 logging.notice(
1699 "%s %d) %s", "*" if pkg in listed else " ", i + 1, pkg
1700 )
Gilad Arnolda0a98062015-07-07 08:34:27 -07001701
Alex Klein1699fab2022-09-08 08:46:06 -06001702 if dry_run or not _ConfirmDeploy(num_updates):
1703 return
David Pursell9476bf42015-03-30 13:34:27 -07001704
Tim Baine4a783b2023-04-21 20:05:51 +00001705 if (
1706 warnings_shown
1707 and not force
1708 and not _ConfirmUpdateDespiteWarnings()
1709 ):
1710 return
1711
Alex Klein1699fab2022-09-08 08:46:06 -06001712 # Select function (emerge or unmerge) and bind args.
1713 if emerge:
1714 func = functools.partial(
1715 _EmergePackages,
1716 pkgs,
1717 device,
1718 strip,
1719 sysroot,
1720 root,
1721 board,
1722 emerge_args,
1723 )
1724 else:
1725 func = functools.partial(
1726 _UnmergePackages, pkgs, device, root, pkgs_attrs
1727 )
David Pursell2e773382015-04-03 14:30:47 -07001728
Alex Klein1699fab2022-09-08 08:46:06 -06001729 # Call the function with the progress bar or with normal output.
1730 if command.UseProgressBar():
1731 op = BrilloDeployOperation(emerge)
1732 op.Run(func, log_level=logging.DEBUG)
1733 else:
1734 func()
David Pursell9476bf42015-03-30 13:34:27 -07001735
Alex Klein1699fab2022-09-08 08:46:06 -06001736 if device.IsSELinuxAvailable():
1737 if sum(x.count("selinux-policy") for x in pkgs):
1738 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001739 "Deploying SELinux policy will not take effect until "
1740 "reboot. SELinux policy is loaded by init. Also, "
1741 "changing the security contexts (labels) of a file "
1742 "will require building a new image and flashing the "
1743 "image onto the device."
Alex Klein1699fab2022-09-08 08:46:06 -06001744 )
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001745
Alex Klein1699fab2022-09-08 08:46:06 -06001746 # This message is read by BrilloDeployOperation.
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001747 logging.warning(
Alex Klein1699fab2022-09-08 08:46:06 -06001748 "Please restart any updated services on the device, "
1749 "or just reboot it."
1750 )
1751 except Exception:
1752 if lsb_release:
1753 lsb_entries = sorted(lsb_release.items())
1754 logging.info(
1755 "Following are the LSB version details of the device:\n%s",
1756 "\n".join("%s=%s" % (k, v) for k, v in lsb_entries),
1757 )
1758 raise