blob: bf42e1534affa0e8683864adc04ec5db182c2fcc [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
Alex Kleinaaddc932020-01-30 15:02:24 -07006"""Deploy packages onto a target device.
7
8Integration tests for this file can be found at cli/cros/tests/cros_vm_tests.py.
9See that file for more information.
10"""
David Pursell9476bf42015-03-30 13:34:27 -070011
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040012from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -070013from __future__ import print_function
14
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070015import bz2
David Pursell9476bf42015-03-30 13:34:27 -070016import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070017import functools
David Pursell9476bf42015-03-30 13:34:27 -070018import json
19import os
Mike Frysinger3f087aa2020-03-20 06:03:16 -040020import sys
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070021import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070022
Ralph Nathane01ccf12015-04-16 10:40:32 -070023from chromite.cli import command
David Pursell9476bf42015-03-30 13:34:27 -070024from chromite.lib import cros_build_lib
25from chromite.lib import cros_logging as logging
Ralph Nathane01ccf12015-04-16 10:40:32 -070026from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070027from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070028from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070029from chromite.lib import remote_access
Andrewc7e1c6b2020-02-27 16:03:53 -080030from chromite.scripts import build_dlc
David Pursell9476bf42015-03-30 13:34:27 -070031try:
32 import portage
33except ImportError:
34 if cros_build_lib.IsInsideChroot():
35 raise
36
37
Mike Frysinger3f087aa2020-03-20 06:03:16 -040038assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
39
40
David Pursell9476bf42015-03-30 13:34:27 -070041_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
42# This is defined in src/platform/dev/builder.py
43_STRIPPED_PACKAGES_DIR = 'stripped-packages'
44
45_MAX_UPDATES_NUM = 10
46_MAX_UPDATES_WARNING = (
47 'You are about to update a large number of installed packages, which '
48 'might take a long time, fail midway, or leave the target in an '
49 'inconsistent state. It is highly recommended that you flash a new image '
50 'instead.')
51
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070052_DLC_ID = 'DLC_ID'
53_DLC_PACKAGE = 'DLC_PACKAGE'
54_ENVIRONMENT_FILENAME = 'environment.bz2'
55_DLC_INSTALL_ROOT = '/var/cache/dlc'
56
David Pursell9476bf42015-03-30 13:34:27 -070057
58class DeployError(Exception):
59 """Thrown when an unrecoverable error is encountered during deploy."""
60
61
Ralph Nathane01ccf12015-04-16 10:40:32 -070062class BrilloDeployOperation(operation.ProgressBarOperation):
63 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070064 # These two variables are used to validate the output in the VM integration
65 # tests. Changes to the output must be reflected here.
66 MERGE_EVENTS = ['NOTICE: Copying', 'NOTICE: Installing', 'WARNING: Ignoring',
Achuith Bhandarkar0487c312019-04-22 12:19:25 -070067 'emerge --usepkg', 'has been installed.']
Ralph Nathan90475a12015-05-20 13:19:01 -070068 UNMERGE_EVENTS = ['NOTICE: Unmerging', 'has been uninstalled.']
Ralph Nathane01ccf12015-04-16 10:40:32 -070069
70 def __init__(self, pkg_count, emerge):
71 """Construct BrilloDeployOperation object.
72
73 Args:
74 pkg_count: number of packages being built.
75 emerge: True if emerge, False is unmerge.
76 """
77 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070078 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070079 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070080 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070081 self._events = self.UNMERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070082 self._total = pkg_count * len(self._events)
83 self._completed = 0
84
Ralph Nathandc14ed92015-04-22 11:17:40 -070085 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070086 """Parse the output of brillo deploy to update a progress bar."""
87 stdout = self._stdout.read()
88 stderr = self._stderr.read()
89 output = stdout + stderr
90 for event in self._events:
91 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040092 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -070093
94
David Pursell9476bf42015-03-30 13:34:27 -070095class _InstallPackageScanner(object):
96 """Finds packages that need to be installed on a target device.
97
98 Scans the sysroot bintree, beginning with a user-provided list of packages,
99 to find all packages that need to be installed. If so instructed,
100 transitively scans forward (mandatory) and backward (optional) dependencies
101 as well. A package will be installed if missing on the target (mandatory
102 packages only), or it will be updated if its sysroot version and build time
103 are different from the target. Common usage:
104
105 pkg_scanner = _InstallPackageScanner(sysroot)
106 pkgs = pkg_scanner.Run(...)
107 """
108
109 class VartreeError(Exception):
110 """An error in the processing of the installed packages tree."""
111
112 class BintreeError(Exception):
113 """An error in the processing of the source binpkgs tree."""
114
115 class PkgInfo(object):
116 """A record containing package information."""
117
118 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
119
120 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
121 self.cpv = cpv
122 self.build_time = build_time
123 self.rdeps_raw = rdeps_raw
124 self.rdeps = set() if rdeps is None else rdeps
125 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
126
127 # Python snippet for dumping vartree info on the target. Instantiate using
128 # _GetVartreeSnippet().
129 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700130import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700131import os
132import portage
133
134# Normalize the path to match what portage will index.
135target_root = os.path.normpath('%(root)s')
136if not target_root.endswith('/'):
137 target_root += '/'
138trees = portage.create_trees(target_root=target_root, config_root='/')
139vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700140pkg_info = []
141for cpv in vartree.dbapi.cpv_all():
142 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
143 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
144 pkg_info.append((cpv, slot, rdep_raw, build_time))
145
146print(json.dumps(pkg_info))
147"""
148
149 def __init__(self, sysroot):
150 self.sysroot = sysroot
151 # Members containing the sysroot (binpkg) and target (installed) package DB.
152 self.target_db = None
153 self.binpkgs_db = None
154 # Members for managing the dependency resolution work queue.
155 self.queue = None
156 self.seen = None
157 self.listed = None
158
159 @staticmethod
160 def _GetCP(cpv):
161 """Returns the CP value for a given CPV string."""
162 attrs = portage_util.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600163 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700164 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600165 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700166
167 @staticmethod
168 def _InDB(cp, slot, db):
169 """Returns whether CP and slot are found in a database (if provided)."""
170 cp_slots = db.get(cp) if db else None
171 return cp_slots is not None and (not slot or slot in cp_slots)
172
173 @staticmethod
174 def _AtomStr(cp, slot):
175 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
176 return '%s:%s' % (cp, slot) if slot else cp
177
178 @classmethod
179 def _GetVartreeSnippet(cls, root='/'):
180 """Returns a code snippet for dumping the vartree on the target.
181
182 Args:
183 root: The installation root.
184
185 Returns:
186 The said code snippet (string) with parameters filled in.
187 """
188 return cls._GET_VARTREE % {'root': root}
189
190 @classmethod
191 def _StripDepAtom(cls, dep_atom, installed_db=None):
192 """Strips a dependency atom and returns a (CP, slot) pair."""
193 # TODO(garnold) This is a gross simplification of ebuild dependency
194 # semantics, stripping and ignoring various qualifiers (versions, slots,
195 # USE flag, negation) and will likely need to be fixed. chromium:447366.
196
197 # Ignore unversioned blockers, leaving them for the user to resolve.
198 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
199 return None, None
200
201 cp = dep_atom
202 slot = None
203 require_installed = False
204
205 # Versioned blockers should be updated, but only if already installed.
206 # These are often used for forcing cascaded updates of multiple packages,
207 # so we're treating them as ordinary constraints with hopes that it'll lead
208 # to the desired result.
209 if cp.startswith('!'):
210 cp = cp.lstrip('!')
211 require_installed = True
212
213 # Remove USE flags.
214 if '[' in cp:
215 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
216
217 # Separate the slot qualifier and strip off subslots.
218 if ':' in cp:
219 cp, slot = cp.split(':')
220 for delim in ('/', '='):
221 slot = slot.split(delim, 1)[0]
222
223 # Strip version wildcards (right), comparators (left).
224 cp = cp.rstrip('*')
225 cp = cp.lstrip('<=>~')
226
227 # Turn into CP form.
228 cp = cls._GetCP(cp)
229
230 if require_installed and not cls._InDB(cp, None, installed_db):
231 return None, None
232
233 return cp, slot
234
235 @classmethod
236 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
237 """Resolves and returns a list of dependencies from a dependency string.
238
239 This parses a dependency string and returns a list of package names and
240 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
241 resolving disjunctive deps, we include all choices that are fully present
242 in |installed_db|. If none is present, we choose an arbitrary one that is
243 available.
244
245 Args:
246 dep_str: A raw dependency string.
247 installed_db: A database of installed packages.
248 avail_db: A database of packages available for installation.
249
250 Returns:
251 A list of pairs (CP, slot).
252
253 Raises:
254 ValueError: the dependencies string is malformed.
255 """
256 def ProcessSubDeps(dep_exp, disjunct):
257 """Parses and processes a dependency (sub)expression."""
258 deps = set()
259 default_deps = set()
260 sub_disjunct = False
261 for dep_sub_exp in dep_exp:
262 sub_deps = set()
263
264 if isinstance(dep_sub_exp, (list, tuple)):
265 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
266 sub_disjunct = False
267 elif sub_disjunct:
268 raise ValueError('Malformed disjunctive operation in deps')
269 elif dep_sub_exp == '||':
270 sub_disjunct = True
271 elif dep_sub_exp.endswith('?'):
272 raise ValueError('Dependencies contain a conditional')
273 else:
274 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
275 if cp:
276 sub_deps = set([(cp, slot)])
277 elif disjunct:
278 raise ValueError('Atom in disjunct ignored')
279
280 # Handle sub-deps of a disjunctive expression.
281 if disjunct:
282 # Make the first available choice the default, for use in case that
283 # no option is installed.
284 if (not default_deps and avail_db is not None and
285 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
286 default_deps = sub_deps
287
288 # If not all sub-deps are installed, then don't consider them.
289 if not all([cls._InDB(cp, slot, installed_db)
290 for cp, slot in sub_deps]):
291 sub_deps = set()
292
293 deps.update(sub_deps)
294
295 return deps or default_deps
296
297 try:
298 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
299 except portage.exception.InvalidDependString as e:
300 raise ValueError('Invalid dep string: %s' % e)
301 except ValueError as e:
302 raise ValueError('%s: %s' % (e, dep_str))
303
304 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
305 installed_db=None):
306 """Returns a database of packages given a list of CPV info.
307
308 Args:
309 cpv_info: A list of tuples containing package CPV and attributes.
310 process_rdeps: Whether to populate forward dependencies.
311 process_rev_rdeps: Whether to populate reverse dependencies.
312 installed_db: A database of installed packages for filtering disjunctive
313 choices against; if None, using own built database.
314
315 Returns:
316 A map from CP values to another dictionary that maps slots to package
317 attribute tuples. Tuples contain a CPV value (string), build time
318 (string), runtime dependencies (set), and reverse dependencies (set,
319 empty if not populated).
320
321 Raises:
322 ValueError: If more than one CPV occupies a single slot.
323 """
324 db = {}
325 logging.debug('Populating package DB...')
326 for cpv, slot, rdeps_raw, build_time in cpv_info:
327 cp = self._GetCP(cpv)
328 cp_slots = db.setdefault(cp, dict())
329 if slot in cp_slots:
330 raise ValueError('More than one package found for %s' %
331 self._AtomStr(cp, slot))
332 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
333 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
334 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
335
336 avail_db = db
337 if installed_db is None:
338 installed_db = db
339 avail_db = None
340
341 # Add approximate forward dependencies.
342 if process_rdeps:
343 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400344 for cp, cp_slots in db.items():
345 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700346 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
347 installed_db, avail_db))
348 logging.debug(' %s (%s) processed rdeps: %s',
349 self._AtomStr(cp, slot), pkg_info.cpv,
350 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
351 for rdep_cp, rdep_slot in pkg_info.rdeps]))
352
353 # Add approximate reverse dependencies (optional).
354 if process_rev_rdeps:
355 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400356 for cp, cp_slots in db.items():
357 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700358 for rdep_cp, rdep_slot in pkg_info.rdeps:
359 to_slots = db.get(rdep_cp)
360 if not to_slots:
361 continue
362
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400363 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700364 if rdep_slot and to_slot != rdep_slot:
365 continue
366 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
367 self._AtomStr(cp, slot), pkg_info.cpv,
368 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
369 to_pkg_info.rev_rdeps.add((cp, slot))
370
371 return db
372
373 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
374 """Initializes a dictionary of packages installed on |device|."""
375 get_vartree_script = self._GetVartreeSnippet(root)
376 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400377 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700378 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700379 except cros_build_lib.RunCommandError as e:
380 logging.error('Cannot get target vartree:\n%s', e.result.error)
381 raise
382
383 try:
384 self.target_db = self._BuildDB(json.loads(result.output),
385 process_rdeps, process_rev_rdeps)
386 except ValueError as e:
387 raise self.VartreeError(str(e))
388
389 def _InitBinpkgDB(self, process_rdeps):
390 """Initializes a dictionary of binary packages for updating the target."""
391 # Get build root trees; portage indexes require a trailing '/'.
392 build_root = os.path.join(self.sysroot, '')
393 trees = portage.create_trees(target_root=build_root, config_root=build_root)
394 bintree = trees[build_root]['bintree']
395 binpkgs_info = []
396 for cpv in bintree.dbapi.cpv_all():
397 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
398 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
399 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
400
401 try:
402 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
403 installed_db=self.target_db)
404 except ValueError as e:
405 raise self.BintreeError(str(e))
406
407 def _InitDepQueue(self):
408 """Initializes the dependency work queue."""
409 self.queue = set()
410 self.seen = {}
411 self.listed = set()
412
413 def _EnqDep(self, dep, listed, optional):
414 """Enqueues a dependency if not seen before or if turned non-optional."""
415 if dep in self.seen and (optional or not self.seen[dep]):
416 return False
417
418 self.queue.add(dep)
419 self.seen[dep] = optional
420 if listed:
421 self.listed.add(dep)
422 return True
423
424 def _DeqDep(self):
425 """Dequeues and returns a dependency, its listed and optional flags.
426
427 This returns listed packages first, if any are present, to ensure that we
428 correctly mark them as such when they are first being processed.
429 """
430 if self.listed:
431 dep = self.listed.pop()
432 self.queue.remove(dep)
433 listed = True
434 else:
435 dep = self.queue.pop()
436 listed = False
437
438 return dep, listed, self.seen[dep]
439
440 def _FindPackageMatches(self, cpv_pattern):
441 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
442
443 This is breaking |cpv_pattern| into its C, P and V components, each of
444 which may or may not be present or contain wildcards. It then scans the
445 binpkgs database to find all atoms that match these components, returning a
446 list of CP and slot qualifier. When the pattern does not specify a version,
447 or when a CP has only one slot in the binpkgs database, we omit the slot
448 qualifier in the result.
449
450 Args:
451 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
452
453 Returns:
454 A list of (CPV, slot) pairs of packages in the binpkgs database that
455 match the pattern.
456 """
457 attrs = portage_util.SplitCPV(cpv_pattern, strict=False)
458 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
459 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400460 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700461 if not fnmatch.fnmatchcase(cp, cp_pattern):
462 continue
463
464 # If no version attribute was given or there's only one slot, omit the
465 # slot qualifier.
466 if not attrs.version or len(cp_slots) == 1:
467 matches.append((cp, None))
468 else:
469 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400470 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700471 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
472 matches.append((cp, slot))
473
474 return matches
475
476 def _FindPackage(self, pkg):
477 """Returns the (CP, slot) pair for a package matching |pkg|.
478
479 Args:
480 pkg: Path to a binary package or a (partial) package CPV specifier.
481
482 Returns:
483 A (CP, slot) pair for the given package; slot may be None (unspecified).
484
485 Raises:
486 ValueError: if |pkg| is not a binpkg file nor does it match something
487 that's in the bintree.
488 """
489 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
490 package = os.path.basename(os.path.splitext(pkg)[0])
491 category = os.path.basename(os.path.dirname(pkg))
492 return self._GetCP(os.path.join(category, package)), None
493
494 matches = self._FindPackageMatches(pkg)
495 if not matches:
496 raise ValueError('No package found for %s' % pkg)
497
498 idx = 0
499 if len(matches) > 1:
500 # Ask user to pick among multiple matches.
501 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
502 ['%s:%s' % (cp, slot) if slot else cp
503 for cp, slot in matches])
504
505 return matches[idx]
506
507 def _NeedsInstall(self, cpv, slot, build_time, optional):
508 """Returns whether a package needs to be installed on the target.
509
510 Args:
511 cpv: Fully qualified CPV (string) of the package.
512 slot: Slot identifier (string).
513 build_time: The BUILT_TIME value (string) of the binpkg.
514 optional: Whether package is optional on the target.
515
516 Returns:
517 A tuple (install, update) indicating whether to |install| the package and
518 whether it is an |update| to an existing package.
519
520 Raises:
521 ValueError: if slot is not provided.
522 """
523 # If not checking installed packages, always install.
524 if not self.target_db:
525 return True, False
526
527 cp = self._GetCP(cpv)
528 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
529 if target_pkg_info is not None:
530 if cpv != target_pkg_info.cpv:
531 attrs = portage_util.SplitCPV(cpv)
532 target_attrs = portage_util.SplitCPV(target_pkg_info.cpv)
533 logging.debug('Updating %s: version (%s) different on target (%s)',
534 cp, attrs.version, target_attrs.version)
535 return True, True
536
537 if build_time != target_pkg_info.build_time:
538 logging.debug('Updating %s: build time (%s) different on target (%s)',
539 cpv, build_time, target_pkg_info.build_time)
540 return True, True
541
542 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
543 cp, target_pkg_info.cpv, target_pkg_info.build_time)
544 return False, False
545
546 if optional:
547 logging.debug('Not installing %s: missing on target but optional', cp)
548 return False, False
549
550 logging.debug('Installing %s: missing on target and non-optional (%s)',
551 cp, cpv)
552 return True, False
553
554 def _ProcessDeps(self, deps, reverse):
555 """Enqueues dependencies for processing.
556
557 Args:
558 deps: List of dependencies to enqueue.
559 reverse: Whether these are reverse dependencies.
560 """
561 if not deps:
562 return
563
564 logging.debug('Processing %d %s dep(s)...', len(deps),
565 'reverse' if reverse else 'forward')
566 num_already_seen = 0
567 for dep in deps:
568 if self._EnqDep(dep, False, reverse):
569 logging.debug(' Queued dep %s', dep)
570 else:
571 num_already_seen += 1
572
573 if num_already_seen:
574 logging.debug('%d dep(s) already seen', num_already_seen)
575
576 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
577 """Returns a dictionary of packages that need to be installed on the target.
578
579 Args:
580 process_rdeps: Whether to trace forward dependencies.
581 process_rev_rdeps: Whether to trace backward dependencies as well.
582
583 Returns:
584 A dictionary mapping CP values (string) to tuples containing a CPV
585 (string), a slot (string), a boolean indicating whether the package
586 was initially listed in the queue, and a boolean indicating whether this
587 is an update to an existing package.
588 """
589 installs = {}
590 while self.queue:
591 dep, listed, optional = self._DeqDep()
592 cp, required_slot = dep
593 if cp in installs:
594 logging.debug('Already updating %s', cp)
595 continue
596
597 cp_slots = self.binpkgs_db.get(cp, dict())
598 logging.debug('Checking packages matching %s%s%s...', cp,
599 ' (slot: %s)' % required_slot if required_slot else '',
600 ' (optional)' if optional else '')
601 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400602 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700603 if required_slot and slot != required_slot:
604 continue
605
606 num_processed += 1
607 logging.debug(' Checking %s...', pkg_info.cpv)
608
609 install, update = self._NeedsInstall(pkg_info.cpv, slot,
610 pkg_info.build_time, optional)
611 if not install:
612 continue
613
614 installs[cp] = (pkg_info.cpv, slot, listed, update)
615
616 # Add forward and backward runtime dependencies to queue.
617 if process_rdeps:
618 self._ProcessDeps(pkg_info.rdeps, False)
619 if process_rev_rdeps:
620 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
621 if target_pkg_info:
622 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
623
624 if num_processed == 0:
625 logging.warning('No qualified bintree package corresponding to %s', cp)
626
627 return installs
628
629 def _SortInstalls(self, installs):
630 """Returns a sorted list of packages to install.
631
632 Performs a topological sort based on dependencies found in the binary
633 package database.
634
635 Args:
636 installs: Dictionary of packages to install indexed by CP.
637
638 Returns:
639 A list of package CPVs (string).
640
641 Raises:
642 ValueError: If dependency graph contains a cycle.
643 """
644 not_visited = set(installs.keys())
645 curr_path = []
646 sorted_installs = []
647
648 def SortFrom(cp):
649 """Traverses dependencies recursively, emitting nodes in reverse order."""
650 cpv, slot, _, _ = installs[cp]
651 if cpv in curr_path:
652 raise ValueError('Dependencies contain a cycle: %s -> %s' %
653 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
654 curr_path.append(cpv)
655 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
656 if rdep_cp in not_visited:
657 not_visited.remove(rdep_cp)
658 SortFrom(rdep_cp)
659
660 sorted_installs.append(cpv)
661 curr_path.pop()
662
663 # So long as there's more packages, keep expanding dependency paths.
664 while not_visited:
665 SortFrom(not_visited.pop())
666
667 return sorted_installs
668
669 def _EnqListedPkg(self, pkg):
670 """Finds and enqueues a listed package."""
671 cp, slot = self._FindPackage(pkg)
672 if cp not in self.binpkgs_db:
673 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
674 self._EnqDep((cp, slot), True, False)
675
676 def _EnqInstalledPkgs(self):
677 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400678 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700679 target_cp_slots = self.target_db.get(cp)
680 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400681 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700682 if slot in target_cp_slots:
683 self._EnqDep((cp, slot), True, False)
684
685 def Run(self, device, root, listed_pkgs, update, process_rdeps,
686 process_rev_rdeps):
687 """Computes the list of packages that need to be installed on a target.
688
689 Args:
690 device: Target handler object.
691 root: Package installation root.
692 listed_pkgs: Package names/files listed by the user.
693 update: Whether to read the target's installed package database.
694 process_rdeps: Whether to trace forward dependencies.
695 process_rev_rdeps: Whether to trace backward dependencies as well.
696
697 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700698 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
699 list of package CPVs (string) to install on the target in an order that
700 satisfies their inter-dependencies, |listed| the subset that was
701 requested by the user, and |num_updates| the number of packages being
702 installed over preexisting versions. Note that installation order should
703 be reversed for removal, |install_attrs| is a dictionary mapping a package
704 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700705 """
706 if process_rev_rdeps and not process_rdeps:
707 raise ValueError('Must processing forward deps when processing rev deps')
708 if process_rdeps and not update:
709 raise ValueError('Must check installed packages when processing deps')
710
711 if update:
712 logging.info('Initializing target intalled packages database...')
713 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
714
715 logging.info('Initializing binary packages database...')
716 self._InitBinpkgDB(process_rdeps)
717
718 logging.info('Finding listed package(s)...')
719 self._InitDepQueue()
720 for pkg in listed_pkgs:
721 if pkg == '@installed':
722 if not update:
723 raise ValueError(
724 'Must check installed packages when updating all of them.')
725 self._EnqInstalledPkgs()
726 else:
727 self._EnqListedPkg(pkg)
728
729 logging.info('Computing set of packages to install...')
730 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
731
732 num_updates = 0
733 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400734 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700735 if listed:
736 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400737 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700738 num_updates += 1
739
740 logging.info('Processed %d package(s), %d will be installed, %d are '
741 'updating existing packages',
742 len(self.seen), len(installs), num_updates)
743
744 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700745
746 install_attrs = {}
747 for pkg in sorted_installs:
748 pkg_path = os.path.join(root, portage.VDB_PATH, pkg)
749 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
750 install_attrs[pkg] = {}
751 if dlc_id and dlc_package:
752 install_attrs[pkg][_DLC_ID] = dlc_id
753
754 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700755
756
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700757def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700758 """Copies |pkg| to |device| and emerges it.
759
760 Args:
761 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700762 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700763 root: Package installation root path.
764 extra_args: Extra arguments to pass to emerge.
765
766 Raises:
767 DeployError: Unrecoverable error during emerge.
768 """
David Pursell9476bf42015-03-30 13:34:27 -0700769 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700770 pkg_name = os.path.basename(pkg_path)
771 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700772 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400773 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
774 # Clean out the dirs first if we had a previous emerge on the device so as to
775 # free up space for this emerge. The last emerge gets implicitly cleaned up
776 # when the device connection deletes its work_dir.
777 device.RunCommand(
778 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
779 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700780
Ralph Nathane01ccf12015-04-16 10:40:32 -0700781 # This message is read by BrilloDeployOperation.
782 logging.notice('Copying %s to device.', pkg_name)
Ilja H. Friedel0ab63e12017-03-28 13:29:48 -0700783 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700784
David Pursell9476bf42015-03-30 13:34:27 -0700785 logging.info('Use portage temp dir %s', portage_tmpdir)
786
Ralph Nathane01ccf12015-04-16 10:40:32 -0700787 # This message is read by BrilloDeployOperation.
788 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700789 pkg_path = os.path.join(pkg_dir, pkg_name)
790
791 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
792 # chromeos-base packages will be skipped due to the configuration
793 # in /etc/protage/make.profile/package.provided. However, there is
794 # a known bug that /usr/local/etc/portage is not setup properly
795 # (crbug.com/312041). This does not affect `cros deploy` because
796 # we do not use the preset PKGDIR.
797 extra_env = {
798 'FEATURES': '-sandbox',
799 'PKGDIR': pkgroot,
800 'PORTAGE_CONFIGROOT': '/usr/local',
801 'PORTAGE_TMPDIR': portage_tmpdir,
802 'PORTDIR': device.work_dir,
803 'CONFIG_PROTECT': '-*',
804 }
Alex Kleinaaddc932020-01-30 15:02:24 -0700805 # --ignore-built-slot-operator-deps because we don't rebuild everything.
806 # It can cause errors, but that's expected with cros deploy since it's just a
807 # best effort to prevent developers avoid rebuilding an image every time.
808 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', pkg_path,
809 '--root=%s' % root]
David Pursell9476bf42015-03-30 13:34:27 -0700810 if extra_args:
811 cmd.append(extra_args)
812
Alex Kleinaaddc932020-01-30 15:02:24 -0700813 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
814 'packages built against the old version may not be able to '
815 'load the new .so. This is expected, and you will just need '
816 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700817 try:
Greg Kerrb96c02c2019-02-08 14:32:41 -0800818 result = device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
819 capture_output=True, debug_level=logging.INFO)
820
821 pattern = ('A requested package will not be merged because '
822 'it is listed in package.provided')
823 output = result.error.replace('\n', ' ').replace('\r', '')
824 if pattern in output:
825 error = ('Package failed to emerge: %s\n'
826 'Remove %s from /etc/portage/make.profile/'
827 'package.provided/chromeos-base.packages\n'
828 '(also see crbug.com/920140 for more context)\n'
829 % (pattern, pkg_name))
830 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700831 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700832 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700833 raise
834 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700835 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700836
837
Qijiang Fand5958192019-07-26 12:32:36 +0900838def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800839 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900840
841 This reads the tarball from pkgpath, and calls restorecon on device to
842 restore SELinux context for files listed in the tarball, assuming those files
843 are installed to /
844
845 Args:
846 device: a ChromiumOSDevice object
847 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900848 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900849 """
Ben Pastene5f03b052019-08-12 18:03:24 -0700850 enforced = device.IsSELinuxEnforced()
Qijiang Fan8a945032019-04-25 20:53:29 +0900851 if enforced:
852 device.RunCommand(['setenforce', '0'])
853 pkgroot = os.path.join(device.work_dir, 'packages')
854 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
855 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
856 # Testing shows restorecon splits on newlines instead of spaces.
857 device.RunCommand(
Qijiang Fand5958192019-07-26 12:32:36 +0900858 ['cd', root, '&&',
859 'tar', 'tf', pkgpath_device, '|',
860 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900861 remote_sudo=True)
862 if enforced:
863 device.RunCommand(['setenforce', '1'])
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900864
865
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700866def _GetPackagesByCPV(cpvs, strip, sysroot):
867 """Returns paths to binary packages corresponding to |cpvs|.
868
869 Args:
870 cpvs: List of CPV components given by portage_util.SplitCPV().
871 strip: True to run strip_package.
872 sysroot: Sysroot path.
873
874 Returns:
875 List of paths corresponding to |cpvs|.
876
877 Raises:
878 DeployError: If a package is missing.
879 """
880 packages_dir = None
881 if strip:
882 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400883 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700884 ['strip_package', '--sysroot', sysroot] +
Alex Klein7078e252018-10-02 10:21:04 -0600885 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700886 packages_dir = _STRIPPED_PACKAGES_DIR
887 except cros_build_lib.RunCommandError:
888 logging.error('Cannot strip packages %s',
889 ' '.join([str(cpv) for cpv in cpvs]))
890 raise
891
892 paths = []
893 for cpv in cpvs:
894 path = portage_util.GetBinaryPackagePath(
895 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
896 packages_dir=packages_dir)
897 if not path:
898 raise DeployError('Missing package %s.' % cpv)
899 paths.append(path)
900
901 return paths
902
903
904def _GetPackagesPaths(pkgs, strip, sysroot):
905 """Returns paths to binary |pkgs|.
906
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700907 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700908 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700909 strip: Whether or not to run strip_package for CPV packages.
910 sysroot: The sysroot path.
911
912 Returns:
913 List of paths corresponding to |pkgs|.
914 """
Ned Nguyend0db4072019-02-22 14:19:21 -0700915 cpvs = [portage_util.SplitCPV(p) for p in pkgs]
916 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700917
918
David Pursell9476bf42015-03-30 13:34:27 -0700919def _Unmerge(device, pkg, root):
920 """Unmerges |pkg| on |device|.
921
922 Args:
923 device: A RemoteDevice object.
924 pkg: A package name.
925 root: Package installation root path.
926 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700927 pkg_name = os.path.basename(pkg)
928 # This message is read by BrilloDeployOperation.
929 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700930 cmd = ['qmerge', '--yes']
931 # Check if qmerge is available on the device. If not, use emerge.
932 if device.RunCommand(
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500933 ['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700934 cmd = ['emerge']
935
936 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
937 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700938 # Always showing the emerge output for clarity.
David Pursell9476bf42015-03-30 13:34:27 -0700939 device.RunCommand(cmd, capture_output=False, remote_sudo=True,
940 debug_level=logging.INFO)
941 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700942 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700943 raise
944 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700945 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700946
947
948def _ConfirmDeploy(num_updates):
949 """Returns whether we can continue deployment."""
950 if num_updates > _MAX_UPDATES_NUM:
951 logging.warning(_MAX_UPDATES_WARNING)
952 return cros_build_lib.BooleanPrompt(default=False)
953
954 return True
955
956
Ralph Nathane01ccf12015-04-16 10:40:32 -0700957def _EmergePackages(pkgs, device, strip, sysroot, root, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800958 """Call _Emerge for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700959 dlc_deployed = False
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700960 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
961 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Ben Pastene5f03b052019-08-12 18:03:24 -0700962 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900963 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800964
965 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
966 if dlc_id and dlc_package:
967 _DeployDLCImage(device, sysroot, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700968 dlc_deployed = True
Andrewc7e1c6b2020-02-27 16:03:53 -0800969 # Clean up directories created by emerging DLCs.
970 device.run(['rm', '-rf', '/build/rootfs/dlc'])
971 device.run(['rmdir', '--ignore-fail-on-non-empty', '/build/rootfs',
972 '/build'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700973
974 # Restart dlcservice so it picks up the newly installed DLC modules (in case
975 # we installed new DLC images).
976 if dlc_deployed:
977 device.RunCommand(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -0700978
979
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700980def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -0700981 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700982 dlc_uninstalled = False
Ralph Nathane01ccf12015-04-16 10:40:32 -0700983 for pkg in pkgs:
984 _Unmerge(device, pkg, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700985 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
986 dlc_uninstalled = True
987
988 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
989 # uninstalled DLC images).
990 if dlc_uninstalled:
991 device.RunCommand(['restart', 'dlcservice'])
992
993
994def _UninstallDLCImage(device, pkg_attrs):
995 """Uninstall a DLC image."""
996 if _DLC_ID in pkg_attrs:
997 dlc_id = pkg_attrs[_DLC_ID]
998 logging.notice('Uninstalling DLC image for %s', dlc_id)
999
1000 device.RunCommand(['sudo', '-u', 'chronos', 'dlcservice_util',
1001 '--uninstall', '--dlc_ids=%s' % dlc_id])
1002 return True
1003 else:
1004 logging.debug('DLC_ID not found in package')
1005 return False
1006
1007
Andrewc7e1c6b2020-02-27 16:03:53 -08001008def _DeployDLCImage(device, sysroot, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001009 """Deploy (install and mount) a DLC image."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001010
Andrewc7e1c6b2020-02-27 16:03:53 -08001011 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1012 try:
1013 device.run(['dlcservice_util', '--dlc_ids=%s' % dlc_id, '--uninstall'])
1014 except cros_build_lib.RunCommandError as e:
1015 logging.info('Failed to uninstall DLC:%s. Continue anyway.',
1016 e.result.error)
1017 except Exception:
1018 logging.error('Failed to uninstall DLC.')
1019 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001020
Andrewc7e1c6b2020-02-27 16:03:53 -08001021 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1022 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1023 # location.
1024 logging.notice('Deploy the DLC image for %s', dlc_id)
1025 dlc_img_path_src = os.path.join(sysroot, build_dlc.DLC_IMAGE_DIR, dlc_id,
1026 dlc_package, build_dlc.DLC_IMAGE)
1027 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1028 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1029 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1030 # Create directories for DLC images.
1031 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1032 # Copy images to the destination directories.
1033 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
1034 build_dlc.DLC_IMAGE),
1035 mode='rsync')
1036 device.run(['cp', os.path.join(dlc_img_path_a, build_dlc.DLC_IMAGE),
1037 os.path.join(dlc_img_path_b, build_dlc.DLC_IMAGE)])
1038
1039 # Set the proper perms and ownership so dlcservice can access the image.
1040 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1041 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001042
1043
1044def _GetDLCInfo(device, pkg_path, from_dut):
1045 """Returns information of a DLC given its package path.
1046
1047 Args:
1048 device: commandline.Device object; None to use the default device.
1049 pkg_path: path to the package.
1050 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1051 info from host.
1052
1053 Returns:
1054 A tuple (dlc_id, dlc_package).
1055 """
1056 environment_content = ''
1057 if from_dut:
1058 # On DUT, |pkg_path| is the directory which contains environment file.
1059 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
1060 result = device.RunCommand(['test', '-f', environment_path],
Woody Chowde57a322020-01-07 16:18:52 +09001061 check=False, encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001062 if result.returncode == 1:
1063 # The package is not installed on DUT yet. Skip extracting info.
1064 return None, None
Woody Chowde57a322020-01-07 16:18:52 +09001065 result = device.RunCommand(['bzip2', '-d', '-c', environment_path],
1066 encoding=None)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001067 environment_content = result.output
1068 else:
1069 # On host, pkg_path is tbz2 file which contains environment file.
1070 # Extract the metadata of the package file.
1071 data = portage.xpak.tbz2(pkg_path).get_data()
1072 # Extract the environment metadata.
Woody Chowde57a322020-01-07 16:18:52 +09001073 environment_content = bz2.decompress(
Alex Klein9a1b3722020-01-28 09:59:35 -07001074 data[_ENVIRONMENT_FILENAME.encode('utf-8')])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001075
1076 with tempfile.NamedTemporaryFile() as f:
1077 # Dumps content into a file so we can use osutils.SourceEnvironment.
1078 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001079 osutils.WriteFile(path, environment_content, mode='wb')
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001080 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE))
1081 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001082
1083
Gilad Arnolda0a98062015-07-07 08:34:27 -07001084def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1085 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1086 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1087 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001088 """Deploys packages to a device.
1089
1090 Args:
David Pursell2e773382015-04-03 14:30:47 -07001091 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001092 packages: List of packages (strings) to deploy to device.
1093 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001094 emerge: True to emerge package, False to unmerge.
1095 update: Check installed version on device.
1096 deep: Install dependencies also. Implies |update|.
1097 deep_rev: Install reverse dependencies. Implies |deep|.
1098 clean_binpkg: Clean outdated binary packages.
1099 root: Package installation root path.
1100 strip: Run strip_package to filter out preset paths in the package.
1101 emerge_args: Extra arguments to pass to emerge.
1102 ssh_private_key: Path to an SSH private key file; None to use test keys.
1103 ping: True to ping the device before trying to connect.
1104 force: Ignore sanity checks and prompts.
1105 dry_run: Print deployment plan but do not deploy anything.
1106
1107 Raises:
1108 ValueError: Invalid parameter or parameter combination.
1109 DeployError: Unrecoverable failure during deploy.
1110 """
1111 if deep_rev:
1112 deep = True
1113 if deep:
1114 update = True
1115
Gilad Arnolda0a98062015-07-07 08:34:27 -07001116 if not packages:
1117 raise DeployError('No packages provided, nothing to deploy.')
1118
David Pursell9476bf42015-03-30 13:34:27 -07001119 if update and not emerge:
1120 raise ValueError('Cannot update and unmerge.')
1121
David Pursell2e773382015-04-03 14:30:47 -07001122 if device:
1123 hostname, username, port = device.hostname, device.username, device.port
1124 else:
1125 hostname, username, port = None, None, None
1126
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001127 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001128 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001129 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001130 # Somewhat confusing to clobber, but here we are.
1131 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001132 with remote_access.ChromiumOSDeviceHandler(
1133 hostname, port=port, username=username, private_key=ssh_private_key,
1134 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001135 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001136
Gilad Arnolda0a98062015-07-07 08:34:27 -07001137 board = cros_build_lib.GetBoard(device_board=device.board,
1138 override_board=board)
1139 if not force and board != device.board:
1140 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001141 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001142
Gilad Arnolda0a98062015-07-07 08:34:27 -07001143 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001144
1145 if clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001146 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001147 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001148
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001149 # Remount rootfs as writable if necessary.
1150 if not device.MountRootfsReadWrite():
1151 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001152
1153 # Obtain list of packages to upgrade/remove.
1154 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001155 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001156 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001157 if emerge:
1158 action_str = 'emerge'
1159 else:
1160 pkgs.reverse()
1161 action_str = 'unmerge'
1162
1163 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001164 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001165 return
1166
Ralph Nathane01ccf12015-04-16 10:40:32 -07001167 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001168 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001169 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001170
1171 if dry_run or not _ConfirmDeploy(num_updates):
1172 return
1173
Ralph Nathane01ccf12015-04-16 10:40:32 -07001174 # Select function (emerge or unmerge) and bind args.
1175 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001176 func = functools.partial(_EmergePackages, pkgs, device, strip,
Ralph Nathane01ccf12015-04-16 10:40:32 -07001177 sysroot, root, emerge_args)
1178 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001179 func = functools.partial(_UnmergePackages, pkgs, device, root,
1180 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001181
1182 # Call the function with the progress bar or with normal output.
1183 if command.UseProgressBar():
1184 op = BrilloDeployOperation(len(pkgs), emerge)
1185 op.Run(func, log_level=logging.DEBUG)
1186 else:
1187 func()
David Pursell9476bf42015-03-30 13:34:27 -07001188
Ben Pastene5f03b052019-08-12 18:03:24 -07001189 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001190 if sum(x.count('selinux-policy') for x in pkgs):
1191 logging.warning(
1192 'Deploying SELinux policy will not take effect until reboot. '
1193 'SELinux policy is loaded by init. Also, security contexts '
1194 '(labels) in files will require manual relabeling by the user '
1195 'if your policy modifies the file contexts.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001196
David Pursell9476bf42015-03-30 13:34:27 -07001197 logging.warning('Please restart any updated services on the device, '
1198 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001199 except Exception:
1200 if lsb_release:
1201 lsb_entries = sorted(lsb_release.items())
1202 logging.info('Following are the LSB version details of the device:\n%s',
1203 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1204 raise