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