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