blob: 3ae35251110f5e896ffdb7d851a9309bc8ee5d7b [file] [log] [blame]
David Pursell9476bf42015-03-30 13:34:27 -07001# Copyright 2015 The Chromium OS Authors. All rights reserved.
2# 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
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070019import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070020
Ralph Nathane01ccf12015-04-16 10:40:32 -070021from chromite.cli import command
Mike Frysinger06a51c82021-04-06 11:39:17 -040022from chromite.lib import build_target_lib
Ram Chandrasekar56152ec2021-11-22 17:10:41 +000023from chromite.lib import constants
David Pursell9476bf42015-03-30 13:34:27 -070024from chromite.lib import cros_build_lib
Alex Klein18a60af2020-06-11 12:08:47 -060025from chromite.lib import dlc_lib
Ralph Nathane01ccf12015-04-16 10:40:32 -070026from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070027from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070028from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070029from chromite.lib import remote_access
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +090030from chromite.lib import workon_helper
Alex Klein18a60af2020-06-11 12:08:47 -060031from chromite.lib.parser import package_info
32
Chris McDonald14ac61d2021-07-21 11:49:56 -060033
David Pursell9476bf42015-03-30 13:34:27 -070034try:
35 import portage
36except ImportError:
37 if cros_build_lib.IsInsideChroot():
38 raise
39
40
41_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
42# This is defined in src/platform/dev/builder.py
43_STRIPPED_PACKAGES_DIR = 'stripped-packages'
44
45_MAX_UPDATES_NUM = 10
46_MAX_UPDATES_WARNING = (
47 'You are about to update a large number of installed packages, which '
48 'might take a long time, fail midway, or leave the target in an '
49 'inconsistent state. It is highly recommended that you flash a new image '
50 'instead.')
51
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070052_DLC_ID = 'DLC_ID'
53_DLC_PACKAGE = 'DLC_PACKAGE'
Andrew67b5fa72020-02-05 14:14:48 -080054_DLC_ENABLED = 'DLC_ENABLED'
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070055_ENVIRONMENT_FILENAME = 'environment.bz2'
56_DLC_INSTALL_ROOT = '/var/cache/dlc'
57
David Pursell9476bf42015-03-30 13:34:27 -070058
59class DeployError(Exception):
60 """Thrown when an unrecoverable error is encountered during deploy."""
61
62
Ralph Nathane01ccf12015-04-16 10:40:32 -070063class BrilloDeployOperation(operation.ProgressBarOperation):
64 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070065 # These two variables are used to validate the output in the VM integration
66 # tests. Changes to the output must be reflected here.
Mike Frysinger63d35512021-01-26 23:16:13 -050067 MERGE_EVENTS = (
68 'Preparing local packages',
69 'NOTICE: Copying binpkgs',
70 'NOTICE: Installing',
71 'been installed.',
72 'Please restart any updated',
73 )
Mike Frysinger22bb5502021-01-29 13:05:46 -050074 UNMERGE_EVENTS = (
75 'NOTICE: Unmerging',
76 'been uninstalled.',
77 'Please restart any updated',
78 )
Ralph Nathane01ccf12015-04-16 10:40:32 -070079
Mike Frysinger63d35512021-01-26 23:16:13 -050080 def __init__(self, emerge):
Ralph Nathane01ccf12015-04-16 10:40:32 -070081 """Construct BrilloDeployOperation object.
82
83 Args:
Ralph Nathane01ccf12015-04-16 10:40:32 -070084 emerge: True if emerge, False is unmerge.
85 """
Jae Hoon Kimad176b82021-07-26 19:29:29 +000086 super().__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070087 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070088 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070089 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070090 self._events = self.UNMERGE_EVENTS
Mike Frysinger63d35512021-01-26 23:16:13 -050091 self._total = len(self._events)
Ralph Nathane01ccf12015-04-16 10:40:32 -070092 self._completed = 0
93
Ralph Nathandc14ed92015-04-22 11:17:40 -070094 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070095 """Parse the output of brillo deploy to update a progress bar."""
96 stdout = self._stdout.read()
97 stderr = self._stderr.read()
98 output = stdout + stderr
99 for event in self._events:
100 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -0400101 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700102
103
David Pursell9476bf42015-03-30 13:34:27 -0700104class _InstallPackageScanner(object):
105 """Finds packages that need to be installed on a target device.
106
107 Scans the sysroot bintree, beginning with a user-provided list of packages,
108 to find all packages that need to be installed. If so instructed,
109 transitively scans forward (mandatory) and backward (optional) dependencies
110 as well. A package will be installed if missing on the target (mandatory
111 packages only), or it will be updated if its sysroot version and build time
112 are different from the target. Common usage:
113
114 pkg_scanner = _InstallPackageScanner(sysroot)
115 pkgs = pkg_scanner.Run(...)
116 """
117
118 class VartreeError(Exception):
119 """An error in the processing of the installed packages tree."""
120
121 class BintreeError(Exception):
122 """An error in the processing of the source binpkgs tree."""
123
124 class PkgInfo(object):
125 """A record containing package information."""
126
127 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
128
129 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
130 self.cpv = cpv
131 self.build_time = build_time
132 self.rdeps_raw = rdeps_raw
133 self.rdeps = set() if rdeps is None else rdeps
134 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
135
136 # Python snippet for dumping vartree info on the target. Instantiate using
137 # _GetVartreeSnippet().
138 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700139import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700140import os
141import portage
142
143# Normalize the path to match what portage will index.
144target_root = os.path.normpath('%(root)s')
145if not target_root.endswith('/'):
146 target_root += '/'
147trees = portage.create_trees(target_root=target_root, config_root='/')
148vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700149pkg_info = []
150for cpv in vartree.dbapi.cpv_all():
151 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
152 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
153 pkg_info.append((cpv, slot, rdep_raw, build_time))
154
155print(json.dumps(pkg_info))
156"""
157
158 def __init__(self, sysroot):
159 self.sysroot = sysroot
160 # Members containing the sysroot (binpkg) and target (installed) package DB.
161 self.target_db = None
162 self.binpkgs_db = None
163 # Members for managing the dependency resolution work queue.
164 self.queue = None
165 self.seen = None
166 self.listed = None
167
168 @staticmethod
169 def _GetCP(cpv):
170 """Returns the CP value for a given CPV string."""
Alex Klein9742cb62020-10-12 19:22:10 +0000171 attrs = package_info.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600172 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700173 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600174 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700175
176 @staticmethod
177 def _InDB(cp, slot, db):
178 """Returns whether CP and slot are found in a database (if provided)."""
179 cp_slots = db.get(cp) if db else None
180 return cp_slots is not None and (not slot or slot in cp_slots)
181
182 @staticmethod
183 def _AtomStr(cp, slot):
184 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
185 return '%s:%s' % (cp, slot) if slot else cp
186
187 @classmethod
188 def _GetVartreeSnippet(cls, root='/'):
189 """Returns a code snippet for dumping the vartree on the target.
190
191 Args:
192 root: The installation root.
193
194 Returns:
195 The said code snippet (string) with parameters filled in.
196 """
197 return cls._GET_VARTREE % {'root': root}
198
199 @classmethod
200 def _StripDepAtom(cls, dep_atom, installed_db=None):
201 """Strips a dependency atom and returns a (CP, slot) pair."""
202 # TODO(garnold) This is a gross simplification of ebuild dependency
203 # semantics, stripping and ignoring various qualifiers (versions, slots,
204 # USE flag, negation) and will likely need to be fixed. chromium:447366.
205
206 # Ignore unversioned blockers, leaving them for the user to resolve.
207 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
208 return None, None
209
210 cp = dep_atom
211 slot = None
212 require_installed = False
213
214 # Versioned blockers should be updated, but only if already installed.
215 # These are often used for forcing cascaded updates of multiple packages,
216 # so we're treating them as ordinary constraints with hopes that it'll lead
217 # to the desired result.
218 if cp.startswith('!'):
219 cp = cp.lstrip('!')
220 require_installed = True
221
222 # Remove USE flags.
223 if '[' in cp:
224 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
225
226 # Separate the slot qualifier and strip off subslots.
227 if ':' in cp:
228 cp, slot = cp.split(':')
229 for delim in ('/', '='):
230 slot = slot.split(delim, 1)[0]
231
232 # Strip version wildcards (right), comparators (left).
233 cp = cp.rstrip('*')
234 cp = cp.lstrip('<=>~')
235
236 # Turn into CP form.
237 cp = cls._GetCP(cp)
238
239 if require_installed and not cls._InDB(cp, None, installed_db):
240 return None, None
241
242 return cp, slot
243
244 @classmethod
245 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
246 """Resolves and returns a list of dependencies from a dependency string.
247
248 This parses a dependency string and returns a list of package names and
249 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
250 resolving disjunctive deps, we include all choices that are fully present
251 in |installed_db|. If none is present, we choose an arbitrary one that is
252 available.
253
254 Args:
255 dep_str: A raw dependency string.
256 installed_db: A database of installed packages.
257 avail_db: A database of packages available for installation.
258
259 Returns:
260 A list of pairs (CP, slot).
261
262 Raises:
263 ValueError: the dependencies string is malformed.
264 """
265 def ProcessSubDeps(dep_exp, disjunct):
266 """Parses and processes a dependency (sub)expression."""
267 deps = set()
268 default_deps = set()
269 sub_disjunct = False
270 for dep_sub_exp in dep_exp:
271 sub_deps = set()
272
273 if isinstance(dep_sub_exp, (list, tuple)):
274 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
275 sub_disjunct = False
276 elif sub_disjunct:
277 raise ValueError('Malformed disjunctive operation in deps')
278 elif dep_sub_exp == '||':
279 sub_disjunct = True
280 elif dep_sub_exp.endswith('?'):
281 raise ValueError('Dependencies contain a conditional')
282 else:
283 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
284 if cp:
285 sub_deps = set([(cp, slot)])
286 elif disjunct:
287 raise ValueError('Atom in disjunct ignored')
288
289 # Handle sub-deps of a disjunctive expression.
290 if disjunct:
291 # Make the first available choice the default, for use in case that
292 # no option is installed.
293 if (not default_deps and avail_db is not None and
Mike Frysinger80ff4542022-05-06 23:52:04 -0400294 all(cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps)):
David Pursell9476bf42015-03-30 13:34:27 -0700295 default_deps = sub_deps
296
297 # If not all sub-deps are installed, then don't consider them.
Mike Frysinger80ff4542022-05-06 23:52:04 -0400298 if not all(cls._InDB(cp, slot, installed_db)
299 for cp, slot in sub_deps):
David Pursell9476bf42015-03-30 13:34:27 -0700300 sub_deps = set()
301
302 deps.update(sub_deps)
303
304 return deps or default_deps
305
306 try:
307 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
308 except portage.exception.InvalidDependString as e:
309 raise ValueError('Invalid dep string: %s' % e)
310 except ValueError as e:
311 raise ValueError('%s: %s' % (e, dep_str))
312
313 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
314 installed_db=None):
315 """Returns a database of packages given a list of CPV info.
316
317 Args:
318 cpv_info: A list of tuples containing package CPV and attributes.
319 process_rdeps: Whether to populate forward dependencies.
320 process_rev_rdeps: Whether to populate reverse dependencies.
321 installed_db: A database of installed packages for filtering disjunctive
322 choices against; if None, using own built database.
323
324 Returns:
325 A map from CP values to another dictionary that maps slots to package
326 attribute tuples. Tuples contain a CPV value (string), build time
327 (string), runtime dependencies (set), and reverse dependencies (set,
328 empty if not populated).
329
330 Raises:
331 ValueError: If more than one CPV occupies a single slot.
332 """
333 db = {}
334 logging.debug('Populating package DB...')
335 for cpv, slot, rdeps_raw, build_time in cpv_info:
336 cp = self._GetCP(cpv)
337 cp_slots = db.setdefault(cp, dict())
338 if slot in cp_slots:
339 raise ValueError('More than one package found for %s' %
340 self._AtomStr(cp, slot))
341 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
342 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
343 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
344
345 avail_db = db
346 if installed_db is None:
347 installed_db = db
348 avail_db = None
349
350 # Add approximate forward dependencies.
351 if process_rdeps:
352 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400353 for cp, cp_slots in db.items():
354 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700355 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
356 installed_db, avail_db))
357 logging.debug(' %s (%s) processed rdeps: %s',
358 self._AtomStr(cp, slot), pkg_info.cpv,
359 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
360 for rdep_cp, rdep_slot in pkg_info.rdeps]))
361
362 # Add approximate reverse dependencies (optional).
363 if process_rev_rdeps:
364 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400365 for cp, cp_slots in db.items():
366 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700367 for rdep_cp, rdep_slot in pkg_info.rdeps:
368 to_slots = db.get(rdep_cp)
369 if not to_slots:
370 continue
371
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400372 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700373 if rdep_slot and to_slot != rdep_slot:
374 continue
375 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
376 self._AtomStr(cp, slot), pkg_info.cpv,
377 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
378 to_pkg_info.rev_rdeps.add((cp, slot))
379
380 return db
381
382 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
383 """Initializes a dictionary of packages installed on |device|."""
384 get_vartree_script = self._GetVartreeSnippet(root)
385 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400386 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700387 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700388 except cros_build_lib.RunCommandError as e:
Mike Frysingera2fac222022-08-17 21:06:33 -0400389 logging.error('Cannot get target vartree:\n%s', e.stderr)
David Pursell9476bf42015-03-30 13:34:27 -0700390 raise
391
392 try:
Mike Frysinger876a8e52022-06-23 18:07:30 -0400393 self.target_db = self._BuildDB(json.loads(result.stdout),
David Pursell9476bf42015-03-30 13:34:27 -0700394 process_rdeps, process_rev_rdeps)
395 except ValueError as e:
396 raise self.VartreeError(str(e))
397
398 def _InitBinpkgDB(self, process_rdeps):
399 """Initializes a dictionary of binary packages for updating the target."""
400 # Get build root trees; portage indexes require a trailing '/'.
401 build_root = os.path.join(self.sysroot, '')
402 trees = portage.create_trees(target_root=build_root, config_root=build_root)
403 bintree = trees[build_root]['bintree']
404 binpkgs_info = []
405 for cpv in bintree.dbapi.cpv_all():
406 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
407 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
408 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
409
410 try:
411 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
412 installed_db=self.target_db)
413 except ValueError as e:
414 raise self.BintreeError(str(e))
415
416 def _InitDepQueue(self):
417 """Initializes the dependency work queue."""
418 self.queue = set()
419 self.seen = {}
420 self.listed = set()
421
422 def _EnqDep(self, dep, listed, optional):
423 """Enqueues a dependency if not seen before or if turned non-optional."""
424 if dep in self.seen and (optional or not self.seen[dep]):
425 return False
426
427 self.queue.add(dep)
428 self.seen[dep] = optional
429 if listed:
430 self.listed.add(dep)
431 return True
432
433 def _DeqDep(self):
434 """Dequeues and returns a dependency, its listed and optional flags.
435
436 This returns listed packages first, if any are present, to ensure that we
437 correctly mark them as such when they are first being processed.
438 """
439 if self.listed:
440 dep = self.listed.pop()
441 self.queue.remove(dep)
442 listed = True
443 else:
444 dep = self.queue.pop()
445 listed = False
446
447 return dep, listed, self.seen[dep]
448
449 def _FindPackageMatches(self, cpv_pattern):
450 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
451
452 This is breaking |cpv_pattern| into its C, P and V components, each of
453 which may or may not be present or contain wildcards. It then scans the
454 binpkgs database to find all atoms that match these components, returning a
455 list of CP and slot qualifier. When the pattern does not specify a version,
456 or when a CP has only one slot in the binpkgs database, we omit the slot
457 qualifier in the result.
458
459 Args:
460 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
461
462 Returns:
463 A list of (CPV, slot) pairs of packages in the binpkgs database that
464 match the pattern.
465 """
Alex Klein9742cb62020-10-12 19:22:10 +0000466 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
David Pursell9476bf42015-03-30 13:34:27 -0700467 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
468 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400469 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700470 if not fnmatch.fnmatchcase(cp, cp_pattern):
471 continue
472
473 # If no version attribute was given or there's only one slot, omit the
474 # slot qualifier.
Alex Klein9742cb62020-10-12 19:22:10 +0000475 if not attrs.version or len(cp_slots) == 1:
David Pursell9476bf42015-03-30 13:34:27 -0700476 matches.append((cp, None))
477 else:
Alex Klein9742cb62020-10-12 19:22:10 +0000478 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400479 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700480 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
481 matches.append((cp, slot))
482
483 return matches
484
485 def _FindPackage(self, pkg):
486 """Returns the (CP, slot) pair for a package matching |pkg|.
487
488 Args:
489 pkg: Path to a binary package or a (partial) package CPV specifier.
490
491 Returns:
492 A (CP, slot) pair for the given package; slot may be None (unspecified).
493
494 Raises:
495 ValueError: if |pkg| is not a binpkg file nor does it match something
496 that's in the bintree.
497 """
498 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
499 package = os.path.basename(os.path.splitext(pkg)[0])
500 category = os.path.basename(os.path.dirname(pkg))
501 return self._GetCP(os.path.join(category, package)), None
502
503 matches = self._FindPackageMatches(pkg)
504 if not matches:
505 raise ValueError('No package found for %s' % pkg)
506
507 idx = 0
508 if len(matches) > 1:
509 # Ask user to pick among multiple matches.
510 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
511 ['%s:%s' % (cp, slot) if slot else cp
512 for cp, slot in matches])
513
514 return matches[idx]
515
516 def _NeedsInstall(self, cpv, slot, build_time, optional):
517 """Returns whether a package needs to be installed on the target.
518
519 Args:
520 cpv: Fully qualified CPV (string) of the package.
521 slot: Slot identifier (string).
522 build_time: The BUILT_TIME value (string) of the binpkg.
523 optional: Whether package is optional on the target.
524
525 Returns:
526 A tuple (install, update) indicating whether to |install| the package and
527 whether it is an |update| to an existing package.
528
529 Raises:
530 ValueError: if slot is not provided.
531 """
532 # If not checking installed packages, always install.
533 if not self.target_db:
534 return True, False
535
536 cp = self._GetCP(cpv)
537 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
538 if target_pkg_info is not None:
539 if cpv != target_pkg_info.cpv:
Alex Klein9742cb62020-10-12 19:22:10 +0000540 attrs = package_info.SplitCPV(cpv)
541 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
David Pursell9476bf42015-03-30 13:34:27 -0700542 logging.debug('Updating %s: version (%s) different on target (%s)',
Alex Klein9742cb62020-10-12 19:22:10 +0000543 cp, attrs.version, target_attrs.version)
David Pursell9476bf42015-03-30 13:34:27 -0700544 return True, True
545
546 if build_time != target_pkg_info.build_time:
547 logging.debug('Updating %s: build time (%s) different on target (%s)',
548 cpv, build_time, target_pkg_info.build_time)
549 return True, True
550
551 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
552 cp, target_pkg_info.cpv, target_pkg_info.build_time)
553 return False, False
554
555 if optional:
556 logging.debug('Not installing %s: missing on target but optional', cp)
557 return False, False
558
559 logging.debug('Installing %s: missing on target and non-optional (%s)',
560 cp, cpv)
561 return True, False
562
563 def _ProcessDeps(self, deps, reverse):
564 """Enqueues dependencies for processing.
565
566 Args:
567 deps: List of dependencies to enqueue.
568 reverse: Whether these are reverse dependencies.
569 """
570 if not deps:
571 return
572
573 logging.debug('Processing %d %s dep(s)...', len(deps),
574 'reverse' if reverse else 'forward')
575 num_already_seen = 0
576 for dep in deps:
577 if self._EnqDep(dep, False, reverse):
578 logging.debug(' Queued dep %s', dep)
579 else:
580 num_already_seen += 1
581
582 if num_already_seen:
583 logging.debug('%d dep(s) already seen', num_already_seen)
584
585 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
586 """Returns a dictionary of packages that need to be installed on the target.
587
588 Args:
589 process_rdeps: Whether to trace forward dependencies.
590 process_rev_rdeps: Whether to trace backward dependencies as well.
591
592 Returns:
593 A dictionary mapping CP values (string) to tuples containing a CPV
594 (string), a slot (string), a boolean indicating whether the package
595 was initially listed in the queue, and a boolean indicating whether this
596 is an update to an existing package.
597 """
598 installs = {}
599 while self.queue:
600 dep, listed, optional = self._DeqDep()
601 cp, required_slot = dep
602 if cp in installs:
603 logging.debug('Already updating %s', cp)
604 continue
605
606 cp_slots = self.binpkgs_db.get(cp, dict())
607 logging.debug('Checking packages matching %s%s%s...', cp,
608 ' (slot: %s)' % required_slot if required_slot else '',
609 ' (optional)' if optional else '')
610 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400611 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700612 if required_slot and slot != required_slot:
613 continue
614
615 num_processed += 1
616 logging.debug(' Checking %s...', pkg_info.cpv)
617
618 install, update = self._NeedsInstall(pkg_info.cpv, slot,
619 pkg_info.build_time, optional)
620 if not install:
621 continue
622
623 installs[cp] = (pkg_info.cpv, slot, listed, update)
624
625 # Add forward and backward runtime dependencies to queue.
626 if process_rdeps:
627 self._ProcessDeps(pkg_info.rdeps, False)
628 if process_rev_rdeps:
629 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
630 if target_pkg_info:
631 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
632
633 if num_processed == 0:
634 logging.warning('No qualified bintree package corresponding to %s', cp)
635
636 return installs
637
638 def _SortInstalls(self, installs):
639 """Returns a sorted list of packages to install.
640
641 Performs a topological sort based on dependencies found in the binary
642 package database.
643
644 Args:
645 installs: Dictionary of packages to install indexed by CP.
646
647 Returns:
648 A list of package CPVs (string).
649
650 Raises:
651 ValueError: If dependency graph contains a cycle.
652 """
653 not_visited = set(installs.keys())
654 curr_path = []
655 sorted_installs = []
656
657 def SortFrom(cp):
658 """Traverses dependencies recursively, emitting nodes in reverse order."""
659 cpv, slot, _, _ = installs[cp]
660 if cpv in curr_path:
661 raise ValueError('Dependencies contain a cycle: %s -> %s' %
662 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
663 curr_path.append(cpv)
664 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
665 if rdep_cp in not_visited:
666 not_visited.remove(rdep_cp)
667 SortFrom(rdep_cp)
668
669 sorted_installs.append(cpv)
670 curr_path.pop()
671
672 # So long as there's more packages, keep expanding dependency paths.
673 while not_visited:
674 SortFrom(not_visited.pop())
675
676 return sorted_installs
677
678 def _EnqListedPkg(self, pkg):
679 """Finds and enqueues a listed package."""
680 cp, slot = self._FindPackage(pkg)
681 if cp not in self.binpkgs_db:
682 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
683 self._EnqDep((cp, slot), True, False)
684
685 def _EnqInstalledPkgs(self):
686 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400687 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700688 target_cp_slots = self.target_db.get(cp)
689 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400690 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700691 if slot in target_cp_slots:
692 self._EnqDep((cp, slot), True, False)
693
694 def Run(self, device, root, listed_pkgs, update, process_rdeps,
695 process_rev_rdeps):
696 """Computes the list of packages that need to be installed on a target.
697
698 Args:
699 device: Target handler object.
700 root: Package installation root.
701 listed_pkgs: Package names/files listed by the user.
702 update: Whether to read the target's installed package database.
703 process_rdeps: Whether to trace forward dependencies.
704 process_rev_rdeps: Whether to trace backward dependencies as well.
705
706 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700707 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
708 list of package CPVs (string) to install on the target in an order that
709 satisfies their inter-dependencies, |listed| the subset that was
710 requested by the user, and |num_updates| the number of packages being
711 installed over preexisting versions. Note that installation order should
712 be reversed for removal, |install_attrs| is a dictionary mapping a package
713 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700714 """
715 if process_rev_rdeps and not process_rdeps:
716 raise ValueError('Must processing forward deps when processing rev deps')
717 if process_rdeps and not update:
718 raise ValueError('Must check installed packages when processing deps')
719
720 if update:
721 logging.info('Initializing target intalled packages database...')
722 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
723
724 logging.info('Initializing binary packages database...')
725 self._InitBinpkgDB(process_rdeps)
726
727 logging.info('Finding listed package(s)...')
728 self._InitDepQueue()
729 for pkg in listed_pkgs:
730 if pkg == '@installed':
731 if not update:
732 raise ValueError(
733 'Must check installed packages when updating all of them.')
734 self._EnqInstalledPkgs()
735 else:
736 self._EnqListedPkg(pkg)
737
738 logging.info('Computing set of packages to install...')
739 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
740
741 num_updates = 0
742 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400743 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700744 if listed:
745 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400746 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700747 num_updates += 1
748
749 logging.info('Processed %d package(s), %d will be installed, %d are '
750 'updating existing packages',
751 len(self.seen), len(installs), num_updates)
752
753 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700754
755 install_attrs = {}
756 for pkg in sorted_installs:
Mike Frysingerada2d1c2020-03-20 05:02:06 -0400757 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700758 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
759 install_attrs[pkg] = {}
760 if dlc_id and dlc_package:
761 install_attrs[pkg][_DLC_ID] = dlc_id
762
763 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700764
765
Mike Frysinger63d35512021-01-26 23:16:13 -0500766def _Emerge(device, pkg_paths, root, extra_args=None):
767 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -0700768
769 Args:
770 device: A ChromiumOSDevice object.
Mike Frysinger63d35512021-01-26 23:16:13 -0500771 pkg_paths: (Local) paths to binary packages.
David Pursell9476bf42015-03-30 13:34:27 -0700772 root: Package installation root path.
773 extra_args: Extra arguments to pass to emerge.
774
775 Raises:
776 DeployError: Unrecoverable error during emerge.
777 """
Mike Frysinger63d35512021-01-26 23:16:13 -0500778 def path_to_name(pkg_path):
779 return os.path.basename(pkg_path)
780 def path_to_category(pkg_path):
781 return os.path.basename(os.path.dirname(pkg_path))
782
783 pkg_names = ', '.join(path_to_name(x) for x in pkg_paths)
784
David Pursell9476bf42015-03-30 13:34:27 -0700785 pkgroot = os.path.join(device.work_dir, 'packages')
Mike Frysinger15a4e012015-05-21 22:18:45 -0400786 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
787 # Clean out the dirs first if we had a previous emerge on the device so as to
788 # free up space for this emerge. The last emerge gets implicitly cleaned up
789 # when the device connection deletes its work_dir.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400790 device.run(
Mike Frysinger63d35512021-01-26 23:16:13 -0500791 f'cd {device.work_dir} && '
792 f'rm -rf packages portage-tmp && '
793 f'mkdir -p portage-tmp packages && '
794 f'cd packages && '
795 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
796 shell=True, remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700797
David Pursell9476bf42015-03-30 13:34:27 -0700798 logging.info('Use portage temp dir %s', portage_tmpdir)
799
Ralph Nathane01ccf12015-04-16 10:40:32 -0700800 # This message is read by BrilloDeployOperation.
Mike Frysinger63d35512021-01-26 23:16:13 -0500801 logging.notice('Copying binpkgs to device.')
802 for pkg_path in pkg_paths:
803 pkg_name = path_to_name(pkg_path)
804 logging.info('Copying %s', pkg_name)
805 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
806 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True,
807 compress=False)
808
809 # This message is read by BrilloDeployOperation.
810 logging.notice('Installing: %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700811
812 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
813 # chromeos-base packages will be skipped due to the configuration
814 # in /etc/protage/make.profile/package.provided. However, there is
815 # a known bug that /usr/local/etc/portage is not setup properly
816 # (crbug.com/312041). This does not affect `cros deploy` because
817 # we do not use the preset PKGDIR.
818 extra_env = {
819 'FEATURES': '-sandbox',
820 'PKGDIR': pkgroot,
821 'PORTAGE_CONFIGROOT': '/usr/local',
822 'PORTAGE_TMPDIR': portage_tmpdir,
823 'PORTDIR': device.work_dir,
824 'CONFIG_PROTECT': '-*',
825 }
Mike Frysinger63d35512021-01-26 23:16:13 -0500826
Alex Kleinaaddc932020-01-30 15:02:24 -0700827 # --ignore-built-slot-operator-deps because we don't rebuild everything.
828 # It can cause errors, but that's expected with cros deploy since it's just a
829 # best effort to prevent developers avoid rebuilding an image every time.
Mike Frysinger63d35512021-01-26 23:16:13 -0500830 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', '--root',
831 root] + [os.path.join(pkgroot, *x.split('/')[-2:]) for x in pkg_paths]
David Pursell9476bf42015-03-30 13:34:27 -0700832 if extra_args:
833 cmd.append(extra_args)
834
Alex Kleinaaddc932020-01-30 15:02:24 -0700835 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
836 'packages built against the old version may not be able to '
837 'load the new .so. This is expected, and you will just need '
838 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700839 try:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400840 result = device.run(cmd, extra_env=extra_env, remote_sudo=True,
841 capture_output=True, debug_level=logging.INFO)
Greg Kerrb96c02c2019-02-08 14:32:41 -0800842
843 pattern = ('A requested package will not be merged because '
844 'it is listed in package.provided')
Mike Frysinger876a8e52022-06-23 18:07:30 -0400845 output = result.stderr.replace('\n', ' ').replace('\r', '')
Greg Kerrb96c02c2019-02-08 14:32:41 -0800846 if pattern in output:
847 error = ('Package failed to emerge: %s\n'
848 'Remove %s from /etc/portage/make.profile/'
849 'package.provided/chromeos-base.packages\n'
850 '(also see crbug.com/920140 for more context)\n'
851 % (pattern, pkg_name))
852 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700853 except Exception:
Mike Frysinger63d35512021-01-26 23:16:13 -0500854 logging.error('Failed to emerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700855 raise
856 else:
Mike Frysinger63d35512021-01-26 23:16:13 -0500857 # This message is read by BrilloDeployOperation.
858 logging.notice('Packages have been installed.')
David Pursell9476bf42015-03-30 13:34:27 -0700859
860
Qijiang Fand5958192019-07-26 12:32:36 +0900861def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800862 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900863
864 This reads the tarball from pkgpath, and calls restorecon on device to
865 restore SELinux context for files listed in the tarball, assuming those files
866 are installed to /
867
868 Args:
869 device: a ChromiumOSDevice object
870 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900871 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900872 """
Qijiang Fan8a945032019-04-25 20:53:29 +0900873 pkgroot = os.path.join(device.work_dir, 'packages')
874 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
875 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
876 # Testing shows restorecon splits on newlines instead of spaces.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400877 device.run(
Qijiang Fand5958192019-07-26 12:32:36 +0900878 ['cd', root, '&&',
879 'tar', 'tf', pkgpath_device, '|',
880 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900881 remote_sudo=True)
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900882
883
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700884def _GetPackagesByCPV(cpvs, strip, sysroot):
885 """Returns paths to binary packages corresponding to |cpvs|.
886
887 Args:
Alex Klein9742cb62020-10-12 19:22:10 +0000888 cpvs: List of CPV components given by package_info.SplitCPV().
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700889 strip: True to run strip_package.
890 sysroot: Sysroot path.
891
892 Returns:
893 List of paths corresponding to |cpvs|.
894
895 Raises:
896 DeployError: If a package is missing.
897 """
898 packages_dir = None
899 if strip:
900 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400901 cros_build_lib.run(
Ram Chandrasekar56152ec2021-11-22 17:10:41 +0000902 [os.path.join(constants.CHROMITE_SCRIPTS_DIR, 'strip_package'),
903 '--sysroot', sysroot] +
Alex Klein9742cb62020-10-12 19:22:10 +0000904 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700905 packages_dir = _STRIPPED_PACKAGES_DIR
906 except cros_build_lib.RunCommandError:
907 logging.error('Cannot strip packages %s',
Alex Klein9742cb62020-10-12 19:22:10 +0000908 ' '.join([str(cpv) for cpv in cpvs]))
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700909 raise
910
911 paths = []
912 for cpv in cpvs:
913 path = portage_util.GetBinaryPackagePath(
914 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
915 packages_dir=packages_dir)
916 if not path:
917 raise DeployError('Missing package %s.' % cpv)
918 paths.append(path)
919
920 return paths
921
922
923def _GetPackagesPaths(pkgs, strip, sysroot):
924 """Returns paths to binary |pkgs|.
925
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700926 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700927 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700928 strip: Whether or not to run strip_package for CPV packages.
929 sysroot: The sysroot path.
930
931 Returns:
932 List of paths corresponding to |pkgs|.
933 """
Alex Klein9742cb62020-10-12 19:22:10 +0000934 cpvs = [package_info.SplitCPV(p) for p in pkgs]
Ned Nguyend0db4072019-02-22 14:19:21 -0700935 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700936
937
Mike Frysinger22bb5502021-01-29 13:05:46 -0500938def _Unmerge(device, pkgs, root):
939 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -0700940
941 Args:
942 device: A RemoteDevice object.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500943 pkgs: Package names.
David Pursell9476bf42015-03-30 13:34:27 -0700944 root: Package installation root path.
945 """
Mike Frysinger22bb5502021-01-29 13:05:46 -0500946 pkg_names = ', '.join(os.path.basename(x) for x in pkgs)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700947 # This message is read by BrilloDeployOperation.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500948 logging.notice('Unmerging %s.', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700949 cmd = ['qmerge', '--yes']
950 # Check if qmerge is available on the device. If not, use emerge.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400951 if device.run(['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700952 cmd = ['emerge']
953
Mike Frysinger22bb5502021-01-29 13:05:46 -0500954 cmd += ['--unmerge', '--root', root]
955 cmd.extend('f={x}' for x in pkgs)
David Pursell9476bf42015-03-30 13:34:27 -0700956 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700957 # Always showing the emerge output for clarity.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400958 device.run(cmd, capture_output=False, remote_sudo=True,
959 debug_level=logging.INFO)
David Pursell9476bf42015-03-30 13:34:27 -0700960 except Exception:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500961 logging.error('Failed to unmerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700962 raise
963 else:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500964 # This message is read by BrilloDeployOperation.
965 logging.notice('Packages have been uninstalled.')
David Pursell9476bf42015-03-30 13:34:27 -0700966
967
968def _ConfirmDeploy(num_updates):
969 """Returns whether we can continue deployment."""
970 if num_updates > _MAX_UPDATES_NUM:
971 logging.warning(_MAX_UPDATES_WARNING)
972 return cros_build_lib.BooleanPrompt(default=False)
973
974 return True
975
976
Andrew06a5f812020-01-23 08:08:32 -0800977def _EmergePackages(pkgs, device, strip, sysroot, root, board, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800978 """Call _Emerge for each package in pkgs."""
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -0500979 if device.IsSELinuxAvailable():
980 enforced = device.IsSELinuxEnforced()
981 if enforced:
982 device.run(['setenforce', '0'])
983 else:
984 enforced = False
985
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700986 dlc_deployed = False
Mike Frysinger63d35512021-01-26 23:16:13 -0500987 # This message is read by BrilloDeployOperation.
988 logging.info('Preparing local packages for transfer.')
989 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
990 # Install all the packages in one pass so inter-package blockers work.
991 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
992 logging.info('Updating SELinux settings & DLC images.')
993 for pkg_path in pkg_paths:
Ben Pastene5f03b052019-08-12 18:03:24 -0700994 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900995 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800996
997 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
998 if dlc_id and dlc_package:
Andrew06a5f812020-01-23 08:08:32 -0800999 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001000 dlc_deployed = True
Mike Frysinger5f4c2742021-02-08 14:37:23 -05001001
1002 if dlc_deployed:
1003 # Clean up empty directories created by emerging DLCs.
1004 device.run(['test', '-d', '/build/rootfs', '&&', 'rmdir',
1005 '--ignore-fail-on-non-empty', '/build/rootfs', '/build'],
1006 check=False)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001007
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001008 if enforced:
1009 device.run(['setenforce', '1'])
1010
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001011 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1012 # we installed new DLC images).
1013 if dlc_deployed:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001014 device.run(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001015
1016
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001017def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001018 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001019 dlc_uninstalled = False
Mike Frysinger22bb5502021-01-29 13:05:46 -05001020 _Unmerge(device, pkgs, root)
1021 logging.info('Cleaning up DLC images.')
Ralph Nathane01ccf12015-04-16 10:40:32 -07001022 for pkg in pkgs:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001023 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1024 dlc_uninstalled = True
1025
1026 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1027 # uninstalled DLC images).
1028 if dlc_uninstalled:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001029 device.run(['restart', 'dlcservice'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001030
1031
1032def _UninstallDLCImage(device, pkg_attrs):
1033 """Uninstall a DLC image."""
1034 if _DLC_ID in pkg_attrs:
1035 dlc_id = pkg_attrs[_DLC_ID]
1036 logging.notice('Uninstalling DLC image for %s', dlc_id)
1037
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001038 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001039 return True
1040 else:
1041 logging.debug('DLC_ID not found in package')
1042 return False
1043
1044
Andrew06a5f812020-01-23 08:08:32 -08001045def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001046 """Deploy (install and mount) a DLC image."""
Andrew67b5fa72020-02-05 14:14:48 -08001047 # Build the DLC image if the image is outdated or doesn't exist.
Andrew5743d382020-06-16 09:55:04 -07001048 dlc_lib.InstallDlcImages(sysroot=sysroot, dlc_id=dlc_id, board=board)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001049
Andrewc7e1c6b2020-02-27 16:03:53 -08001050 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1051 try:
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001052 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Andrewc7e1c6b2020-02-27 16:03:53 -08001053 except cros_build_lib.RunCommandError as e:
Mike Frysingera2fac222022-08-17 21:06:33 -04001054 logging.info('Failed to uninstall DLC:%s. Continue anyway.', e.stderr)
Andrewc7e1c6b2020-02-27 16:03:53 -08001055 except Exception:
1056 logging.error('Failed to uninstall DLC.')
1057 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001058
Andrewc7e1c6b2020-02-27 16:03:53 -08001059 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1060 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1061 # location.
1062 logging.notice('Deploy the DLC image for %s', dlc_id)
Andrew5743d382020-06-16 09:55:04 -07001063 dlc_img_path_src = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1064 dlc_package, dlc_lib.DLC_IMAGE)
Andrewc7e1c6b2020-02-27 16:03:53 -08001065 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1066 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1067 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1068 # Create directories for DLC images.
1069 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1070 # Copy images to the destination directories.
1071 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
Andrew5743d382020-06-16 09:55:04 -07001072 dlc_lib.DLC_IMAGE),
Andrewc7e1c6b2020-02-27 16:03:53 -08001073 mode='rsync')
Andrew5743d382020-06-16 09:55:04 -07001074 device.run(['cp', os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1075 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE)])
Andrewc7e1c6b2020-02-27 16:03:53 -08001076
1077 # Set the proper perms and ownership so dlcservice can access the image.
1078 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1079 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001080
Andrew67b5fa72020-02-05 14:14:48 -08001081 # Copy metadata to device.
Andrew5743d382020-06-16 09:55:04 -07001082 dest_mata_dir = os.path.join('/', dlc_lib.DLC_META_DIR, dlc_id,
1083 dlc_package)
Andrew67b5fa72020-02-05 14:14:48 -08001084 device.run(['mkdir', '-p', dest_mata_dir])
Andrew5743d382020-06-16 09:55:04 -07001085 src_meta_dir = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1086 dlc_package, dlc_lib.DLC_TMP_META_DIR)
Andrew67b5fa72020-02-05 14:14:48 -08001087 device.CopyToDevice(src_meta_dir + '/',
1088 dest_mata_dir,
1089 mode='rsync',
1090 recursive=True,
1091 remote_sudo=True)
1092
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001093
1094def _GetDLCInfo(device, pkg_path, from_dut):
1095 """Returns information of a DLC given its package path.
1096
1097 Args:
1098 device: commandline.Device object; None to use the default device.
1099 pkg_path: path to the package.
1100 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1101 info from host.
1102
1103 Returns:
1104 A tuple (dlc_id, dlc_package).
1105 """
1106 environment_content = ''
1107 if from_dut:
1108 # On DUT, |pkg_path| is the directory which contains environment file.
1109 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001110 try:
1111 environment_data = device.CatFile(
1112 environment_path, max_size=None, encoding=None)
1113 except remote_access.CatFileError:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001114 # The package is not installed on DUT yet. Skip extracting info.
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001115 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001116 else:
1117 # On host, pkg_path is tbz2 file which contains environment file.
1118 # Extract the metadata of the package file.
1119 data = portage.xpak.tbz2(pkg_path).get_data()
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001120 environment_data = data[_ENVIRONMENT_FILENAME.encode('utf-8')]
1121
1122 # Extract the environment metadata.
1123 environment_content = bz2.decompress(environment_data)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001124
1125 with tempfile.NamedTemporaryFile() as f:
1126 # Dumps content into a file so we can use osutils.SourceEnvironment.
1127 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001128 osutils.WriteFile(path, environment_content, mode='wb')
Andrew67b5fa72020-02-05 14:14:48 -08001129 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE,
1130 _DLC_ENABLED))
1131
1132 dlc_enabled = content.get(_DLC_ENABLED)
1133 if dlc_enabled is not None and (dlc_enabled is False or
1134 str(dlc_enabled) == 'false'):
1135 logging.info('Installing DLC in rootfs.')
1136 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001137 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001138
1139
Gilad Arnolda0a98062015-07-07 08:34:27 -07001140def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1141 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1142 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1143 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001144 """Deploys packages to a device.
1145
1146 Args:
David Pursell2e773382015-04-03 14:30:47 -07001147 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001148 packages: List of packages (strings) to deploy to device.
1149 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001150 emerge: True to emerge package, False to unmerge.
1151 update: Check installed version on device.
1152 deep: Install dependencies also. Implies |update|.
1153 deep_rev: Install reverse dependencies. Implies |deep|.
1154 clean_binpkg: Clean outdated binary packages.
1155 root: Package installation root path.
1156 strip: Run strip_package to filter out preset paths in the package.
1157 emerge_args: Extra arguments to pass to emerge.
1158 ssh_private_key: Path to an SSH private key file; None to use test keys.
1159 ping: True to ping the device before trying to connect.
Sloan Johnsoncdd53b72022-06-07 20:29:24 +00001160 force: Ignore confidence checks and prompts.
David Pursell9476bf42015-03-30 13:34:27 -07001161 dry_run: Print deployment plan but do not deploy anything.
1162
1163 Raises:
1164 ValueError: Invalid parameter or parameter combination.
1165 DeployError: Unrecoverable failure during deploy.
1166 """
1167 if deep_rev:
1168 deep = True
1169 if deep:
1170 update = True
1171
Gilad Arnolda0a98062015-07-07 08:34:27 -07001172 if not packages:
1173 raise DeployError('No packages provided, nothing to deploy.')
1174
David Pursell9476bf42015-03-30 13:34:27 -07001175 if update and not emerge:
1176 raise ValueError('Cannot update and unmerge.')
1177
David Pursell2e773382015-04-03 14:30:47 -07001178 if device:
1179 hostname, username, port = device.hostname, device.username, device.port
1180 else:
1181 hostname, username, port = None, None, None
1182
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001183 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001184 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001185 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001186 # Somewhat confusing to clobber, but here we are.
1187 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001188 with remote_access.ChromiumOSDeviceHandler(
1189 hostname, port=port, username=username, private_key=ssh_private_key,
1190 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001191 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001192
Gilad Arnolda0a98062015-07-07 08:34:27 -07001193 board = cros_build_lib.GetBoard(device_board=device.board,
1194 override_board=board)
1195 if not force and board != device.board:
1196 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001197 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001198
Mike Frysinger06a51c82021-04-06 11:39:17 -04001199 sysroot = build_target_lib.get_default_sysroot_path(board)
David Pursell9476bf42015-03-30 13:34:27 -07001200
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001201 # Don't bother trying to clean for unmerges. We won't use the local db,
1202 # and it just slows things down for the user.
1203 if emerge and clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001204 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001205 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001206
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001207 # Remount rootfs as writable if necessary.
1208 if not device.MountRootfsReadWrite():
1209 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001210
1211 # Obtain list of packages to upgrade/remove.
1212 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001213 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001214 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001215 if emerge:
1216 action_str = 'emerge'
1217 else:
1218 pkgs.reverse()
1219 action_str = 'unmerge'
1220
1221 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001222 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001223 return
1224
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001225 # Warn when the user installs & didn't `cros workon start`.
1226 if emerge:
Brian Norris2eee8892021-04-06 16:23:23 -07001227 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(use_all=True)
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001228 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1229 for package in listed:
1230 cp = package_info.SplitCPV(package).cp
Brian Norris2eee8892021-04-06 16:23:23 -07001231 if cp in all_workon and cp not in worked_on_cps:
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001232 logging.warning(
1233 'Are you intentionally deploying unmodified packages, or did '
1234 'you forget to run `cros workon --board=$BOARD start %s`?', cp)
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +09001235
Ralph Nathane01ccf12015-04-16 10:40:32 -07001236 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001237 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001238 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001239
1240 if dry_run or not _ConfirmDeploy(num_updates):
1241 return
1242
Ralph Nathane01ccf12015-04-16 10:40:32 -07001243 # Select function (emerge or unmerge) and bind args.
1244 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001245 func = functools.partial(_EmergePackages, pkgs, device, strip,
Andrew06a5f812020-01-23 08:08:32 -08001246 sysroot, root, board, emerge_args)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001247 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001248 func = functools.partial(_UnmergePackages, pkgs, device, root,
1249 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001250
1251 # Call the function with the progress bar or with normal output.
1252 if command.UseProgressBar():
Mike Frysinger63d35512021-01-26 23:16:13 -05001253 op = BrilloDeployOperation(emerge)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001254 op.Run(func, log_level=logging.DEBUG)
1255 else:
1256 func()
David Pursell9476bf42015-03-30 13:34:27 -07001257
Ben Pastene5f03b052019-08-12 18:03:24 -07001258 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001259 if sum(x.count('selinux-policy') for x in pkgs):
1260 logging.warning(
1261 'Deploying SELinux policy will not take effect until reboot. '
Ian Barkley-Yeung6b2d8672020-08-13 18:58:10 -07001262 'SELinux policy is loaded by init. Also, changing the security '
1263 'contexts (labels) of a file will require building a new image '
1264 'and flashing the image onto the device.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001265
Mike Frysinger63d35512021-01-26 23:16:13 -05001266 # This message is read by BrilloDeployOperation.
David Pursell9476bf42015-03-30 13:34:27 -07001267 logging.warning('Please restart any updated services on the device, '
1268 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001269 except Exception:
1270 if lsb_release:
1271 lsb_entries = sorted(lsb_release.items())
1272 logging.info('Following are the LSB version details of the device:\n%s',
1273 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1274 raise