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