blob: 533ebce1e07e3d9bf6ec9d869a14320114a47560 [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 = """
David Pursell9476bf42015-03-30 13:34:27 -0700111import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700112import os
113import portage
114
115# Normalize the path to match what portage will index.
116target_root = os.path.normpath('%(root)s')
117if not target_root.endswith('/'):
118 target_root += '/'
119trees = portage.create_trees(target_root=target_root, config_root='/')
120vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700121pkg_info = []
122for cpv in vartree.dbapi.cpv_all():
123 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
124 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
125 pkg_info.append((cpv, slot, rdep_raw, build_time))
126
127print(json.dumps(pkg_info))
128"""
129
130 def __init__(self, sysroot):
131 self.sysroot = sysroot
132 # Members containing the sysroot (binpkg) and target (installed) package DB.
133 self.target_db = None
134 self.binpkgs_db = None
135 # Members for managing the dependency resolution work queue.
136 self.queue = None
137 self.seen = None
138 self.listed = None
139
140 @staticmethod
141 def _GetCP(cpv):
142 """Returns the CP value for a given CPV string."""
143 attrs = portage_util.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600144 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700145 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600146 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700147
148 @staticmethod
149 def _InDB(cp, slot, db):
150 """Returns whether CP and slot are found in a database (if provided)."""
151 cp_slots = db.get(cp) if db else None
152 return cp_slots is not None and (not slot or slot in cp_slots)
153
154 @staticmethod
155 def _AtomStr(cp, slot):
156 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
157 return '%s:%s' % (cp, slot) if slot else cp
158
159 @classmethod
160 def _GetVartreeSnippet(cls, root='/'):
161 """Returns a code snippet for dumping the vartree on the target.
162
163 Args:
164 root: The installation root.
165
166 Returns:
167 The said code snippet (string) with parameters filled in.
168 """
169 return cls._GET_VARTREE % {'root': root}
170
171 @classmethod
172 def _StripDepAtom(cls, dep_atom, installed_db=None):
173 """Strips a dependency atom and returns a (CP, slot) pair."""
174 # TODO(garnold) This is a gross simplification of ebuild dependency
175 # semantics, stripping and ignoring various qualifiers (versions, slots,
176 # USE flag, negation) and will likely need to be fixed. chromium:447366.
177
178 # Ignore unversioned blockers, leaving them for the user to resolve.
179 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
180 return None, None
181
182 cp = dep_atom
183 slot = None
184 require_installed = False
185
186 # Versioned blockers should be updated, but only if already installed.
187 # These are often used for forcing cascaded updates of multiple packages,
188 # so we're treating them as ordinary constraints with hopes that it'll lead
189 # to the desired result.
190 if cp.startswith('!'):
191 cp = cp.lstrip('!')
192 require_installed = True
193
194 # Remove USE flags.
195 if '[' in cp:
196 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
197
198 # Separate the slot qualifier and strip off subslots.
199 if ':' in cp:
200 cp, slot = cp.split(':')
201 for delim in ('/', '='):
202 slot = slot.split(delim, 1)[0]
203
204 # Strip version wildcards (right), comparators (left).
205 cp = cp.rstrip('*')
206 cp = cp.lstrip('<=>~')
207
208 # Turn into CP form.
209 cp = cls._GetCP(cp)
210
211 if require_installed and not cls._InDB(cp, None, installed_db):
212 return None, None
213
214 return cp, slot
215
216 @classmethod
217 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
218 """Resolves and returns a list of dependencies from a dependency string.
219
220 This parses a dependency string and returns a list of package names and
221 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
222 resolving disjunctive deps, we include all choices that are fully present
223 in |installed_db|. If none is present, we choose an arbitrary one that is
224 available.
225
226 Args:
227 dep_str: A raw dependency string.
228 installed_db: A database of installed packages.
229 avail_db: A database of packages available for installation.
230
231 Returns:
232 A list of pairs (CP, slot).
233
234 Raises:
235 ValueError: the dependencies string is malformed.
236 """
237 def ProcessSubDeps(dep_exp, disjunct):
238 """Parses and processes a dependency (sub)expression."""
239 deps = set()
240 default_deps = set()
241 sub_disjunct = False
242 for dep_sub_exp in dep_exp:
243 sub_deps = set()
244
245 if isinstance(dep_sub_exp, (list, tuple)):
246 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
247 sub_disjunct = False
248 elif sub_disjunct:
249 raise ValueError('Malformed disjunctive operation in deps')
250 elif dep_sub_exp == '||':
251 sub_disjunct = True
252 elif dep_sub_exp.endswith('?'):
253 raise ValueError('Dependencies contain a conditional')
254 else:
255 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
256 if cp:
257 sub_deps = set([(cp, slot)])
258 elif disjunct:
259 raise ValueError('Atom in disjunct ignored')
260
261 # Handle sub-deps of a disjunctive expression.
262 if disjunct:
263 # Make the first available choice the default, for use in case that
264 # no option is installed.
265 if (not default_deps and avail_db is not None and
266 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
267 default_deps = sub_deps
268
269 # If not all sub-deps are installed, then don't consider them.
270 if not all([cls._InDB(cp, slot, installed_db)
271 for cp, slot in sub_deps]):
272 sub_deps = set()
273
274 deps.update(sub_deps)
275
276 return deps or default_deps
277
278 try:
279 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
280 except portage.exception.InvalidDependString as e:
281 raise ValueError('Invalid dep string: %s' % e)
282 except ValueError as e:
283 raise ValueError('%s: %s' % (e, dep_str))
284
285 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
286 installed_db=None):
287 """Returns a database of packages given a list of CPV info.
288
289 Args:
290 cpv_info: A list of tuples containing package CPV and attributes.
291 process_rdeps: Whether to populate forward dependencies.
292 process_rev_rdeps: Whether to populate reverse dependencies.
293 installed_db: A database of installed packages for filtering disjunctive
294 choices against; if None, using own built database.
295
296 Returns:
297 A map from CP values to another dictionary that maps slots to package
298 attribute tuples. Tuples contain a CPV value (string), build time
299 (string), runtime dependencies (set), and reverse dependencies (set,
300 empty if not populated).
301
302 Raises:
303 ValueError: If more than one CPV occupies a single slot.
304 """
305 db = {}
306 logging.debug('Populating package DB...')
307 for cpv, slot, rdeps_raw, build_time in cpv_info:
308 cp = self._GetCP(cpv)
309 cp_slots = db.setdefault(cp, dict())
310 if slot in cp_slots:
311 raise ValueError('More than one package found for %s' %
312 self._AtomStr(cp, slot))
313 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
314 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
315 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
316
317 avail_db = db
318 if installed_db is None:
319 installed_db = db
320 avail_db = None
321
322 # Add approximate forward dependencies.
323 if process_rdeps:
324 logging.debug('Populating forward dependencies...')
325 for cp, cp_slots in db.iteritems():
326 for slot, pkg_info in cp_slots.iteritems():
327 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
328 installed_db, avail_db))
329 logging.debug(' %s (%s) processed rdeps: %s',
330 self._AtomStr(cp, slot), pkg_info.cpv,
331 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
332 for rdep_cp, rdep_slot in pkg_info.rdeps]))
333
334 # Add approximate reverse dependencies (optional).
335 if process_rev_rdeps:
336 logging.debug('Populating reverse dependencies...')
337 for cp, cp_slots in db.iteritems():
338 for slot, pkg_info in cp_slots.iteritems():
339 for rdep_cp, rdep_slot in pkg_info.rdeps:
340 to_slots = db.get(rdep_cp)
341 if not to_slots:
342 continue
343
344 for to_slot, to_pkg_info in to_slots.iteritems():
345 if rdep_slot and to_slot != rdep_slot:
346 continue
347 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
348 self._AtomStr(cp, slot), pkg_info.cpv,
349 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
350 to_pkg_info.rev_rdeps.add((cp, slot))
351
352 return db
353
354 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
355 """Initializes a dictionary of packages installed on |device|."""
356 get_vartree_script = self._GetVartreeSnippet(root)
357 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400358 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700359 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700360 except cros_build_lib.RunCommandError as e:
361 logging.error('Cannot get target vartree:\n%s', e.result.error)
362 raise
363
364 try:
365 self.target_db = self._BuildDB(json.loads(result.output),
366 process_rdeps, process_rev_rdeps)
367 except ValueError as e:
368 raise self.VartreeError(str(e))
369
370 def _InitBinpkgDB(self, process_rdeps):
371 """Initializes a dictionary of binary packages for updating the target."""
372 # Get build root trees; portage indexes require a trailing '/'.
373 build_root = os.path.join(self.sysroot, '')
374 trees = portage.create_trees(target_root=build_root, config_root=build_root)
375 bintree = trees[build_root]['bintree']
376 binpkgs_info = []
377 for cpv in bintree.dbapi.cpv_all():
378 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
379 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
380 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
381
382 try:
383 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
384 installed_db=self.target_db)
385 except ValueError as e:
386 raise self.BintreeError(str(e))
387
388 def _InitDepQueue(self):
389 """Initializes the dependency work queue."""
390 self.queue = set()
391 self.seen = {}
392 self.listed = set()
393
394 def _EnqDep(self, dep, listed, optional):
395 """Enqueues a dependency if not seen before or if turned non-optional."""
396 if dep in self.seen and (optional or not self.seen[dep]):
397 return False
398
399 self.queue.add(dep)
400 self.seen[dep] = optional
401 if listed:
402 self.listed.add(dep)
403 return True
404
405 def _DeqDep(self):
406 """Dequeues and returns a dependency, its listed and optional flags.
407
408 This returns listed packages first, if any are present, to ensure that we
409 correctly mark them as such when they are first being processed.
410 """
411 if self.listed:
412 dep = self.listed.pop()
413 self.queue.remove(dep)
414 listed = True
415 else:
416 dep = self.queue.pop()
417 listed = False
418
419 return dep, listed, self.seen[dep]
420
421 def _FindPackageMatches(self, cpv_pattern):
422 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
423
424 This is breaking |cpv_pattern| into its C, P and V components, each of
425 which may or may not be present or contain wildcards. It then scans the
426 binpkgs database to find all atoms that match these components, returning a
427 list of CP and slot qualifier. When the pattern does not specify a version,
428 or when a CP has only one slot in the binpkgs database, we omit the slot
429 qualifier in the result.
430
431 Args:
432 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
433
434 Returns:
435 A list of (CPV, slot) pairs of packages in the binpkgs database that
436 match the pattern.
437 """
438 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
439 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
440 matches = []
441 for cp, cp_slots in self.binpkgs_db.iteritems():
442 if not fnmatch.fnmatchcase(cp, cp_pattern):
443 continue
444
445 # If no version attribute was given or there's only one slot, omit the
446 # slot qualifier.
447 if not attrs.version or len(cp_slots) == 1:
448 matches.append((cp, None))
449 else:
450 cpv_pattern = '%s-%s' % (cp, attrs.version)
451 for slot, pkg_info in cp_slots.iteritems():
452 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
453 matches.append((cp, slot))
454
455 return matches
456
457 def _FindPackage(self, pkg):
458 """Returns the (CP, slot) pair for a package matching |pkg|.
459
460 Args:
461 pkg: Path to a binary package or a (partial) package CPV specifier.
462
463 Returns:
464 A (CP, slot) pair for the given package; slot may be None (unspecified).
465
466 Raises:
467 ValueError: if |pkg| is not a binpkg file nor does it match something
468 that's in the bintree.
469 """
470 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
471 package = os.path.basename(os.path.splitext(pkg)[0])
472 category = os.path.basename(os.path.dirname(pkg))
473 return self._GetCP(os.path.join(category, package)), None
474
475 matches = self._FindPackageMatches(pkg)
476 if not matches:
477 raise ValueError('No package found for %s' % pkg)
478
479 idx = 0
480 if len(matches) > 1:
481 # Ask user to pick among multiple matches.
482 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
483 ['%s:%s' % (cp, slot) if slot else cp
484 for cp, slot in matches])
485
486 return matches[idx]
487
488 def _NeedsInstall(self, cpv, slot, build_time, optional):
489 """Returns whether a package needs to be installed on the target.
490
491 Args:
492 cpv: Fully qualified CPV (string) of the package.
493 slot: Slot identifier (string).
494 build_time: The BUILT_TIME value (string) of the binpkg.
495 optional: Whether package is optional on the target.
496
497 Returns:
498 A tuple (install, update) indicating whether to |install| the package and
499 whether it is an |update| to an existing package.
500
501 Raises:
502 ValueError: if slot is not provided.
503 """
504 # If not checking installed packages, always install.
505 if not self.target_db:
506 return True, False
507
508 cp = self._GetCP(cpv)
509 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
510 if target_pkg_info is not None:
511 if cpv != target_pkg_info.cpv:
512 attrs = portage_util.SplitCPV(cpv)
513 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
514 logging.debug('Updating %s: version (%s) different on target (%s)',
515 cp, attrs.version, target_attrs.version)
516 return True, True
517
518 if build_time != target_pkg_info.build_time:
519 logging.debug('Updating %s: build time (%s) different on target (%s)',
520 cpv, build_time, target_pkg_info.build_time)
521 return True, True
522
523 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
524 cp, target_pkg_info.cpv, target_pkg_info.build_time)
525 return False, False
526
527 if optional:
528 logging.debug('Not installing %s: missing on target but optional', cp)
529 return False, False
530
531 logging.debug('Installing %s: missing on target and non-optional (%s)',
532 cp, cpv)
533 return True, False
534
535 def _ProcessDeps(self, deps, reverse):
536 """Enqueues dependencies for processing.
537
538 Args:
539 deps: List of dependencies to enqueue.
540 reverse: Whether these are reverse dependencies.
541 """
542 if not deps:
543 return
544
545 logging.debug('Processing %d %s dep(s)...', len(deps),
546 'reverse' if reverse else 'forward')
547 num_already_seen = 0
548 for dep in deps:
549 if self._EnqDep(dep, False, reverse):
550 logging.debug(' Queued dep %s', dep)
551 else:
552 num_already_seen += 1
553
554 if num_already_seen:
555 logging.debug('%d dep(s) already seen', num_already_seen)
556
557 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
558 """Returns a dictionary of packages that need to be installed on the target.
559
560 Args:
561 process_rdeps: Whether to trace forward dependencies.
562 process_rev_rdeps: Whether to trace backward dependencies as well.
563
564 Returns:
565 A dictionary mapping CP values (string) to tuples containing a CPV
566 (string), a slot (string), a boolean indicating whether the package
567 was initially listed in the queue, and a boolean indicating whether this
568 is an update to an existing package.
569 """
570 installs = {}
571 while self.queue:
572 dep, listed, optional = self._DeqDep()
573 cp, required_slot = dep
574 if cp in installs:
575 logging.debug('Already updating %s', cp)
576 continue
577
578 cp_slots = self.binpkgs_db.get(cp, dict())
579 logging.debug('Checking packages matching %s%s%s...', cp,
580 ' (slot: %s)' % required_slot if required_slot else '',
581 ' (optional)' if optional else '')
582 num_processed = 0
583 for slot, pkg_info in cp_slots.iteritems():
584 if required_slot and slot != required_slot:
585 continue
586
587 num_processed += 1
588 logging.debug(' Checking %s...', pkg_info.cpv)
589
590 install, update = self._NeedsInstall(pkg_info.cpv, slot,
591 pkg_info.build_time, optional)
592 if not install:
593 continue
594
595 installs[cp] = (pkg_info.cpv, slot, listed, update)
596
597 # Add forward and backward runtime dependencies to queue.
598 if process_rdeps:
599 self._ProcessDeps(pkg_info.rdeps, False)
600 if process_rev_rdeps:
601 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
602 if target_pkg_info:
603 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
604
605 if num_processed == 0:
606 logging.warning('No qualified bintree package corresponding to %s', cp)
607
608 return installs
609
610 def _SortInstalls(self, installs):
611 """Returns a sorted list of packages to install.
612
613 Performs a topological sort based on dependencies found in the binary
614 package database.
615
616 Args:
617 installs: Dictionary of packages to install indexed by CP.
618
619 Returns:
620 A list of package CPVs (string).
621
622 Raises:
623 ValueError: If dependency graph contains a cycle.
624 """
625 not_visited = set(installs.keys())
626 curr_path = []
627 sorted_installs = []
628
629 def SortFrom(cp):
630 """Traverses dependencies recursively, emitting nodes in reverse order."""
631 cpv, slot, _, _ = installs[cp]
632 if cpv in curr_path:
633 raise ValueError('Dependencies contain a cycle: %s -> %s' %
634 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
635 curr_path.append(cpv)
636 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
637 if rdep_cp in not_visited:
638 not_visited.remove(rdep_cp)
639 SortFrom(rdep_cp)
640
641 sorted_installs.append(cpv)
642 curr_path.pop()
643
644 # So long as there's more packages, keep expanding dependency paths.
645 while not_visited:
646 SortFrom(not_visited.pop())
647
648 return sorted_installs
649
650 def _EnqListedPkg(self, pkg):
651 """Finds and enqueues a listed package."""
652 cp, slot = self._FindPackage(pkg)
653 if cp not in self.binpkgs_db:
654 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
655 self._EnqDep((cp, slot), True, False)
656
657 def _EnqInstalledPkgs(self):
658 """Enqueues all available binary packages that are already installed."""
659 for cp, cp_slots in self.binpkgs_db.iteritems():
660 target_cp_slots = self.target_db.get(cp)
661 if target_cp_slots:
662 for slot in cp_slots.iterkeys():
663 if slot in target_cp_slots:
664 self._EnqDep((cp, slot), True, False)
665
666 def Run(self, device, root, listed_pkgs, update, process_rdeps,
667 process_rev_rdeps):
668 """Computes the list of packages that need to be installed on a target.
669
670 Args:
671 device: Target handler object.
672 root: Package installation root.
673 listed_pkgs: Package names/files listed by the user.
674 update: Whether to read the target's installed package database.
675 process_rdeps: Whether to trace forward dependencies.
676 process_rev_rdeps: Whether to trace backward dependencies as well.
677
678 Returns:
679 A tuple (sorted, listed, num_updates) where |sorted| is a list of package
680 CPVs (string) to install on the target in an order that satisfies their
681 inter-dependencies, |listed| the subset that was requested by the user,
682 and |num_updates| the number of packages being installed over preexisting
683 versions. Note that installation order should be reversed for removal.
684 """
685 if process_rev_rdeps and not process_rdeps:
686 raise ValueError('Must processing forward deps when processing rev deps')
687 if process_rdeps and not update:
688 raise ValueError('Must check installed packages when processing deps')
689
690 if update:
691 logging.info('Initializing target intalled packages database...')
692 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
693
694 logging.info('Initializing binary packages database...')
695 self._InitBinpkgDB(process_rdeps)
696
697 logging.info('Finding listed package(s)...')
698 self._InitDepQueue()
699 for pkg in listed_pkgs:
700 if pkg == '@installed':
701 if not update:
702 raise ValueError(
703 'Must check installed packages when updating all of them.')
704 self._EnqInstalledPkgs()
705 else:
706 self._EnqListedPkg(pkg)
707
708 logging.info('Computing set of packages to install...')
709 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
710
711 num_updates = 0
712 listed_installs = []
713 for cpv, _, listed, update in installs.itervalues():
714 if listed:
715 listed_installs.append(cpv)
716 if update:
717 num_updates += 1
718
719 logging.info('Processed %d package(s), %d will be installed, %d are '
720 'updating existing packages',
721 len(self.seen), len(installs), num_updates)
722
723 sorted_installs = self._SortInstalls(installs)
724 return sorted_installs, listed_installs, num_updates
725
726
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700727def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700728 """Copies |pkg| to |device| and emerges it.
729
730 Args:
731 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700732 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700733 root: Package installation root path.
734 extra_args: Extra arguments to pass to emerge.
735
736 Raises:
737 DeployError: Unrecoverable error during emerge.
738 """
David Pursell9476bf42015-03-30 13:34:27 -0700739 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700740 pkg_name = os.path.basename(pkg_path)
741 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700742 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400743 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
744 # Clean out the dirs first if we had a previous emerge on the device so as to
745 # free up space for this emerge. The last emerge gets implicitly cleaned up
746 # when the device connection deletes its work_dir.
747 device.RunCommand(
748 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
749 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700750
Ralph Nathane01ccf12015-04-16 10:40:32 -0700751 # This message is read by BrilloDeployOperation.
752 logging.notice('Copying %s to device.', pkg_name)
Ilja H. Friedel0ab63e12017-03-28 13:29:48 -0700753 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700754
David Pursell9476bf42015-03-30 13:34:27 -0700755 logging.info('Use portage temp dir %s', portage_tmpdir)
756
Ralph Nathane01ccf12015-04-16 10:40:32 -0700757 # This message is read by BrilloDeployOperation.
758 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700759 pkg_path = os.path.join(pkg_dir, pkg_name)
760
761 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
762 # chromeos-base packages will be skipped due to the configuration
763 # in /etc/protage/make.profile/package.provided. However, there is
764 # a known bug that /usr/local/etc/portage is not setup properly
765 # (crbug.com/312041). This does not affect `cros deploy` because
766 # we do not use the preset PKGDIR.
767 extra_env = {
768 'FEATURES': '-sandbox',
769 'PKGDIR': pkgroot,
770 'PORTAGE_CONFIGROOT': '/usr/local',
771 'PORTAGE_TMPDIR': portage_tmpdir,
772 'PORTDIR': device.work_dir,
773 'CONFIG_PROTECT': '-*',
774 }
775 cmd = ['emerge', '--usepkg', pkg_path, '--root=%s' % root]
776 if extra_args:
777 cmd.append(extra_args)
778
779 try:
Greg Kerrb96c02c2019-02-08 14:32:41 -0800780 result = device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
781 capture_output=True, debug_level=logging.INFO)
782
783 pattern = ('A requested package will not be merged because '
784 'it is listed in package.provided')
785 output = result.error.replace('\n', ' ').replace('\r', '')
786 if pattern in output:
787 error = ('Package failed to emerge: %s\n'
788 'Remove %s from /etc/portage/make.profile/'
789 'package.provided/chromeos-base.packages\n'
790 '(also see crbug.com/920140 for more context)\n'
791 % (pattern, pkg_name))
792 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700793 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700794 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700795 raise
796 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700797 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700798
799
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700800def _GetPackagesByCPV(cpvs, strip, sysroot):
801 """Returns paths to binary packages corresponding to |cpvs|.
802
803 Args:
804 cpvs: List of CPV components given by portage_util.SplitCPV().
805 strip: True to run strip_package.
806 sysroot: Sysroot path.
807
808 Returns:
809 List of paths corresponding to |cpvs|.
810
811 Raises:
812 DeployError: If a package is missing.
813 """
814 packages_dir = None
815 if strip:
816 try:
817 cros_build_lib.RunCommand(
818 ['strip_package', '--sysroot', sysroot] +
Alex Klein7078e252018-10-02 10:21:04 -0600819 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700820 packages_dir = _STRIPPED_PACKAGES_DIR
821 except cros_build_lib.RunCommandError:
822 logging.error('Cannot strip packages %s',
823 ' '.join([str(cpv) for cpv in cpvs]))
824 raise
825
826 paths = []
827 for cpv in cpvs:
828 path = portage_util.GetBinaryPackagePath(
829 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
830 packages_dir=packages_dir)
831 if not path:
832 raise DeployError('Missing package %s.' % cpv)
833 paths.append(path)
834
835 return paths
836
837
838def _GetPackagesPaths(pkgs, strip, sysroot):
839 """Returns paths to binary |pkgs|.
840
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700841 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700842 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700843 strip: Whether or not to run strip_package for CPV packages.
844 sysroot: The sysroot path.
845
846 Returns:
847 List of paths corresponding to |pkgs|.
848 """
Ned Nguyend0db4072019-02-22 14:19:21 -0700849 cpvs = [portage_util.SplitCPV(p) for p in pkgs]
850 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700851
852
David Pursell9476bf42015-03-30 13:34:27 -0700853def _Unmerge(device, pkg, root):
854 """Unmerges |pkg| on |device|.
855
856 Args:
857 device: A RemoteDevice object.
858 pkg: A package name.
859 root: Package installation root path.
860 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700861 pkg_name = os.path.basename(pkg)
862 # This message is read by BrilloDeployOperation.
863 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700864 cmd = ['qmerge', '--yes']
865 # Check if qmerge is available on the device. If not, use emerge.
866 if device.RunCommand(
867 ['qmerge', '--version'], error_code_ok=True).returncode != 0:
868 cmd = ['emerge']
869
870 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
871 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700872 # Always showing the emerge output for clarity.
David Pursell9476bf42015-03-30 13:34:27 -0700873 device.RunCommand(cmd, capture_output=False, remote_sudo=True,
874 debug_level=logging.INFO)
875 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700876 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700877 raise
878 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700879 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700880
881
882def _ConfirmDeploy(num_updates):
883 """Returns whether we can continue deployment."""
884 if num_updates > _MAX_UPDATES_NUM:
885 logging.warning(_MAX_UPDATES_WARNING)
886 return cros_build_lib.BooleanPrompt(default=False)
887
888 return True
889
890
Ralph Nathane01ccf12015-04-16 10:40:32 -0700891def _EmergePackages(pkgs, device, strip, sysroot, root, emerge_args):
892 """Call _Emerge for each packge in pkgs."""
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700893 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
894 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700895
896
897def _UnmergePackages(pkgs, device, root):
898 """Call _Unmege for each package in pkgs."""
899 for pkg in pkgs:
900 _Unmerge(device, pkg, root)
901
902
Gilad Arnolda0a98062015-07-07 08:34:27 -0700903def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
904 deep_rev=False, clean_binpkg=True, root='/', strip=True,
905 emerge_args=None, ssh_private_key=None, ping=True, force=False,
906 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -0700907 """Deploys packages to a device.
908
909 Args:
David Pursell2e773382015-04-03 14:30:47 -0700910 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -0700911 packages: List of packages (strings) to deploy to device.
912 board: Board to use; None to automatically detect.
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
Gilad Arnolda0a98062015-07-07 08:34:27 -0700935 if not packages:
936 raise DeployError('No packages provided, nothing to deploy.')
937
David Pursell9476bf42015-03-30 13:34:27 -0700938 if update and not emerge:
939 raise ValueError('Cannot update and unmerge.')
940
David Pursell2e773382015-04-03 14:30:47 -0700941 if device:
942 hostname, username, port = device.hostname, device.username, device.port
943 else:
944 hostname, username, port = None, None, None
945
Gilad Arnold4d3ade72015-04-28 15:13:35 -0700946 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -0700947 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -0700948 try:
Gilad Arnold5dc243a2015-07-07 08:22:43 -0700949 with remote_access.ChromiumOSDeviceHandler(
950 hostname, port=port, username=username, private_key=ssh_private_key,
951 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -0400952 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -0700953
Gilad Arnolda0a98062015-07-07 08:34:27 -0700954 board = cros_build_lib.GetBoard(device_board=device.board,
955 override_board=board)
956 if not force and board != device.board:
957 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -0700958 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -0700959
Gilad Arnolda0a98062015-07-07 08:34:27 -0700960 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -0700961
962 if clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700963 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -0700964 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -0700965
Mike Frysinger539db512015-05-21 18:14:01 -0400966 if not device.IsDirWritable(root):
David Pursell9476bf42015-03-30 13:34:27 -0700967 # Only remounts rootfs if the given root is not writable.
Mike Frysinger539db512015-05-21 18:14:01 -0400968 if not device.MountRootfsReadWrite():
David Pursell9476bf42015-03-30 13:34:27 -0700969 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
970
971 # Obtain list of packages to upgrade/remove.
972 pkg_scanner = _InstallPackageScanner(sysroot)
Gilad Arnold655e67d2015-04-29 11:14:18 -0700973 pkgs, listed, num_updates = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -0400974 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -0700975 if emerge:
976 action_str = 'emerge'
977 else:
978 pkgs.reverse()
979 action_str = 'unmerge'
980
981 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700982 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -0700983 return
984
Ralph Nathane01ccf12015-04-16 10:40:32 -0700985 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -0700986 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -0700987 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -0700988
989 if dry_run or not _ConfirmDeploy(num_updates):
990 return
991
Ralph Nathane01ccf12015-04-16 10:40:32 -0700992 # Select function (emerge or unmerge) and bind args.
993 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -0400994 func = functools.partial(_EmergePackages, pkgs, device, strip,
Ralph Nathane01ccf12015-04-16 10:40:32 -0700995 sysroot, root, emerge_args)
996 else:
Mike Frysinger539db512015-05-21 18:14:01 -0400997 func = functools.partial(_UnmergePackages, pkgs, device, root)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700998
999 # Call the function with the progress bar or with normal output.
1000 if command.UseProgressBar():
1001 op = BrilloDeployOperation(len(pkgs), emerge)
1002 op.Run(func, log_level=logging.DEBUG)
1003 else:
1004 func()
David Pursell9476bf42015-03-30 13:34:27 -07001005
1006 logging.warning('Please restart any updated services on the device, '
1007 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001008 except Exception:
1009 if lsb_release:
1010 lsb_entries = sorted(lsb_release.items())
1011 logging.info('Following are the LSB version details of the device:\n%s',
1012 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1013 raise