blob: e25b57683fdb4b25d0c76ff84dcbbb562c6fbbd7 [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
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070020import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070021
Ralph Nathane01ccf12015-04-16 10:40:32 -070022from chromite.cli import command
David Pursell9476bf42015-03-30 13:34:27 -070023from chromite.lib import cros_build_lib
24from chromite.lib import cros_logging as logging
Ralph Nathane01ccf12015-04-16 10:40:32 -070025from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070026from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070027from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070028from chromite.lib import remote_access
Andrewc7e1c6b2020-02-27 16:03:53 -080029from chromite.scripts import build_dlc
David Pursell9476bf42015-03-30 13:34:27 -070030try:
31 import portage
32except ImportError:
33 if cros_build_lib.IsInsideChroot():
34 raise
35
36
37_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
38# This is defined in src/platform/dev/builder.py
39_STRIPPED_PACKAGES_DIR = 'stripped-packages'
40
41_MAX_UPDATES_NUM = 10
42_MAX_UPDATES_WARNING = (
43 'You are about to update a large number of installed packages, which '
44 'might take a long time, fail midway, or leave the target in an '
45 'inconsistent state. It is highly recommended that you flash a new image '
46 'instead.')
47
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070048_DLC_ID = 'DLC_ID'
49_DLC_PACKAGE = 'DLC_PACKAGE'
50_ENVIRONMENT_FILENAME = 'environment.bz2'
51_DLC_INSTALL_ROOT = '/var/cache/dlc'
52
David Pursell9476bf42015-03-30 13:34:27 -070053
54class DeployError(Exception):
55 """Thrown when an unrecoverable error is encountered during deploy."""
56
57
Ralph Nathane01ccf12015-04-16 10:40:32 -070058class BrilloDeployOperation(operation.ProgressBarOperation):
59 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070060 # These two variables are used to validate the output in the VM integration
61 # tests. Changes to the output must be reflected here.
62 MERGE_EVENTS = ['NOTICE: Copying', 'NOTICE: Installing', 'WARNING: Ignoring',
Achuith Bhandarkar0487c312019-04-22 12:19:25 -070063 'emerge --usepkg', 'has been installed.']
Ralph Nathan90475a12015-05-20 13:19:01 -070064 UNMERGE_EVENTS = ['NOTICE: Unmerging', 'has been uninstalled.']
Ralph Nathane01ccf12015-04-16 10:40:32 -070065
66 def __init__(self, pkg_count, emerge):
67 """Construct BrilloDeployOperation object.
68
69 Args:
70 pkg_count: number of packages being built.
71 emerge: True if emerge, False is unmerge.
72 """
73 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070074 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070075 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070076 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070077 self._events = self.UNMERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070078 self._total = pkg_count * len(self._events)
79 self._completed = 0
80
Ralph Nathandc14ed92015-04-22 11:17:40 -070081 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070082 """Parse the output of brillo deploy to update a progress bar."""
83 stdout = self._stdout.read()
84 stderr = self._stderr.read()
85 output = stdout + stderr
86 for event in self._events:
87 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040088 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -070089
90
David Pursell9476bf42015-03-30 13:34:27 -070091class _InstallPackageScanner(object):
92 """Finds packages that need to be installed on a target device.
93
94 Scans the sysroot bintree, beginning with a user-provided list of packages,
95 to find all packages that need to be installed. If so instructed,
96 transitively scans forward (mandatory) and backward (optional) dependencies
97 as well. A package will be installed if missing on the target (mandatory
98 packages only), or it will be updated if its sysroot version and build time
99 are different from the target. Common usage:
100
101 pkg_scanner = _InstallPackageScanner(sysroot)
102 pkgs = pkg_scanner.Run(...)
103 """
104
105 class VartreeError(Exception):
106 """An error in the processing of the installed packages tree."""
107
108 class BintreeError(Exception):
109 """An error in the processing of the source binpkgs tree."""
110
111 class PkgInfo(object):
112 """A record containing package information."""
113
114 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
115
116 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
117 self.cpv = cpv
118 self.build_time = build_time
119 self.rdeps_raw = rdeps_raw
120 self.rdeps = set() if rdeps is None else rdeps
121 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
122
123 # Python snippet for dumping vartree info on the target. Instantiate using
124 # _GetVartreeSnippet().
125 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700126import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700127import os
128import portage
129
130# Normalize the path to match what portage will index.
131target_root = os.path.normpath('%(root)s')
132if not target_root.endswith('/'):
133 target_root += '/'
134trees = portage.create_trees(target_root=target_root, config_root='/')
135vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700136pkg_info = []
137for cpv in vartree.dbapi.cpv_all():
138 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
139 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
140 pkg_info.append((cpv, slot, rdep_raw, build_time))
141
142print(json.dumps(pkg_info))
143"""
144
145 def __init__(self, sysroot):
146 self.sysroot = sysroot
147 # Members containing the sysroot (binpkg) and target (installed) package DB.
148 self.target_db = None
149 self.binpkgs_db = None
150 # Members for managing the dependency resolution work queue.
151 self.queue = None
152 self.seen = None
153 self.listed = None
154
155 @staticmethod
156 def _GetCP(cpv):
157 """Returns the CP value for a given CPV string."""
158 attrs = portage_util.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600159 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700160 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600161 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700162
163 @staticmethod
164 def _InDB(cp, slot, db):
165 """Returns whether CP and slot are found in a database (if provided)."""
166 cp_slots = db.get(cp) if db else None
167 return cp_slots is not None and (not slot or slot in cp_slots)
168
169 @staticmethod
170 def _AtomStr(cp, slot):
171 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
172 return '%s:%s' % (cp, slot) if slot else cp
173
174 @classmethod
175 def _GetVartreeSnippet(cls, root='/'):
176 """Returns a code snippet for dumping the vartree on the target.
177
178 Args:
179 root: The installation root.
180
181 Returns:
182 The said code snippet (string) with parameters filled in.
183 """
184 return cls._GET_VARTREE % {'root': root}
185
186 @classmethod
187 def _StripDepAtom(cls, dep_atom, installed_db=None):
188 """Strips a dependency atom and returns a (CP, slot) pair."""
189 # TODO(garnold) This is a gross simplification of ebuild dependency
190 # semantics, stripping and ignoring various qualifiers (versions, slots,
191 # USE flag, negation) and will likely need to be fixed. chromium:447366.
192
193 # Ignore unversioned blockers, leaving them for the user to resolve.
194 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
195 return None, None
196
197 cp = dep_atom
198 slot = None
199 require_installed = False
200
201 # Versioned blockers should be updated, but only if already installed.
202 # These are often used for forcing cascaded updates of multiple packages,
203 # so we're treating them as ordinary constraints with hopes that it'll lead
204 # to the desired result.
205 if cp.startswith('!'):
206 cp = cp.lstrip('!')
207 require_installed = True
208
209 # Remove USE flags.
210 if '[' in cp:
211 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
212
213 # Separate the slot qualifier and strip off subslots.
214 if ':' in cp:
215 cp, slot = cp.split(':')
216 for delim in ('/', '='):
217 slot = slot.split(delim, 1)[0]
218
219 # Strip version wildcards (right), comparators (left).
220 cp = cp.rstrip('*')
221 cp = cp.lstrip('<=>~')
222
223 # Turn into CP form.
224 cp = cls._GetCP(cp)
225
226 if require_installed and not cls._InDB(cp, None, installed_db):
227 return None, None
228
229 return cp, slot
230
231 @classmethod
232 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
233 """Resolves and returns a list of dependencies from a dependency string.
234
235 This parses a dependency string and returns a list of package names and
236 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
237 resolving disjunctive deps, we include all choices that are fully present
238 in |installed_db|. If none is present, we choose an arbitrary one that is
239 available.
240
241 Args:
242 dep_str: A raw dependency string.
243 installed_db: A database of installed packages.
244 avail_db: A database of packages available for installation.
245
246 Returns:
247 A list of pairs (CP, slot).
248
249 Raises:
250 ValueError: the dependencies string is malformed.
251 """
252 def ProcessSubDeps(dep_exp, disjunct):
253 """Parses and processes a dependency (sub)expression."""
254 deps = set()
255 default_deps = set()
256 sub_disjunct = False
257 for dep_sub_exp in dep_exp:
258 sub_deps = set()
259
260 if isinstance(dep_sub_exp, (list, tuple)):
261 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
262 sub_disjunct = False
263 elif sub_disjunct:
264 raise ValueError('Malformed disjunctive operation in deps')
265 elif dep_sub_exp == '||':
266 sub_disjunct = True
267 elif dep_sub_exp.endswith('?'):
268 raise ValueError('Dependencies contain a conditional')
269 else:
270 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
271 if cp:
272 sub_deps = set([(cp, slot)])
273 elif disjunct:
274 raise ValueError('Atom in disjunct ignored')
275
276 # Handle sub-deps of a disjunctive expression.
277 if disjunct:
278 # Make the first available choice the default, for use in case that
279 # no option is installed.
280 if (not default_deps and avail_db is not None and
281 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
282 default_deps = sub_deps
283
284 # If not all sub-deps are installed, then don't consider them.
285 if not all([cls._InDB(cp, slot, installed_db)
286 for cp, slot in sub_deps]):
287 sub_deps = set()
288
289 deps.update(sub_deps)
290
291 return deps or default_deps
292
293 try:
294 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
295 except portage.exception.InvalidDependString as e:
296 raise ValueError('Invalid dep string: %s' % e)
297 except ValueError as e:
298 raise ValueError('%s: %s' % (e, dep_str))
299
300 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
301 installed_db=None):
302 """Returns a database of packages given a list of CPV info.
303
304 Args:
305 cpv_info: A list of tuples containing package CPV and attributes.
306 process_rdeps: Whether to populate forward dependencies.
307 process_rev_rdeps: Whether to populate reverse dependencies.
308 installed_db: A database of installed packages for filtering disjunctive
309 choices against; if None, using own built database.
310
311 Returns:
312 A map from CP values to another dictionary that maps slots to package
313 attribute tuples. Tuples contain a CPV value (string), build time
314 (string), runtime dependencies (set), and reverse dependencies (set,
315 empty if not populated).
316
317 Raises:
318 ValueError: If more than one CPV occupies a single slot.
319 """
320 db = {}
321 logging.debug('Populating package DB...')
322 for cpv, slot, rdeps_raw, build_time in cpv_info:
323 cp = self._GetCP(cpv)
324 cp_slots = db.setdefault(cp, dict())
325 if slot in cp_slots:
326 raise ValueError('More than one package found for %s' %
327 self._AtomStr(cp, slot))
328 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
329 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
330 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
331
332 avail_db = db
333 if installed_db is None:
334 installed_db = db
335 avail_db = None
336
337 # Add approximate forward dependencies.
338 if process_rdeps:
339 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400340 for cp, cp_slots in db.items():
341 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700342 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
343 installed_db, avail_db))
344 logging.debug(' %s (%s) processed rdeps: %s',
345 self._AtomStr(cp, slot), pkg_info.cpv,
346 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
347 for rdep_cp, rdep_slot in pkg_info.rdeps]))
348
349 # Add approximate reverse dependencies (optional).
350 if process_rev_rdeps:
351 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400352 for cp, cp_slots in db.items():
353 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700354 for rdep_cp, rdep_slot in pkg_info.rdeps:
355 to_slots = db.get(rdep_cp)
356 if not to_slots:
357 continue
358
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400359 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700360 if rdep_slot and to_slot != rdep_slot:
361 continue
362 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
363 self._AtomStr(cp, slot), pkg_info.cpv,
364 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
365 to_pkg_info.rev_rdeps.add((cp, slot))
366
367 return db
368
369 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
370 """Initializes a dictionary of packages installed on |device|."""
371 get_vartree_script = self._GetVartreeSnippet(root)
372 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400373 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700374 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700375 except cros_build_lib.RunCommandError as e:
376 logging.error('Cannot get target vartree:\n%s', e.result.error)
377 raise
378
379 try:
380 self.target_db = self._BuildDB(json.loads(result.output),
381 process_rdeps, process_rev_rdeps)
382 except ValueError as e:
383 raise self.VartreeError(str(e))
384
385 def _InitBinpkgDB(self, process_rdeps):
386 """Initializes a dictionary of binary packages for updating the target."""
387 # Get build root trees; portage indexes require a trailing '/'.
388 build_root = os.path.join(self.sysroot, '')
389 trees = portage.create_trees(target_root=build_root, config_root=build_root)
390 bintree = trees[build_root]['bintree']
391 binpkgs_info = []
392 for cpv in bintree.dbapi.cpv_all():
393 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
394 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
395 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
396
397 try:
398 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
399 installed_db=self.target_db)
400 except ValueError as e:
401 raise self.BintreeError(str(e))
402
403 def _InitDepQueue(self):
404 """Initializes the dependency work queue."""
405 self.queue = set()
406 self.seen = {}
407 self.listed = set()
408
409 def _EnqDep(self, dep, listed, optional):
410 """Enqueues a dependency if not seen before or if turned non-optional."""
411 if dep in self.seen and (optional or not self.seen[dep]):
412 return False
413
414 self.queue.add(dep)
415 self.seen[dep] = optional
416 if listed:
417 self.listed.add(dep)
418 return True
419
420 def _DeqDep(self):
421 """Dequeues and returns a dependency, its listed and optional flags.
422
423 This returns listed packages first, if any are present, to ensure that we
424 correctly mark them as such when they are first being processed.
425 """
426 if self.listed:
427 dep = self.listed.pop()
428 self.queue.remove(dep)
429 listed = True
430 else:
431 dep = self.queue.pop()
432 listed = False
433
434 return dep, listed, self.seen[dep]
435
436 def _FindPackageMatches(self, cpv_pattern):
437 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
438
439 This is breaking |cpv_pattern| into its C, P and V components, each of
440 which may or may not be present or contain wildcards. It then scans the
441 binpkgs database to find all atoms that match these components, returning a
442 list of CP and slot qualifier. When the pattern does not specify a version,
443 or when a CP has only one slot in the binpkgs database, we omit the slot
444 qualifier in the result.
445
446 Args:
447 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
448
449 Returns:
450 A list of (CPV, slot) pairs of packages in the binpkgs database that
451 match the pattern.
452 """
453 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
454 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
455 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400456 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700457 if not fnmatch.fnmatchcase(cp, cp_pattern):
458 continue
459
460 # If no version attribute was given or there's only one slot, omit the
461 # slot qualifier.
462 if not attrs.version or len(cp_slots) == 1:
463 matches.append((cp, None))
464 else:
465 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400466 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700467 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
468 matches.append((cp, slot))
469
470 return matches
471
472 def _FindPackage(self, pkg):
473 """Returns the (CP, slot) pair for a package matching |pkg|.
474
475 Args:
476 pkg: Path to a binary package or a (partial) package CPV specifier.
477
478 Returns:
479 A (CP, slot) pair for the given package; slot may be None (unspecified).
480
481 Raises:
482 ValueError: if |pkg| is not a binpkg file nor does it match something
483 that's in the bintree.
484 """
485 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
486 package = os.path.basename(os.path.splitext(pkg)[0])
487 category = os.path.basename(os.path.dirname(pkg))
488 return self._GetCP(os.path.join(category, package)), None
489
490 matches = self._FindPackageMatches(pkg)
491 if not matches:
492 raise ValueError('No package found for %s' % pkg)
493
494 idx = 0
495 if len(matches) > 1:
496 # Ask user to pick among multiple matches.
497 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
498 ['%s:%s' % (cp, slot) if slot else cp
499 for cp, slot in matches])
500
501 return matches[idx]
502
503 def _NeedsInstall(self, cpv, slot, build_time, optional):
504 """Returns whether a package needs to be installed on the target.
505
506 Args:
507 cpv: Fully qualified CPV (string) of the package.
508 slot: Slot identifier (string).
509 build_time: The BUILT_TIME value (string) of the binpkg.
510 optional: Whether package is optional on the target.
511
512 Returns:
513 A tuple (install, update) indicating whether to |install| the package and
514 whether it is an |update| to an existing package.
515
516 Raises:
517 ValueError: if slot is not provided.
518 """
519 # If not checking installed packages, always install.
520 if not self.target_db:
521 return True, False
522
523 cp = self._GetCP(cpv)
524 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
525 if target_pkg_info is not None:
526 if cpv != target_pkg_info.cpv:
527 attrs = portage_util.SplitCPV(cpv)
528 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
529 logging.debug('Updating %s: version (%s) different on target (%s)',
530 cp, attrs.version, target_attrs.version)
531 return True, True
532
533 if build_time != target_pkg_info.build_time:
534 logging.debug('Updating %s: build time (%s) different on target (%s)',
535 cpv, build_time, target_pkg_info.build_time)
536 return True, True
537
538 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
539 cp, target_pkg_info.cpv, target_pkg_info.build_time)
540 return False, False
541
542 if optional:
543 logging.debug('Not installing %s: missing on target but optional', cp)
544 return False, False
545
546 logging.debug('Installing %s: missing on target and non-optional (%s)',
547 cp, cpv)
548 return True, False
549
550 def _ProcessDeps(self, deps, reverse):
551 """Enqueues dependencies for processing.
552
553 Args:
554 deps: List of dependencies to enqueue.
555 reverse: Whether these are reverse dependencies.
556 """
557 if not deps:
558 return
559
560 logging.debug('Processing %d %s dep(s)...', len(deps),
561 'reverse' if reverse else 'forward')
562 num_already_seen = 0
563 for dep in deps:
564 if self._EnqDep(dep, False, reverse):
565 logging.debug(' Queued dep %s', dep)
566 else:
567 num_already_seen += 1
568
569 if num_already_seen:
570 logging.debug('%d dep(s) already seen', num_already_seen)
571
572 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
573 """Returns a dictionary of packages that need to be installed on the target.
574
575 Args:
576 process_rdeps: Whether to trace forward dependencies.
577 process_rev_rdeps: Whether to trace backward dependencies as well.
578
579 Returns:
580 A dictionary mapping CP values (string) to tuples containing a CPV
581 (string), a slot (string), a boolean indicating whether the package
582 was initially listed in the queue, and a boolean indicating whether this
583 is an update to an existing package.
584 """
585 installs = {}
586 while self.queue:
587 dep, listed, optional = self._DeqDep()
588 cp, required_slot = dep
589 if cp in installs:
590 logging.debug('Already updating %s', cp)
591 continue
592
593 cp_slots = self.binpkgs_db.get(cp, dict())
594 logging.debug('Checking packages matching %s%s%s...', cp,
595 ' (slot: %s)' % required_slot if required_slot else '',
596 ' (optional)' if optional else '')
597 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400598 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700599 if required_slot and slot != required_slot:
600 continue
601
602 num_processed += 1
603 logging.debug(' Checking %s...', pkg_info.cpv)
604
605 install, update = self._NeedsInstall(pkg_info.cpv, slot,
606 pkg_info.build_time, optional)
607 if not install:
608 continue
609
610 installs[cp] = (pkg_info.cpv, slot, listed, update)
611
612 # Add forward and backward runtime dependencies to queue.
613 if process_rdeps:
614 self._ProcessDeps(pkg_info.rdeps, False)
615 if process_rev_rdeps:
616 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
617 if target_pkg_info:
618 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
619
620 if num_processed == 0:
621 logging.warning('No qualified bintree package corresponding to %s', cp)
622
623 return installs
624
625 def _SortInstalls(self, installs):
626 """Returns a sorted list of packages to install.
627
628 Performs a topological sort based on dependencies found in the binary
629 package database.
630
631 Args:
632 installs: Dictionary of packages to install indexed by CP.
633
634 Returns:
635 A list of package CPVs (string).
636
637 Raises:
638 ValueError: If dependency graph contains a cycle.
639 """
640 not_visited = set(installs.keys())
641 curr_path = []
642 sorted_installs = []
643
644 def SortFrom(cp):
645 """Traverses dependencies recursively, emitting nodes in reverse order."""
646 cpv, slot, _, _ = installs[cp]
647 if cpv in curr_path:
648 raise ValueError('Dependencies contain a cycle: %s -> %s' %
649 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
650 curr_path.append(cpv)
651 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
652 if rdep_cp in not_visited:
653 not_visited.remove(rdep_cp)
654 SortFrom(rdep_cp)
655
656 sorted_installs.append(cpv)
657 curr_path.pop()
658
659 # So long as there's more packages, keep expanding dependency paths.
660 while not_visited:
661 SortFrom(not_visited.pop())
662
663 return sorted_installs
664
665 def _EnqListedPkg(self, pkg):
666 """Finds and enqueues a listed package."""
667 cp, slot = self._FindPackage(pkg)
668 if cp not in self.binpkgs_db:
669 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
670 self._EnqDep((cp, slot), True, False)
671
672 def _EnqInstalledPkgs(self):
673 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400674 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700675 target_cp_slots = self.target_db.get(cp)
676 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400677 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700678 if slot in target_cp_slots:
679 self._EnqDep((cp, slot), True, False)
680
681 def Run(self, device, root, listed_pkgs, update, process_rdeps,
682 process_rev_rdeps):
683 """Computes the list of packages that need to be installed on a target.
684
685 Args:
686 device: Target handler object.
687 root: Package installation root.
688 listed_pkgs: Package names/files listed by the user.
689 update: Whether to read the target's installed package database.
690 process_rdeps: Whether to trace forward dependencies.
691 process_rev_rdeps: Whether to trace backward dependencies as well.
692
693 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700694 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
695 list of package CPVs (string) to install on the target in an order that
696 satisfies their inter-dependencies, |listed| the subset that was
697 requested by the user, and |num_updates| the number of packages being
698 installed over preexisting versions. Note that installation order should
699 be reversed for removal, |install_attrs| is a dictionary mapping a package
700 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700701 """
702 if process_rev_rdeps and not process_rdeps:
703 raise ValueError('Must processing forward deps when processing rev deps')
704 if process_rdeps and not update:
705 raise ValueError('Must check installed packages when processing deps')
706
707 if update:
708 logging.info('Initializing target intalled packages database...')
709 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
710
711 logging.info('Initializing binary packages database...')
712 self._InitBinpkgDB(process_rdeps)
713
714 logging.info('Finding listed package(s)...')
715 self._InitDepQueue()
716 for pkg in listed_pkgs:
717 if pkg == '@installed':
718 if not update:
719 raise ValueError(
720 'Must check installed packages when updating all of them.')
721 self._EnqInstalledPkgs()
722 else:
723 self._EnqListedPkg(pkg)
724
725 logging.info('Computing set of packages to install...')
726 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
727
728 num_updates = 0
729 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400730 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700731 if listed:
732 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400733 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700734 num_updates += 1
735
736 logging.info('Processed %d package(s), %d will be installed, %d are '
737 'updating existing packages',
738 len(self.seen), len(installs), num_updates)
739
740 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700741
742 install_attrs = {}
743 for pkg in sorted_installs:
744 pkg_path = os.path.join(root, portage.VDB_PATH, pkg)
745 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
746 install_attrs[pkg] = {}
747 if dlc_id and dlc_package:
748 install_attrs[pkg][_DLC_ID] = dlc_id
749
750 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700751
752
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700753def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700754 """Copies |pkg| to |device| and emerges it.
755
756 Args:
757 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700758 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700759 root: Package installation root path.
760 extra_args: Extra arguments to pass to emerge.
761
762 Raises:
763 DeployError: Unrecoverable error during emerge.
764 """
David Pursell9476bf42015-03-30 13:34:27 -0700765 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700766 pkg_name = os.path.basename(pkg_path)
767 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700768 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400769 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
770 # Clean out the dirs first if we had a previous emerge on the device so as to
771 # free up space for this emerge. The last emerge gets implicitly cleaned up
772 # when the device connection deletes its work_dir.
773 device.RunCommand(
774 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
775 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700776
Ralph Nathane01ccf12015-04-16 10:40:32 -0700777 # This message is read by BrilloDeployOperation.
778 logging.notice('Copying %s to device.', pkg_name)
Ilja H. Friedel0ab63e12017-03-28 13:29:48 -0700779 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700780
David Pursell9476bf42015-03-30 13:34:27 -0700781 logging.info('Use portage temp dir %s', portage_tmpdir)
782
Ralph Nathane01ccf12015-04-16 10:40:32 -0700783 # This message is read by BrilloDeployOperation.
784 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700785 pkg_path = os.path.join(pkg_dir, pkg_name)
786
787 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
788 # chromeos-base packages will be skipped due to the configuration
789 # in /etc/protage/make.profile/package.provided. However, there is
790 # a known bug that /usr/local/etc/portage is not setup properly
791 # (crbug.com/312041). This does not affect `cros deploy` because
792 # we do not use the preset PKGDIR.
793 extra_env = {
794 'FEATURES': '-sandbox',
795 'PKGDIR': pkgroot,
796 'PORTAGE_CONFIGROOT': '/usr/local',
797 'PORTAGE_TMPDIR': portage_tmpdir,
798 'PORTDIR': device.work_dir,
799 'CONFIG_PROTECT': '-*',
800 }
Alex Kleinaaddc932020-01-30 15:02:24 -0700801 # --ignore-built-slot-operator-deps because we don't rebuild everything.
802 # It can cause errors, but that's expected with cros deploy since it's just a
803 # best effort to prevent developers avoid rebuilding an image every time.
804 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', pkg_path,
805 '--root=%s' % root]
David Pursell9476bf42015-03-30 13:34:27 -0700806 if extra_args:
807 cmd.append(extra_args)
808
Alex Kleinaaddc932020-01-30 15:02:24 -0700809 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
810 'packages built against the old version may not be able to '
811 'load the new .so. This is expected, and you will just need '
812 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700813 try:
Greg Kerrb96c02c2019-02-08 14:32:41 -0800814 result = device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
815 capture_output=True, debug_level=logging.INFO)
816
817 pattern = ('A requested package will not be merged because '
818 'it is listed in package.provided')
819 output = result.error.replace('\n', ' ').replace('\r', '')
820 if pattern in output:
821 error = ('Package failed to emerge: %s\n'
822 'Remove %s from /etc/portage/make.profile/'
823 'package.provided/chromeos-base.packages\n'
824 '(also see crbug.com/920140 for more context)\n'
825 % (pattern, pkg_name))
826 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700827 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700828 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700829 raise
830 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700831 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700832
833
Qijiang Fand5958192019-07-26 12:32:36 +0900834def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800835 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900836
837 This reads the tarball from pkgpath, and calls restorecon on device to
838 restore SELinux context for files listed in the tarball, assuming those files
839 are installed to /
840
841 Args:
842 device: a ChromiumOSDevice object
843 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900844 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900845 """
Ben Pastene5f03b052019-08-12 18:03:24 -0700846 enforced = device.IsSELinuxEnforced()
Qijiang Fan8a945032019-04-25 20:53:29 +0900847 if enforced:
848 device.RunCommand(['setenforce', '0'])
849 pkgroot = os.path.join(device.work_dir, 'packages')
850 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
851 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
852 # Testing shows restorecon splits on newlines instead of spaces.
853 device.RunCommand(
Qijiang Fand5958192019-07-26 12:32:36 +0900854 ['cd', root, '&&',
855 'tar', 'tf', pkgpath_device, '|',
856 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900857 remote_sudo=True)
858 if enforced:
859 device.RunCommand(['setenforce', '1'])
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900860
861
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700862def _GetPackagesByCPV(cpvs, strip, sysroot):
863 """Returns paths to binary packages corresponding to |cpvs|.
864
865 Args:
866 cpvs: List of CPV components given by portage_util.SplitCPV().
867 strip: True to run strip_package.
868 sysroot: Sysroot path.
869
870 Returns:
871 List of paths corresponding to |cpvs|.
872
873 Raises:
874 DeployError: If a package is missing.
875 """
876 packages_dir = None
877 if strip:
878 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400879 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700880 ['strip_package', '--sysroot', sysroot] +
Alex Klein7078e252018-10-02 10:21:04 -0600881 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700882 packages_dir = _STRIPPED_PACKAGES_DIR
883 except cros_build_lib.RunCommandError:
884 logging.error('Cannot strip packages %s',
885 ' '.join([str(cpv) for cpv in cpvs]))
886 raise
887
888 paths = []
889 for cpv in cpvs:
890 path = portage_util.GetBinaryPackagePath(
891 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
892 packages_dir=packages_dir)
893 if not path:
894 raise DeployError('Missing package %s.' % cpv)
895 paths.append(path)
896
897 return paths
898
899
900def _GetPackagesPaths(pkgs, strip, sysroot):
901 """Returns paths to binary |pkgs|.
902
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700903 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700904 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700905 strip: Whether or not to run strip_package for CPV packages.
906 sysroot: The sysroot path.
907
908 Returns:
909 List of paths corresponding to |pkgs|.
910 """
Ned Nguyend0db4072019-02-22 14:19:21 -0700911 cpvs = [portage_util.SplitCPV(p) for p in pkgs]
912 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700913
914
David Pursell9476bf42015-03-30 13:34:27 -0700915def _Unmerge(device, pkg, root):
916 """Unmerges |pkg| on |device|.
917
918 Args:
919 device: A RemoteDevice object.
920 pkg: A package name.
921 root: Package installation root path.
922 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700923 pkg_name = os.path.basename(pkg)
924 # This message is read by BrilloDeployOperation.
925 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700926 cmd = ['qmerge', '--yes']
927 # Check if qmerge is available on the device. If not, use emerge.
928 if device.RunCommand(
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500929 ['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700930 cmd = ['emerge']
931
932 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
933 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700934 # Always showing the emerge output for clarity.
David Pursell9476bf42015-03-30 13:34:27 -0700935 device.RunCommand(cmd, capture_output=False, remote_sudo=True,
936 debug_level=logging.INFO)
937 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700938 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700939 raise
940 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700941 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700942
943
944def _ConfirmDeploy(num_updates):
945 """Returns whether we can continue deployment."""
946 if num_updates > _MAX_UPDATES_NUM:
947 logging.warning(_MAX_UPDATES_WARNING)
948 return cros_build_lib.BooleanPrompt(default=False)
949
950 return True
951
952
Ralph Nathane01ccf12015-04-16 10:40:32 -0700953def _EmergePackages(pkgs, device, strip, sysroot, root, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800954 """Call _Emerge for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700955 dlc_deployed = False
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700956 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
957 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Ben Pastene5f03b052019-08-12 18:03:24 -0700958 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900959 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800960
961 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
962 if dlc_id and dlc_package:
963 _DeployDLCImage(device, sysroot, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700964 dlc_deployed = True
Andrewc7e1c6b2020-02-27 16:03:53 -0800965 # Clean up directories created by emerging DLCs.
966 device.run(['rm', '-rf', '/build/rootfs/dlc'])
967 device.run(['rmdir', '--ignore-fail-on-non-empty', '/build/rootfs',
968 '/build'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700969
970 # Restart dlcservice so it picks up the newly installed DLC modules (in case
971 # we installed new DLC images).
972 if dlc_deployed:
973 device.RunCommand(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -0700974
975
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700976def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -0700977 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700978 dlc_uninstalled = False
Ralph Nathane01ccf12015-04-16 10:40:32 -0700979 for pkg in pkgs:
980 _Unmerge(device, pkg, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700981 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
982 dlc_uninstalled = True
983
984 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
985 # uninstalled DLC images).
986 if dlc_uninstalled:
987 device.RunCommand(['restart', 'dlcservice'])
988
989
990def _UninstallDLCImage(device, pkg_attrs):
991 """Uninstall a DLC image."""
992 if _DLC_ID in pkg_attrs:
993 dlc_id = pkg_attrs[_DLC_ID]
994 logging.notice('Uninstalling DLC image for %s', dlc_id)
995
996 device.RunCommand(['sudo', '-u', 'chronos', 'dlcservice_util',
997 '--uninstall', '--dlc_ids=%s' % dlc_id])
998 return True
999 else:
1000 logging.debug('DLC_ID not found in package')
1001 return False
1002
1003
Andrewc7e1c6b2020-02-27 16:03:53 -08001004def _DeployDLCImage(device, sysroot, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001005 """Deploy (install and mount) a DLC image."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001006
Andrewc7e1c6b2020-02-27 16:03:53 -08001007 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1008 try:
1009 device.run(['dlcservice_util', '--dlc_ids=%s' % dlc_id, '--uninstall'])
1010 except cros_build_lib.RunCommandError as e:
1011 logging.info('Failed to uninstall DLC:%s. Continue anyway.',
1012 e.result.error)
1013 except Exception:
1014 logging.error('Failed to uninstall DLC.')
1015 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001016
Andrewc7e1c6b2020-02-27 16:03:53 -08001017 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1018 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1019 # location.
1020 logging.notice('Deploy the DLC image for %s', dlc_id)
1021 dlc_img_path_src = os.path.join(sysroot, build_dlc.DLC_IMAGE_DIR, dlc_id,
1022 dlc_package, build_dlc.DLC_IMAGE)
1023 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1024 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1025 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1026 # Create directories for DLC images.
1027 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1028 # Copy images to the destination directories.
1029 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
1030 build_dlc.DLC_IMAGE),
1031 mode='rsync')
1032 device.run(['cp', os.path.join(dlc_img_path_a, build_dlc.DLC_IMAGE),
1033 os.path.join(dlc_img_path_b, build_dlc.DLC_IMAGE)])
1034
1035 # Set the proper perms and ownership so dlcservice can access the image.
1036 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1037 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001038
1039
1040def _GetDLCInfo(device, pkg_path, from_dut):
1041 """Returns information of a DLC given its package path.
1042
1043 Args:
1044 device: commandline.Device object; None to use the default device.
1045 pkg_path: path to the package.
1046 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1047 info from host.
1048
1049 Returns:
1050 A tuple (dlc_id, dlc_package).
1051 """
1052 environment_content = ''
1053 if from_dut:
1054 # On DUT, |pkg_path| is the directory which contains environment file.
1055 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1056 result = device.RunCommand(['test', '-f', environment_path],
Woody Chowde57a322020-01-07 16:18:52 +09001057 check=False, encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001058 if result.returncode == 1:
1059 # The package is not installed on DUT yet. Skip extracting info.
1060 return None, None
Woody Chowde57a322020-01-07 16:18:52 +09001061 result = device.RunCommand(['bzip2', '-d', '-c', environment_path],
1062 encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001063 environment_content = result.output
1064 else:
1065 # On host, pkg_path is tbz2 file which contains environment file.
1066 # Extract the metadata of the package file.
1067 data = portage.xpak.tbz2(pkg_path).get_data()
1068 # Extract the environment metadata.
Woody Chowde57a322020-01-07 16:18:52 +09001069 environment_content = bz2.decompress(
Alex Klein9a1b3722020-01-28 09:59:35 -07001070 data[_ENVIRONMENT_FILENAME.encode('utf-8')])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001071
1072 with tempfile.NamedTemporaryFile() as f:
1073 # Dumps content into a file so we can use osutils.SourceEnvironment.
1074 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001075 osutils.WriteFile(path, environment_content, mode='wb')
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001076 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE))
1077 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001078
1079
Gilad Arnolda0a98062015-07-07 08:34:27 -07001080def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1081 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1082 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1083 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001084 """Deploys packages to a device.
1085
1086 Args:
David Pursell2e773382015-04-03 14:30:47 -07001087 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001088 packages: List of packages (strings) to deploy to device.
1089 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001090 emerge: True to emerge package, False to unmerge.
1091 update: Check installed version on device.
1092 deep: Install dependencies also. Implies |update|.
1093 deep_rev: Install reverse dependencies. Implies |deep|.
1094 clean_binpkg: Clean outdated binary packages.
1095 root: Package installation root path.
1096 strip: Run strip_package to filter out preset paths in the package.
1097 emerge_args: Extra arguments to pass to emerge.
1098 ssh_private_key: Path to an SSH private key file; None to use test keys.
1099 ping: True to ping the device before trying to connect.
1100 force: Ignore sanity checks and prompts.
1101 dry_run: Print deployment plan but do not deploy anything.
1102
1103 Raises:
1104 ValueError: Invalid parameter or parameter combination.
1105 DeployError: Unrecoverable failure during deploy.
1106 """
1107 if deep_rev:
1108 deep = True
1109 if deep:
1110 update = True
1111
Gilad Arnolda0a98062015-07-07 08:34:27 -07001112 if not packages:
1113 raise DeployError('No packages provided, nothing to deploy.')
1114
David Pursell9476bf42015-03-30 13:34:27 -07001115 if update and not emerge:
1116 raise ValueError('Cannot update and unmerge.')
1117
David Pursell2e773382015-04-03 14:30:47 -07001118 if device:
1119 hostname, username, port = device.hostname, device.username, device.port
1120 else:
1121 hostname, username, port = None, None, None
1122
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001123 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001124 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001125 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001126 # Somewhat confusing to clobber, but here we are.
1127 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001128 with remote_access.ChromiumOSDeviceHandler(
1129 hostname, port=port, username=username, private_key=ssh_private_key,
1130 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001131 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001132
Gilad Arnolda0a98062015-07-07 08:34:27 -07001133 board = cros_build_lib.GetBoard(device_board=device.board,
1134 override_board=board)
1135 if not force and board != device.board:
1136 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001137 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001138
Gilad Arnolda0a98062015-07-07 08:34:27 -07001139 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001140
1141 if clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001142 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001143 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001144
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001145 # Remount rootfs as writable if necessary.
1146 if not device.MountRootfsReadWrite():
1147 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001148
1149 # Obtain list of packages to upgrade/remove.
1150 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001151 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001152 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001153 if emerge:
1154 action_str = 'emerge'
1155 else:
1156 pkgs.reverse()
1157 action_str = 'unmerge'
1158
1159 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001160 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001161 return
1162
Ralph Nathane01ccf12015-04-16 10:40:32 -07001163 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001164 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001165 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001166
1167 if dry_run or not _ConfirmDeploy(num_updates):
1168 return
1169
Ralph Nathane01ccf12015-04-16 10:40:32 -07001170 # Select function (emerge or unmerge) and bind args.
1171 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001172 func = functools.partial(_EmergePackages, pkgs, device, strip,
Ralph Nathane01ccf12015-04-16 10:40:32 -07001173 sysroot, root, emerge_args)
1174 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001175 func = functools.partial(_UnmergePackages, pkgs, device, root,
1176 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001177
1178 # Call the function with the progress bar or with normal output.
1179 if command.UseProgressBar():
1180 op = BrilloDeployOperation(len(pkgs), emerge)
1181 op.Run(func, log_level=logging.DEBUG)
1182 else:
1183 func()
David Pursell9476bf42015-03-30 13:34:27 -07001184
Ben Pastene5f03b052019-08-12 18:03:24 -07001185 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001186 if sum(x.count('selinux-policy') for x in pkgs):
1187 logging.warning(
1188 'Deploying SELinux policy will not take effect until reboot. '
1189 'SELinux policy is loaded by init. Also, security contexts '
1190 '(labels) in files will require manual relabeling by the user '
1191 'if your policy modifies the file contexts.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001192
David Pursell9476bf42015-03-30 13:34:27 -07001193 logging.warning('Please restart any updated services on the device, '
1194 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001195 except Exception:
1196 if lsb_release:
1197 lsb_entries = sorted(lsb_release.items())
1198 logging.info('Following are the LSB version details of the device:\n%s',
1199 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1200 raise