blob: f981c90ed40fffce922a1a3ae445137fac03e4eb [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
David Pursell9476bf42015-03-30 13:34:27 -07002# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Alex Kleinaaddc932020-01-30 15:02:24 -07006"""Deploy packages onto a target device.
7
8Integration tests for this file can be found at cli/cros/tests/cros_vm_tests.py.
9See that file for more information.
10"""
David Pursell9476bf42015-03-30 13:34:27 -070011
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040012from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -070013from __future__ import print_function
14
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070015import bz2
David Pursell9476bf42015-03-30 13:34:27 -070016import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070017import functools
David Pursell9476bf42015-03-30 13:34:27 -070018import json
19import os
Mike Frysinger3f087aa2020-03-20 06:03:16 -040020import sys
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070021import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070022
Ralph Nathane01ccf12015-04-16 10:40:32 -070023from chromite.cli import command
David Pursell9476bf42015-03-30 13:34:27 -070024from chromite.lib import cros_build_lib
25from chromite.lib import cros_logging as logging
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
Andrew5743d382020-06-16 09:55:04 -070030from chromite.lib import dlc_lib
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +090031from chromite.lib import workon_helper
David Pursell9476bf42015-03-30 13:34:27 -070032try:
33 import portage
34except ImportError:
35 if cros_build_lib.IsInsideChroot():
36 raise
37
38
Mike Frysinger3f087aa2020-03-20 06:03:16 -040039assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
40
41
David Pursell9476bf42015-03-30 13:34:27 -070042_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
43# This is defined in src/platform/dev/builder.py
44_STRIPPED_PACKAGES_DIR = 'stripped-packages'
45
46_MAX_UPDATES_NUM = 10
47_MAX_UPDATES_WARNING = (
48 '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
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070053_DLC_ID = 'DLC_ID'
54_DLC_PACKAGE = 'DLC_PACKAGE'
Andrew67b5fa72020-02-05 14:14:48 -080055_DLC_ENABLED = 'DLC_ENABLED'
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070056_ENVIRONMENT_FILENAME = 'environment.bz2'
57_DLC_INSTALL_ROOT = '/var/cache/dlc'
58
David Pursell9476bf42015-03-30 13:34:27 -070059
60class DeployError(Exception):
61 """Thrown when an unrecoverable error is encountered during deploy."""
62
63
Ralph Nathane01ccf12015-04-16 10:40:32 -070064class BrilloDeployOperation(operation.ProgressBarOperation):
65 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070066 # These two variables are used to validate the output in the VM integration
67 # tests. Changes to the output must be reflected here.
68 MERGE_EVENTS = ['NOTICE: Copying', 'NOTICE: Installing', 'WARNING: Ignoring',
Achuith Bhandarkar0487c312019-04-22 12:19:25 -070069 'emerge --usepkg', 'has been installed.']
Ralph Nathan90475a12015-05-20 13:19:01 -070070 UNMERGE_EVENTS = ['NOTICE: Unmerging', 'has been uninstalled.']
Ralph Nathane01ccf12015-04-16 10:40:32 -070071
72 def __init__(self, pkg_count, emerge):
73 """Construct BrilloDeployOperation object.
74
75 Args:
76 pkg_count: number of packages being built.
77 emerge: True if emerge, False is unmerge.
78 """
79 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070080 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070081 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070082 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070083 self._events = self.UNMERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070084 self._total = pkg_count * len(self._events)
85 self._completed = 0
86
Ralph Nathandc14ed92015-04-22 11:17:40 -070087 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070088 """Parse the output of brillo deploy to update a progress bar."""
89 stdout = self._stdout.read()
90 stderr = self._stderr.read()
91 output = stdout + stderr
92 for event in self._events:
93 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040094 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -070095
96
David Pursell9476bf42015-03-30 13:34:27 -070097class _InstallPackageScanner(object):
98 """Finds packages that need to be installed on a target device.
99
100 Scans the sysroot bintree, beginning with a user-provided list of packages,
101 to find all packages that need to be installed. If so instructed,
102 transitively scans forward (mandatory) and backward (optional) dependencies
103 as well. A package will be installed if missing on the target (mandatory
104 packages only), or it will be updated if its sysroot version and build time
105 are different from the target. Common usage:
106
107 pkg_scanner = _InstallPackageScanner(sysroot)
108 pkgs = pkg_scanner.Run(...)
109 """
110
111 class VartreeError(Exception):
112 """An error in the processing of the installed packages tree."""
113
114 class BintreeError(Exception):
115 """An error in the processing of the source binpkgs tree."""
116
117 class PkgInfo(object):
118 """A record containing package information."""
119
120 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
121
122 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
123 self.cpv = cpv
124 self.build_time = build_time
125 self.rdeps_raw = rdeps_raw
126 self.rdeps = set() if rdeps is None else rdeps
127 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
128
129 # Python snippet for dumping vartree info on the target. Instantiate using
130 # _GetVartreeSnippet().
131 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700132import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700133import os
134import portage
135
136# Normalize the path to match what portage will index.
137target_root = os.path.normpath('%(root)s')
138if not target_root.endswith('/'):
139 target_root += '/'
140trees = portage.create_trees(target_root=target_root, config_root='/')
141vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700142pkg_info = []
143for cpv in vartree.dbapi.cpv_all():
144 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
145 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
146 pkg_info.append((cpv, slot, rdep_raw, build_time))
147
148print(json.dumps(pkg_info))
149"""
150
151 def __init__(self, sysroot):
152 self.sysroot = sysroot
153 # Members containing the sysroot (binpkg) and target (installed) package DB.
154 self.target_db = None
155 self.binpkgs_db = None
156 # Members for managing the dependency resolution work queue.
157 self.queue = None
158 self.seen = None
159 self.listed = None
160
161 @staticmethod
162 def _GetCP(cpv):
163 """Returns the CP value for a given CPV string."""
164 attrs = portage_util.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600165 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700166 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600167 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700168
169 @staticmethod
170 def _InDB(cp, slot, db):
171 """Returns whether CP and slot are found in a database (if provided)."""
172 cp_slots = db.get(cp) if db else None
173 return cp_slots is not None and (not slot or slot in cp_slots)
174
175 @staticmethod
176 def _AtomStr(cp, slot):
177 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
178 return '%s:%s' % (cp, slot) if slot else cp
179
180 @classmethod
181 def _GetVartreeSnippet(cls, root='/'):
182 """Returns a code snippet for dumping the vartree on the target.
183
184 Args:
185 root: The installation root.
186
187 Returns:
188 The said code snippet (string) with parameters filled in.
189 """
190 return cls._GET_VARTREE % {'root': root}
191
192 @classmethod
193 def _StripDepAtom(cls, dep_atom, installed_db=None):
194 """Strips a dependency atom and returns a (CP, slot) pair."""
195 # TODO(garnold) This is a gross simplification of ebuild dependency
196 # semantics, stripping and ignoring various qualifiers (versions, slots,
197 # USE flag, negation) and will likely need to be fixed. chromium:447366.
198
199 # Ignore unversioned blockers, leaving them for the user to resolve.
200 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
201 return None, None
202
203 cp = dep_atom
204 slot = None
205 require_installed = False
206
207 # Versioned blockers should be updated, but only if already installed.
208 # These are often used for forcing cascaded updates of multiple packages,
209 # so we're treating them as ordinary constraints with hopes that it'll lead
210 # to the desired result.
211 if cp.startswith('!'):
212 cp = cp.lstrip('!')
213 require_installed = True
214
215 # Remove USE flags.
216 if '[' in cp:
217 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
218
219 # Separate the slot qualifier and strip off subslots.
220 if ':' in cp:
221 cp, slot = cp.split(':')
222 for delim in ('/', '='):
223 slot = slot.split(delim, 1)[0]
224
225 # Strip version wildcards (right), comparators (left).
226 cp = cp.rstrip('*')
227 cp = cp.lstrip('<=>~')
228
229 # Turn into CP form.
230 cp = cls._GetCP(cp)
231
232 if require_installed and not cls._InDB(cp, None, installed_db):
233 return None, None
234
235 return cp, slot
236
237 @classmethod
238 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
239 """Resolves and returns a list of dependencies from a dependency string.
240
241 This parses a dependency string and returns a list of package names and
242 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
243 resolving disjunctive deps, we include all choices that are fully present
244 in |installed_db|. If none is present, we choose an arbitrary one that is
245 available.
246
247 Args:
248 dep_str: A raw dependency string.
249 installed_db: A database of installed packages.
250 avail_db: A database of packages available for installation.
251
252 Returns:
253 A list of pairs (CP, slot).
254
255 Raises:
256 ValueError: the dependencies string is malformed.
257 """
258 def ProcessSubDeps(dep_exp, disjunct):
259 """Parses and processes a dependency (sub)expression."""
260 deps = set()
261 default_deps = set()
262 sub_disjunct = False
263 for dep_sub_exp in dep_exp:
264 sub_deps = set()
265
266 if isinstance(dep_sub_exp, (list, tuple)):
267 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
268 sub_disjunct = False
269 elif sub_disjunct:
270 raise ValueError('Malformed disjunctive operation in deps')
271 elif dep_sub_exp == '||':
272 sub_disjunct = True
273 elif dep_sub_exp.endswith('?'):
274 raise ValueError('Dependencies contain a conditional')
275 else:
276 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
277 if cp:
278 sub_deps = set([(cp, slot)])
279 elif disjunct:
280 raise ValueError('Atom in disjunct ignored')
281
282 # Handle sub-deps of a disjunctive expression.
283 if disjunct:
284 # Make the first available choice the default, for use in case that
285 # no option is installed.
286 if (not default_deps and avail_db is not None and
287 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
288 default_deps = sub_deps
289
290 # If not all sub-deps are installed, then don't consider them.
291 if not all([cls._InDB(cp, slot, installed_db)
292 for cp, slot in sub_deps]):
293 sub_deps = set()
294
295 deps.update(sub_deps)
296
297 return deps or default_deps
298
299 try:
300 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
301 except portage.exception.InvalidDependString as e:
302 raise ValueError('Invalid dep string: %s' % e)
303 except ValueError as e:
304 raise ValueError('%s: %s' % (e, dep_str))
305
306 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
307 installed_db=None):
308 """Returns a database of packages given a list of CPV info.
309
310 Args:
311 cpv_info: A list of tuples containing package CPV and attributes.
312 process_rdeps: Whether to populate forward dependencies.
313 process_rev_rdeps: Whether to populate reverse dependencies.
314 installed_db: A database of installed packages for filtering disjunctive
315 choices against; if None, using own built database.
316
317 Returns:
318 A map from CP values to another dictionary that maps slots to package
319 attribute tuples. Tuples contain a CPV value (string), build time
320 (string), runtime dependencies (set), and reverse dependencies (set,
321 empty if not populated).
322
323 Raises:
324 ValueError: If more than one CPV occupies a single slot.
325 """
326 db = {}
327 logging.debug('Populating package DB...')
328 for cpv, slot, rdeps_raw, build_time in cpv_info:
329 cp = self._GetCP(cpv)
330 cp_slots = db.setdefault(cp, dict())
331 if slot in cp_slots:
332 raise ValueError('More than one package found for %s' %
333 self._AtomStr(cp, slot))
334 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
335 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
336 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
337
338 avail_db = db
339 if installed_db is None:
340 installed_db = db
341 avail_db = None
342
343 # Add approximate forward dependencies.
344 if process_rdeps:
345 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400346 for cp, cp_slots in db.items():
347 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700348 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
349 installed_db, avail_db))
350 logging.debug(' %s (%s) processed rdeps: %s',
351 self._AtomStr(cp, slot), pkg_info.cpv,
352 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
353 for rdep_cp, rdep_slot in pkg_info.rdeps]))
354
355 # Add approximate reverse dependencies (optional).
356 if process_rev_rdeps:
357 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400358 for cp, cp_slots in db.items():
359 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700360 for rdep_cp, rdep_slot in pkg_info.rdeps:
361 to_slots = db.get(rdep_cp)
362 if not to_slots:
363 continue
364
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400365 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700366 if rdep_slot and to_slot != rdep_slot:
367 continue
368 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
369 self._AtomStr(cp, slot), pkg_info.cpv,
370 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
371 to_pkg_info.rev_rdeps.add((cp, slot))
372
373 return db
374
375 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
376 """Initializes a dictionary of packages installed on |device|."""
377 get_vartree_script = self._GetVartreeSnippet(root)
378 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400379 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700380 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700381 except cros_build_lib.RunCommandError as e:
382 logging.error('Cannot get target vartree:\n%s', e.result.error)
383 raise
384
385 try:
386 self.target_db = self._BuildDB(json.loads(result.output),
387 process_rdeps, process_rev_rdeps)
388 except ValueError as e:
389 raise self.VartreeError(str(e))
390
391 def _InitBinpkgDB(self, process_rdeps):
392 """Initializes a dictionary of binary packages for updating the target."""
393 # Get build root trees; portage indexes require a trailing '/'.
394 build_root = os.path.join(self.sysroot, '')
395 trees = portage.create_trees(target_root=build_root, config_root=build_root)
396 bintree = trees[build_root]['bintree']
397 binpkgs_info = []
398 for cpv in bintree.dbapi.cpv_all():
399 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
400 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
401 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
402
403 try:
404 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
405 installed_db=self.target_db)
406 except ValueError as e:
407 raise self.BintreeError(str(e))
408
409 def _InitDepQueue(self):
410 """Initializes the dependency work queue."""
411 self.queue = set()
412 self.seen = {}
413 self.listed = set()
414
415 def _EnqDep(self, dep, listed, optional):
416 """Enqueues a dependency if not seen before or if turned non-optional."""
417 if dep in self.seen and (optional or not self.seen[dep]):
418 return False
419
420 self.queue.add(dep)
421 self.seen[dep] = optional
422 if listed:
423 self.listed.add(dep)
424 return True
425
426 def _DeqDep(self):
427 """Dequeues and returns a dependency, its listed and optional flags.
428
429 This returns listed packages first, if any are present, to ensure that we
430 correctly mark them as such when they are first being processed.
431 """
432 if self.listed:
433 dep = self.listed.pop()
434 self.queue.remove(dep)
435 listed = True
436 else:
437 dep = self.queue.pop()
438 listed = False
439
440 return dep, listed, self.seen[dep]
441
442 def _FindPackageMatches(self, cpv_pattern):
443 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
444
445 This is breaking |cpv_pattern| into its C, P and V components, each of
446 which may or may not be present or contain wildcards. It then scans the
447 binpkgs database to find all atoms that match these components, returning a
448 list of CP and slot qualifier. When the pattern does not specify a version,
449 or when a CP has only one slot in the binpkgs database, we omit the slot
450 qualifier in the result.
451
452 Args:
453 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
454
455 Returns:
456 A list of (CPV, slot) pairs of packages in the binpkgs database that
457 match the pattern.
458 """
459 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
460 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
461 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400462 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700463 if not fnmatch.fnmatchcase(cp, cp_pattern):
464 continue
465
466 # If no version attribute was given or there's only one slot, omit the
467 # slot qualifier.
468 if not attrs.version or len(cp_slots) == 1:
469 matches.append((cp, None))
470 else:
471 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400472 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700473 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
474 matches.append((cp, slot))
475
476 return matches
477
478 def _FindPackage(self, pkg):
479 """Returns the (CP, slot) pair for a package matching |pkg|.
480
481 Args:
482 pkg: Path to a binary package or a (partial) package CPV specifier.
483
484 Returns:
485 A (CP, slot) pair for the given package; slot may be None (unspecified).
486
487 Raises:
488 ValueError: if |pkg| is not a binpkg file nor does it match something
489 that's in the bintree.
490 """
491 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
492 package = os.path.basename(os.path.splitext(pkg)[0])
493 category = os.path.basename(os.path.dirname(pkg))
494 return self._GetCP(os.path.join(category, package)), None
495
496 matches = self._FindPackageMatches(pkg)
497 if not matches:
498 raise ValueError('No package found for %s' % pkg)
499
500 idx = 0
501 if len(matches) > 1:
502 # Ask user to pick among multiple matches.
503 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
504 ['%s:%s' % (cp, slot) if slot else cp
505 for cp, slot in matches])
506
507 return matches[idx]
508
509 def _NeedsInstall(self, cpv, slot, build_time, optional):
510 """Returns whether a package needs to be installed on the target.
511
512 Args:
513 cpv: Fully qualified CPV (string) of the package.
514 slot: Slot identifier (string).
515 build_time: The BUILT_TIME value (string) of the binpkg.
516 optional: Whether package is optional on the target.
517
518 Returns:
519 A tuple (install, update) indicating whether to |install| the package and
520 whether it is an |update| to an existing package.
521
522 Raises:
523 ValueError: if slot is not provided.
524 """
525 # If not checking installed packages, always install.
526 if not self.target_db:
527 return True, False
528
529 cp = self._GetCP(cpv)
530 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
531 if target_pkg_info is not None:
532 if cpv != target_pkg_info.cpv:
533 attrs = portage_util.SplitCPV(cpv)
534 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
535 logging.debug('Updating %s: version (%s) different on target (%s)',
536 cp, attrs.version, target_attrs.version)
537 return True, True
538
539 if build_time != target_pkg_info.build_time:
540 logging.debug('Updating %s: build time (%s) different on target (%s)',
541 cpv, build_time, target_pkg_info.build_time)
542 return True, True
543
544 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
545 cp, target_pkg_info.cpv, target_pkg_info.build_time)
546 return False, False
547
548 if optional:
549 logging.debug('Not installing %s: missing on target but optional', cp)
550 return False, False
551
552 logging.debug('Installing %s: missing on target and non-optional (%s)',
553 cp, cpv)
554 return True, False
555
556 def _ProcessDeps(self, deps, reverse):
557 """Enqueues dependencies for processing.
558
559 Args:
560 deps: List of dependencies to enqueue.
561 reverse: Whether these are reverse dependencies.
562 """
563 if not deps:
564 return
565
566 logging.debug('Processing %d %s dep(s)...', len(deps),
567 'reverse' if reverse else 'forward')
568 num_already_seen = 0
569 for dep in deps:
570 if self._EnqDep(dep, False, reverse):
571 logging.debug(' Queued dep %s', dep)
572 else:
573 num_already_seen += 1
574
575 if num_already_seen:
576 logging.debug('%d dep(s) already seen', num_already_seen)
577
578 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
579 """Returns a dictionary of packages that need to be installed on the target.
580
581 Args:
582 process_rdeps: Whether to trace forward dependencies.
583 process_rev_rdeps: Whether to trace backward dependencies as well.
584
585 Returns:
586 A dictionary mapping CP values (string) to tuples containing a CPV
587 (string), a slot (string), a boolean indicating whether the package
588 was initially listed in the queue, and a boolean indicating whether this
589 is an update to an existing package.
590 """
591 installs = {}
592 while self.queue:
593 dep, listed, optional = self._DeqDep()
594 cp, required_slot = dep
595 if cp in installs:
596 logging.debug('Already updating %s', cp)
597 continue
598
599 cp_slots = self.binpkgs_db.get(cp, dict())
600 logging.debug('Checking packages matching %s%s%s...', cp,
601 ' (slot: %s)' % required_slot if required_slot else '',
602 ' (optional)' if optional else '')
603 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400604 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700605 if required_slot and slot != required_slot:
606 continue
607
608 num_processed += 1
609 logging.debug(' Checking %s...', pkg_info.cpv)
610
611 install, update = self._NeedsInstall(pkg_info.cpv, slot,
612 pkg_info.build_time, optional)
613 if not install:
614 continue
615
616 installs[cp] = (pkg_info.cpv, slot, listed, update)
617
618 # Add forward and backward runtime dependencies to queue.
619 if process_rdeps:
620 self._ProcessDeps(pkg_info.rdeps, False)
621 if process_rev_rdeps:
622 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
623 if target_pkg_info:
624 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
625
626 if num_processed == 0:
627 logging.warning('No qualified bintree package corresponding to %s', cp)
628
629 return installs
630
631 def _SortInstalls(self, installs):
632 """Returns a sorted list of packages to install.
633
634 Performs a topological sort based on dependencies found in the binary
635 package database.
636
637 Args:
638 installs: Dictionary of packages to install indexed by CP.
639
640 Returns:
641 A list of package CPVs (string).
642
643 Raises:
644 ValueError: If dependency graph contains a cycle.
645 """
646 not_visited = set(installs.keys())
647 curr_path = []
648 sorted_installs = []
649
650 def SortFrom(cp):
651 """Traverses dependencies recursively, emitting nodes in reverse order."""
652 cpv, slot, _, _ = installs[cp]
653 if cpv in curr_path:
654 raise ValueError('Dependencies contain a cycle: %s -> %s' %
655 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
656 curr_path.append(cpv)
657 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
658 if rdep_cp in not_visited:
659 not_visited.remove(rdep_cp)
660 SortFrom(rdep_cp)
661
662 sorted_installs.append(cpv)
663 curr_path.pop()
664
665 # So long as there's more packages, keep expanding dependency paths.
666 while not_visited:
667 SortFrom(not_visited.pop())
668
669 return sorted_installs
670
671 def _EnqListedPkg(self, pkg):
672 """Finds and enqueues a listed package."""
673 cp, slot = self._FindPackage(pkg)
674 if cp not in self.binpkgs_db:
675 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
676 self._EnqDep((cp, slot), True, False)
677
678 def _EnqInstalledPkgs(self):
679 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400680 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700681 target_cp_slots = self.target_db.get(cp)
682 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400683 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700684 if slot in target_cp_slots:
685 self._EnqDep((cp, slot), True, False)
686
687 def Run(self, device, root, listed_pkgs, update, process_rdeps,
688 process_rev_rdeps):
689 """Computes the list of packages that need to be installed on a target.
690
691 Args:
692 device: Target handler object.
693 root: Package installation root.
694 listed_pkgs: Package names/files listed by the user.
695 update: Whether to read the target's installed package database.
696 process_rdeps: Whether to trace forward dependencies.
697 process_rev_rdeps: Whether to trace backward dependencies as well.
698
699 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700700 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
701 list of package CPVs (string) to install on the target in an order that
702 satisfies their inter-dependencies, |listed| the subset that was
703 requested by the user, and |num_updates| the number of packages being
704 installed over preexisting versions. Note that installation order should
705 be reversed for removal, |install_attrs| is a dictionary mapping a package
706 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700707 """
708 if process_rev_rdeps and not process_rdeps:
709 raise ValueError('Must processing forward deps when processing rev deps')
710 if process_rdeps and not update:
711 raise ValueError('Must check installed packages when processing deps')
712
713 if update:
714 logging.info('Initializing target intalled packages database...')
715 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
716
717 logging.info('Initializing binary packages database...')
718 self._InitBinpkgDB(process_rdeps)
719
720 logging.info('Finding listed package(s)...')
721 self._InitDepQueue()
722 for pkg in listed_pkgs:
723 if pkg == '@installed':
724 if not update:
725 raise ValueError(
726 'Must check installed packages when updating all of them.')
727 self._EnqInstalledPkgs()
728 else:
729 self._EnqListedPkg(pkg)
730
731 logging.info('Computing set of packages to install...')
732 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
733
734 num_updates = 0
735 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400736 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700737 if listed:
738 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400739 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700740 num_updates += 1
741
742 logging.info('Processed %d package(s), %d will be installed, %d are '
743 'updating existing packages',
744 len(self.seen), len(installs), num_updates)
745
746 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700747
748 install_attrs = {}
749 for pkg in sorted_installs:
Mike Frysingerada2d1c2020-03-20 05:02:06 -0400750 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700751 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
752 install_attrs[pkg] = {}
753 if dlc_id and dlc_package:
754 install_attrs[pkg][_DLC_ID] = dlc_id
755
756 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700757
758
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700759def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700760 """Copies |pkg| to |device| and emerges it.
761
762 Args:
763 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700764 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700765 root: Package installation root path.
766 extra_args: Extra arguments to pass to emerge.
767
768 Raises:
769 DeployError: Unrecoverable error during emerge.
770 """
David Pursell9476bf42015-03-30 13:34:27 -0700771 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700772 pkg_name = os.path.basename(pkg_path)
773 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700774 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400775 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
776 # Clean out the dirs first if we had a previous emerge on the device so as to
777 # free up space for this emerge. The last emerge gets implicitly cleaned up
778 # when the device connection deletes its work_dir.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400779 device.run(
Mike Frysinger15a4e012015-05-21 22:18:45 -0400780 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
781 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700782
Ralph Nathane01ccf12015-04-16 10:40:32 -0700783 # This message is read by BrilloDeployOperation.
784 logging.notice('Copying %s to device.', pkg_name)
Ilja H. Friedel0ab63e12017-03-28 13:29:48 -0700785 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700786
David Pursell9476bf42015-03-30 13:34:27 -0700787 logging.info('Use portage temp dir %s', portage_tmpdir)
788
Ralph Nathane01ccf12015-04-16 10:40:32 -0700789 # This message is read by BrilloDeployOperation.
790 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700791 pkg_path = os.path.join(pkg_dir, pkg_name)
792
793 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
794 # chromeos-base packages will be skipped due to the configuration
795 # in /etc/protage/make.profile/package.provided. However, there is
796 # a known bug that /usr/local/etc/portage is not setup properly
797 # (crbug.com/312041). This does not affect `cros deploy` because
798 # we do not use the preset PKGDIR.
799 extra_env = {
800 'FEATURES': '-sandbox',
801 'PKGDIR': pkgroot,
802 'PORTAGE_CONFIGROOT': '/usr/local',
803 'PORTAGE_TMPDIR': portage_tmpdir,
804 'PORTDIR': device.work_dir,
805 'CONFIG_PROTECT': '-*',
806 }
Alex Kleinaaddc932020-01-30 15:02:24 -0700807 # --ignore-built-slot-operator-deps because we don't rebuild everything.
808 # It can cause errors, but that's expected with cros deploy since it's just a
809 # best effort to prevent developers avoid rebuilding an image every time.
810 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', pkg_path,
811 '--root=%s' % root]
David Pursell9476bf42015-03-30 13:34:27 -0700812 if extra_args:
813 cmd.append(extra_args)
814
Alex Kleinaaddc932020-01-30 15:02:24 -0700815 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
816 'packages built against the old version may not be able to '
817 'load the new .so. This is expected, and you will just need '
818 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700819 try:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400820 result = device.run(cmd, extra_env=extra_env, remote_sudo=True,
821 capture_output=True, debug_level=logging.INFO)
Greg Kerrb96c02c2019-02-08 14:32:41 -0800822
823 pattern = ('A requested package will not be merged because '
824 'it is listed in package.provided')
825 output = result.error.replace('\n', ' ').replace('\r', '')
826 if pattern in output:
827 error = ('Package failed to emerge: %s\n'
828 'Remove %s from /etc/portage/make.profile/'
829 'package.provided/chromeos-base.packages\n'
830 '(also see crbug.com/920140 for more context)\n'
831 % (pattern, pkg_name))
832 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700833 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700834 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700835 raise
836 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700837 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700838
839
Qijiang Fand5958192019-07-26 12:32:36 +0900840def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800841 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900842
843 This reads the tarball from pkgpath, and calls restorecon on device to
844 restore SELinux context for files listed in the tarball, assuming those files
845 are installed to /
846
847 Args:
848 device: a ChromiumOSDevice object
849 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900850 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900851 """
Ben Pastene5f03b052019-08-12 18:03:24 -0700852 enforced = device.IsSELinuxEnforced()
Qijiang Fan8a945032019-04-25 20:53:29 +0900853 if enforced:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400854 device.run(['setenforce', '0'])
Qijiang Fan8a945032019-04-25 20:53:29 +0900855 pkgroot = os.path.join(device.work_dir, 'packages')
856 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
857 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
858 # Testing shows restorecon splits on newlines instead of spaces.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400859 device.run(
Qijiang Fand5958192019-07-26 12:32:36 +0900860 ['cd', root, '&&',
861 'tar', 'tf', pkgpath_device, '|',
862 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900863 remote_sudo=True)
864 if enforced:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400865 device.run(['setenforce', '1'])
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900866
867
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700868def _GetPackagesByCPV(cpvs, strip, sysroot):
869 """Returns paths to binary packages corresponding to |cpvs|.
870
871 Args:
872 cpvs: List of CPV components given by portage_util.SplitCPV().
873 strip: True to run strip_package.
874 sysroot: Sysroot path.
875
876 Returns:
877 List of paths corresponding to |cpvs|.
878
879 Raises:
880 DeployError: If a package is missing.
881 """
882 packages_dir = None
883 if strip:
884 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400885 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700886 ['strip_package', '--sysroot', sysroot] +
Alex Klein7078e252018-10-02 10:21:04 -0600887 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700888 packages_dir = _STRIPPED_PACKAGES_DIR
889 except cros_build_lib.RunCommandError:
890 logging.error('Cannot strip packages %s',
891 ' '.join([str(cpv) for cpv in cpvs]))
892 raise
893
894 paths = []
895 for cpv in cpvs:
896 path = portage_util.GetBinaryPackagePath(
897 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
898 packages_dir=packages_dir)
899 if not path:
900 raise DeployError('Missing package %s.' % cpv)
901 paths.append(path)
902
903 return paths
904
905
906def _GetPackagesPaths(pkgs, strip, sysroot):
907 """Returns paths to binary |pkgs|.
908
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700909 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700910 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700911 strip: Whether or not to run strip_package for CPV packages.
912 sysroot: The sysroot path.
913
914 Returns:
915 List of paths corresponding to |pkgs|.
916 """
Ned Nguyend0db4072019-02-22 14:19:21 -0700917 cpvs = [portage_util.SplitCPV(p) for p in pkgs]
918 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700919
920
David Pursell9476bf42015-03-30 13:34:27 -0700921def _Unmerge(device, pkg, root):
922 """Unmerges |pkg| on |device|.
923
924 Args:
925 device: A RemoteDevice object.
926 pkg: A package name.
927 root: Package installation root path.
928 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700929 pkg_name = os.path.basename(pkg)
930 # This message is read by BrilloDeployOperation.
931 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700932 cmd = ['qmerge', '--yes']
933 # Check if qmerge is available on the device. If not, use emerge.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400934 if device.run(['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700935 cmd = ['emerge']
936
937 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
938 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700939 # Always showing the emerge output for clarity.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400940 device.run(cmd, capture_output=False, remote_sudo=True,
941 debug_level=logging.INFO)
David Pursell9476bf42015-03-30 13:34:27 -0700942 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700943 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700944 raise
945 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700946 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700947
948
949def _ConfirmDeploy(num_updates):
950 """Returns whether we can continue deployment."""
951 if num_updates > _MAX_UPDATES_NUM:
952 logging.warning(_MAX_UPDATES_WARNING)
953 return cros_build_lib.BooleanPrompt(default=False)
954
955 return True
956
957
Andrew06a5f812020-01-23 08:08:32 -0800958def _EmergePackages(pkgs, device, strip, sysroot, root, board, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800959 """Call _Emerge for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700960 dlc_deployed = False
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700961 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
962 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Ben Pastene5f03b052019-08-12 18:03:24 -0700963 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900964 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800965
966 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
967 if dlc_id and dlc_package:
Andrew06a5f812020-01-23 08:08:32 -0800968 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700969 dlc_deployed = True
Andrew67b5fa72020-02-05 14:14:48 -0800970 # Clean up empty directories created by emerging DLCs.
971 device.run(['test', '-d', '/build/rootfs', '&&', 'rmdir',
972 '--ignore-fail-on-non-empty', '/build/rootfs', '/build'],
973 check=False)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700974
975 # Restart dlcservice so it picks up the newly installed DLC modules (in case
976 # we installed new DLC images).
977 if dlc_deployed:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400978 device.run(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -0700979
980
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700981def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -0700982 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700983 dlc_uninstalled = False
Ralph Nathane01ccf12015-04-16 10:40:32 -0700984 for pkg in pkgs:
985 _Unmerge(device, pkg, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700986 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
987 dlc_uninstalled = True
988
989 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
990 # uninstalled DLC images).
991 if dlc_uninstalled:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400992 device.run(['restart', 'dlcservice'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700993
994
995def _UninstallDLCImage(device, pkg_attrs):
996 """Uninstall a DLC image."""
997 if _DLC_ID in pkg_attrs:
998 dlc_id = pkg_attrs[_DLC_ID]
999 logging.notice('Uninstalling DLC image for %s', dlc_id)
1000
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001001 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001002 return True
1003 else:
1004 logging.debug('DLC_ID not found in package')
1005 return False
1006
1007
Andrew06a5f812020-01-23 08:08:32 -08001008def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001009 """Deploy (install and mount) a DLC image."""
Andrew67b5fa72020-02-05 14:14:48 -08001010 # Build the DLC image if the image is outdated or doesn't exist.
Andrew5743d382020-06-16 09:55:04 -07001011 dlc_lib.InstallDlcImages(sysroot=sysroot, dlc_id=dlc_id, board=board)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001012
Andrewc7e1c6b2020-02-27 16:03:53 -08001013 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1014 try:
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001015 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Andrewc7e1c6b2020-02-27 16:03:53 -08001016 except cros_build_lib.RunCommandError as e:
1017 logging.info('Failed to uninstall DLC:%s. Continue anyway.',
1018 e.result.error)
1019 except Exception:
1020 logging.error('Failed to uninstall DLC.')
1021 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001022
Andrewc7e1c6b2020-02-27 16:03:53 -08001023 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1024 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1025 # location.
1026 logging.notice('Deploy the DLC image for %s', dlc_id)
Andrew5743d382020-06-16 09:55:04 -07001027 dlc_img_path_src = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1028 dlc_package, dlc_lib.DLC_IMAGE)
Andrewc7e1c6b2020-02-27 16:03:53 -08001029 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1030 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1031 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1032 # Create directories for DLC images.
1033 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1034 # Copy images to the destination directories.
1035 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
Andrew5743d382020-06-16 09:55:04 -07001036 dlc_lib.DLC_IMAGE),
Andrewc7e1c6b2020-02-27 16:03:53 -08001037 mode='rsync')
Andrew5743d382020-06-16 09:55:04 -07001038 device.run(['cp', os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1039 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE)])
Andrewc7e1c6b2020-02-27 16:03:53 -08001040
1041 # Set the proper perms and ownership so dlcservice can access the image.
1042 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1043 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001044
Andrew67b5fa72020-02-05 14:14:48 -08001045 # Copy metadata to device.
Andrew5743d382020-06-16 09:55:04 -07001046 dest_mata_dir = os.path.join('/', dlc_lib.DLC_META_DIR, dlc_id,
1047 dlc_package)
Andrew67b5fa72020-02-05 14:14:48 -08001048 device.run(['mkdir', '-p', dest_mata_dir])
Andrew5743d382020-06-16 09:55:04 -07001049 src_meta_dir = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1050 dlc_package, dlc_lib.DLC_TMP_META_DIR)
Andrew67b5fa72020-02-05 14:14:48 -08001051 device.CopyToDevice(src_meta_dir + '/',
1052 dest_mata_dir,
1053 mode='rsync',
1054 recursive=True,
1055 remote_sudo=True)
1056
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001057
1058def _GetDLCInfo(device, pkg_path, from_dut):
1059 """Returns information of a DLC given its package path.
1060
1061 Args:
1062 device: commandline.Device object; None to use the default device.
1063 pkg_path: path to the package.
1064 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1065 info from host.
1066
1067 Returns:
1068 A tuple (dlc_id, dlc_package).
1069 """
1070 environment_content = ''
1071 if from_dut:
1072 # On DUT, |pkg_path| is the directory which contains environment file.
1073 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
Mike Frysinger3459bf52020-03-31 00:52:11 -04001074 result = device.run(['test', '-f', environment_path],
1075 check=False, encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001076 if result.returncode == 1:
1077 # The package is not installed on DUT yet. Skip extracting info.
1078 return None, None
Mike Frysinger3459bf52020-03-31 00:52:11 -04001079 result = device.run(['bzip2', '-d', '-c', environment_path], encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001080 environment_content = result.output
1081 else:
1082 # On host, pkg_path is tbz2 file which contains environment file.
1083 # Extract the metadata of the package file.
1084 data = portage.xpak.tbz2(pkg_path).get_data()
1085 # Extract the environment metadata.
Woody Chowde57a322020-01-07 16:18:52 +09001086 environment_content = bz2.decompress(
Alex Klein9a1b3722020-01-28 09:59:35 -07001087 data[_ENVIRONMENT_FILENAME.encode('utf-8')])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001088
1089 with tempfile.NamedTemporaryFile() as f:
1090 # Dumps content into a file so we can use osutils.SourceEnvironment.
1091 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001092 osutils.WriteFile(path, environment_content, mode='wb')
Andrew67b5fa72020-02-05 14:14:48 -08001093 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE,
1094 _DLC_ENABLED))
1095
1096 dlc_enabled = content.get(_DLC_ENABLED)
1097 if dlc_enabled is not None and (dlc_enabled is False or
1098 str(dlc_enabled) == 'false'):
1099 logging.info('Installing DLC in rootfs.')
1100 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001101 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001102
1103
Gilad Arnolda0a98062015-07-07 08:34:27 -07001104def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1105 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1106 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1107 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001108 """Deploys packages to a device.
1109
1110 Args:
David Pursell2e773382015-04-03 14:30:47 -07001111 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001112 packages: List of packages (strings) to deploy to device.
1113 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001114 emerge: True to emerge package, False to unmerge.
1115 update: Check installed version on device.
1116 deep: Install dependencies also. Implies |update|.
1117 deep_rev: Install reverse dependencies. Implies |deep|.
1118 clean_binpkg: Clean outdated binary packages.
1119 root: Package installation root path.
1120 strip: Run strip_package to filter out preset paths in the package.
1121 emerge_args: Extra arguments to pass to emerge.
1122 ssh_private_key: Path to an SSH private key file; None to use test keys.
1123 ping: True to ping the device before trying to connect.
1124 force: Ignore sanity checks and prompts.
1125 dry_run: Print deployment plan but do not deploy anything.
1126
1127 Raises:
1128 ValueError: Invalid parameter or parameter combination.
1129 DeployError: Unrecoverable failure during deploy.
1130 """
1131 if deep_rev:
1132 deep = True
1133 if deep:
1134 update = True
1135
Gilad Arnolda0a98062015-07-07 08:34:27 -07001136 if not packages:
1137 raise DeployError('No packages provided, nothing to deploy.')
1138
David Pursell9476bf42015-03-30 13:34:27 -07001139 if update and not emerge:
1140 raise ValueError('Cannot update and unmerge.')
1141
David Pursell2e773382015-04-03 14:30:47 -07001142 if device:
1143 hostname, username, port = device.hostname, device.username, device.port
1144 else:
1145 hostname, username, port = None, None, None
1146
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001147 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001148 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001149 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001150 # Somewhat confusing to clobber, but here we are.
1151 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001152 with remote_access.ChromiumOSDeviceHandler(
1153 hostname, port=port, username=username, private_key=ssh_private_key,
1154 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001155 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001156
Gilad Arnolda0a98062015-07-07 08:34:27 -07001157 board = cros_build_lib.GetBoard(device_board=device.board,
1158 override_board=board)
1159 if not force and board != device.board:
1160 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001161 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001162
Gilad Arnolda0a98062015-07-07 08:34:27 -07001163 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001164
1165 if clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001166 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001167 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001168
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001169 # Remount rootfs as writable if necessary.
1170 if not device.MountRootfsReadWrite():
1171 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001172
1173 # Obtain list of packages to upgrade/remove.
1174 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001175 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001176 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001177 if emerge:
1178 action_str = 'emerge'
1179 else:
1180 pkgs.reverse()
1181 action_str = 'unmerge'
1182
1183 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001184 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001185 return
1186
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +09001187 # Warn when the user seems to forget `cros workon start`.
1188 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1189 for package in listed:
1190 cp = portage_util.SplitCPV(package).cp
1191 if cp not in worked_on_cps:
1192 logging.warning(
1193 'Are you intentionally deploying unmodified packages, or did '
1194 'you forget to run `cros workon --board=$BOARD start %s`?', cp)
1195
Ralph Nathane01ccf12015-04-16 10:40:32 -07001196 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001197 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001198 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001199
1200 if dry_run or not _ConfirmDeploy(num_updates):
1201 return
1202
Ralph Nathane01ccf12015-04-16 10:40:32 -07001203 # Select function (emerge or unmerge) and bind args.
1204 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001205 func = functools.partial(_EmergePackages, pkgs, device, strip,
Andrew06a5f812020-01-23 08:08:32 -08001206 sysroot, root, board, emerge_args)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001207 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001208 func = functools.partial(_UnmergePackages, pkgs, device, root,
1209 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001210
1211 # Call the function with the progress bar or with normal output.
1212 if command.UseProgressBar():
1213 op = BrilloDeployOperation(len(pkgs), emerge)
1214 op.Run(func, log_level=logging.DEBUG)
1215 else:
1216 func()
David Pursell9476bf42015-03-30 13:34:27 -07001217
Ben Pastene5f03b052019-08-12 18:03:24 -07001218 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001219 if sum(x.count('selinux-policy') for x in pkgs):
1220 logging.warning(
1221 'Deploying SELinux policy will not take effect until reboot. '
Ian Barkley-Yeung6b2d8672020-08-13 18:58:10 -07001222 'SELinux policy is loaded by init. Also, changing the security '
1223 'contexts (labels) of a file will require building a new image '
1224 'and flashing the image onto the device.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001225
David Pursell9476bf42015-03-30 13:34:27 -07001226 logging.warning('Please restart any updated services on the device, '
1227 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001228 except Exception:
1229 if lsb_release:
1230 lsb_entries = sorted(lsb_release.items())
1231 logging.info('Following are the LSB version details of the device:\n%s',
1232 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1233 raise