blob: 789a2248f7baadff7dc75a9cceb054edf7bf45de [file] [log] [blame]
David Pursell9476bf42015-03-30 13:34:27 -07001# Copyright 2015 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Deploy packages onto a target device."""
6
7from __future__ import print_function
8
9import fnmatch
10import json
11import os
12
13from chromite.lib import brick_lib
14from chromite.lib import cros_build_lib
15from chromite.lib import cros_logging as logging
16from chromite.lib import portage_util
17from chromite.lib import project_sdk
18from chromite.lib import remote_access
19try:
20 import portage
21except ImportError:
22 if cros_build_lib.IsInsideChroot():
23 raise
24
25
26_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
27# This is defined in src/platform/dev/builder.py
28_STRIPPED_PACKAGES_DIR = 'stripped-packages'
29
30_MAX_UPDATES_NUM = 10
31_MAX_UPDATES_WARNING = (
32 'You are about to update a large number of installed packages, which '
33 'might take a long time, fail midway, or leave the target in an '
34 'inconsistent state. It is highly recommended that you flash a new image '
35 'instead.')
36
37
38class DeployError(Exception):
39 """Thrown when an unrecoverable error is encountered during deploy."""
40
41
42class _InstallPackageScanner(object):
43 """Finds packages that need to be installed on a target device.
44
45 Scans the sysroot bintree, beginning with a user-provided list of packages,
46 to find all packages that need to be installed. If so instructed,
47 transitively scans forward (mandatory) and backward (optional) dependencies
48 as well. A package will be installed if missing on the target (mandatory
49 packages only), or it will be updated if its sysroot version and build time
50 are different from the target. Common usage:
51
52 pkg_scanner = _InstallPackageScanner(sysroot)
53 pkgs = pkg_scanner.Run(...)
54 """
55
56 class VartreeError(Exception):
57 """An error in the processing of the installed packages tree."""
58
59 class BintreeError(Exception):
60 """An error in the processing of the source binpkgs tree."""
61
62 class PkgInfo(object):
63 """A record containing package information."""
64
65 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
66
67 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
68 self.cpv = cpv
69 self.build_time = build_time
70 self.rdeps_raw = rdeps_raw
71 self.rdeps = set() if rdeps is None else rdeps
72 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
73
74 # Python snippet for dumping vartree info on the target. Instantiate using
75 # _GetVartreeSnippet().
76 _GET_VARTREE = """
77import portage
78import json
79trees = portage.create_trees(target_root='%(root)s', config_root='/')
80vartree = trees['%(root)s']['vartree']
81pkg_info = []
82for cpv in vartree.dbapi.cpv_all():
83 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
84 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
85 pkg_info.append((cpv, slot, rdep_raw, build_time))
86
87print(json.dumps(pkg_info))
88"""
89
90 def __init__(self, sysroot):
91 self.sysroot = sysroot
92 # Members containing the sysroot (binpkg) and target (installed) package DB.
93 self.target_db = None
94 self.binpkgs_db = None
95 # Members for managing the dependency resolution work queue.
96 self.queue = None
97 self.seen = None
98 self.listed = None
99
100 @staticmethod
101 def _GetCP(cpv):
102 """Returns the CP value for a given CPV string."""
103 attrs = portage_util.SplitCPV(cpv, strict=False)
104 if not (attrs.category and attrs.package):
105 raise ValueError('Cannot get CP value for %s' % cpv)
106 return os.path.join(attrs.category, attrs.package)
107
108 @staticmethod
109 def _InDB(cp, slot, db):
110 """Returns whether CP and slot are found in a database (if provided)."""
111 cp_slots = db.get(cp) if db else None
112 return cp_slots is not None and (not slot or slot in cp_slots)
113
114 @staticmethod
115 def _AtomStr(cp, slot):
116 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
117 return '%s:%s' % (cp, slot) if slot else cp
118
119 @classmethod
120 def _GetVartreeSnippet(cls, root='/'):
121 """Returns a code snippet for dumping the vartree on the target.
122
123 Args:
124 root: The installation root.
125
126 Returns:
127 The said code snippet (string) with parameters filled in.
128 """
129 return cls._GET_VARTREE % {'root': root}
130
131 @classmethod
132 def _StripDepAtom(cls, dep_atom, installed_db=None):
133 """Strips a dependency atom and returns a (CP, slot) pair."""
134 # TODO(garnold) This is a gross simplification of ebuild dependency
135 # semantics, stripping and ignoring various qualifiers (versions, slots,
136 # USE flag, negation) and will likely need to be fixed. chromium:447366.
137
138 # Ignore unversioned blockers, leaving them for the user to resolve.
139 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
140 return None, None
141
142 cp = dep_atom
143 slot = None
144 require_installed = False
145
146 # Versioned blockers should be updated, but only if already installed.
147 # These are often used for forcing cascaded updates of multiple packages,
148 # so we're treating them as ordinary constraints with hopes that it'll lead
149 # to the desired result.
150 if cp.startswith('!'):
151 cp = cp.lstrip('!')
152 require_installed = True
153
154 # Remove USE flags.
155 if '[' in cp:
156 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
157
158 # Separate the slot qualifier and strip off subslots.
159 if ':' in cp:
160 cp, slot = cp.split(':')
161 for delim in ('/', '='):
162 slot = slot.split(delim, 1)[0]
163
164 # Strip version wildcards (right), comparators (left).
165 cp = cp.rstrip('*')
166 cp = cp.lstrip('<=>~')
167
168 # Turn into CP form.
169 cp = cls._GetCP(cp)
170
171 if require_installed and not cls._InDB(cp, None, installed_db):
172 return None, None
173
174 return cp, slot
175
176 @classmethod
177 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
178 """Resolves and returns a list of dependencies from a dependency string.
179
180 This parses a dependency string and returns a list of package names and
181 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
182 resolving disjunctive deps, we include all choices that are fully present
183 in |installed_db|. If none is present, we choose an arbitrary one that is
184 available.
185
186 Args:
187 dep_str: A raw dependency string.
188 installed_db: A database of installed packages.
189 avail_db: A database of packages available for installation.
190
191 Returns:
192 A list of pairs (CP, slot).
193
194 Raises:
195 ValueError: the dependencies string is malformed.
196 """
197 def ProcessSubDeps(dep_exp, disjunct):
198 """Parses and processes a dependency (sub)expression."""
199 deps = set()
200 default_deps = set()
201 sub_disjunct = False
202 for dep_sub_exp in dep_exp:
203 sub_deps = set()
204
205 if isinstance(dep_sub_exp, (list, tuple)):
206 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
207 sub_disjunct = False
208 elif sub_disjunct:
209 raise ValueError('Malformed disjunctive operation in deps')
210 elif dep_sub_exp == '||':
211 sub_disjunct = True
212 elif dep_sub_exp.endswith('?'):
213 raise ValueError('Dependencies contain a conditional')
214 else:
215 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
216 if cp:
217 sub_deps = set([(cp, slot)])
218 elif disjunct:
219 raise ValueError('Atom in disjunct ignored')
220
221 # Handle sub-deps of a disjunctive expression.
222 if disjunct:
223 # Make the first available choice the default, for use in case that
224 # no option is installed.
225 if (not default_deps and avail_db is not None and
226 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
227 default_deps = sub_deps
228
229 # If not all sub-deps are installed, then don't consider them.
230 if not all([cls._InDB(cp, slot, installed_db)
231 for cp, slot in sub_deps]):
232 sub_deps = set()
233
234 deps.update(sub_deps)
235
236 return deps or default_deps
237
238 try:
239 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
240 except portage.exception.InvalidDependString as e:
241 raise ValueError('Invalid dep string: %s' % e)
242 except ValueError as e:
243 raise ValueError('%s: %s' % (e, dep_str))
244
245 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
246 installed_db=None):
247 """Returns a database of packages given a list of CPV info.
248
249 Args:
250 cpv_info: A list of tuples containing package CPV and attributes.
251 process_rdeps: Whether to populate forward dependencies.
252 process_rev_rdeps: Whether to populate reverse dependencies.
253 installed_db: A database of installed packages for filtering disjunctive
254 choices against; if None, using own built database.
255
256 Returns:
257 A map from CP values to another dictionary that maps slots to package
258 attribute tuples. Tuples contain a CPV value (string), build time
259 (string), runtime dependencies (set), and reverse dependencies (set,
260 empty if not populated).
261
262 Raises:
263 ValueError: If more than one CPV occupies a single slot.
264 """
265 db = {}
266 logging.debug('Populating package DB...')
267 for cpv, slot, rdeps_raw, build_time in cpv_info:
268 cp = self._GetCP(cpv)
269 cp_slots = db.setdefault(cp, dict())
270 if slot in cp_slots:
271 raise ValueError('More than one package found for %s' %
272 self._AtomStr(cp, slot))
273 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
274 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
275 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
276
277 avail_db = db
278 if installed_db is None:
279 installed_db = db
280 avail_db = None
281
282 # Add approximate forward dependencies.
283 if process_rdeps:
284 logging.debug('Populating forward dependencies...')
285 for cp, cp_slots in db.iteritems():
286 for slot, pkg_info in cp_slots.iteritems():
287 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
288 installed_db, avail_db))
289 logging.debug(' %s (%s) processed rdeps: %s',
290 self._AtomStr(cp, slot), pkg_info.cpv,
291 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
292 for rdep_cp, rdep_slot in pkg_info.rdeps]))
293
294 # Add approximate reverse dependencies (optional).
295 if process_rev_rdeps:
296 logging.debug('Populating reverse dependencies...')
297 for cp, cp_slots in db.iteritems():
298 for slot, pkg_info in cp_slots.iteritems():
299 for rdep_cp, rdep_slot in pkg_info.rdeps:
300 to_slots = db.get(rdep_cp)
301 if not to_slots:
302 continue
303
304 for to_slot, to_pkg_info in to_slots.iteritems():
305 if rdep_slot and to_slot != rdep_slot:
306 continue
307 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
308 self._AtomStr(cp, slot), pkg_info.cpv,
309 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
310 to_pkg_info.rev_rdeps.add((cp, slot))
311
312 return db
313
314 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
315 """Initializes a dictionary of packages installed on |device|."""
316 get_vartree_script = self._GetVartreeSnippet(root)
317 try:
318 result = device.agent.RemoteSh('python', remote_sudo=True,
319 input=get_vartree_script)
320 except cros_build_lib.RunCommandError as e:
321 logging.error('Cannot get target vartree:\n%s', e.result.error)
322 raise
323
324 try:
325 self.target_db = self._BuildDB(json.loads(result.output),
326 process_rdeps, process_rev_rdeps)
327 except ValueError as e:
328 raise self.VartreeError(str(e))
329
330 def _InitBinpkgDB(self, process_rdeps):
331 """Initializes a dictionary of binary packages for updating the target."""
332 # Get build root trees; portage indexes require a trailing '/'.
333 build_root = os.path.join(self.sysroot, '')
334 trees = portage.create_trees(target_root=build_root, config_root=build_root)
335 bintree = trees[build_root]['bintree']
336 binpkgs_info = []
337 for cpv in bintree.dbapi.cpv_all():
338 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
339 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
340 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
341
342 try:
343 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
344 installed_db=self.target_db)
345 except ValueError as e:
346 raise self.BintreeError(str(e))
347
348 def _InitDepQueue(self):
349 """Initializes the dependency work queue."""
350 self.queue = set()
351 self.seen = {}
352 self.listed = set()
353
354 def _EnqDep(self, dep, listed, optional):
355 """Enqueues a dependency if not seen before or if turned non-optional."""
356 if dep in self.seen and (optional or not self.seen[dep]):
357 return False
358
359 self.queue.add(dep)
360 self.seen[dep] = optional
361 if listed:
362 self.listed.add(dep)
363 return True
364
365 def _DeqDep(self):
366 """Dequeues and returns a dependency, its listed and optional flags.
367
368 This returns listed packages first, if any are present, to ensure that we
369 correctly mark them as such when they are first being processed.
370 """
371 if self.listed:
372 dep = self.listed.pop()
373 self.queue.remove(dep)
374 listed = True
375 else:
376 dep = self.queue.pop()
377 listed = False
378
379 return dep, listed, self.seen[dep]
380
381 def _FindPackageMatches(self, cpv_pattern):
382 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
383
384 This is breaking |cpv_pattern| into its C, P and V components, each of
385 which may or may not be present or contain wildcards. It then scans the
386 binpkgs database to find all atoms that match these components, returning a
387 list of CP and slot qualifier. When the pattern does not specify a version,
388 or when a CP has only one slot in the binpkgs database, we omit the slot
389 qualifier in the result.
390
391 Args:
392 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
393
394 Returns:
395 A list of (CPV, slot) pairs of packages in the binpkgs database that
396 match the pattern.
397 """
398 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
399 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
400 matches = []
401 for cp, cp_slots in self.binpkgs_db.iteritems():
402 if not fnmatch.fnmatchcase(cp, cp_pattern):
403 continue
404
405 # If no version attribute was given or there's only one slot, omit the
406 # slot qualifier.
407 if not attrs.version or len(cp_slots) == 1:
408 matches.append((cp, None))
409 else:
410 cpv_pattern = '%s-%s' % (cp, attrs.version)
411 for slot, pkg_info in cp_slots.iteritems():
412 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
413 matches.append((cp, slot))
414
415 return matches
416
417 def _FindPackage(self, pkg):
418 """Returns the (CP, slot) pair for a package matching |pkg|.
419
420 Args:
421 pkg: Path to a binary package or a (partial) package CPV specifier.
422
423 Returns:
424 A (CP, slot) pair for the given package; slot may be None (unspecified).
425
426 Raises:
427 ValueError: if |pkg| is not a binpkg file nor does it match something
428 that's in the bintree.
429 """
430 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
431 package = os.path.basename(os.path.splitext(pkg)[0])
432 category = os.path.basename(os.path.dirname(pkg))
433 return self._GetCP(os.path.join(category, package)), None
434
435 matches = self._FindPackageMatches(pkg)
436 if not matches:
437 raise ValueError('No package found for %s' % pkg)
438
439 idx = 0
440 if len(matches) > 1:
441 # Ask user to pick among multiple matches.
442 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
443 ['%s:%s' % (cp, slot) if slot else cp
444 for cp, slot in matches])
445
446 return matches[idx]
447
448 def _NeedsInstall(self, cpv, slot, build_time, optional):
449 """Returns whether a package needs to be installed on the target.
450
451 Args:
452 cpv: Fully qualified CPV (string) of the package.
453 slot: Slot identifier (string).
454 build_time: The BUILT_TIME value (string) of the binpkg.
455 optional: Whether package is optional on the target.
456
457 Returns:
458 A tuple (install, update) indicating whether to |install| the package and
459 whether it is an |update| to an existing package.
460
461 Raises:
462 ValueError: if slot is not provided.
463 """
464 # If not checking installed packages, always install.
465 if not self.target_db:
466 return True, False
467
468 cp = self._GetCP(cpv)
469 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
470 if target_pkg_info is not None:
471 if cpv != target_pkg_info.cpv:
472 attrs = portage_util.SplitCPV(cpv)
473 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
474 logging.debug('Updating %s: version (%s) different on target (%s)',
475 cp, attrs.version, target_attrs.version)
476 return True, True
477
478 if build_time != target_pkg_info.build_time:
479 logging.debug('Updating %s: build time (%s) different on target (%s)',
480 cpv, build_time, target_pkg_info.build_time)
481 return True, True
482
483 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
484 cp, target_pkg_info.cpv, target_pkg_info.build_time)
485 return False, False
486
487 if optional:
488 logging.debug('Not installing %s: missing on target but optional', cp)
489 return False, False
490
491 logging.debug('Installing %s: missing on target and non-optional (%s)',
492 cp, cpv)
493 return True, False
494
495 def _ProcessDeps(self, deps, reverse):
496 """Enqueues dependencies for processing.
497
498 Args:
499 deps: List of dependencies to enqueue.
500 reverse: Whether these are reverse dependencies.
501 """
502 if not deps:
503 return
504
505 logging.debug('Processing %d %s dep(s)...', len(deps),
506 'reverse' if reverse else 'forward')
507 num_already_seen = 0
508 for dep in deps:
509 if self._EnqDep(dep, False, reverse):
510 logging.debug(' Queued dep %s', dep)
511 else:
512 num_already_seen += 1
513
514 if num_already_seen:
515 logging.debug('%d dep(s) already seen', num_already_seen)
516
517 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
518 """Returns a dictionary of packages that need to be installed on the target.
519
520 Args:
521 process_rdeps: Whether to trace forward dependencies.
522 process_rev_rdeps: Whether to trace backward dependencies as well.
523
524 Returns:
525 A dictionary mapping CP values (string) to tuples containing a CPV
526 (string), a slot (string), a boolean indicating whether the package
527 was initially listed in the queue, and a boolean indicating whether this
528 is an update to an existing package.
529 """
530 installs = {}
531 while self.queue:
532 dep, listed, optional = self._DeqDep()
533 cp, required_slot = dep
534 if cp in installs:
535 logging.debug('Already updating %s', cp)
536 continue
537
538 cp_slots = self.binpkgs_db.get(cp, dict())
539 logging.debug('Checking packages matching %s%s%s...', cp,
540 ' (slot: %s)' % required_slot if required_slot else '',
541 ' (optional)' if optional else '')
542 num_processed = 0
543 for slot, pkg_info in cp_slots.iteritems():
544 if required_slot and slot != required_slot:
545 continue
546
547 num_processed += 1
548 logging.debug(' Checking %s...', pkg_info.cpv)
549
550 install, update = self._NeedsInstall(pkg_info.cpv, slot,
551 pkg_info.build_time, optional)
552 if not install:
553 continue
554
555 installs[cp] = (pkg_info.cpv, slot, listed, update)
556
557 # Add forward and backward runtime dependencies to queue.
558 if process_rdeps:
559 self._ProcessDeps(pkg_info.rdeps, False)
560 if process_rev_rdeps:
561 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
562 if target_pkg_info:
563 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
564
565 if num_processed == 0:
566 logging.warning('No qualified bintree package corresponding to %s', cp)
567
568 return installs
569
570 def _SortInstalls(self, installs):
571 """Returns a sorted list of packages to install.
572
573 Performs a topological sort based on dependencies found in the binary
574 package database.
575
576 Args:
577 installs: Dictionary of packages to install indexed by CP.
578
579 Returns:
580 A list of package CPVs (string).
581
582 Raises:
583 ValueError: If dependency graph contains a cycle.
584 """
585 not_visited = set(installs.keys())
586 curr_path = []
587 sorted_installs = []
588
589 def SortFrom(cp):
590 """Traverses dependencies recursively, emitting nodes in reverse order."""
591 cpv, slot, _, _ = installs[cp]
592 if cpv in curr_path:
593 raise ValueError('Dependencies contain a cycle: %s -> %s' %
594 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
595 curr_path.append(cpv)
596 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
597 if rdep_cp in not_visited:
598 not_visited.remove(rdep_cp)
599 SortFrom(rdep_cp)
600
601 sorted_installs.append(cpv)
602 curr_path.pop()
603
604 # So long as there's more packages, keep expanding dependency paths.
605 while not_visited:
606 SortFrom(not_visited.pop())
607
608 return sorted_installs
609
610 def _EnqListedPkg(self, pkg):
611 """Finds and enqueues a listed package."""
612 cp, slot = self._FindPackage(pkg)
613 if cp not in self.binpkgs_db:
614 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
615 self._EnqDep((cp, slot), True, False)
616
617 def _EnqInstalledPkgs(self):
618 """Enqueues all available binary packages that are already installed."""
619 for cp, cp_slots in self.binpkgs_db.iteritems():
620 target_cp_slots = self.target_db.get(cp)
621 if target_cp_slots:
622 for slot in cp_slots.iterkeys():
623 if slot in target_cp_slots:
624 self._EnqDep((cp, slot), True, False)
625
626 def Run(self, device, root, listed_pkgs, update, process_rdeps,
627 process_rev_rdeps):
628 """Computes the list of packages that need to be installed on a target.
629
630 Args:
631 device: Target handler object.
632 root: Package installation root.
633 listed_pkgs: Package names/files listed by the user.
634 update: Whether to read the target's installed package database.
635 process_rdeps: Whether to trace forward dependencies.
636 process_rev_rdeps: Whether to trace backward dependencies as well.
637
638 Returns:
639 A tuple (sorted, listed, num_updates) where |sorted| is a list of package
640 CPVs (string) to install on the target in an order that satisfies their
641 inter-dependencies, |listed| the subset that was requested by the user,
642 and |num_updates| the number of packages being installed over preexisting
643 versions. Note that installation order should be reversed for removal.
644 """
645 if process_rev_rdeps and not process_rdeps:
646 raise ValueError('Must processing forward deps when processing rev deps')
647 if process_rdeps and not update:
648 raise ValueError('Must check installed packages when processing deps')
649
650 if update:
651 logging.info('Initializing target intalled packages database...')
652 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
653
654 logging.info('Initializing binary packages database...')
655 self._InitBinpkgDB(process_rdeps)
656
657 logging.info('Finding listed package(s)...')
658 self._InitDepQueue()
659 for pkg in listed_pkgs:
660 if pkg == '@installed':
661 if not update:
662 raise ValueError(
663 'Must check installed packages when updating all of them.')
664 self._EnqInstalledPkgs()
665 else:
666 self._EnqListedPkg(pkg)
667
668 logging.info('Computing set of packages to install...')
669 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
670
671 num_updates = 0
672 listed_installs = []
673 for cpv, _, listed, update in installs.itervalues():
674 if listed:
675 listed_installs.append(cpv)
676 if update:
677 num_updates += 1
678
679 logging.info('Processed %d package(s), %d will be installed, %d are '
680 'updating existing packages',
681 len(self.seen), len(installs), num_updates)
682
683 sorted_installs = self._SortInstalls(installs)
684 return sorted_installs, listed_installs, num_updates
685
686
687def _GetPackageByCPV(cpv, strip, board, sysroot):
688 """Returns the path to a binary package corresponding to |cpv|.
689
690 Args:
691 cpv: CPV components given by portage_util.SplitCPV().
692 strip: True to run strip_package.
693 board: Board to use.
694 sysroot: Board sysroot path.
695 """
696 packages_dir = None
697 if strip:
698 try:
699 cros_build_lib.RunCommand(
700 ['strip_package', '--board', board,
701 os.path.join(cpv.category, '%s' % (cpv.pv))])
702 packages_dir = _STRIPPED_PACKAGES_DIR
703 except cros_build_lib.RunCommandError:
704 logging.error('Cannot strip package %s', cpv)
705 raise
706
707 return portage_util.GetBinaryPackagePath(
708 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
709 packages_dir=packages_dir)
710
711
712def _Emerge(device, pkg, strip, board, sysroot, root, extra_args=None):
713 """Copies |pkg| to |device| and emerges it.
714
715 Args:
716 device: A ChromiumOSDevice object.
717 pkg: A package CPV or a binary package file.
718 strip: True to run strip_package.
719 board: Board to use.
720 sysroot: Board sysroot path.
721 root: Package installation root path.
722 extra_args: Extra arguments to pass to emerge.
723
724 Raises:
725 DeployError: Unrecoverable error during emerge.
726 """
727 if os.path.isfile(pkg):
728 latest_pkg = pkg
729 else:
730 latest_pkg = _GetPackageByCPV(portage_util.SplitCPV(pkg), strip, board,
731 sysroot)
732
733 if not latest_pkg:
734 raise DeployError('Missing package %s.' % pkg)
735
736 pkgroot = os.path.join(device.work_dir, 'packages')
737 pkg_name = os.path.basename(latest_pkg)
738 pkg_dirname = os.path.basename(os.path.dirname(latest_pkg))
739 pkg_dir = os.path.join(pkgroot, pkg_dirname)
740 device.RunCommand(['mkdir', '-p', pkg_dir], remote_sudo=True)
741
742 logging.info('Copying %s to device...', latest_pkg)
743 device.CopyToDevice(latest_pkg, pkg_dir, remote_sudo=True)
744
745 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
746 device.RunCommand(['mkdir', '-p', portage_tmpdir], remote_sudo=True)
747 logging.info('Use portage temp dir %s', portage_tmpdir)
748
749 logging.info('Installing %s...', latest_pkg)
750 pkg_path = os.path.join(pkg_dir, pkg_name)
751
752 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
753 # chromeos-base packages will be skipped due to the configuration
754 # in /etc/protage/make.profile/package.provided. However, there is
755 # a known bug that /usr/local/etc/portage is not setup properly
756 # (crbug.com/312041). This does not affect `cros deploy` because
757 # we do not use the preset PKGDIR.
758 extra_env = {
759 'FEATURES': '-sandbox',
760 'PKGDIR': pkgroot,
761 'PORTAGE_CONFIGROOT': '/usr/local',
762 'PORTAGE_TMPDIR': portage_tmpdir,
763 'PORTDIR': device.work_dir,
764 'CONFIG_PROTECT': '-*',
765 }
766 cmd = ['emerge', '--usepkg', pkg_path, '--root=%s' % root]
767 if extra_args:
768 cmd.append(extra_args)
769
770 try:
771 # Always showing the emerge output for clarity.
772 device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
773 capture_output=False, debug_level=logging.INFO)
774 except Exception:
775 logging.error('Failed to emerge package %s', pkg)
776 raise
777 else:
778 logging.info('%s has been installed.', pkg)
779 finally:
780 # Free up the space for other packages.
781 device.RunCommand(['rm', '-rf', portage_tmpdir, pkg_dir],
782 error_code_ok=True, remote_sudo=True)
783
784
785def _Unmerge(device, pkg, root):
786 """Unmerges |pkg| on |device|.
787
788 Args:
789 device: A RemoteDevice object.
790 pkg: A package name.
791 root: Package installation root path.
792 """
793 logging.info('Unmerging %s...', pkg)
794 cmd = ['qmerge', '--yes']
795 # Check if qmerge is available on the device. If not, use emerge.
796 if device.RunCommand(
797 ['qmerge', '--version'], error_code_ok=True).returncode != 0:
798 cmd = ['emerge']
799
800 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
801 try:
802 # Always showing the qmerge/emerge output for clarity.
803 device.RunCommand(cmd, capture_output=False, remote_sudo=True,
804 debug_level=logging.INFO)
805 except Exception:
806 logging.error('Failed to unmerge package %s', pkg)
807 raise
808 else:
809 logging.info('%s has been uninstalled.', pkg)
810
811
812def _ConfirmDeploy(num_updates):
813 """Returns whether we can continue deployment."""
814 if num_updates > _MAX_UPDATES_NUM:
815 logging.warning(_MAX_UPDATES_WARNING)
816 return cros_build_lib.BooleanPrompt(default=False)
817
818 return True
819
820
821def Deploy(device, packages, board=None, brick=None, emerge=True, update=False,
822 deep=False, deep_rev=False, clean_binpkg=True, root='/', strip=True,
823 emerge_args=None, ssh_private_key=None, ping=True, force=False,
824 dry_run=False):
825 """Deploys packages to a device.
826
827 Args:
828 device: commandline.Device object.
829 packages: List of packages (strings) to deploy to device.
830 board: Board to use; None to automatically detect.
831 brick: Brick locator to use. Overrides |board| if not None.
832 emerge: True to emerge package, False to unmerge.
833 update: Check installed version on device.
834 deep: Install dependencies also. Implies |update|.
835 deep_rev: Install reverse dependencies. Implies |deep|.
836 clean_binpkg: Clean outdated binary packages.
837 root: Package installation root path.
838 strip: Run strip_package to filter out preset paths in the package.
839 emerge_args: Extra arguments to pass to emerge.
840 ssh_private_key: Path to an SSH private key file; None to use test keys.
841 ping: True to ping the device before trying to connect.
842 force: Ignore sanity checks and prompts.
843 dry_run: Print deployment plan but do not deploy anything.
844
845 Raises:
846 ValueError: Invalid parameter or parameter combination.
847 DeployError: Unrecoverable failure during deploy.
848 """
849 if deep_rev:
850 deep = True
851 if deep:
852 update = True
853
854 if update and not emerge:
855 raise ValueError('Cannot update and unmerge.')
856
857 brick = brick_lib.Brick(brick) if brick else None
858 if brick:
859 board = brick.FriendlyName()
860
861 with remote_access.ChromiumOSDeviceHandler(
862 device.hostname, port=device.port, username=device.username,
863 private_key=ssh_private_key, base_dir=_DEVICE_BASE_DIR,
864 ping=ping) as device:
865 try:
866 board = cros_build_lib.GetBoard(device_board=device.board,
867 override_board=board)
868 logging.info('Board is %s', board)
869
870 if not force:
871 # If a brick is specified, it must be compatible with the device.
872 if brick:
873 if not brick.Inherits(device.board):
874 raise DeployError('Device (%s) is incompatible with brick' %
875 device.board)
876 elif board != device.board:
877 raise DeployError('Device (%s) is incompatible with board' %
878 device.board)
879
880 # If this is an official SDK, check that the target is compatible.
881 sdk_version = project_sdk.FindVersion()
882 if sdk_version and device.sdk_version != sdk_version:
883 raise DeployError('Device SDK version (%s) is incompatible with '
884 'your environment (%s)' %
885 (device.sdk_version or 'unknown', sdk_version))
886
887 sysroot = cros_build_lib.GetSysroot(board=board)
888
889 # If no packages were listed, find the brick's main packages.
890 packages = packages or (brick and brick.MainPackages())
891 if not packages:
892 raise DeployError('No packages found, nothing to deploy.')
893
894 if clean_binpkg:
895 logging.info('Cleaning outdated binary packages for %s', board)
896 portage_util.CleanOutdatedBinaryPackages(board)
897
898 if not device.IsPathWritable(root):
899 # Only remounts rootfs if the given root is not writable.
900 if not device.MountRootfsReadWrite():
901 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
902
903 # Obtain list of packages to upgrade/remove.
904 pkg_scanner = _InstallPackageScanner(sysroot)
905 pkgs, listed, num_updates = pkg_scanner.Run(device, root, packages,
906 update, deep, deep_rev)
907 if emerge:
908 action_str = 'emerge'
909 else:
910 pkgs.reverse()
911 action_str = 'unmerge'
912
913 if not pkgs:
914 logging.info('No packages to %s', action_str)
915 return
916
917 logging.info('These are the packages to %s:', action_str)
918 for i, pkg in enumerate(pkgs):
919 logging.info('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
920
921 if dry_run or not _ConfirmDeploy(num_updates):
922 return
923
924 for pkg in pkgs:
925 if emerge:
926 _Emerge(device, pkg, strip, board, sysroot, root,
927 extra_args=emerge_args)
928 else:
929 _Unmerge(device, pkg, root)
930
931 logging.warning('Please restart any updated services on the device, '
932 'or just reboot it.')
933 except Exception:
934 if device.lsb_release:
935 lsb_entries = sorted(device.lsb_release.items())
936 logging.info('Following are the LSB version details of the device:\n%s',
937 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
938 raise