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