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