blob: 2b96d7a8c5de15d8ab6d06dea4c04525cf4fc91a [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2015 The ChromiumOS Authors
David Pursell9476bf42015-03-30 13:34:27 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Alex Kleinaaddc932020-01-30 15:02:24 -07005"""Deploy packages onto a target device.
6
7Integration tests for this file can be found at cli/cros/tests/cros_vm_tests.py.
8See that file for more information.
9"""
David Pursell9476bf42015-03-30 13:34:27 -070010
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040011from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -070012
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070013import bz2
David Pursell9476bf42015-03-30 13:34:27 -070014import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070015import functools
David Pursell9476bf42015-03-30 13:34:27 -070016import json
Chris McDonald14ac61d2021-07-21 11:49:56 -060017import logging
David Pursell9476bf42015-03-30 13:34:27 -070018import os
Jae Hoon Kim2376e142022-09-03 00:18:58 +000019from pathlib import Path
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070020import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070021
Ralph Nathane01ccf12015-04-16 10:40:32 -070022from chromite.cli import command
Mike Frysinger06a51c82021-04-06 11:39:17 -040023from chromite.lib import build_target_lib
Ram Chandrasekar56152ec2021-11-22 17:10:41 +000024from chromite.lib import constants
David Pursell9476bf42015-03-30 13:34:27 -070025from chromite.lib import cros_build_lib
Alex Klein18a60af2020-06-11 12:08:47 -060026from chromite.lib import dlc_lib
Ralph Nathane01ccf12015-04-16 10:40:32 -070027from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070028from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070029from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070030from chromite.lib import remote_access
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +090031from chromite.lib import workon_helper
Alex Klein18a60af2020-06-11 12:08:47 -060032from chromite.lib.parser import package_info
33
Chris McDonald14ac61d2021-07-21 11:49:56 -060034
David Pursell9476bf42015-03-30 13:34:27 -070035try:
Alex Klein1699fab2022-09-08 08:46:06 -060036 import portage
David Pursell9476bf42015-03-30 13:34:27 -070037except ImportError:
Alex Klein1699fab2022-09-08 08:46:06 -060038 if cros_build_lib.IsInsideChroot():
39 raise
David Pursell9476bf42015-03-30 13:34:27 -070040
41
Alex Klein1699fab2022-09-08 08:46:06 -060042_DEVICE_BASE_DIR = "/usr/local/tmp/cros-deploy"
David Pursell9476bf42015-03-30 13:34:27 -070043# This is defined in src/platform/dev/builder.py
Alex Klein1699fab2022-09-08 08:46:06 -060044_STRIPPED_PACKAGES_DIR = "stripped-packages"
David Pursell9476bf42015-03-30 13:34:27 -070045
46_MAX_UPDATES_NUM = 10
47_MAX_UPDATES_WARNING = (
Alex Klein1699fab2022-09-08 08:46:06 -060048 "You are about to update a large number of installed packages, which "
49 "might take a long time, fail midway, or leave the target in an "
50 "inconsistent state. It is highly recommended that you flash a new image "
51 "instead."
52)
David Pursell9476bf42015-03-30 13:34:27 -070053
Alex Klein1699fab2022-09-08 08:46:06 -060054_DLC_ID = "DLC_ID"
55_DLC_PACKAGE = "DLC_PACKAGE"
56_DLC_ENABLED = "DLC_ENABLED"
57_ENVIRONMENT_FILENAME = "environment.bz2"
58_DLC_INSTALL_ROOT = "/var/cache/dlc"
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070059
David Pursell9476bf42015-03-30 13:34:27 -070060
61class DeployError(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060062 """Thrown when an unrecoverable error is encountered during deploy."""
David Pursell9476bf42015-03-30 13:34:27 -070063
64
Ralph Nathane01ccf12015-04-16 10:40:32 -070065class BrilloDeployOperation(operation.ProgressBarOperation):
Alex Klein1699fab2022-09-08 08:46:06 -060066 """ProgressBarOperation specific for brillo deploy."""
Ralph Nathane01ccf12015-04-16 10:40:32 -070067
Alex Klein1699fab2022-09-08 08:46:06 -060068 # These two variables are used to validate the output in the VM integration
69 # tests. Changes to the output must be reflected here.
70 MERGE_EVENTS = (
71 "Preparing local packages",
72 "NOTICE: Copying binpkgs",
73 "NOTICE: Installing",
74 "been installed.",
75 "Please restart any updated",
76 )
77 UNMERGE_EVENTS = (
78 "NOTICE: Unmerging",
79 "been uninstalled.",
80 "Please restart any updated",
81 )
Ralph Nathane01ccf12015-04-16 10:40:32 -070082
Alex Klein1699fab2022-09-08 08:46:06 -060083 def __init__(self, emerge):
84 """Construct BrilloDeployOperation object.
Ralph Nathane01ccf12015-04-16 10:40:32 -070085
Alex Klein1699fab2022-09-08 08:46:06 -060086 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -060087 emerge: True if emerge, False is unmerge.
Alex Klein1699fab2022-09-08 08:46:06 -060088 """
89 super().__init__()
90 if emerge:
91 self._events = self.MERGE_EVENTS
92 else:
93 self._events = self.UNMERGE_EVENTS
94 self._total = len(self._events)
95 self._completed = 0
96
97 def ParseOutput(self, output=None):
98 """Parse the output of brillo deploy to update a progress bar."""
99 stdout = self._stdout.read()
100 stderr = self._stderr.read()
101 output = stdout + stderr
102 for event in self._events:
103 self._completed += output.count(event)
104 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700105
106
David Pursell9476bf42015-03-30 13:34:27 -0700107class _InstallPackageScanner(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600108 """Finds packages that need to be installed on a target device.
David Pursell9476bf42015-03-30 13:34:27 -0700109
Alex Klein1699fab2022-09-08 08:46:06 -0600110 Scans the sysroot bintree, beginning with a user-provided list of packages,
111 to find all packages that need to be installed. If so instructed,
112 transitively scans forward (mandatory) and backward (optional) dependencies
113 as well. A package will be installed if missing on the target (mandatory
114 packages only), or it will be updated if its sysroot version and build time
115 are different from the target. Common usage:
David Pursell9476bf42015-03-30 13:34:27 -0700116
Alex Klein53cc3bf2022-10-13 08:50:01 -0600117 pkg_scanner = _InstallPackageScanner(sysroot)
118 pkgs = pkg_scanner.Run(...)
Alex Klein1699fab2022-09-08 08:46:06 -0600119 """
David Pursell9476bf42015-03-30 13:34:27 -0700120
Alex Klein1699fab2022-09-08 08:46:06 -0600121 class VartreeError(Exception):
122 """An error in the processing of the installed packages tree."""
David Pursell9476bf42015-03-30 13:34:27 -0700123
Alex Klein1699fab2022-09-08 08:46:06 -0600124 class BintreeError(Exception):
125 """An error in the processing of the source binpkgs tree."""
David Pursell9476bf42015-03-30 13:34:27 -0700126
Alex Klein1699fab2022-09-08 08:46:06 -0600127 class PkgInfo(object):
128 """A record containing package information."""
David Pursell9476bf42015-03-30 13:34:27 -0700129
Alex Klein1699fab2022-09-08 08:46:06 -0600130 __slots__ = ("cpv", "build_time", "rdeps_raw", "rdeps", "rev_rdeps")
David Pursell9476bf42015-03-30 13:34:27 -0700131
Alex Klein1699fab2022-09-08 08:46:06 -0600132 def __init__(
133 self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None
134 ):
135 self.cpv = cpv
136 self.build_time = build_time
137 self.rdeps_raw = rdeps_raw
138 self.rdeps = set() if rdeps is None else rdeps
139 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
David Pursell9476bf42015-03-30 13:34:27 -0700140
Alex Klein1699fab2022-09-08 08:46:06 -0600141 # Python snippet for dumping vartree info on the target. Instantiate using
142 # _GetVartreeSnippet().
143 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700144import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700145import os
146import portage
147
148# Normalize the path to match what portage will index.
149target_root = os.path.normpath('%(root)s')
150if not target_root.endswith('/'):
151 target_root += '/'
152trees = portage.create_trees(target_root=target_root, config_root='/')
153vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700154pkg_info = []
155for cpv in vartree.dbapi.cpv_all():
156 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
157 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
158 pkg_info.append((cpv, slot, rdep_raw, build_time))
159
160print(json.dumps(pkg_info))
161"""
162
Alex Klein1699fab2022-09-08 08:46:06 -0600163 def __init__(self, sysroot):
164 self.sysroot = sysroot
Alex Klein975e86c2023-01-23 16:49:10 -0700165 # Members containing the sysroot (binpkg) and target (installed) package
166 # DB.
Alex Klein1699fab2022-09-08 08:46:06 -0600167 self.target_db = None
168 self.binpkgs_db = None
169 # Members for managing the dependency resolution work queue.
170 self.queue = None
171 self.seen = None
172 self.listed = None
David Pursell9476bf42015-03-30 13:34:27 -0700173
Alex Klein1699fab2022-09-08 08:46:06 -0600174 @staticmethod
175 def _GetCP(cpv):
176 """Returns the CP value for a given CPV string."""
177 attrs = package_info.SplitCPV(cpv, strict=False)
178 if not attrs.cp:
179 raise ValueError("Cannot get CP value for %s" % cpv)
180 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700181
Alex Klein1699fab2022-09-08 08:46:06 -0600182 @staticmethod
183 def _InDB(cp, slot, db):
184 """Returns whether CP and slot are found in a database (if provided)."""
185 cp_slots = db.get(cp) if db else None
186 return cp_slots is not None and (not slot or slot in cp_slots)
David Pursell9476bf42015-03-30 13:34:27 -0700187
Alex Klein1699fab2022-09-08 08:46:06 -0600188 @staticmethod
189 def _AtomStr(cp, slot):
190 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
191 return "%s:%s" % (cp, slot) if slot else cp
David Pursell9476bf42015-03-30 13:34:27 -0700192
Alex Klein1699fab2022-09-08 08:46:06 -0600193 @classmethod
194 def _GetVartreeSnippet(cls, root="/"):
195 """Returns a code snippet for dumping the vartree on the target.
David Pursell9476bf42015-03-30 13:34:27 -0700196
Alex Klein1699fab2022-09-08 08:46:06 -0600197 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600198 root: The installation root.
David Pursell9476bf42015-03-30 13:34:27 -0700199
Alex Klein1699fab2022-09-08 08:46:06 -0600200 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600201 The said code snippet (string) with parameters filled in.
Alex Klein1699fab2022-09-08 08:46:06 -0600202 """
203 return cls._GET_VARTREE % {"root": root}
David Pursell9476bf42015-03-30 13:34:27 -0700204
Alex Klein1699fab2022-09-08 08:46:06 -0600205 @classmethod
206 def _StripDepAtom(cls, dep_atom, installed_db=None):
207 """Strips a dependency atom and returns a (CP, slot) pair."""
208 # TODO(garnold) This is a gross simplification of ebuild dependency
209 # semantics, stripping and ignoring various qualifiers (versions, slots,
210 # USE flag, negation) and will likely need to be fixed. chromium:447366.
David Pursell9476bf42015-03-30 13:34:27 -0700211
Alex Klein1699fab2022-09-08 08:46:06 -0600212 # Ignore unversioned blockers, leaving them for the user to resolve.
213 if dep_atom[0] == "!" and dep_atom[1] not in "<=>~":
214 return None, None
David Pursell9476bf42015-03-30 13:34:27 -0700215
Alex Klein1699fab2022-09-08 08:46:06 -0600216 cp = dep_atom
217 slot = None
218 require_installed = False
David Pursell9476bf42015-03-30 13:34:27 -0700219
Alex Klein1699fab2022-09-08 08:46:06 -0600220 # Versioned blockers should be updated, but only if already installed.
Alex Klein975e86c2023-01-23 16:49:10 -0700221 # These are often used for forcing cascaded updates of multiple
222 # packages, so we're treating them as ordinary constraints with hopes
223 # that it'll lead to the desired result.
Alex Klein1699fab2022-09-08 08:46:06 -0600224 if cp.startswith("!"):
225 cp = cp.lstrip("!")
226 require_installed = True
David Pursell9476bf42015-03-30 13:34:27 -0700227
Alex Klein1699fab2022-09-08 08:46:06 -0600228 # Remove USE flags.
229 if "[" in cp:
230 cp = cp[: cp.index("[")] + cp[cp.index("]") + 1 :]
David Pursell9476bf42015-03-30 13:34:27 -0700231
Alex Klein1699fab2022-09-08 08:46:06 -0600232 # Separate the slot qualifier and strip off subslots.
233 if ":" in cp:
234 cp, slot = cp.split(":")
235 for delim in ("/", "="):
236 slot = slot.split(delim, 1)[0]
David Pursell9476bf42015-03-30 13:34:27 -0700237
Alex Klein1699fab2022-09-08 08:46:06 -0600238 # Strip version wildcards (right), comparators (left).
239 cp = cp.rstrip("*")
240 cp = cp.lstrip("<=>~")
David Pursell9476bf42015-03-30 13:34:27 -0700241
Alex Klein1699fab2022-09-08 08:46:06 -0600242 # Turn into CP form.
243 cp = cls._GetCP(cp)
David Pursell9476bf42015-03-30 13:34:27 -0700244
Alex Klein1699fab2022-09-08 08:46:06 -0600245 if require_installed and not cls._InDB(cp, None, installed_db):
246 return None, None
David Pursell9476bf42015-03-30 13:34:27 -0700247
Alex Klein1699fab2022-09-08 08:46:06 -0600248 return cp, slot
David Pursell9476bf42015-03-30 13:34:27 -0700249
Alex Klein1699fab2022-09-08 08:46:06 -0600250 @classmethod
251 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
252 """Resolves and returns a list of dependencies from a dependency string.
David Pursell9476bf42015-03-30 13:34:27 -0700253
Alex Klein1699fab2022-09-08 08:46:06 -0600254 This parses a dependency string and returns a list of package names and
Alex Klein975e86c2023-01-23 16:49:10 -0700255 slots. Other atom qualifiers (version, sub-slot, block) are ignored.
256 When resolving disjunctive deps, we include all choices that are fully
257 present in |installed_db|. If none is present, we choose an arbitrary
258 one that is available.
David Pursell9476bf42015-03-30 13:34:27 -0700259
Alex Klein1699fab2022-09-08 08:46:06 -0600260 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600261 dep_str: A raw dependency string.
262 installed_db: A database of installed packages.
263 avail_db: A database of packages available for installation.
David Pursell9476bf42015-03-30 13:34:27 -0700264
Alex Klein1699fab2022-09-08 08:46:06 -0600265 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600266 A list of pairs (CP, slot).
David Pursell9476bf42015-03-30 13:34:27 -0700267
Alex Klein1699fab2022-09-08 08:46:06 -0600268 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600269 ValueError: the dependencies string is malformed.
Alex Klein1699fab2022-09-08 08:46:06 -0600270 """
David Pursell9476bf42015-03-30 13:34:27 -0700271
Alex Klein1699fab2022-09-08 08:46:06 -0600272 def ProcessSubDeps(dep_exp, disjunct):
273 """Parses and processes a dependency (sub)expression."""
274 deps = set()
275 default_deps = set()
276 sub_disjunct = False
277 for dep_sub_exp in dep_exp:
278 sub_deps = set()
David Pursell9476bf42015-03-30 13:34:27 -0700279
Alex Klein1699fab2022-09-08 08:46:06 -0600280 if isinstance(dep_sub_exp, (list, tuple)):
281 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
282 sub_disjunct = False
283 elif sub_disjunct:
284 raise ValueError("Malformed disjunctive operation in deps")
285 elif dep_sub_exp == "||":
286 sub_disjunct = True
287 elif dep_sub_exp.endswith("?"):
288 raise ValueError("Dependencies contain a conditional")
289 else:
290 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
291 if cp:
292 sub_deps = set([(cp, slot)])
293 elif disjunct:
294 raise ValueError("Atom in disjunct ignored")
David Pursell9476bf42015-03-30 13:34:27 -0700295
Alex Klein1699fab2022-09-08 08:46:06 -0600296 # Handle sub-deps of a disjunctive expression.
297 if disjunct:
Alex Klein975e86c2023-01-23 16:49:10 -0700298 # Make the first available choice the default, for use in
299 # case that no option is installed.
Alex Klein1699fab2022-09-08 08:46:06 -0600300 if (
301 not default_deps
302 and avail_db is not None
303 and all(
304 cls._InDB(cp, slot, avail_db)
305 for cp, slot in sub_deps
306 )
307 ):
308 default_deps = sub_deps
David Pursell9476bf42015-03-30 13:34:27 -0700309
Alex Klein975e86c2023-01-23 16:49:10 -0700310 # If not all sub-deps are installed, then don't consider
311 # them.
Alex Klein1699fab2022-09-08 08:46:06 -0600312 if not all(
313 cls._InDB(cp, slot, installed_db)
314 for cp, slot in sub_deps
315 ):
316 sub_deps = set()
David Pursell9476bf42015-03-30 13:34:27 -0700317
Alex Klein1699fab2022-09-08 08:46:06 -0600318 deps.update(sub_deps)
David Pursell9476bf42015-03-30 13:34:27 -0700319
Alex Klein1699fab2022-09-08 08:46:06 -0600320 return deps or default_deps
David Pursell9476bf42015-03-30 13:34:27 -0700321
Alex Klein1699fab2022-09-08 08:46:06 -0600322 try:
323 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
324 except portage.exception.InvalidDependString as e:
325 raise ValueError("Invalid dep string: %s" % e)
326 except ValueError as e:
327 raise ValueError("%s: %s" % (e, dep_str))
David Pursell9476bf42015-03-30 13:34:27 -0700328
Alex Klein1699fab2022-09-08 08:46:06 -0600329 def _BuildDB(
330 self, cpv_info, process_rdeps, process_rev_rdeps, installed_db=None
331 ):
332 """Returns a database of packages given a list of CPV info.
David Pursell9476bf42015-03-30 13:34:27 -0700333
Alex Klein1699fab2022-09-08 08:46:06 -0600334 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600335 cpv_info: A list of tuples containing package CPV and attributes.
336 process_rdeps: Whether to populate forward dependencies.
337 process_rev_rdeps: Whether to populate reverse dependencies.
338 installed_db: A database of installed packages for filtering
339 disjunctive choices against; if None, using own built database.
David Pursell9476bf42015-03-30 13:34:27 -0700340
Alex Klein1699fab2022-09-08 08:46:06 -0600341 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600342 A map from CP values to another dictionary that maps slots
343 to package attribute tuples. Tuples contain a CPV value
344 (string), build time (string), runtime dependencies (set),
345 and reverse dependencies (set, empty if not populated).
David Pursell9476bf42015-03-30 13:34:27 -0700346
Alex Klein1699fab2022-09-08 08:46:06 -0600347 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600348 ValueError: If more than one CPV occupies a single slot.
Alex Klein1699fab2022-09-08 08:46:06 -0600349 """
350 db = {}
351 logging.debug("Populating package DB...")
352 for cpv, slot, rdeps_raw, build_time in cpv_info:
353 cp = self._GetCP(cpv)
354 cp_slots = db.setdefault(cp, dict())
355 if slot in cp_slots:
356 raise ValueError(
357 "More than one package found for %s"
358 % self._AtomStr(cp, slot)
359 )
360 logging.debug(
361 " %s -> %s, built %s, raw rdeps: %s",
362 self._AtomStr(cp, slot),
363 cpv,
364 build_time,
365 rdeps_raw,
366 )
367 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
David Pursell9476bf42015-03-30 13:34:27 -0700368
Alex Klein1699fab2022-09-08 08:46:06 -0600369 avail_db = db
370 if installed_db is None:
371 installed_db = db
372 avail_db = None
David Pursell9476bf42015-03-30 13:34:27 -0700373
Alex Klein1699fab2022-09-08 08:46:06 -0600374 # Add approximate forward dependencies.
David Pursell9476bf42015-03-30 13:34:27 -0700375 if process_rdeps:
Alex Klein1699fab2022-09-08 08:46:06 -0600376 logging.debug("Populating forward dependencies...")
377 for cp, cp_slots in db.items():
378 for slot, pkg_info in cp_slots.items():
379 pkg_info.rdeps.update(
380 self._ProcessDepStr(
381 pkg_info.rdeps_raw, installed_db, avail_db
382 )
383 )
384 logging.debug(
385 " %s (%s) processed rdeps: %s",
386 self._AtomStr(cp, slot),
387 pkg_info.cpv,
388 " ".join(
389 [
390 self._AtomStr(rdep_cp, rdep_slot)
391 for rdep_cp, rdep_slot in pkg_info.rdeps
392 ]
393 ),
394 )
395
396 # Add approximate reverse dependencies (optional).
David Pursell9476bf42015-03-30 13:34:27 -0700397 if process_rev_rdeps:
Alex Klein1699fab2022-09-08 08:46:06 -0600398 logging.debug("Populating reverse dependencies...")
399 for cp, cp_slots in db.items():
400 for slot, pkg_info in cp_slots.items():
401 for rdep_cp, rdep_slot in pkg_info.rdeps:
402 to_slots = db.get(rdep_cp)
403 if not to_slots:
404 continue
David Pursell9476bf42015-03-30 13:34:27 -0700405
Alex Klein1699fab2022-09-08 08:46:06 -0600406 for to_slot, to_pkg_info in to_slots.items():
407 if rdep_slot and to_slot != rdep_slot:
408 continue
409 logging.debug(
410 " %s (%s) added as rev rdep for %s (%s)",
411 self._AtomStr(cp, slot),
412 pkg_info.cpv,
413 self._AtomStr(rdep_cp, to_slot),
414 to_pkg_info.cpv,
415 )
416 to_pkg_info.rev_rdeps.add((cp, slot))
David Pursell9476bf42015-03-30 13:34:27 -0700417
Alex Klein1699fab2022-09-08 08:46:06 -0600418 return db
David Pursell9476bf42015-03-30 13:34:27 -0700419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
421 """Initializes a dictionary of packages installed on |device|."""
422 get_vartree_script = self._GetVartreeSnippet(root)
423 try:
Mike Frysingerc0780a62022-08-29 04:41:56 -0400424 result = device.agent.RemoteSh(
Alex Klein1699fab2022-09-08 08:46:06 -0600425 ["python"], remote_sudo=True, input=get_vartree_script
426 )
427 except cros_build_lib.RunCommandError as e:
428 logging.error("Cannot get target vartree:\n%s", e.stderr)
429 raise
David Pursell9476bf42015-03-30 13:34:27 -0700430
Alex Klein1699fab2022-09-08 08:46:06 -0600431 try:
432 self.target_db = self._BuildDB(
433 json.loads(result.stdout), process_rdeps, process_rev_rdeps
434 )
435 except ValueError as e:
436 raise self.VartreeError(str(e))
David Pursell9476bf42015-03-30 13:34:27 -0700437
Alex Klein1699fab2022-09-08 08:46:06 -0600438 def _InitBinpkgDB(self, process_rdeps):
Alex Klein975e86c2023-01-23 16:49:10 -0700439 """Initializes a dictionary of binpkgs for updating the target."""
Alex Klein1699fab2022-09-08 08:46:06 -0600440 # Get build root trees; portage indexes require a trailing '/'.
441 build_root = os.path.join(self.sysroot, "")
442 trees = portage.create_trees(
443 target_root=build_root, config_root=build_root
444 )
445 bintree = trees[build_root]["bintree"]
446 binpkgs_info = []
447 for cpv in bintree.dbapi.cpv_all():
448 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
449 cpv, ["SLOT", "RDEPEND", "BUILD_TIME"]
450 )
451 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
David Pursell9476bf42015-03-30 13:34:27 -0700452
Alex Klein1699fab2022-09-08 08:46:06 -0600453 try:
454 self.binpkgs_db = self._BuildDB(
455 binpkgs_info, process_rdeps, False, installed_db=self.target_db
456 )
457 except ValueError as e:
458 raise self.BintreeError(str(e))
David Pursell9476bf42015-03-30 13:34:27 -0700459
Alex Klein1699fab2022-09-08 08:46:06 -0600460 def _InitDepQueue(self):
461 """Initializes the dependency work queue."""
462 self.queue = set()
463 self.seen = {}
464 self.listed = set()
David Pursell9476bf42015-03-30 13:34:27 -0700465
Alex Klein1699fab2022-09-08 08:46:06 -0600466 def _EnqDep(self, dep, listed, optional):
Alex Klein975e86c2023-01-23 16:49:10 -0700467 """Enqueues a dependency if not seen before or if set non-optional."""
Alex Klein1699fab2022-09-08 08:46:06 -0600468 if dep in self.seen and (optional or not self.seen[dep]):
469 return False
David Pursell9476bf42015-03-30 13:34:27 -0700470
Alex Klein1699fab2022-09-08 08:46:06 -0600471 self.queue.add(dep)
472 self.seen[dep] = optional
473 if listed:
474 self.listed.add(dep)
475 return True
David Pursell9476bf42015-03-30 13:34:27 -0700476
Alex Klein1699fab2022-09-08 08:46:06 -0600477 def _DeqDep(self):
478 """Dequeues and returns a dependency, its listed and optional flags.
David Pursell9476bf42015-03-30 13:34:27 -0700479
Alex Klein975e86c2023-01-23 16:49:10 -0700480 This returns listed packages first, if any are present, to ensure that
481 we correctly mark them as such when they are first being processed.
Alex Klein1699fab2022-09-08 08:46:06 -0600482 """
483 if self.listed:
484 dep = self.listed.pop()
485 self.queue.remove(dep)
486 listed = True
487 else:
488 dep = self.queue.pop()
489 listed = False
David Pursell9476bf42015-03-30 13:34:27 -0700490
Alex Klein1699fab2022-09-08 08:46:06 -0600491 return dep, listed, self.seen[dep]
David Pursell9476bf42015-03-30 13:34:27 -0700492
Alex Klein1699fab2022-09-08 08:46:06 -0600493 def _FindPackageMatches(self, cpv_pattern):
494 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
David Pursell9476bf42015-03-30 13:34:27 -0700495
Alex Klein1699fab2022-09-08 08:46:06 -0600496 This is breaking |cpv_pattern| into its C, P and V components, each of
497 which may or may not be present or contain wildcards. It then scans the
Alex Klein975e86c2023-01-23 16:49:10 -0700498 binpkgs database to find all atoms that match these components,
499 returning a list of CP and slot qualifier. When the pattern does not
500 specify a version, or when a CP has only one slot in the binpkgs
501 database, we omit the slot qualifier in the result.
David Pursell9476bf42015-03-30 13:34:27 -0700502
Alex Klein1699fab2022-09-08 08:46:06 -0600503 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600504 cpv_pattern: A CPV pattern, potentially partial and/or having
505 wildcards.
David Pursell9476bf42015-03-30 13:34:27 -0700506
Alex Klein1699fab2022-09-08 08:46:06 -0600507 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600508 A list of (CPV, slot) pairs of packages in the binpkgs database that
509 match the pattern.
Alex Klein1699fab2022-09-08 08:46:06 -0600510 """
511 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
512 cp_pattern = os.path.join(attrs.category or "*", attrs.package or "*")
513 matches = []
514 for cp, cp_slots in self.binpkgs_db.items():
515 if not fnmatch.fnmatchcase(cp, cp_pattern):
516 continue
David Pursell9476bf42015-03-30 13:34:27 -0700517
Alex Klein975e86c2023-01-23 16:49:10 -0700518 # If no version attribute was given or there's only one slot, omit
519 # the slot qualifier.
Alex Klein1699fab2022-09-08 08:46:06 -0600520 if not attrs.version or len(cp_slots) == 1:
521 matches.append((cp, None))
522 else:
523 cpv_pattern = "%s-%s" % (cp, attrs.version)
524 for slot, pkg_info in cp_slots.items():
525 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
526 matches.append((cp, slot))
David Pursell9476bf42015-03-30 13:34:27 -0700527
Alex Klein1699fab2022-09-08 08:46:06 -0600528 return matches
David Pursell9476bf42015-03-30 13:34:27 -0700529
Alex Klein1699fab2022-09-08 08:46:06 -0600530 def _FindPackage(self, pkg):
531 """Returns the (CP, slot) pair for a package matching |pkg|.
David Pursell9476bf42015-03-30 13:34:27 -0700532
Alex Klein1699fab2022-09-08 08:46:06 -0600533 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600534 pkg: Path to a binary package or a (partial) package CPV specifier.
David Pursell9476bf42015-03-30 13:34:27 -0700535
Alex Klein1699fab2022-09-08 08:46:06 -0600536 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600537 A (CP, slot) pair for the given package; slot may be None
538 (unspecified).
David Pursell9476bf42015-03-30 13:34:27 -0700539
Alex Klein1699fab2022-09-08 08:46:06 -0600540 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600541 ValueError: if |pkg| is not a binpkg file nor does it match
542 something that's in the bintree.
Alex Klein1699fab2022-09-08 08:46:06 -0600543 """
544 if pkg.endswith(".tbz2") and os.path.isfile(pkg):
545 package = os.path.basename(os.path.splitext(pkg)[0])
546 category = os.path.basename(os.path.dirname(pkg))
547 return self._GetCP(os.path.join(category, package)), None
David Pursell9476bf42015-03-30 13:34:27 -0700548
Alex Klein1699fab2022-09-08 08:46:06 -0600549 matches = self._FindPackageMatches(pkg)
550 if not matches:
551 raise ValueError("No package found for %s" % pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700552
Alex Klein1699fab2022-09-08 08:46:06 -0600553 idx = 0
554 if len(matches) > 1:
555 # Ask user to pick among multiple matches.
556 idx = cros_build_lib.GetChoice(
557 "Multiple matches found for %s: " % pkg,
558 ["%s:%s" % (cp, slot) if slot else cp for cp, slot in matches],
559 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700560
Alex Klein1699fab2022-09-08 08:46:06 -0600561 return matches[idx]
562
563 def _NeedsInstall(self, cpv, slot, build_time, optional):
564 """Returns whether a package needs to be installed on the target.
565
566 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600567 cpv: Fully qualified CPV (string) of the package.
568 slot: Slot identifier (string).
569 build_time: The BUILT_TIME value (string) of the binpkg.
570 optional: Whether package is optional on the target.
Alex Klein1699fab2022-09-08 08:46:06 -0600571
572 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600573 A tuple (install, update) indicating whether to |install| the
574 package and whether it is an |update| to an existing package.
Alex Klein1699fab2022-09-08 08:46:06 -0600575
576 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600577 ValueError: if slot is not provided.
Alex Klein1699fab2022-09-08 08:46:06 -0600578 """
579 # If not checking installed packages, always install.
580 if not self.target_db:
581 return True, False
582
583 cp = self._GetCP(cpv)
584 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
585 if target_pkg_info is not None:
586 if cpv != target_pkg_info.cpv:
587 attrs = package_info.SplitCPV(cpv)
588 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
589 logging.debug(
590 "Updating %s: version (%s) different on target (%s)",
591 cp,
592 attrs.version,
593 target_attrs.version,
594 )
595 return True, True
596
597 if build_time != target_pkg_info.build_time:
598 logging.debug(
599 "Updating %s: build time (%s) different on target (%s)",
600 cpv,
601 build_time,
602 target_pkg_info.build_time,
603 )
604 return True, True
605
606 logging.debug(
607 "Not updating %s: already up-to-date (%s, built %s)",
608 cp,
609 target_pkg_info.cpv,
610 target_pkg_info.build_time,
611 )
612 return False, False
613
614 if optional:
615 logging.debug(
616 "Not installing %s: missing on target but optional", cp
617 )
618 return False, False
619
620 logging.debug(
621 "Installing %s: missing on target and non-optional (%s)", cp, cpv
622 )
623 return True, False
624
625 def _ProcessDeps(self, deps, reverse):
626 """Enqueues dependencies for processing.
627
628 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600629 deps: List of dependencies to enqueue.
630 reverse: Whether these are reverse dependencies.
Alex Klein1699fab2022-09-08 08:46:06 -0600631 """
632 if not deps:
633 return
634
635 logging.debug(
636 "Processing %d %s dep(s)...",
637 len(deps),
638 "reverse" if reverse else "forward",
639 )
640 num_already_seen = 0
641 for dep in deps:
642 if self._EnqDep(dep, False, reverse):
643 logging.debug(" Queued dep %s", dep)
644 else:
645 num_already_seen += 1
646
647 if num_already_seen:
648 logging.debug("%d dep(s) already seen", num_already_seen)
649
650 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
Alex Klein975e86c2023-01-23 16:49:10 -0700651 """Returns a dict of packages that need to be installed on the target.
Alex Klein1699fab2022-09-08 08:46:06 -0600652
653 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600654 process_rdeps: Whether to trace forward dependencies.
655 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600656
657 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600658 A dictionary mapping CP values (string) to tuples containing
659 a CPV (string), a slot (string), a boolean indicating whether
660 the package was initially listed in the queue, and a boolean
661 indicating whether this is an update to an existing package.
Alex Klein1699fab2022-09-08 08:46:06 -0600662 """
663 installs = {}
664 while self.queue:
665 dep, listed, optional = self._DeqDep()
666 cp, required_slot = dep
667 if cp in installs:
668 logging.debug("Already updating %s", cp)
669 continue
670
671 cp_slots = self.binpkgs_db.get(cp, dict())
672 logging.debug(
673 "Checking packages matching %s%s%s...",
674 cp,
675 " (slot: %s)" % required_slot if required_slot else "",
676 " (optional)" if optional else "",
677 )
678 num_processed = 0
679 for slot, pkg_info in cp_slots.items():
680 if required_slot and slot != required_slot:
681 continue
682
683 num_processed += 1
684 logging.debug(" Checking %s...", pkg_info.cpv)
685
686 install, update = self._NeedsInstall(
687 pkg_info.cpv, slot, pkg_info.build_time, optional
688 )
689 if not install:
690 continue
691
692 installs[cp] = (pkg_info.cpv, slot, listed, update)
693
694 # Add forward and backward runtime dependencies to queue.
695 if process_rdeps:
696 self._ProcessDeps(pkg_info.rdeps, False)
697 if process_rev_rdeps:
698 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
699 if target_pkg_info:
700 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
701
702 if num_processed == 0:
703 logging.warning(
704 "No qualified bintree package corresponding to %s", cp
705 )
706
707 return installs
708
709 def _SortInstalls(self, installs):
710 """Returns a sorted list of packages to install.
711
712 Performs a topological sort based on dependencies found in the binary
713 package database.
714
715 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600716 installs: Dictionary of packages to install indexed by CP.
Alex Klein1699fab2022-09-08 08:46:06 -0600717
718 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600719 A list of package CPVs (string).
Alex Klein1699fab2022-09-08 08:46:06 -0600720
721 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600722 ValueError: If dependency graph contains a cycle.
Alex Klein1699fab2022-09-08 08:46:06 -0600723 """
724 not_visited = set(installs.keys())
725 curr_path = []
726 sorted_installs = []
727
728 def SortFrom(cp):
Alex Klein975e86c2023-01-23 16:49:10 -0700729 """Traverses deps recursively, emitting nodes in reverse order."""
Alex Klein1699fab2022-09-08 08:46:06 -0600730 cpv, slot, _, _ = installs[cp]
731 if cpv in curr_path:
732 raise ValueError(
733 "Dependencies contain a cycle: %s -> %s"
734 % (" -> ".join(curr_path[curr_path.index(cpv) :]), cpv)
735 )
736 curr_path.append(cpv)
737 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
738 if rdep_cp in not_visited:
739 not_visited.remove(rdep_cp)
740 SortFrom(rdep_cp)
741
742 sorted_installs.append(cpv)
743 curr_path.pop()
744
745 # So long as there's more packages, keep expanding dependency paths.
746 while not_visited:
747 SortFrom(not_visited.pop())
748
749 return sorted_installs
750
751 def _EnqListedPkg(self, pkg):
752 """Finds and enqueues a listed package."""
753 cp, slot = self._FindPackage(pkg)
754 if cp not in self.binpkgs_db:
755 raise self.BintreeError(
756 "Package %s not found in binpkgs tree" % pkg
757 )
758 self._EnqDep((cp, slot), True, False)
759
760 def _EnqInstalledPkgs(self):
761 """Enqueues all available binary packages that are already installed."""
762 for cp, cp_slots in self.binpkgs_db.items():
763 target_cp_slots = self.target_db.get(cp)
764 if target_cp_slots:
765 for slot in cp_slots.keys():
766 if slot in target_cp_slots:
767 self._EnqDep((cp, slot), True, False)
768
769 def Run(
770 self,
771 device,
772 root,
773 listed_pkgs,
774 update,
775 process_rdeps,
776 process_rev_rdeps,
777 ):
778 """Computes the list of packages that need to be installed on a target.
779
780 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600781 device: Target handler object.
782 root: Package installation root.
783 listed_pkgs: Package names/files listed by the user.
784 update: Whether to read the target's installed package database.
785 process_rdeps: Whether to trace forward dependencies.
786 process_rev_rdeps: Whether to trace backward dependencies as well.
Alex Klein1699fab2022-09-08 08:46:06 -0600787
788 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600789 A tuple (sorted, listed, num_updates, install_attrs) where |sorted|
790 is a list of package CPVs (string) to install on the target
791 in an order that satisfies their inter-dependencies, |listed|
792 the subset that was requested by the user, and |num_updates|
793 the number of packages being installed over preexisting
794 versions. Note that installation order should be reversed for
795 removal, |install_attrs| is a dictionary mapping a package
796 CPV (string) to some of its extracted environment attributes.
Alex Klein1699fab2022-09-08 08:46:06 -0600797 """
798 if process_rev_rdeps and not process_rdeps:
799 raise ValueError(
800 "Must processing forward deps when processing rev deps"
801 )
802 if process_rdeps and not update:
803 raise ValueError(
804 "Must check installed packages when processing deps"
805 )
806
807 if update:
808 logging.info("Initializing target intalled packages database...")
809 self._InitTargetVarDB(
810 device, root, process_rdeps, process_rev_rdeps
811 )
812
813 logging.info("Initializing binary packages database...")
814 self._InitBinpkgDB(process_rdeps)
815
816 logging.info("Finding listed package(s)...")
817 self._InitDepQueue()
818 for pkg in listed_pkgs:
819 if pkg == "@installed":
820 if not update:
821 raise ValueError(
Alex Klein975e86c2023-01-23 16:49:10 -0700822 "Must check installed packages when updating all of "
823 "them."
Alex Klein1699fab2022-09-08 08:46:06 -0600824 )
825 self._EnqInstalledPkgs()
826 else:
827 self._EnqListedPkg(pkg)
828
829 logging.info("Computing set of packages to install...")
830 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
831
832 num_updates = 0
833 listed_installs = []
834 for cpv, _, listed, isupdate in installs.values():
835 if listed:
836 listed_installs.append(cpv)
837 if isupdate:
838 num_updates += 1
839
840 logging.info(
841 "Processed %d package(s), %d will be installed, %d are "
842 "updating existing packages",
843 len(self.seen),
844 len(installs),
845 num_updates,
846 )
847
848 sorted_installs = self._SortInstalls(installs)
849
850 install_attrs = {}
851 for pkg in sorted_installs:
852 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
853 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
854 install_attrs[pkg] = {}
855 if dlc_id and dlc_package:
856 install_attrs[pkg][_DLC_ID] = dlc_id
857
858 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700859
860
Mike Frysinger63d35512021-01-26 23:16:13 -0500861def _Emerge(device, pkg_paths, root, extra_args=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600862 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -0700863
Alex Klein1699fab2022-09-08 08:46:06 -0600864 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600865 device: A ChromiumOSDevice object.
866 pkg_paths: (Local) paths to binary packages.
867 root: Package installation root path.
868 extra_args: Extra arguments to pass to emerge.
David Pursell9476bf42015-03-30 13:34:27 -0700869
Alex Klein1699fab2022-09-08 08:46:06 -0600870 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600871 DeployError: Unrecoverable error during emerge.
Alex Klein1699fab2022-09-08 08:46:06 -0600872 """
Mike Frysinger63d35512021-01-26 23:16:13 -0500873
Alex Klein1699fab2022-09-08 08:46:06 -0600874 def path_to_name(pkg_path):
875 return os.path.basename(pkg_path)
Mike Frysinger63d35512021-01-26 23:16:13 -0500876
Alex Klein1699fab2022-09-08 08:46:06 -0600877 def path_to_category(pkg_path):
878 return os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700879
Alex Klein1699fab2022-09-08 08:46:06 -0600880 pkg_names = ", ".join(path_to_name(x) for x in pkg_paths)
David Pursell9476bf42015-03-30 13:34:27 -0700881
Alex Klein1699fab2022-09-08 08:46:06 -0600882 pkgroot = os.path.join(device.work_dir, "packages")
883 portage_tmpdir = os.path.join(device.work_dir, "portage-tmp")
Alex Klein975e86c2023-01-23 16:49:10 -0700884 # Clean out the dirs first if we had a previous emerge on the device so as
885 # to free up space for this emerge. The last emerge gets implicitly cleaned
886 # up when the device connection deletes its work_dir.
Alex Klein1699fab2022-09-08 08:46:06 -0600887 device.run(
888 f"cd {device.work_dir} && "
889 f"rm -rf packages portage-tmp && "
890 f"mkdir -p portage-tmp packages && "
891 f"cd packages && "
892 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
893 shell=True,
894 remote_sudo=True,
895 )
Mike Frysinger63d35512021-01-26 23:16:13 -0500896
Alex Klein1699fab2022-09-08 08:46:06 -0600897 logging.info("Use portage temp dir %s", portage_tmpdir)
David Pursell9476bf42015-03-30 13:34:27 -0700898
Mike Frysinger63d35512021-01-26 23:16:13 -0500899 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -0600900 logging.notice("Copying binpkgs to device.")
901 for pkg_path in pkg_paths:
902 pkg_name = path_to_name(pkg_path)
903 logging.info("Copying %s", pkg_name)
904 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
905 device.CopyToDevice(
906 pkg_path, pkg_dir, mode="rsync", remote_sudo=True, compress=False
907 )
908
909 # This message is read by BrilloDeployOperation.
910 logging.notice("Installing: %s", pkg_names)
911
912 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
913 # chromeos-base packages will be skipped due to the configuration
914 # in /etc/protage/make.profile/package.provided. However, there is
915 # a known bug that /usr/local/etc/portage is not setup properly
916 # (crbug.com/312041). This does not affect `cros deploy` because
917 # we do not use the preset PKGDIR.
918 extra_env = {
919 "FEATURES": "-sandbox",
920 "PKGDIR": pkgroot,
921 "PORTAGE_CONFIGROOT": "/usr/local",
922 "PORTAGE_TMPDIR": portage_tmpdir,
923 "PORTDIR": device.work_dir,
924 "CONFIG_PROTECT": "-*",
925 }
926
Alex Klein975e86c2023-01-23 16:49:10 -0700927 # --ignore-built-slot-operator-deps because we don't rebuild everything. It
928 # can cause errors, but that's expected with cros deploy since it's just a
Alex Klein1699fab2022-09-08 08:46:06 -0600929 # best effort to prevent developers avoid rebuilding an image every time.
930 cmd = [
931 "emerge",
932 "--usepkg",
933 "--ignore-built-slot-operator-deps=y",
934 "--root",
935 root,
936 ] + [os.path.join(pkgroot, *x.split("/")[-2:]) for x in pkg_paths]
937 if extra_args:
938 cmd.append(extra_args)
939
940 logging.warning(
941 "Ignoring slot dependencies! This may break things! e.g. "
942 "packages built against the old version may not be able to "
943 "load the new .so. This is expected, and you will just need "
944 "to build and flash a new image if you have problems."
945 )
946 try:
947 result = device.run(
948 cmd,
949 extra_env=extra_env,
950 remote_sudo=True,
951 capture_output=True,
952 debug_level=logging.INFO,
953 )
954
955 pattern = (
956 "A requested package will not be merged because "
957 "it is listed in package.provided"
958 )
959 output = result.stderr.replace("\n", " ").replace("\r", "")
960 if pattern in output:
961 error = (
962 "Package failed to emerge: %s\n"
963 "Remove %s from /etc/portage/make.profile/"
964 "package.provided/chromeos-base.packages\n"
965 "(also see crbug.com/920140 for more context)\n"
966 % (pattern, pkg_name)
967 )
968 cros_build_lib.Die(error)
969 except Exception:
970 logging.error("Failed to emerge packages %s", pkg_names)
971 raise
972 else:
973 # This message is read by BrilloDeployOperation.
974 logging.notice("Packages have been installed.")
David Pursell9476bf42015-03-30 13:34:27 -0700975
976
Qijiang Fand5958192019-07-26 12:32:36 +0900977def _RestoreSELinuxContext(device, pkgpath, root):
Alex Klein1699fab2022-09-08 08:46:06 -0600978 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900979
Alex Klein1699fab2022-09-08 08:46:06 -0600980 This reads the tarball from pkgpath, and calls restorecon on device to
Alex Klein975e86c2023-01-23 16:49:10 -0700981 restore SELinux context for files listed in the tarball, assuming those
982 files are installed to /
Qijiang Fan8a945032019-04-25 20:53:29 +0900983
Alex Klein1699fab2022-09-08 08:46:06 -0600984 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600985 device: a ChromiumOSDevice object
986 pkgpath: path to tarball
987 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -0600988 """
989 pkgroot = os.path.join(device.work_dir, "packages")
990 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
991 pkgpath_device = os.path.join(
992 pkgroot, pkg_dirname, os.path.basename(pkgpath)
993 )
994 # Testing shows restorecon splits on newlines instead of spaces.
995 device.run(
996 [
997 "cd",
998 root,
999 "&&",
1000 "tar",
1001 "tf",
1002 pkgpath_device,
1003 "|",
1004 "restorecon",
1005 "-i",
1006 "-f",
1007 "-",
1008 ],
1009 remote_sudo=True,
1010 )
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001011
1012
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001013def _GetPackagesByCPV(cpvs, strip, sysroot):
Alex Klein1699fab2022-09-08 08:46:06 -06001014 """Returns paths to binary packages corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001015
Alex Klein1699fab2022-09-08 08:46:06 -06001016 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001017 cpvs: List of CPV components given by package_info.SplitCPV().
1018 strip: True to run strip_package.
1019 sysroot: Sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001020
Alex Klein1699fab2022-09-08 08:46:06 -06001021 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001022 List of paths corresponding to |cpvs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001023
Alex Klein1699fab2022-09-08 08:46:06 -06001024 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001025 DeployError: If a package is missing.
Alex Klein1699fab2022-09-08 08:46:06 -06001026 """
1027 packages_dir = None
1028 if strip:
1029 try:
1030 cros_build_lib.run(
1031 [
Mike Frysinger5429f302023-03-27 15:48:52 -04001032 constants.CHROMITE_SCRIPTS_DIR / "strip_package",
Alex Klein1699fab2022-09-08 08:46:06 -06001033 "--sysroot",
1034 sysroot,
1035 ]
1036 + [cpv.cpf for cpv in cpvs]
1037 )
1038 packages_dir = _STRIPPED_PACKAGES_DIR
1039 except cros_build_lib.RunCommandError:
1040 logging.error(
1041 "Cannot strip packages %s", " ".join([str(cpv) for cpv in cpvs])
1042 )
1043 raise
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001044
Alex Klein1699fab2022-09-08 08:46:06 -06001045 paths = []
1046 for cpv in cpvs:
1047 path = portage_util.GetBinaryPackagePath(
1048 cpv.category,
1049 cpv.package,
1050 cpv.version,
1051 sysroot=sysroot,
1052 packages_dir=packages_dir,
1053 )
1054 if not path:
1055 raise DeployError("Missing package %s." % cpv)
1056 paths.append(path)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001057
Alex Klein1699fab2022-09-08 08:46:06 -06001058 return paths
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001059
1060
1061def _GetPackagesPaths(pkgs, strip, sysroot):
Alex Klein1699fab2022-09-08 08:46:06 -06001062 """Returns paths to binary |pkgs|.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001063
Alex Klein1699fab2022-09-08 08:46:06 -06001064 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001065 pkgs: List of package CPVs string.
1066 strip: Whether or not to run strip_package for CPV packages.
1067 sysroot: The sysroot path.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001068
Alex Klein1699fab2022-09-08 08:46:06 -06001069 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001070 List of paths corresponding to |pkgs|.
Alex Klein1699fab2022-09-08 08:46:06 -06001071 """
1072 cpvs = [package_info.SplitCPV(p) for p in pkgs]
1073 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -07001074
1075
Mike Frysinger22bb5502021-01-29 13:05:46 -05001076def _Unmerge(device, pkgs, root):
Alex Klein1699fab2022-09-08 08:46:06 -06001077 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -07001078
Alex Klein1699fab2022-09-08 08:46:06 -06001079 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001080 device: A RemoteDevice object.
1081 pkgs: Package names.
1082 root: Package installation root path.
Alex Klein1699fab2022-09-08 08:46:06 -06001083 """
1084 pkg_names = ", ".join(os.path.basename(x) for x in pkgs)
Mike Frysinger22bb5502021-01-29 13:05:46 -05001085 # This message is read by BrilloDeployOperation.
Alex Klein1699fab2022-09-08 08:46:06 -06001086 logging.notice("Unmerging %s.", pkg_names)
1087 cmd = ["qmerge", "--yes"]
1088 # Check if qmerge is available on the device. If not, use emerge.
1089 if device.run(["qmerge", "--version"], check=False).returncode != 0:
1090 cmd = ["emerge"]
1091
1092 cmd += ["--unmerge", "--root", root]
1093 cmd.extend("f={x}" for x in pkgs)
1094 try:
1095 # Always showing the emerge output for clarity.
1096 device.run(
1097 cmd,
1098 capture_output=False,
1099 remote_sudo=True,
1100 debug_level=logging.INFO,
1101 )
1102 except Exception:
1103 logging.error("Failed to unmerge packages %s", pkg_names)
1104 raise
1105 else:
1106 # This message is read by BrilloDeployOperation.
1107 logging.notice("Packages have been uninstalled.")
David Pursell9476bf42015-03-30 13:34:27 -07001108
1109
1110def _ConfirmDeploy(num_updates):
Alex Klein1699fab2022-09-08 08:46:06 -06001111 """Returns whether we can continue deployment."""
1112 if num_updates > _MAX_UPDATES_NUM:
1113 logging.warning(_MAX_UPDATES_WARNING)
1114 return cros_build_lib.BooleanPrompt(default=False)
David Pursell9476bf42015-03-30 13:34:27 -07001115
Alex Klein1699fab2022-09-08 08:46:06 -06001116 return True
David Pursell9476bf42015-03-30 13:34:27 -07001117
1118
Andrew06a5f812020-01-23 08:08:32 -08001119def _EmergePackages(pkgs, device, strip, sysroot, root, board, emerge_args):
Alex Klein1699fab2022-09-08 08:46:06 -06001120 """Call _Emerge for each package in pkgs."""
Ben Pastene5f03b052019-08-12 18:03:24 -07001121 if device.IsSELinuxAvailable():
Alex Klein1699fab2022-09-08 08:46:06 -06001122 enforced = device.IsSELinuxEnforced()
1123 if enforced:
1124 device.run(["setenforce", "0"])
1125 else:
1126 enforced = False
Andrewc7e1c6b2020-02-27 16:03:53 -08001127
Alex Klein1699fab2022-09-08 08:46:06 -06001128 dlc_deployed = False
1129 # This message is read by BrilloDeployOperation.
1130 logging.info("Preparing local packages for transfer.")
1131 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
1132 # Install all the packages in one pass so inter-package blockers work.
1133 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
1134 logging.info("Updating SELinux settings & DLC images.")
1135 for pkg_path in pkg_paths:
1136 if device.IsSELinuxAvailable():
1137 _RestoreSELinuxContext(device, pkg_path, root)
Mike Frysinger5f4c2742021-02-08 14:37:23 -05001138
Alex Klein1699fab2022-09-08 08:46:06 -06001139 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
1140 if dlc_id and dlc_package:
1141 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
1142 dlc_deployed = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001143
Alex Klein1699fab2022-09-08 08:46:06 -06001144 if dlc_deployed:
1145 # Clean up empty directories created by emerging DLCs.
1146 device.run(
1147 [
1148 "test",
1149 "-d",
1150 "/build/rootfs",
1151 "&&",
1152 "rmdir",
1153 "--ignore-fail-on-non-empty",
1154 "/build/rootfs",
1155 "/build",
1156 ],
1157 check=False,
1158 )
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001159
Alex Klein1699fab2022-09-08 08:46:06 -06001160 if enforced:
1161 device.run(["setenforce", "1"])
1162
1163 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1164 # we installed new DLC images).
1165 if dlc_deployed:
1166 device.run(["restart", "dlcservice"])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001167
1168
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001169def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Alex Klein1699fab2022-09-08 08:46:06 -06001170 """Call _Unmege for each package in pkgs."""
1171 dlc_uninstalled = False
1172 _Unmerge(device, pkgs, root)
1173 logging.info("Cleaning up DLC images.")
1174 for pkg in pkgs:
1175 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1176 dlc_uninstalled = True
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001177
Alex Klein1699fab2022-09-08 08:46:06 -06001178 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1179 # uninstalled DLC images).
1180 if dlc_uninstalled:
1181 device.run(["restart", "dlcservice"])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001182
1183
1184def _UninstallDLCImage(device, pkg_attrs):
Alex Klein1699fab2022-09-08 08:46:06 -06001185 """Uninstall a DLC image."""
1186 if _DLC_ID in pkg_attrs:
1187 dlc_id = pkg_attrs[_DLC_ID]
1188 logging.notice("Uninstalling DLC image for %s", dlc_id)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001189
Alex Klein1699fab2022-09-08 08:46:06 -06001190 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1191 return True
1192 else:
1193 logging.debug("DLC_ID not found in package")
1194 return False
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001195
1196
Andrew06a5f812020-01-23 08:08:32 -08001197def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
Alex Klein1699fab2022-09-08 08:46:06 -06001198 """Deploy (install and mount) a DLC image.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001199
Alex Klein1699fab2022-09-08 08:46:06 -06001200 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001201 device: A device object.
1202 sysroot: The sysroot path.
1203 board: Board to use.
1204 dlc_id: The DLC ID.
1205 dlc_package: The DLC package name.
Alex Klein1699fab2022-09-08 08:46:06 -06001206 """
1207 # Requires `sudo_rm` because installations of files are running with sudo.
1208 with osutils.TempDir(sudo_rm=True) as tempdir:
1209 temp_rootfs = Path(tempdir)
1210 # Build the DLC image if the image is outdated or doesn't exist.
1211 dlc_lib.InstallDlcImages(
1212 sysroot=sysroot, rootfs=temp_rootfs, dlc_id=dlc_id, board=board
1213 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001214
Alex Klein1699fab2022-09-08 08:46:06 -06001215 logging.debug("Uninstall DLC %s if it is installed.", dlc_id)
1216 try:
1217 device.run(["dlcservice_util", "--uninstall", "--id=%s" % dlc_id])
1218 except cros_build_lib.RunCommandError as e:
1219 logging.info(
1220 "Failed to uninstall DLC:%s. Continue anyway.", e.stderr
1221 )
1222 except Exception:
1223 logging.error("Failed to uninstall DLC.")
1224 raise
Andrewc7e1c6b2020-02-27 16:03:53 -08001225
Alex Klein1699fab2022-09-08 08:46:06 -06001226 logging.notice("Deploy the DLC image for %s", dlc_id)
1227 dlc_img_path_src = os.path.join(
1228 sysroot,
1229 dlc_lib.DLC_BUILD_DIR,
1230 dlc_id,
1231 dlc_package,
1232 dlc_lib.DLC_IMAGE,
1233 )
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001234 if not os.path.exists(dlc_img_path_src):
1235 dlc_img_path_src = os.path.join(
1236 sysroot,
1237 dlc_lib.DLC_BUILD_DIR_SCALED,
1238 dlc_id,
1239 dlc_package,
1240 dlc_lib.DLC_IMAGE,
1241 )
1242
Alex Klein1699fab2022-09-08 08:46:06 -06001243 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1244 dlc_img_path_a = os.path.join(dlc_img_path, "dlc_a")
1245 dlc_img_path_b = os.path.join(dlc_img_path, "dlc_b")
1246 # Create directories for DLC images.
1247 device.run(["mkdir", "-p", dlc_img_path_a, dlc_img_path_b])
1248 # Copy images to the destination directories.
1249 device.CopyToDevice(
1250 dlc_img_path_src,
1251 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1252 mode="rsync",
1253 )
1254 device.run(
1255 [
1256 "cp",
1257 os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1258 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE),
1259 ]
1260 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001261
Alex Klein1699fab2022-09-08 08:46:06 -06001262 # Set the proper perms and ownership so dlcservice can access the image.
1263 device.run(["chmod", "-R", "u+rwX,go+rX,go-w", _DLC_INSTALL_ROOT])
1264 device.run(["chown", "-R", "dlcservice:dlcservice", _DLC_INSTALL_ROOT])
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001265
Alex Klein1699fab2022-09-08 08:46:06 -06001266 # Copy metadata to device.
1267 dest_meta_dir = Path("/") / dlc_lib.DLC_META_DIR / dlc_id / dlc_package
1268 device.run(["mkdir", "-p", dest_meta_dir])
1269 src_meta_dir = os.path.join(
1270 sysroot,
1271 dlc_lib.DLC_BUILD_DIR,
1272 dlc_id,
1273 dlc_package,
1274 dlc_lib.DLC_TMP_META_DIR,
1275 )
Jae Hoon Kimc3cf2272022-10-28 23:59:10 +00001276 if not os.path.exists(src_meta_dir):
1277 src_meta_dir = os.path.join(
1278 sysroot,
1279 dlc_lib.DLC_BUILD_DIR_SCALED,
1280 dlc_id,
1281 dlc_package,
1282 dlc_lib.DLC_TMP_META_DIR,
1283 )
Alex Klein1699fab2022-09-08 08:46:06 -06001284 device.CopyToDevice(
1285 src_meta_dir + "/",
1286 dest_meta_dir,
1287 mode="rsync",
1288 recursive=True,
1289 remote_sudo=True,
1290 )
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001291
Alex Klein1699fab2022-09-08 08:46:06 -06001292 # TODO(kimjae): Make this generic so it recomputes all the DLCs + copies
Alex Klein975e86c2023-01-23 16:49:10 -07001293 # over a fresh list of dm-verity digests instead of appending and
1294 # keeping the stale digests when developers are testing.
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001295
Alex Klein1699fab2022-09-08 08:46:06 -06001296 # Copy the LoadPin dm-verity digests to device.
1297 loadpin = dlc_lib.DLC_LOADPIN_TRUSTED_VERITY_DIGESTS
1298 dst_loadpin = Path("/") / dlc_lib.DLC_META_DIR / loadpin
1299 src_loadpin = temp_rootfs / dlc_lib.DLC_META_DIR / loadpin
1300 if src_loadpin.exists():
1301 digests = set(osutils.ReadFile(src_loadpin).split())
1302 try:
1303 digests.update(device.CatFile(dst_loadpin).split())
1304 except remote_access.CatFileError:
1305 pass
Jae Hoon Kim2376e142022-09-03 00:18:58 +00001306
Alex Klein1699fab2022-09-08 08:46:06 -06001307 with tempfile.NamedTemporaryFile(dir=temp_rootfs) as f:
1308 osutils.WriteFile(f.name, "\n".join(digests))
1309 device.CopyToDevice(
1310 f.name, dst_loadpin, mode="rsync", remote_sudo=True
1311 )
Andrew67b5fa72020-02-05 14:14:48 -08001312
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001313
1314def _GetDLCInfo(device, pkg_path, from_dut):
Alex Klein1699fab2022-09-08 08:46:06 -06001315 """Returns information of a DLC given its package path.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001316
Alex Klein1699fab2022-09-08 08:46:06 -06001317 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001318 device: commandline.Device object; None to use the default device.
1319 pkg_path: path to the package.
1320 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1321 info from host.
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001322
Alex Klein1699fab2022-09-08 08:46:06 -06001323 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001324 A tuple (dlc_id, dlc_package).
Alex Klein1699fab2022-09-08 08:46:06 -06001325 """
1326 environment_content = ""
1327 if from_dut:
1328 # On DUT, |pkg_path| is the directory which contains environment file.
1329 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1330 try:
1331 environment_data = device.CatFile(
1332 environment_path, max_size=None, encoding=None
1333 )
1334 except remote_access.CatFileError:
1335 # The package is not installed on DUT yet. Skip extracting info.
1336 return None, None
1337 else:
1338 # On host, pkg_path is tbz2 file which contains environment file.
1339 # Extract the metadata of the package file.
1340 data = portage.xpak.tbz2(pkg_path).get_data()
1341 environment_data = data[_ENVIRONMENT_FILENAME.encode("utf-8")]
1342
1343 # Extract the environment metadata.
1344 environment_content = bz2.decompress(environment_data)
1345
1346 with tempfile.NamedTemporaryFile() as f:
1347 # Dumps content into a file so we can use osutils.SourceEnvironment.
1348 path = os.path.realpath(f.name)
1349 osutils.WriteFile(path, environment_content, mode="wb")
1350 content = osutils.SourceEnvironment(
1351 path, (_DLC_ID, _DLC_PACKAGE, _DLC_ENABLED)
1352 )
1353
1354 dlc_enabled = content.get(_DLC_ENABLED)
1355 if dlc_enabled is not None and (
1356 dlc_enabled is False or str(dlc_enabled) == "false"
1357 ):
1358 logging.info("Installing DLC in rootfs.")
1359 return None, None
1360 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
1361
1362
1363def Deploy(
1364 device,
1365 packages,
1366 board=None,
1367 emerge=True,
1368 update=False,
1369 deep=False,
1370 deep_rev=False,
1371 clean_binpkg=True,
1372 root="/",
1373 strip=True,
1374 emerge_args=None,
1375 ssh_private_key=None,
1376 ping=True,
1377 force=False,
1378 dry_run=False,
1379):
1380 """Deploys packages to a device.
1381
1382 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001383 device: commandline.Device object; None to use the default device.
1384 packages: List of packages (strings) to deploy to device.
1385 board: Board to use; None to automatically detect.
1386 emerge: True to emerge package, False to unmerge.
1387 update: Check installed version on device.
1388 deep: Install dependencies also. Implies |update|.
1389 deep_rev: Install reverse dependencies. Implies |deep|.
1390 clean_binpkg: Clean outdated binary packages.
1391 root: Package installation root path.
1392 strip: Run strip_package to filter out preset paths in the package.
1393 emerge_args: Extra arguments to pass to emerge.
1394 ssh_private_key: Path to an SSH private key file; None to use test keys.
1395 ping: True to ping the device before trying to connect.
1396 force: Ignore confidence checks and prompts.
1397 dry_run: Print deployment plan but do not deploy anything.
Alex Klein1699fab2022-09-08 08:46:06 -06001398
1399 Raises:
Alex Klein53cc3bf2022-10-13 08:50:01 -06001400 ValueError: Invalid parameter or parameter combination.
1401 DeployError: Unrecoverable failure during deploy.
Alex Klein1699fab2022-09-08 08:46:06 -06001402 """
1403 if deep_rev:
1404 deep = True
1405 if deep:
1406 update = True
1407
1408 if not packages:
1409 raise DeployError("No packages provided, nothing to deploy.")
1410
1411 if update and not emerge:
1412 raise ValueError("Cannot update and unmerge.")
1413
1414 if device:
1415 hostname, username, port = device.hostname, device.username, device.port
1416 else:
1417 hostname, username, port = None, None, None
1418
1419 lsb_release = None
1420 sysroot = None
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001421 try:
Alex Klein1699fab2022-09-08 08:46:06 -06001422 # Somewhat confusing to clobber, but here we are.
1423 # pylint: disable=redefined-argument-from-local
1424 with remote_access.ChromiumOSDeviceHandler(
1425 hostname,
1426 port=port,
1427 username=username,
1428 private_key=ssh_private_key,
1429 base_dir=_DEVICE_BASE_DIR,
1430 ping=ping,
1431 ) as device:
1432 lsb_release = device.lsb_release
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001433
Alex Klein1699fab2022-09-08 08:46:06 -06001434 board = cros_build_lib.GetBoard(
1435 device_board=device.board, override_board=board
1436 )
1437 if not force and board != device.board:
1438 raise DeployError(
1439 "Device (%s) is incompatible with board %s. Use "
1440 "--force to deploy anyway." % (device.board, board)
1441 )
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001442
Alex Klein1699fab2022-09-08 08:46:06 -06001443 sysroot = build_target_lib.get_default_sysroot_path(board)
Andrew67b5fa72020-02-05 14:14:48 -08001444
Alex Klein975e86c2023-01-23 16:49:10 -07001445 # Don't bother trying to clean for unmerges. We won't use the local
1446 # db, and it just slows things down for the user.
Alex Klein1699fab2022-09-08 08:46:06 -06001447 if emerge and clean_binpkg:
1448 logging.notice(
1449 "Cleaning outdated binary packages from %s", sysroot
1450 )
1451 portage_util.CleanOutdatedBinaryPackages(sysroot)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001452
Alex Klein1699fab2022-09-08 08:46:06 -06001453 # Remount rootfs as writable if necessary.
1454 if not device.MountRootfsReadWrite():
1455 raise DeployError(
1456 "Cannot remount rootfs as read-write. Exiting."
1457 )
Ralph Nathane01ccf12015-04-16 10:40:32 -07001458
Alex Klein1699fab2022-09-08 08:46:06 -06001459 # Obtain list of packages to upgrade/remove.
1460 pkg_scanner = _InstallPackageScanner(sysroot)
1461 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
1462 device, root, packages, update, deep, deep_rev
1463 )
1464 if emerge:
1465 action_str = "emerge"
1466 else:
1467 pkgs.reverse()
1468 action_str = "unmerge"
David Pursell9476bf42015-03-30 13:34:27 -07001469
Alex Klein1699fab2022-09-08 08:46:06 -06001470 if not pkgs:
1471 logging.notice("No packages to %s", action_str)
1472 return
David Pursell9476bf42015-03-30 13:34:27 -07001473
Alex Klein1699fab2022-09-08 08:46:06 -06001474 # Warn when the user installs & didn't `cros workon start`.
1475 if emerge:
1476 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(
1477 use_all=True
1478 )
1479 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1480 for package in listed:
1481 cp = package_info.SplitCPV(package).cp
1482 if cp in all_workon and cp not in worked_on_cps:
1483 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001484 "Are you intentionally deploying unmodified "
1485 "packages, or did you forget to run "
1486 "`cros workon --board=$BOARD start %s`?",
Alex Klein1699fab2022-09-08 08:46:06 -06001487 cp,
1488 )
David Pursell9476bf42015-03-30 13:34:27 -07001489
Alex Klein1699fab2022-09-08 08:46:06 -06001490 logging.notice("These are the packages to %s:", action_str)
1491 for i, pkg in enumerate(pkgs):
1492 logging.notice(
1493 "%s %d) %s", "*" if pkg in listed else " ", i + 1, pkg
1494 )
Gilad Arnolda0a98062015-07-07 08:34:27 -07001495
Alex Klein1699fab2022-09-08 08:46:06 -06001496 if dry_run or not _ConfirmDeploy(num_updates):
1497 return
David Pursell9476bf42015-03-30 13:34:27 -07001498
Alex Klein1699fab2022-09-08 08:46:06 -06001499 # Select function (emerge or unmerge) and bind args.
1500 if emerge:
1501 func = functools.partial(
1502 _EmergePackages,
1503 pkgs,
1504 device,
1505 strip,
1506 sysroot,
1507 root,
1508 board,
1509 emerge_args,
1510 )
1511 else:
1512 func = functools.partial(
1513 _UnmergePackages, pkgs, device, root, pkgs_attrs
1514 )
David Pursell2e773382015-04-03 14:30:47 -07001515
Alex Klein1699fab2022-09-08 08:46:06 -06001516 # Call the function with the progress bar or with normal output.
1517 if command.UseProgressBar():
1518 op = BrilloDeployOperation(emerge)
1519 op.Run(func, log_level=logging.DEBUG)
1520 else:
1521 func()
David Pursell9476bf42015-03-30 13:34:27 -07001522
Alex Klein1699fab2022-09-08 08:46:06 -06001523 if device.IsSELinuxAvailable():
1524 if sum(x.count("selinux-policy") for x in pkgs):
1525 logging.warning(
Alex Klein975e86c2023-01-23 16:49:10 -07001526 "Deploying SELinux policy will not take effect until "
1527 "reboot. SELinux policy is loaded by init. Also, "
1528 "changing the security contexts (labels) of a file "
1529 "will require building a new image and flashing the "
1530 "image onto the device."
Alex Klein1699fab2022-09-08 08:46:06 -06001531 )
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001532
Alex Klein1699fab2022-09-08 08:46:06 -06001533 # This message is read by BrilloDeployOperation.
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001534 logging.warning(
Alex Klein1699fab2022-09-08 08:46:06 -06001535 "Please restart any updated services on the device, "
1536 "or just reboot it."
1537 )
1538 except Exception:
1539 if lsb_release:
1540 lsb_entries = sorted(lsb_release.items())
1541 logging.info(
1542 "Following are the LSB version details of the device:\n%s",
1543 "\n".join("%s=%s" % (k, v) for k, v in lsb_entries),
1544 )
1545 raise