blob: 23002caf4fcc4a257e9d63059fc171560ebd5e7c [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',
Achuith Bhandarkar0487c312019-04-22 12:19:25 -070047 'emerge --usepkg', 'has been installed.']
Ralph Nathan90475a12015-05-20 13:19:01 -070048 UNMERGE_EVENTS = ['NOTICE: Unmerging', 'has been uninstalled.']
Ralph Nathane01ccf12015-04-16 10:40:32 -070049
50 def __init__(self, pkg_count, emerge):
51 """Construct BrilloDeployOperation object.
52
53 Args:
54 pkg_count: number of packages being built.
55 emerge: True if emerge, False is unmerge.
56 """
57 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070058 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070059 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070060 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070061 self._events = self.UNMERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070062 self._total = pkg_count * len(self._events)
63 self._completed = 0
64
Ralph Nathandc14ed92015-04-22 11:17:40 -070065 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070066 """Parse the output of brillo deploy to update a progress bar."""
67 stdout = self._stdout.read()
68 stderr = self._stderr.read()
69 output = stdout + stderr
70 for event in self._events:
71 self._completed += output.count(event)
72 self.ProgressBar(float(self._completed) / self._total)
73
74
David Pursell9476bf42015-03-30 13:34:27 -070075class _InstallPackageScanner(object):
76 """Finds packages that need to be installed on a target device.
77
78 Scans the sysroot bintree, beginning with a user-provided list of packages,
79 to find all packages that need to be installed. If so instructed,
80 transitively scans forward (mandatory) and backward (optional) dependencies
81 as well. A package will be installed if missing on the target (mandatory
82 packages only), or it will be updated if its sysroot version and build time
83 are different from the target. Common usage:
84
85 pkg_scanner = _InstallPackageScanner(sysroot)
86 pkgs = pkg_scanner.Run(...)
87 """
88
89 class VartreeError(Exception):
90 """An error in the processing of the installed packages tree."""
91
92 class BintreeError(Exception):
93 """An error in the processing of the source binpkgs tree."""
94
95 class PkgInfo(object):
96 """A record containing package information."""
97
98 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
99
100 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
101 self.cpv = cpv
102 self.build_time = build_time
103 self.rdeps_raw = rdeps_raw
104 self.rdeps = set() if rdeps is None else rdeps
105 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
106
107 # Python snippet for dumping vartree info on the target. Instantiate using
108 # _GetVartreeSnippet().
109 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700110import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700111import os
112import portage
113
114# Normalize the path to match what portage will index.
115target_root = os.path.normpath('%(root)s')
116if not target_root.endswith('/'):
117 target_root += '/'
118trees = portage.create_trees(target_root=target_root, config_root='/')
119vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700120pkg_info = []
121for cpv in vartree.dbapi.cpv_all():
122 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
123 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
124 pkg_info.append((cpv, slot, rdep_raw, build_time))
125
126print(json.dumps(pkg_info))
127"""
128
129 def __init__(self, sysroot):
130 self.sysroot = sysroot
131 # Members containing the sysroot (binpkg) and target (installed) package DB.
132 self.target_db = None
133 self.binpkgs_db = None
134 # Members for managing the dependency resolution work queue.
135 self.queue = None
136 self.seen = None
137 self.listed = None
138
139 @staticmethod
140 def _GetCP(cpv):
141 """Returns the CP value for a given CPV string."""
142 attrs = portage_util.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600143 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700144 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600145 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700146
147 @staticmethod
148 def _InDB(cp, slot, db):
149 """Returns whether CP and slot are found in a database (if provided)."""
150 cp_slots = db.get(cp) if db else None
151 return cp_slots is not None and (not slot or slot in cp_slots)
152
153 @staticmethod
154 def _AtomStr(cp, slot):
155 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
156 return '%s:%s' % (cp, slot) if slot else cp
157
158 @classmethod
159 def _GetVartreeSnippet(cls, root='/'):
160 """Returns a code snippet for dumping the vartree on the target.
161
162 Args:
163 root: The installation root.
164
165 Returns:
166 The said code snippet (string) with parameters filled in.
167 """
168 return cls._GET_VARTREE % {'root': root}
169
170 @classmethod
171 def _StripDepAtom(cls, dep_atom, installed_db=None):
172 """Strips a dependency atom and returns a (CP, slot) pair."""
173 # TODO(garnold) This is a gross simplification of ebuild dependency
174 # semantics, stripping and ignoring various qualifiers (versions, slots,
175 # USE flag, negation) and will likely need to be fixed. chromium:447366.
176
177 # Ignore unversioned blockers, leaving them for the user to resolve.
178 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
179 return None, None
180
181 cp = dep_atom
182 slot = None
183 require_installed = False
184
185 # Versioned blockers should be updated, but only if already installed.
186 # These are often used for forcing cascaded updates of multiple packages,
187 # so we're treating them as ordinary constraints with hopes that it'll lead
188 # to the desired result.
189 if cp.startswith('!'):
190 cp = cp.lstrip('!')
191 require_installed = True
192
193 # Remove USE flags.
194 if '[' in cp:
195 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
196
197 # Separate the slot qualifier and strip off subslots.
198 if ':' in cp:
199 cp, slot = cp.split(':')
200 for delim in ('/', '='):
201 slot = slot.split(delim, 1)[0]
202
203 # Strip version wildcards (right), comparators (left).
204 cp = cp.rstrip('*')
205 cp = cp.lstrip('<=>~')
206
207 # Turn into CP form.
208 cp = cls._GetCP(cp)
209
210 if require_installed and not cls._InDB(cp, None, installed_db):
211 return None, None
212
213 return cp, slot
214
215 @classmethod
216 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
217 """Resolves and returns a list of dependencies from a dependency string.
218
219 This parses a dependency string and returns a list of package names and
220 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
221 resolving disjunctive deps, we include all choices that are fully present
222 in |installed_db|. If none is present, we choose an arbitrary one that is
223 available.
224
225 Args:
226 dep_str: A raw dependency string.
227 installed_db: A database of installed packages.
228 avail_db: A database of packages available for installation.
229
230 Returns:
231 A list of pairs (CP, slot).
232
233 Raises:
234 ValueError: the dependencies string is malformed.
235 """
236 def ProcessSubDeps(dep_exp, disjunct):
237 """Parses and processes a dependency (sub)expression."""
238 deps = set()
239 default_deps = set()
240 sub_disjunct = False
241 for dep_sub_exp in dep_exp:
242 sub_deps = set()
243
244 if isinstance(dep_sub_exp, (list, tuple)):
245 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
246 sub_disjunct = False
247 elif sub_disjunct:
248 raise ValueError('Malformed disjunctive operation in deps')
249 elif dep_sub_exp == '||':
250 sub_disjunct = True
251 elif dep_sub_exp.endswith('?'):
252 raise ValueError('Dependencies contain a conditional')
253 else:
254 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
255 if cp:
256 sub_deps = set([(cp, slot)])
257 elif disjunct:
258 raise ValueError('Atom in disjunct ignored')
259
260 # Handle sub-deps of a disjunctive expression.
261 if disjunct:
262 # Make the first available choice the default, for use in case that
263 # no option is installed.
264 if (not default_deps and avail_db is not None and
265 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
266 default_deps = sub_deps
267
268 # If not all sub-deps are installed, then don't consider them.
269 if not all([cls._InDB(cp, slot, installed_db)
270 for cp, slot in sub_deps]):
271 sub_deps = set()
272
273 deps.update(sub_deps)
274
275 return deps or default_deps
276
277 try:
278 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
279 except portage.exception.InvalidDependString as e:
280 raise ValueError('Invalid dep string: %s' % e)
281 except ValueError as e:
282 raise ValueError('%s: %s' % (e, dep_str))
283
284 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
285 installed_db=None):
286 """Returns a database of packages given a list of CPV info.
287
288 Args:
289 cpv_info: A list of tuples containing package CPV and attributes.
290 process_rdeps: Whether to populate forward dependencies.
291 process_rev_rdeps: Whether to populate reverse dependencies.
292 installed_db: A database of installed packages for filtering disjunctive
293 choices against; if None, using own built database.
294
295 Returns:
296 A map from CP values to another dictionary that maps slots to package
297 attribute tuples. Tuples contain a CPV value (string), build time
298 (string), runtime dependencies (set), and reverse dependencies (set,
299 empty if not populated).
300
301 Raises:
302 ValueError: If more than one CPV occupies a single slot.
303 """
304 db = {}
305 logging.debug('Populating package DB...')
306 for cpv, slot, rdeps_raw, build_time in cpv_info:
307 cp = self._GetCP(cpv)
308 cp_slots = db.setdefault(cp, dict())
309 if slot in cp_slots:
310 raise ValueError('More than one package found for %s' %
311 self._AtomStr(cp, slot))
312 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
313 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
314 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
315
316 avail_db = db
317 if installed_db is None:
318 installed_db = db
319 avail_db = None
320
321 # Add approximate forward dependencies.
322 if process_rdeps:
323 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400324 for cp, cp_slots in db.items():
325 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700326 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
327 installed_db, avail_db))
328 logging.debug(' %s (%s) processed rdeps: %s',
329 self._AtomStr(cp, slot), pkg_info.cpv,
330 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
331 for rdep_cp, rdep_slot in pkg_info.rdeps]))
332
333 # Add approximate reverse dependencies (optional).
334 if process_rev_rdeps:
335 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400336 for cp, cp_slots in db.items():
337 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700338 for rdep_cp, rdep_slot in pkg_info.rdeps:
339 to_slots = db.get(rdep_cp)
340 if not to_slots:
341 continue
342
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400343 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700344 if rdep_slot and to_slot != rdep_slot:
345 continue
346 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
347 self._AtomStr(cp, slot), pkg_info.cpv,
348 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
349 to_pkg_info.rev_rdeps.add((cp, slot))
350
351 return db
352
353 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
354 """Initializes a dictionary of packages installed on |device|."""
355 get_vartree_script = self._GetVartreeSnippet(root)
356 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400357 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700358 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700359 except cros_build_lib.RunCommandError as e:
360 logging.error('Cannot get target vartree:\n%s', e.result.error)
361 raise
362
363 try:
364 self.target_db = self._BuildDB(json.loads(result.output),
365 process_rdeps, process_rev_rdeps)
366 except ValueError as e:
367 raise self.VartreeError(str(e))
368
369 def _InitBinpkgDB(self, process_rdeps):
370 """Initializes a dictionary of binary packages for updating the target."""
371 # Get build root trees; portage indexes require a trailing '/'.
372 build_root = os.path.join(self.sysroot, '')
373 trees = portage.create_trees(target_root=build_root, config_root=build_root)
374 bintree = trees[build_root]['bintree']
375 binpkgs_info = []
376 for cpv in bintree.dbapi.cpv_all():
377 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
378 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
379 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
380
381 try:
382 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
383 installed_db=self.target_db)
384 except ValueError as e:
385 raise self.BintreeError(str(e))
386
387 def _InitDepQueue(self):
388 """Initializes the dependency work queue."""
389 self.queue = set()
390 self.seen = {}
391 self.listed = set()
392
393 def _EnqDep(self, dep, listed, optional):
394 """Enqueues a dependency if not seen before or if turned non-optional."""
395 if dep in self.seen and (optional or not self.seen[dep]):
396 return False
397
398 self.queue.add(dep)
399 self.seen[dep] = optional
400 if listed:
401 self.listed.add(dep)
402 return True
403
404 def _DeqDep(self):
405 """Dequeues and returns a dependency, its listed and optional flags.
406
407 This returns listed packages first, if any are present, to ensure that we
408 correctly mark them as such when they are first being processed.
409 """
410 if self.listed:
411 dep = self.listed.pop()
412 self.queue.remove(dep)
413 listed = True
414 else:
415 dep = self.queue.pop()
416 listed = False
417
418 return dep, listed, self.seen[dep]
419
420 def _FindPackageMatches(self, cpv_pattern):
421 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
422
423 This is breaking |cpv_pattern| into its C, P and V components, each of
424 which may or may not be present or contain wildcards. It then scans the
425 binpkgs database to find all atoms that match these components, returning a
426 list of CP and slot qualifier. When the pattern does not specify a version,
427 or when a CP has only one slot in the binpkgs database, we omit the slot
428 qualifier in the result.
429
430 Args:
431 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
432
433 Returns:
434 A list of (CPV, slot) pairs of packages in the binpkgs database that
435 match the pattern.
436 """
437 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
438 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
439 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400440 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700441 if not fnmatch.fnmatchcase(cp, cp_pattern):
442 continue
443
444 # If no version attribute was given or there's only one slot, omit the
445 # slot qualifier.
446 if not attrs.version or len(cp_slots) == 1:
447 matches.append((cp, None))
448 else:
449 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400450 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700451 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
452 matches.append((cp, slot))
453
454 return matches
455
456 def _FindPackage(self, pkg):
457 """Returns the (CP, slot) pair for a package matching |pkg|.
458
459 Args:
460 pkg: Path to a binary package or a (partial) package CPV specifier.
461
462 Returns:
463 A (CP, slot) pair for the given package; slot may be None (unspecified).
464
465 Raises:
466 ValueError: if |pkg| is not a binpkg file nor does it match something
467 that's in the bintree.
468 """
469 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
470 package = os.path.basename(os.path.splitext(pkg)[0])
471 category = os.path.basename(os.path.dirname(pkg))
472 return self._GetCP(os.path.join(category, package)), None
473
474 matches = self._FindPackageMatches(pkg)
475 if not matches:
476 raise ValueError('No package found for %s' % pkg)
477
478 idx = 0
479 if len(matches) > 1:
480 # Ask user to pick among multiple matches.
481 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
482 ['%s:%s' % (cp, slot) if slot else cp
483 for cp, slot in matches])
484
485 return matches[idx]
486
487 def _NeedsInstall(self, cpv, slot, build_time, optional):
488 """Returns whether a package needs to be installed on the target.
489
490 Args:
491 cpv: Fully qualified CPV (string) of the package.
492 slot: Slot identifier (string).
493 build_time: The BUILT_TIME value (string) of the binpkg.
494 optional: Whether package is optional on the target.
495
496 Returns:
497 A tuple (install, update) indicating whether to |install| the package and
498 whether it is an |update| to an existing package.
499
500 Raises:
501 ValueError: if slot is not provided.
502 """
503 # If not checking installed packages, always install.
504 if not self.target_db:
505 return True, False
506
507 cp = self._GetCP(cpv)
508 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
509 if target_pkg_info is not None:
510 if cpv != target_pkg_info.cpv:
511 attrs = portage_util.SplitCPV(cpv)
512 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
513 logging.debug('Updating %s: version (%s) different on target (%s)',
514 cp, attrs.version, target_attrs.version)
515 return True, True
516
517 if build_time != target_pkg_info.build_time:
518 logging.debug('Updating %s: build time (%s) different on target (%s)',
519 cpv, build_time, target_pkg_info.build_time)
520 return True, True
521
522 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
523 cp, target_pkg_info.cpv, target_pkg_info.build_time)
524 return False, False
525
526 if optional:
527 logging.debug('Not installing %s: missing on target but optional', cp)
528 return False, False
529
530 logging.debug('Installing %s: missing on target and non-optional (%s)',
531 cp, cpv)
532 return True, False
533
534 def _ProcessDeps(self, deps, reverse):
535 """Enqueues dependencies for processing.
536
537 Args:
538 deps: List of dependencies to enqueue.
539 reverse: Whether these are reverse dependencies.
540 """
541 if not deps:
542 return
543
544 logging.debug('Processing %d %s dep(s)...', len(deps),
545 'reverse' if reverse else 'forward')
546 num_already_seen = 0
547 for dep in deps:
548 if self._EnqDep(dep, False, reverse):
549 logging.debug(' Queued dep %s', dep)
550 else:
551 num_already_seen += 1
552
553 if num_already_seen:
554 logging.debug('%d dep(s) already seen', num_already_seen)
555
556 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
557 """Returns a dictionary of packages that need to be installed on the target.
558
559 Args:
560 process_rdeps: Whether to trace forward dependencies.
561 process_rev_rdeps: Whether to trace backward dependencies as well.
562
563 Returns:
564 A dictionary mapping CP values (string) to tuples containing a CPV
565 (string), a slot (string), a boolean indicating whether the package
566 was initially listed in the queue, and a boolean indicating whether this
567 is an update to an existing package.
568 """
569 installs = {}
570 while self.queue:
571 dep, listed, optional = self._DeqDep()
572 cp, required_slot = dep
573 if cp in installs:
574 logging.debug('Already updating %s', cp)
575 continue
576
577 cp_slots = self.binpkgs_db.get(cp, dict())
578 logging.debug('Checking packages matching %s%s%s...', cp,
579 ' (slot: %s)' % required_slot if required_slot else '',
580 ' (optional)' if optional else '')
581 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400582 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700583 if required_slot and slot != required_slot:
584 continue
585
586 num_processed += 1
587 logging.debug(' Checking %s...', pkg_info.cpv)
588
589 install, update = self._NeedsInstall(pkg_info.cpv, slot,
590 pkg_info.build_time, optional)
591 if not install:
592 continue
593
594 installs[cp] = (pkg_info.cpv, slot, listed, update)
595
596 # Add forward and backward runtime dependencies to queue.
597 if process_rdeps:
598 self._ProcessDeps(pkg_info.rdeps, False)
599 if process_rev_rdeps:
600 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
601 if target_pkg_info:
602 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
603
604 if num_processed == 0:
605 logging.warning('No qualified bintree package corresponding to %s', cp)
606
607 return installs
608
609 def _SortInstalls(self, installs):
610 """Returns a sorted list of packages to install.
611
612 Performs a topological sort based on dependencies found in the binary
613 package database.
614
615 Args:
616 installs: Dictionary of packages to install indexed by CP.
617
618 Returns:
619 A list of package CPVs (string).
620
621 Raises:
622 ValueError: If dependency graph contains a cycle.
623 """
624 not_visited = set(installs.keys())
625 curr_path = []
626 sorted_installs = []
627
628 def SortFrom(cp):
629 """Traverses dependencies recursively, emitting nodes in reverse order."""
630 cpv, slot, _, _ = installs[cp]
631 if cpv in curr_path:
632 raise ValueError('Dependencies contain a cycle: %s -> %s' %
633 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
634 curr_path.append(cpv)
635 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
636 if rdep_cp in not_visited:
637 not_visited.remove(rdep_cp)
638 SortFrom(rdep_cp)
639
640 sorted_installs.append(cpv)
641 curr_path.pop()
642
643 # So long as there's more packages, keep expanding dependency paths.
644 while not_visited:
645 SortFrom(not_visited.pop())
646
647 return sorted_installs
648
649 def _EnqListedPkg(self, pkg):
650 """Finds and enqueues a listed package."""
651 cp, slot = self._FindPackage(pkg)
652 if cp not in self.binpkgs_db:
653 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
654 self._EnqDep((cp, slot), True, False)
655
656 def _EnqInstalledPkgs(self):
657 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400658 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700659 target_cp_slots = self.target_db.get(cp)
660 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400661 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700662 if slot in target_cp_slots:
663 self._EnqDep((cp, slot), True, False)
664
665 def Run(self, device, root, listed_pkgs, update, process_rdeps,
666 process_rev_rdeps):
667 """Computes the list of packages that need to be installed on a target.
668
669 Args:
670 device: Target handler object.
671 root: Package installation root.
672 listed_pkgs: Package names/files listed by the user.
673 update: Whether to read the target's installed package database.
674 process_rdeps: Whether to trace forward dependencies.
675 process_rev_rdeps: Whether to trace backward dependencies as well.
676
677 Returns:
678 A tuple (sorted, listed, num_updates) where |sorted| is a list of package
679 CPVs (string) to install on the target in an order that satisfies their
680 inter-dependencies, |listed| the subset that was requested by the user,
681 and |num_updates| the number of packages being installed over preexisting
682 versions. Note that installation order should be reversed for removal.
683 """
684 if process_rev_rdeps and not process_rdeps:
685 raise ValueError('Must processing forward deps when processing rev deps')
686 if process_rdeps and not update:
687 raise ValueError('Must check installed packages when processing deps')
688
689 if update:
690 logging.info('Initializing target intalled packages database...')
691 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
692
693 logging.info('Initializing binary packages database...')
694 self._InitBinpkgDB(process_rdeps)
695
696 logging.info('Finding listed package(s)...')
697 self._InitDepQueue()
698 for pkg in listed_pkgs:
699 if pkg == '@installed':
700 if not update:
701 raise ValueError(
702 'Must check installed packages when updating all of them.')
703 self._EnqInstalledPkgs()
704 else:
705 self._EnqListedPkg(pkg)
706
707 logging.info('Computing set of packages to install...')
708 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
709
710 num_updates = 0
711 listed_installs = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400712 for cpv, _, listed, update in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700713 if listed:
714 listed_installs.append(cpv)
715 if update:
716 num_updates += 1
717
718 logging.info('Processed %d package(s), %d will be installed, %d are '
719 'updating existing packages',
720 len(self.seen), len(installs), num_updates)
721
722 sorted_installs = self._SortInstalls(installs)
723 return sorted_installs, listed_installs, num_updates
724
725
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700726def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700727 """Copies |pkg| to |device| and emerges it.
728
729 Args:
730 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700731 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700732 root: Package installation root path.
733 extra_args: Extra arguments to pass to emerge.
734
735 Raises:
736 DeployError: Unrecoverable error during emerge.
737 """
David Pursell9476bf42015-03-30 13:34:27 -0700738 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700739 pkg_name = os.path.basename(pkg_path)
740 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700741 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400742 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
743 # Clean out the dirs first if we had a previous emerge on the device so as to
744 # free up space for this emerge. The last emerge gets implicitly cleaned up
745 # when the device connection deletes its work_dir.
746 device.RunCommand(
747 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
748 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700749
Ralph Nathane01ccf12015-04-16 10:40:32 -0700750 # This message is read by BrilloDeployOperation.
751 logging.notice('Copying %s to device.', pkg_name)
Ilja H. Friedel0ab63e12017-03-28 13:29:48 -0700752 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700753
David Pursell9476bf42015-03-30 13:34:27 -0700754 logging.info('Use portage temp dir %s', portage_tmpdir)
755
Ralph Nathane01ccf12015-04-16 10:40:32 -0700756 # This message is read by BrilloDeployOperation.
757 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700758 pkg_path = os.path.join(pkg_dir, pkg_name)
759
760 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
761 # chromeos-base packages will be skipped due to the configuration
762 # in /etc/protage/make.profile/package.provided. However, there is
763 # a known bug that /usr/local/etc/portage is not setup properly
764 # (crbug.com/312041). This does not affect `cros deploy` because
765 # we do not use the preset PKGDIR.
766 extra_env = {
767 'FEATURES': '-sandbox',
768 'PKGDIR': pkgroot,
769 'PORTAGE_CONFIGROOT': '/usr/local',
770 'PORTAGE_TMPDIR': portage_tmpdir,
771 'PORTDIR': device.work_dir,
772 'CONFIG_PROTECT': '-*',
773 }
774 cmd = ['emerge', '--usepkg', pkg_path, '--root=%s' % root]
775 if extra_args:
776 cmd.append(extra_args)
777
778 try:
Greg Kerrb96c02c2019-02-08 14:32:41 -0800779 result = device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
780 capture_output=True, debug_level=logging.INFO)
781
782 pattern = ('A requested package will not be merged because '
783 'it is listed in package.provided')
784 output = result.error.replace('\n', ' ').replace('\r', '')
785 if pattern in output:
786 error = ('Package failed to emerge: %s\n'
787 'Remove %s from /etc/portage/make.profile/'
788 'package.provided/chromeos-base.packages\n'
789 '(also see crbug.com/920140 for more context)\n'
790 % (pattern, pkg_name))
791 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700792 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700793 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700794 raise
795 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700796 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700797
798
Qijiang Fan8a945032019-04-25 20:53:29 +0900799def _HasSELinux(device):
800 """Check whether the device has SELinux-enabled.
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900801
802 Args:
803 device: A ChromiumOSDevice object.
804 """
805 try:
Qijiang Fan8a945032019-04-25 20:53:29 +0900806 device.CatFile('/sys/fs/selinux/enforce', max_size=None)
807 return True
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900808 except remote_access.CatFileError:
Qijiang Fan8a945032019-04-25 20:53:29 +0900809 return False
810
811
812def _IsSELinuxEnforced(device):
813 """Check whether the device has SELinux-enforced.
814
815 Args:
816 device: A ChromiumOSDevice object
817 """
818 return device.CatFile('/sys/fs/selinux/enforce', max_size=None).strip() == '1'
819
820
Qijiang Fand5958192019-07-26 12:32:36 +0900821def _RestoreSELinuxContext(device, pkgpath, root):
Qijiang Fan8a945032019-04-25 20:53:29 +0900822 """Restore SELinux context for files in a given pacakge.
823
824 This reads the tarball from pkgpath, and calls restorecon on device to
825 restore SELinux context for files listed in the tarball, assuming those files
826 are installed to /
827
828 Args:
829 device: a ChromiumOSDevice object
830 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900831 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900832 """
833 enforced = _IsSELinuxEnforced(device)
834 if enforced:
835 device.RunCommand(['setenforce', '0'])
836 pkgroot = os.path.join(device.work_dir, 'packages')
837 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
838 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
839 # Testing shows restorecon splits on newlines instead of spaces.
840 device.RunCommand(
Qijiang Fand5958192019-07-26 12:32:36 +0900841 ['cd', root, '&&',
842 'tar', 'tf', pkgpath_device, '|',
843 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900844 remote_sudo=True)
845 if enforced:
846 device.RunCommand(['setenforce', '1'])
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900847
848
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700849def _GetPackagesByCPV(cpvs, strip, sysroot):
850 """Returns paths to binary packages corresponding to |cpvs|.
851
852 Args:
853 cpvs: List of CPV components given by portage_util.SplitCPV().
854 strip: True to run strip_package.
855 sysroot: Sysroot path.
856
857 Returns:
858 List of paths corresponding to |cpvs|.
859
860 Raises:
861 DeployError: If a package is missing.
862 """
863 packages_dir = None
864 if strip:
865 try:
866 cros_build_lib.RunCommand(
867 ['strip_package', '--sysroot', sysroot] +
Alex Klein7078e252018-10-02 10:21:04 -0600868 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700869 packages_dir = _STRIPPED_PACKAGES_DIR
870 except cros_build_lib.RunCommandError:
871 logging.error('Cannot strip packages %s',
872 ' '.join([str(cpv) for cpv in cpvs]))
873 raise
874
875 paths = []
876 for cpv in cpvs:
877 path = portage_util.GetBinaryPackagePath(
878 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
879 packages_dir=packages_dir)
880 if not path:
881 raise DeployError('Missing package %s.' % cpv)
882 paths.append(path)
883
884 return paths
885
886
887def _GetPackagesPaths(pkgs, strip, sysroot):
888 """Returns paths to binary |pkgs|.
889
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700890 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700891 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700892 strip: Whether or not to run strip_package for CPV packages.
893 sysroot: The sysroot path.
894
895 Returns:
896 List of paths corresponding to |pkgs|.
897 """
Ned Nguyend0db4072019-02-22 14:19:21 -0700898 cpvs = [portage_util.SplitCPV(p) for p in pkgs]
899 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700900
901
David Pursell9476bf42015-03-30 13:34:27 -0700902def _Unmerge(device, pkg, root):
903 """Unmerges |pkg| on |device|.
904
905 Args:
906 device: A RemoteDevice object.
907 pkg: A package name.
908 root: Package installation root path.
909 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700910 pkg_name = os.path.basename(pkg)
911 # This message is read by BrilloDeployOperation.
912 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700913 cmd = ['qmerge', '--yes']
914 # Check if qmerge is available on the device. If not, use emerge.
915 if device.RunCommand(
916 ['qmerge', '--version'], error_code_ok=True).returncode != 0:
917 cmd = ['emerge']
918
919 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
920 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700921 # Always showing the emerge output for clarity.
David Pursell9476bf42015-03-30 13:34:27 -0700922 device.RunCommand(cmd, capture_output=False, remote_sudo=True,
923 debug_level=logging.INFO)
924 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700925 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700926 raise
927 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700928 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700929
930
931def _ConfirmDeploy(num_updates):
932 """Returns whether we can continue deployment."""
933 if num_updates > _MAX_UPDATES_NUM:
934 logging.warning(_MAX_UPDATES_WARNING)
935 return cros_build_lib.BooleanPrompt(default=False)
936
937 return True
938
939
Ralph Nathane01ccf12015-04-16 10:40:32 -0700940def _EmergePackages(pkgs, device, strip, sysroot, root, emerge_args):
941 """Call _Emerge for each packge in pkgs."""
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700942 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
943 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Qijiang Fan8a945032019-04-25 20:53:29 +0900944 if _HasSELinux(device):
Qijiang Fand5958192019-07-26 12:32:36 +0900945 _RestoreSELinuxContext(device, pkg_path, root)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700946
947
948def _UnmergePackages(pkgs, device, root):
949 """Call _Unmege for each package in pkgs."""
950 for pkg in pkgs:
951 _Unmerge(device, pkg, root)
952
953
Gilad Arnolda0a98062015-07-07 08:34:27 -0700954def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
955 deep_rev=False, clean_binpkg=True, root='/', strip=True,
956 emerge_args=None, ssh_private_key=None, ping=True, force=False,
957 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -0700958 """Deploys packages to a device.
959
960 Args:
David Pursell2e773382015-04-03 14:30:47 -0700961 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -0700962 packages: List of packages (strings) to deploy to device.
963 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -0700964 emerge: True to emerge package, False to unmerge.
965 update: Check installed version on device.
966 deep: Install dependencies also. Implies |update|.
967 deep_rev: Install reverse dependencies. Implies |deep|.
968 clean_binpkg: Clean outdated binary packages.
969 root: Package installation root path.
970 strip: Run strip_package to filter out preset paths in the package.
971 emerge_args: Extra arguments to pass to emerge.
972 ssh_private_key: Path to an SSH private key file; None to use test keys.
973 ping: True to ping the device before trying to connect.
974 force: Ignore sanity checks and prompts.
975 dry_run: Print deployment plan but do not deploy anything.
976
977 Raises:
978 ValueError: Invalid parameter or parameter combination.
979 DeployError: Unrecoverable failure during deploy.
980 """
981 if deep_rev:
982 deep = True
983 if deep:
984 update = True
985
Gilad Arnolda0a98062015-07-07 08:34:27 -0700986 if not packages:
987 raise DeployError('No packages provided, nothing to deploy.')
988
David Pursell9476bf42015-03-30 13:34:27 -0700989 if update and not emerge:
990 raise ValueError('Cannot update and unmerge.')
991
David Pursell2e773382015-04-03 14:30:47 -0700992 if device:
993 hostname, username, port = device.hostname, device.username, device.port
994 else:
995 hostname, username, port = None, None, None
996
Gilad Arnold4d3ade72015-04-28 15:13:35 -0700997 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -0700998 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -0700999 try:
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001000 with remote_access.ChromiumOSDeviceHandler(
1001 hostname, port=port, username=username, private_key=ssh_private_key,
1002 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001003 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001004
Gilad Arnolda0a98062015-07-07 08:34:27 -07001005 board = cros_build_lib.GetBoard(device_board=device.board,
1006 override_board=board)
1007 if not force and board != device.board:
1008 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001009 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001010
Gilad Arnolda0a98062015-07-07 08:34:27 -07001011 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001012
1013 if clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001014 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001015 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001016
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001017 # Remount rootfs as writable if necessary.
1018 if not device.MountRootfsReadWrite():
1019 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001020
1021 # Obtain list of packages to upgrade/remove.
1022 pkg_scanner = _InstallPackageScanner(sysroot)
Gilad Arnold655e67d2015-04-29 11:14:18 -07001023 pkgs, listed, num_updates = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001024 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001025 if emerge:
1026 action_str = 'emerge'
1027 else:
1028 pkgs.reverse()
1029 action_str = 'unmerge'
1030
1031 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001032 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001033 return
1034
Ralph Nathane01ccf12015-04-16 10:40:32 -07001035 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001036 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001037 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001038
1039 if dry_run or not _ConfirmDeploy(num_updates):
1040 return
1041
Ralph Nathane01ccf12015-04-16 10:40:32 -07001042 # Select function (emerge or unmerge) and bind args.
1043 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001044 func = functools.partial(_EmergePackages, pkgs, device, strip,
Ralph Nathane01ccf12015-04-16 10:40:32 -07001045 sysroot, root, emerge_args)
1046 else:
Mike Frysinger539db512015-05-21 18:14:01 -04001047 func = functools.partial(_UnmergePackages, pkgs, device, root)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001048
1049 # Call the function with the progress bar or with normal output.
1050 if command.UseProgressBar():
1051 op = BrilloDeployOperation(len(pkgs), emerge)
1052 op.Run(func, log_level=logging.DEBUG)
1053 else:
1054 func()
David Pursell9476bf42015-03-30 13:34:27 -07001055
Qijiang Fan8a945032019-04-25 20:53:29 +09001056 if _HasSELinux(device):
1057 if sum(x.count('selinux-policy') for x in pkgs):
1058 logging.warning(
1059 'Deploying SELinux policy will not take effect until reboot. '
1060 'SELinux policy is loaded by init. Also, security contexts '
1061 '(labels) in files will require manual relabeling by the user '
1062 'if your policy modifies the file contexts.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001063
David Pursell9476bf42015-03-30 13:34:27 -07001064 logging.warning('Please restart any updated services on the device, '
1065 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001066 except Exception:
1067 if lsb_release:
1068 lsb_entries = sorted(lsb_release.items())
1069 logging.info('Following are the LSB version details of the device:\n%s',
1070 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1071 raise