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