blob: 111c68de8798b8c301ae3a327db3db741c1cee5b [file] [log] [blame]
David Pursell9476bf42015-03-30 13:34:27 -07001# Copyright 2015 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Alex Kleinaaddc932020-01-30 15:02:24 -07005"""Deploy packages onto a target device.
6
7Integration tests for this file can be found at cli/cros/tests/cros_vm_tests.py.
8See that file for more information.
9"""
David Pursell9476bf42015-03-30 13:34:27 -070010
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040011from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -070012
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070013import bz2
David Pursell9476bf42015-03-30 13:34:27 -070014import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070015import functools
David Pursell9476bf42015-03-30 13:34:27 -070016import json
17import os
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070018import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070019
Ralph Nathane01ccf12015-04-16 10:40:32 -070020from chromite.cli import command
Mike Frysinger06a51c82021-04-06 11:39:17 -040021from chromite.lib import build_target_lib
David Pursell9476bf42015-03-30 13:34:27 -070022from chromite.lib import cros_build_lib
23from chromite.lib import cros_logging as logging
Alex Klein18a60af2020-06-11 12:08:47 -060024from chromite.lib import dlc_lib
Ralph Nathane01ccf12015-04-16 10:40:32 -070025from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070026from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070027from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070028from chromite.lib import remote_access
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +090029from chromite.lib import workon_helper
Alex Klein18a60af2020-06-11 12:08:47 -060030from chromite.lib.parser import package_info
31
David Pursell9476bf42015-03-30 13:34:27 -070032try:
33 import portage
34except ImportError:
35 if cros_build_lib.IsInsideChroot():
36 raise
37
38
39_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
40# This is defined in src/platform/dev/builder.py
41_STRIPPED_PACKAGES_DIR = 'stripped-packages'
42
43_MAX_UPDATES_NUM = 10
44_MAX_UPDATES_WARNING = (
45 'You are about to update a large number of installed packages, which '
46 'might take a long time, fail midway, or leave the target in an '
47 'inconsistent state. It is highly recommended that you flash a new image '
48 'instead.')
49
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070050_DLC_ID = 'DLC_ID'
51_DLC_PACKAGE = 'DLC_PACKAGE'
Andrew67b5fa72020-02-05 14:14:48 -080052_DLC_ENABLED = 'DLC_ENABLED'
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070053_ENVIRONMENT_FILENAME = 'environment.bz2'
54_DLC_INSTALL_ROOT = '/var/cache/dlc'
55
David Pursell9476bf42015-03-30 13:34:27 -070056
57class DeployError(Exception):
58 """Thrown when an unrecoverable error is encountered during deploy."""
59
60
Ralph Nathane01ccf12015-04-16 10:40:32 -070061class BrilloDeployOperation(operation.ProgressBarOperation):
62 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070063 # These two variables are used to validate the output in the VM integration
64 # tests. Changes to the output must be reflected here.
Mike Frysinger63d35512021-01-26 23:16:13 -050065 MERGE_EVENTS = (
66 'Preparing local packages',
67 'NOTICE: Copying binpkgs',
68 'NOTICE: Installing',
69 'been installed.',
70 'Please restart any updated',
71 )
Mike Frysinger22bb5502021-01-29 13:05:46 -050072 UNMERGE_EVENTS = (
73 'NOTICE: Unmerging',
74 'been uninstalled.',
75 'Please restart any updated',
76 )
Ralph Nathane01ccf12015-04-16 10:40:32 -070077
Mike Frysinger63d35512021-01-26 23:16:13 -050078 def __init__(self, emerge):
Ralph Nathane01ccf12015-04-16 10:40:32 -070079 """Construct BrilloDeployOperation object.
80
81 Args:
Ralph Nathane01ccf12015-04-16 10:40:32 -070082 emerge: True if emerge, False is unmerge.
83 """
84 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070085 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070086 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070087 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070088 self._events = self.UNMERGE_EVENTS
Mike Frysinger63d35512021-01-26 23:16:13 -050089 self._total = len(self._events)
Ralph Nathane01ccf12015-04-16 10:40:32 -070090 self._completed = 0
91
Ralph Nathandc14ed92015-04-22 11:17:40 -070092 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070093 """Parse the output of brillo deploy to update a progress bar."""
94 stdout = self._stdout.read()
95 stderr = self._stderr.read()
96 output = stdout + stderr
97 for event in self._events:
98 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040099 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700100
101
David Pursell9476bf42015-03-30 13:34:27 -0700102class _InstallPackageScanner(object):
103 """Finds packages that need to be installed on a target device.
104
105 Scans the sysroot bintree, beginning with a user-provided list of packages,
106 to find all packages that need to be installed. If so instructed,
107 transitively scans forward (mandatory) and backward (optional) dependencies
108 as well. A package will be installed if missing on the target (mandatory
109 packages only), or it will be updated if its sysroot version and build time
110 are different from the target. Common usage:
111
112 pkg_scanner = _InstallPackageScanner(sysroot)
113 pkgs = pkg_scanner.Run(...)
114 """
115
116 class VartreeError(Exception):
117 """An error in the processing of the installed packages tree."""
118
119 class BintreeError(Exception):
120 """An error in the processing of the source binpkgs tree."""
121
122 class PkgInfo(object):
123 """A record containing package information."""
124
125 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
126
127 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
128 self.cpv = cpv
129 self.build_time = build_time
130 self.rdeps_raw = rdeps_raw
131 self.rdeps = set() if rdeps is None else rdeps
132 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
133
134 # Python snippet for dumping vartree info on the target. Instantiate using
135 # _GetVartreeSnippet().
136 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700137import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700138import os
139import portage
140
141# Normalize the path to match what portage will index.
142target_root = os.path.normpath('%(root)s')
143if not target_root.endswith('/'):
144 target_root += '/'
145trees = portage.create_trees(target_root=target_root, config_root='/')
146vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700147pkg_info = []
148for cpv in vartree.dbapi.cpv_all():
149 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
150 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
151 pkg_info.append((cpv, slot, rdep_raw, build_time))
152
153print(json.dumps(pkg_info))
154"""
155
156 def __init__(self, sysroot):
157 self.sysroot = sysroot
158 # Members containing the sysroot (binpkg) and target (installed) package DB.
159 self.target_db = None
160 self.binpkgs_db = None
161 # Members for managing the dependency resolution work queue.
162 self.queue = None
163 self.seen = None
164 self.listed = None
165
166 @staticmethod
167 def _GetCP(cpv):
168 """Returns the CP value for a given CPV string."""
Alex Klein9742cb62020-10-12 19:22:10 +0000169 attrs = package_info.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600170 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700171 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600172 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700173
174 @staticmethod
175 def _InDB(cp, slot, db):
176 """Returns whether CP and slot are found in a database (if provided)."""
177 cp_slots = db.get(cp) if db else None
178 return cp_slots is not None and (not slot or slot in cp_slots)
179
180 @staticmethod
181 def _AtomStr(cp, slot):
182 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
183 return '%s:%s' % (cp, slot) if slot else cp
184
185 @classmethod
186 def _GetVartreeSnippet(cls, root='/'):
187 """Returns a code snippet for dumping the vartree on the target.
188
189 Args:
190 root: The installation root.
191
192 Returns:
193 The said code snippet (string) with parameters filled in.
194 """
195 return cls._GET_VARTREE % {'root': root}
196
197 @classmethod
198 def _StripDepAtom(cls, dep_atom, installed_db=None):
199 """Strips a dependency atom and returns a (CP, slot) pair."""
200 # TODO(garnold) This is a gross simplification of ebuild dependency
201 # semantics, stripping and ignoring various qualifiers (versions, slots,
202 # USE flag, negation) and will likely need to be fixed. chromium:447366.
203
204 # Ignore unversioned blockers, leaving them for the user to resolve.
205 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
206 return None, None
207
208 cp = dep_atom
209 slot = None
210 require_installed = False
211
212 # Versioned blockers should be updated, but only if already installed.
213 # These are often used for forcing cascaded updates of multiple packages,
214 # so we're treating them as ordinary constraints with hopes that it'll lead
215 # to the desired result.
216 if cp.startswith('!'):
217 cp = cp.lstrip('!')
218 require_installed = True
219
220 # Remove USE flags.
221 if '[' in cp:
222 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
223
224 # Separate the slot qualifier and strip off subslots.
225 if ':' in cp:
226 cp, slot = cp.split(':')
227 for delim in ('/', '='):
228 slot = slot.split(delim, 1)[0]
229
230 # Strip version wildcards (right), comparators (left).
231 cp = cp.rstrip('*')
232 cp = cp.lstrip('<=>~')
233
234 # Turn into CP form.
235 cp = cls._GetCP(cp)
236
237 if require_installed and not cls._InDB(cp, None, installed_db):
238 return None, None
239
240 return cp, slot
241
242 @classmethod
243 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
244 """Resolves and returns a list of dependencies from a dependency string.
245
246 This parses a dependency string and returns a list of package names and
247 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
248 resolving disjunctive deps, we include all choices that are fully present
249 in |installed_db|. If none is present, we choose an arbitrary one that is
250 available.
251
252 Args:
253 dep_str: A raw dependency string.
254 installed_db: A database of installed packages.
255 avail_db: A database of packages available for installation.
256
257 Returns:
258 A list of pairs (CP, slot).
259
260 Raises:
261 ValueError: the dependencies string is malformed.
262 """
263 def ProcessSubDeps(dep_exp, disjunct):
264 """Parses and processes a dependency (sub)expression."""
265 deps = set()
266 default_deps = set()
267 sub_disjunct = False
268 for dep_sub_exp in dep_exp:
269 sub_deps = set()
270
271 if isinstance(dep_sub_exp, (list, tuple)):
272 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
273 sub_disjunct = False
274 elif sub_disjunct:
275 raise ValueError('Malformed disjunctive operation in deps')
276 elif dep_sub_exp == '||':
277 sub_disjunct = True
278 elif dep_sub_exp.endswith('?'):
279 raise ValueError('Dependencies contain a conditional')
280 else:
281 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
282 if cp:
283 sub_deps = set([(cp, slot)])
284 elif disjunct:
285 raise ValueError('Atom in disjunct ignored')
286
287 # Handle sub-deps of a disjunctive expression.
288 if disjunct:
289 # Make the first available choice the default, for use in case that
290 # no option is installed.
291 if (not default_deps and avail_db is not None and
292 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
293 default_deps = sub_deps
294
295 # If not all sub-deps are installed, then don't consider them.
296 if not all([cls._InDB(cp, slot, installed_db)
297 for cp, slot in sub_deps]):
298 sub_deps = set()
299
300 deps.update(sub_deps)
301
302 return deps or default_deps
303
304 try:
305 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
306 except portage.exception.InvalidDependString as e:
307 raise ValueError('Invalid dep string: %s' % e)
308 except ValueError as e:
309 raise ValueError('%s: %s' % (e, dep_str))
310
311 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
312 installed_db=None):
313 """Returns a database of packages given a list of CPV info.
314
315 Args:
316 cpv_info: A list of tuples containing package CPV and attributes.
317 process_rdeps: Whether to populate forward dependencies.
318 process_rev_rdeps: Whether to populate reverse dependencies.
319 installed_db: A database of installed packages for filtering disjunctive
320 choices against; if None, using own built database.
321
322 Returns:
323 A map from CP values to another dictionary that maps slots to package
324 attribute tuples. Tuples contain a CPV value (string), build time
325 (string), runtime dependencies (set), and reverse dependencies (set,
326 empty if not populated).
327
328 Raises:
329 ValueError: If more than one CPV occupies a single slot.
330 """
331 db = {}
332 logging.debug('Populating package DB...')
333 for cpv, slot, rdeps_raw, build_time in cpv_info:
334 cp = self._GetCP(cpv)
335 cp_slots = db.setdefault(cp, dict())
336 if slot in cp_slots:
337 raise ValueError('More than one package found for %s' %
338 self._AtomStr(cp, slot))
339 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
340 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
341 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
342
343 avail_db = db
344 if installed_db is None:
345 installed_db = db
346 avail_db = None
347
348 # Add approximate forward dependencies.
349 if process_rdeps:
350 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400351 for cp, cp_slots in db.items():
352 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700353 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
354 installed_db, avail_db))
355 logging.debug(' %s (%s) processed rdeps: %s',
356 self._AtomStr(cp, slot), pkg_info.cpv,
357 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
358 for rdep_cp, rdep_slot in pkg_info.rdeps]))
359
360 # Add approximate reverse dependencies (optional).
361 if process_rev_rdeps:
362 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400363 for cp, cp_slots in db.items():
364 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700365 for rdep_cp, rdep_slot in pkg_info.rdeps:
366 to_slots = db.get(rdep_cp)
367 if not to_slots:
368 continue
369
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400370 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700371 if rdep_slot and to_slot != rdep_slot:
372 continue
373 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
374 self._AtomStr(cp, slot), pkg_info.cpv,
375 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
376 to_pkg_info.rev_rdeps.add((cp, slot))
377
378 return db
379
380 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
381 """Initializes a dictionary of packages installed on |device|."""
382 get_vartree_script = self._GetVartreeSnippet(root)
383 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400384 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700385 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700386 except cros_build_lib.RunCommandError as e:
387 logging.error('Cannot get target vartree:\n%s', e.result.error)
388 raise
389
390 try:
391 self.target_db = self._BuildDB(json.loads(result.output),
392 process_rdeps, process_rev_rdeps)
393 except ValueError as e:
394 raise self.VartreeError(str(e))
395
396 def _InitBinpkgDB(self, process_rdeps):
397 """Initializes a dictionary of binary packages for updating the target."""
398 # Get build root trees; portage indexes require a trailing '/'.
399 build_root = os.path.join(self.sysroot, '')
400 trees = portage.create_trees(target_root=build_root, config_root=build_root)
401 bintree = trees[build_root]['bintree']
402 binpkgs_info = []
403 for cpv in bintree.dbapi.cpv_all():
404 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
405 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
406 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
407
408 try:
409 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
410 installed_db=self.target_db)
411 except ValueError as e:
412 raise self.BintreeError(str(e))
413
414 def _InitDepQueue(self):
415 """Initializes the dependency work queue."""
416 self.queue = set()
417 self.seen = {}
418 self.listed = set()
419
420 def _EnqDep(self, dep, listed, optional):
421 """Enqueues a dependency if not seen before or if turned non-optional."""
422 if dep in self.seen and (optional or not self.seen[dep]):
423 return False
424
425 self.queue.add(dep)
426 self.seen[dep] = optional
427 if listed:
428 self.listed.add(dep)
429 return True
430
431 def _DeqDep(self):
432 """Dequeues and returns a dependency, its listed and optional flags.
433
434 This returns listed packages first, if any are present, to ensure that we
435 correctly mark them as such when they are first being processed.
436 """
437 if self.listed:
438 dep = self.listed.pop()
439 self.queue.remove(dep)
440 listed = True
441 else:
442 dep = self.queue.pop()
443 listed = False
444
445 return dep, listed, self.seen[dep]
446
447 def _FindPackageMatches(self, cpv_pattern):
448 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
449
450 This is breaking |cpv_pattern| into its C, P and V components, each of
451 which may or may not be present or contain wildcards. It then scans the
452 binpkgs database to find all atoms that match these components, returning a
453 list of CP and slot qualifier. When the pattern does not specify a version,
454 or when a CP has only one slot in the binpkgs database, we omit the slot
455 qualifier in the result.
456
457 Args:
458 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
459
460 Returns:
461 A list of (CPV, slot) pairs of packages in the binpkgs database that
462 match the pattern.
463 """
Alex Klein9742cb62020-10-12 19:22:10 +0000464 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
David Pursell9476bf42015-03-30 13:34:27 -0700465 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
466 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400467 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700468 if not fnmatch.fnmatchcase(cp, cp_pattern):
469 continue
470
471 # If no version attribute was given or there's only one slot, omit the
472 # slot qualifier.
Alex Klein9742cb62020-10-12 19:22:10 +0000473 if not attrs.version or len(cp_slots) == 1:
David Pursell9476bf42015-03-30 13:34:27 -0700474 matches.append((cp, None))
475 else:
Alex Klein9742cb62020-10-12 19:22:10 +0000476 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400477 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700478 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
479 matches.append((cp, slot))
480
481 return matches
482
483 def _FindPackage(self, pkg):
484 """Returns the (CP, slot) pair for a package matching |pkg|.
485
486 Args:
487 pkg: Path to a binary package or a (partial) package CPV specifier.
488
489 Returns:
490 A (CP, slot) pair for the given package; slot may be None (unspecified).
491
492 Raises:
493 ValueError: if |pkg| is not a binpkg file nor does it match something
494 that's in the bintree.
495 """
496 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
497 package = os.path.basename(os.path.splitext(pkg)[0])
498 category = os.path.basename(os.path.dirname(pkg))
499 return self._GetCP(os.path.join(category, package)), None
500
501 matches = self._FindPackageMatches(pkg)
502 if not matches:
503 raise ValueError('No package found for %s' % pkg)
504
505 idx = 0
506 if len(matches) > 1:
507 # Ask user to pick among multiple matches.
508 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
509 ['%s:%s' % (cp, slot) if slot else cp
510 for cp, slot in matches])
511
512 return matches[idx]
513
514 def _NeedsInstall(self, cpv, slot, build_time, optional):
515 """Returns whether a package needs to be installed on the target.
516
517 Args:
518 cpv: Fully qualified CPV (string) of the package.
519 slot: Slot identifier (string).
520 build_time: The BUILT_TIME value (string) of the binpkg.
521 optional: Whether package is optional on the target.
522
523 Returns:
524 A tuple (install, update) indicating whether to |install| the package and
525 whether it is an |update| to an existing package.
526
527 Raises:
528 ValueError: if slot is not provided.
529 """
530 # If not checking installed packages, always install.
531 if not self.target_db:
532 return True, False
533
534 cp = self._GetCP(cpv)
535 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
536 if target_pkg_info is not None:
537 if cpv != target_pkg_info.cpv:
Alex Klein9742cb62020-10-12 19:22:10 +0000538 attrs = package_info.SplitCPV(cpv)
539 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
David Pursell9476bf42015-03-30 13:34:27 -0700540 logging.debug('Updating %s: version (%s) different on target (%s)',
Alex Klein9742cb62020-10-12 19:22:10 +0000541 cp, attrs.version, target_attrs.version)
David Pursell9476bf42015-03-30 13:34:27 -0700542 return True, True
543
544 if build_time != target_pkg_info.build_time:
545 logging.debug('Updating %s: build time (%s) different on target (%s)',
546 cpv, build_time, target_pkg_info.build_time)
547 return True, True
548
549 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
550 cp, target_pkg_info.cpv, target_pkg_info.build_time)
551 return False, False
552
553 if optional:
554 logging.debug('Not installing %s: missing on target but optional', cp)
555 return False, False
556
557 logging.debug('Installing %s: missing on target and non-optional (%s)',
558 cp, cpv)
559 return True, False
560
561 def _ProcessDeps(self, deps, reverse):
562 """Enqueues dependencies for processing.
563
564 Args:
565 deps: List of dependencies to enqueue.
566 reverse: Whether these are reverse dependencies.
567 """
568 if not deps:
569 return
570
571 logging.debug('Processing %d %s dep(s)...', len(deps),
572 'reverse' if reverse else 'forward')
573 num_already_seen = 0
574 for dep in deps:
575 if self._EnqDep(dep, False, reverse):
576 logging.debug(' Queued dep %s', dep)
577 else:
578 num_already_seen += 1
579
580 if num_already_seen:
581 logging.debug('%d dep(s) already seen', num_already_seen)
582
583 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
584 """Returns a dictionary of packages that need to be installed on the target.
585
586 Args:
587 process_rdeps: Whether to trace forward dependencies.
588 process_rev_rdeps: Whether to trace backward dependencies as well.
589
590 Returns:
591 A dictionary mapping CP values (string) to tuples containing a CPV
592 (string), a slot (string), a boolean indicating whether the package
593 was initially listed in the queue, and a boolean indicating whether this
594 is an update to an existing package.
595 """
596 installs = {}
597 while self.queue:
598 dep, listed, optional = self._DeqDep()
599 cp, required_slot = dep
600 if cp in installs:
601 logging.debug('Already updating %s', cp)
602 continue
603
604 cp_slots = self.binpkgs_db.get(cp, dict())
605 logging.debug('Checking packages matching %s%s%s...', cp,
606 ' (slot: %s)' % required_slot if required_slot else '',
607 ' (optional)' if optional else '')
608 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400609 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700610 if required_slot and slot != required_slot:
611 continue
612
613 num_processed += 1
614 logging.debug(' Checking %s...', pkg_info.cpv)
615
616 install, update = self._NeedsInstall(pkg_info.cpv, slot,
617 pkg_info.build_time, optional)
618 if not install:
619 continue
620
621 installs[cp] = (pkg_info.cpv, slot, listed, update)
622
623 # Add forward and backward runtime dependencies to queue.
624 if process_rdeps:
625 self._ProcessDeps(pkg_info.rdeps, False)
626 if process_rev_rdeps:
627 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
628 if target_pkg_info:
629 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
630
631 if num_processed == 0:
632 logging.warning('No qualified bintree package corresponding to %s', cp)
633
634 return installs
635
636 def _SortInstalls(self, installs):
637 """Returns a sorted list of packages to install.
638
639 Performs a topological sort based on dependencies found in the binary
640 package database.
641
642 Args:
643 installs: Dictionary of packages to install indexed by CP.
644
645 Returns:
646 A list of package CPVs (string).
647
648 Raises:
649 ValueError: If dependency graph contains a cycle.
650 """
651 not_visited = set(installs.keys())
652 curr_path = []
653 sorted_installs = []
654
655 def SortFrom(cp):
656 """Traverses dependencies recursively, emitting nodes in reverse order."""
657 cpv, slot, _, _ = installs[cp]
658 if cpv in curr_path:
659 raise ValueError('Dependencies contain a cycle: %s -> %s' %
660 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
661 curr_path.append(cpv)
662 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
663 if rdep_cp in not_visited:
664 not_visited.remove(rdep_cp)
665 SortFrom(rdep_cp)
666
667 sorted_installs.append(cpv)
668 curr_path.pop()
669
670 # So long as there's more packages, keep expanding dependency paths.
671 while not_visited:
672 SortFrom(not_visited.pop())
673
674 return sorted_installs
675
676 def _EnqListedPkg(self, pkg):
677 """Finds and enqueues a listed package."""
678 cp, slot = self._FindPackage(pkg)
679 if cp not in self.binpkgs_db:
680 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
681 self._EnqDep((cp, slot), True, False)
682
683 def _EnqInstalledPkgs(self):
684 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400685 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700686 target_cp_slots = self.target_db.get(cp)
687 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400688 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700689 if slot in target_cp_slots:
690 self._EnqDep((cp, slot), True, False)
691
692 def Run(self, device, root, listed_pkgs, update, process_rdeps,
693 process_rev_rdeps):
694 """Computes the list of packages that need to be installed on a target.
695
696 Args:
697 device: Target handler object.
698 root: Package installation root.
699 listed_pkgs: Package names/files listed by the user.
700 update: Whether to read the target's installed package database.
701 process_rdeps: Whether to trace forward dependencies.
702 process_rev_rdeps: Whether to trace backward dependencies as well.
703
704 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700705 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
706 list of package CPVs (string) to install on the target in an order that
707 satisfies their inter-dependencies, |listed| the subset that was
708 requested by the user, and |num_updates| the number of packages being
709 installed over preexisting versions. Note that installation order should
710 be reversed for removal, |install_attrs| is a dictionary mapping a package
711 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700712 """
713 if process_rev_rdeps and not process_rdeps:
714 raise ValueError('Must processing forward deps when processing rev deps')
715 if process_rdeps and not update:
716 raise ValueError('Must check installed packages when processing deps')
717
718 if update:
719 logging.info('Initializing target intalled packages database...')
720 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
721
722 logging.info('Initializing binary packages database...')
723 self._InitBinpkgDB(process_rdeps)
724
725 logging.info('Finding listed package(s)...')
726 self._InitDepQueue()
727 for pkg in listed_pkgs:
728 if pkg == '@installed':
729 if not update:
730 raise ValueError(
731 'Must check installed packages when updating all of them.')
732 self._EnqInstalledPkgs()
733 else:
734 self._EnqListedPkg(pkg)
735
736 logging.info('Computing set of packages to install...')
737 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
738
739 num_updates = 0
740 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400741 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700742 if listed:
743 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400744 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700745 num_updates += 1
746
747 logging.info('Processed %d package(s), %d will be installed, %d are '
748 'updating existing packages',
749 len(self.seen), len(installs), num_updates)
750
751 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700752
753 install_attrs = {}
754 for pkg in sorted_installs:
Mike Frysingerada2d1c2020-03-20 05:02:06 -0400755 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700756 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
757 install_attrs[pkg] = {}
758 if dlc_id and dlc_package:
759 install_attrs[pkg][_DLC_ID] = dlc_id
760
761 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700762
763
Mike Frysinger63d35512021-01-26 23:16:13 -0500764def _Emerge(device, pkg_paths, root, extra_args=None):
765 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -0700766
767 Args:
768 device: A ChromiumOSDevice object.
Mike Frysinger63d35512021-01-26 23:16:13 -0500769 pkg_paths: (Local) paths to binary packages.
David Pursell9476bf42015-03-30 13:34:27 -0700770 root: Package installation root path.
771 extra_args: Extra arguments to pass to emerge.
772
773 Raises:
774 DeployError: Unrecoverable error during emerge.
775 """
Mike Frysinger63d35512021-01-26 23:16:13 -0500776 def path_to_name(pkg_path):
777 return os.path.basename(pkg_path)
778 def path_to_category(pkg_path):
779 return os.path.basename(os.path.dirname(pkg_path))
780
781 pkg_names = ', '.join(path_to_name(x) for x in pkg_paths)
782
David Pursell9476bf42015-03-30 13:34:27 -0700783 pkgroot = os.path.join(device.work_dir, 'packages')
Mike Frysinger15a4e012015-05-21 22:18:45 -0400784 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
785 # Clean out the dirs first if we had a previous emerge on the device so as to
786 # free up space for this emerge. The last emerge gets implicitly cleaned up
787 # when the device connection deletes its work_dir.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400788 device.run(
Mike Frysinger63d35512021-01-26 23:16:13 -0500789 f'cd {device.work_dir} && '
790 f'rm -rf packages portage-tmp && '
791 f'mkdir -p portage-tmp packages && '
792 f'cd packages && '
793 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
794 shell=True, remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700795
David Pursell9476bf42015-03-30 13:34:27 -0700796 logging.info('Use portage temp dir %s', portage_tmpdir)
797
Ralph Nathane01ccf12015-04-16 10:40:32 -0700798 # This message is read by BrilloDeployOperation.
Mike Frysinger63d35512021-01-26 23:16:13 -0500799 logging.notice('Copying binpkgs to device.')
800 for pkg_path in pkg_paths:
801 pkg_name = path_to_name(pkg_path)
802 logging.info('Copying %s', pkg_name)
803 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
804 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True,
805 compress=False)
806
807 # This message is read by BrilloDeployOperation.
808 logging.notice('Installing: %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700809
810 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
811 # chromeos-base packages will be skipped due to the configuration
812 # in /etc/protage/make.profile/package.provided. However, there is
813 # a known bug that /usr/local/etc/portage is not setup properly
814 # (crbug.com/312041). This does not affect `cros deploy` because
815 # we do not use the preset PKGDIR.
816 extra_env = {
817 'FEATURES': '-sandbox',
818 'PKGDIR': pkgroot,
819 'PORTAGE_CONFIGROOT': '/usr/local',
820 'PORTAGE_TMPDIR': portage_tmpdir,
821 'PORTDIR': device.work_dir,
822 'CONFIG_PROTECT': '-*',
823 }
Mike Frysinger63d35512021-01-26 23:16:13 -0500824
Alex Kleinaaddc932020-01-30 15:02:24 -0700825 # --ignore-built-slot-operator-deps because we don't rebuild everything.
826 # It can cause errors, but that's expected with cros deploy since it's just a
827 # best effort to prevent developers avoid rebuilding an image every time.
Mike Frysinger63d35512021-01-26 23:16:13 -0500828 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', '--root',
829 root] + [os.path.join(pkgroot, *x.split('/')[-2:]) for x in pkg_paths]
David Pursell9476bf42015-03-30 13:34:27 -0700830 if extra_args:
831 cmd.append(extra_args)
832
Alex Kleinaaddc932020-01-30 15:02:24 -0700833 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
834 'packages built against the old version may not be able to '
835 'load the new .so. This is expected, and you will just need '
836 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700837 try:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400838 result = device.run(cmd, extra_env=extra_env, remote_sudo=True,
839 capture_output=True, debug_level=logging.INFO)
Greg Kerrb96c02c2019-02-08 14:32:41 -0800840
841 pattern = ('A requested package will not be merged because '
842 'it is listed in package.provided')
843 output = result.error.replace('\n', ' ').replace('\r', '')
844 if pattern in output:
845 error = ('Package failed to emerge: %s\n'
846 'Remove %s from /etc/portage/make.profile/'
847 'package.provided/chromeos-base.packages\n'
848 '(also see crbug.com/920140 for more context)\n'
849 % (pattern, pkg_name))
850 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700851 except Exception:
Mike Frysinger63d35512021-01-26 23:16:13 -0500852 logging.error('Failed to emerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700853 raise
854 else:
Mike Frysinger63d35512021-01-26 23:16:13 -0500855 # This message is read by BrilloDeployOperation.
856 logging.notice('Packages have been installed.')
David Pursell9476bf42015-03-30 13:34:27 -0700857
858
Qijiang Fand5958192019-07-26 12:32:36 +0900859def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800860 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900861
862 This reads the tarball from pkgpath, and calls restorecon on device to
863 restore SELinux context for files listed in the tarball, assuming those files
864 are installed to /
865
866 Args:
867 device: a ChromiumOSDevice object
868 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900869 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900870 """
Qijiang Fan8a945032019-04-25 20:53:29 +0900871 pkgroot = os.path.join(device.work_dir, 'packages')
872 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
873 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
874 # Testing shows restorecon splits on newlines instead of spaces.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400875 device.run(
Qijiang Fand5958192019-07-26 12:32:36 +0900876 ['cd', root, '&&',
877 'tar', 'tf', pkgpath_device, '|',
878 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900879 remote_sudo=True)
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900880
881
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700882def _GetPackagesByCPV(cpvs, strip, sysroot):
883 """Returns paths to binary packages corresponding to |cpvs|.
884
885 Args:
Alex Klein9742cb62020-10-12 19:22:10 +0000886 cpvs: List of CPV components given by package_info.SplitCPV().
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700887 strip: True to run strip_package.
888 sysroot: Sysroot path.
889
890 Returns:
891 List of paths corresponding to |cpvs|.
892
893 Raises:
894 DeployError: If a package is missing.
895 """
896 packages_dir = None
897 if strip:
898 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400899 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700900 ['strip_package', '--sysroot', sysroot] +
Alex Klein9742cb62020-10-12 19:22:10 +0000901 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700902 packages_dir = _STRIPPED_PACKAGES_DIR
903 except cros_build_lib.RunCommandError:
904 logging.error('Cannot strip packages %s',
Alex Klein9742cb62020-10-12 19:22:10 +0000905 ' '.join([str(cpv) for cpv in cpvs]))
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700906 raise
907
908 paths = []
909 for cpv in cpvs:
910 path = portage_util.GetBinaryPackagePath(
911 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
912 packages_dir=packages_dir)
913 if not path:
914 raise DeployError('Missing package %s.' % cpv)
915 paths.append(path)
916
917 return paths
918
919
920def _GetPackagesPaths(pkgs, strip, sysroot):
921 """Returns paths to binary |pkgs|.
922
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700923 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700924 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700925 strip: Whether or not to run strip_package for CPV packages.
926 sysroot: The sysroot path.
927
928 Returns:
929 List of paths corresponding to |pkgs|.
930 """
Alex Klein9742cb62020-10-12 19:22:10 +0000931 cpvs = [package_info.SplitCPV(p) for p in pkgs]
Ned Nguyend0db4072019-02-22 14:19:21 -0700932 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700933
934
Mike Frysinger22bb5502021-01-29 13:05:46 -0500935def _Unmerge(device, pkgs, root):
936 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -0700937
938 Args:
939 device: A RemoteDevice object.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500940 pkgs: Package names.
David Pursell9476bf42015-03-30 13:34:27 -0700941 root: Package installation root path.
942 """
Mike Frysinger22bb5502021-01-29 13:05:46 -0500943 pkg_names = ', '.join(os.path.basename(x) for x in pkgs)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700944 # This message is read by BrilloDeployOperation.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500945 logging.notice('Unmerging %s.', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700946 cmd = ['qmerge', '--yes']
947 # Check if qmerge is available on the device. If not, use emerge.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400948 if device.run(['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700949 cmd = ['emerge']
950
Mike Frysinger22bb5502021-01-29 13:05:46 -0500951 cmd += ['--unmerge', '--root', root]
952 cmd.extend('f={x}' for x in pkgs)
David Pursell9476bf42015-03-30 13:34:27 -0700953 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700954 # Always showing the emerge output for clarity.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400955 device.run(cmd, capture_output=False, remote_sudo=True,
956 debug_level=logging.INFO)
David Pursell9476bf42015-03-30 13:34:27 -0700957 except Exception:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500958 logging.error('Failed to unmerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700959 raise
960 else:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500961 # This message is read by BrilloDeployOperation.
962 logging.notice('Packages have been uninstalled.')
David Pursell9476bf42015-03-30 13:34:27 -0700963
964
965def _ConfirmDeploy(num_updates):
966 """Returns whether we can continue deployment."""
967 if num_updates > _MAX_UPDATES_NUM:
968 logging.warning(_MAX_UPDATES_WARNING)
969 return cros_build_lib.BooleanPrompt(default=False)
970
971 return True
972
973
Andrew06a5f812020-01-23 08:08:32 -0800974def _EmergePackages(pkgs, device, strip, sysroot, root, board, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800975 """Call _Emerge for each package in pkgs."""
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -0500976 if device.IsSELinuxAvailable():
977 enforced = device.IsSELinuxEnforced()
978 if enforced:
979 device.run(['setenforce', '0'])
980 else:
981 enforced = False
982
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700983 dlc_deployed = False
Mike Frysinger63d35512021-01-26 23:16:13 -0500984 # This message is read by BrilloDeployOperation.
985 logging.info('Preparing local packages for transfer.')
986 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
987 # Install all the packages in one pass so inter-package blockers work.
988 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
989 logging.info('Updating SELinux settings & DLC images.')
990 for pkg_path in pkg_paths:
Ben Pastene5f03b052019-08-12 18:03:24 -0700991 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900992 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800993
994 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
995 if dlc_id and dlc_package:
Andrew06a5f812020-01-23 08:08:32 -0800996 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700997 dlc_deployed = True
Mike Frysinger5f4c2742021-02-08 14:37:23 -0500998
999 if dlc_deployed:
1000 # Clean up empty directories created by emerging DLCs.
1001 device.run(['test', '-d', '/build/rootfs', '&&', 'rmdir',
1002 '--ignore-fail-on-non-empty', '/build/rootfs', '/build'],
1003 check=False)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001004
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001005 if enforced:
1006 device.run(['setenforce', '1'])
1007
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001008 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1009 # we installed new DLC images).
1010 if dlc_deployed:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001011 device.run(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001012
1013
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001014def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001015 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001016 dlc_uninstalled = False
Mike Frysinger22bb5502021-01-29 13:05:46 -05001017 _Unmerge(device, pkgs, root)
1018 logging.info('Cleaning up DLC images.')
Ralph Nathane01ccf12015-04-16 10:40:32 -07001019 for pkg in pkgs:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001020 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1021 dlc_uninstalled = True
1022
1023 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1024 # uninstalled DLC images).
1025 if dlc_uninstalled:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001026 device.run(['restart', 'dlcservice'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001027
1028
1029def _UninstallDLCImage(device, pkg_attrs):
1030 """Uninstall a DLC image."""
1031 if _DLC_ID in pkg_attrs:
1032 dlc_id = pkg_attrs[_DLC_ID]
1033 logging.notice('Uninstalling DLC image for %s', dlc_id)
1034
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001035 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001036 return True
1037 else:
1038 logging.debug('DLC_ID not found in package')
1039 return False
1040
1041
Andrew06a5f812020-01-23 08:08:32 -08001042def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001043 """Deploy (install and mount) a DLC image."""
Andrew67b5fa72020-02-05 14:14:48 -08001044 # Build the DLC image if the image is outdated or doesn't exist.
Andrew5743d382020-06-16 09:55:04 -07001045 dlc_lib.InstallDlcImages(sysroot=sysroot, dlc_id=dlc_id, board=board)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001046
Andrewc7e1c6b2020-02-27 16:03:53 -08001047 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1048 try:
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001049 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Andrewc7e1c6b2020-02-27 16:03:53 -08001050 except cros_build_lib.RunCommandError as e:
1051 logging.info('Failed to uninstall DLC:%s. Continue anyway.',
1052 e.result.error)
1053 except Exception:
1054 logging.error('Failed to uninstall DLC.')
1055 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001056
Andrewc7e1c6b2020-02-27 16:03:53 -08001057 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1058 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1059 # location.
1060 logging.notice('Deploy the DLC image for %s', dlc_id)
Andrew5743d382020-06-16 09:55:04 -07001061 dlc_img_path_src = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1062 dlc_package, dlc_lib.DLC_IMAGE)
Andrewc7e1c6b2020-02-27 16:03:53 -08001063 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1064 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1065 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1066 # Create directories for DLC images.
1067 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1068 # Copy images to the destination directories.
1069 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
Andrew5743d382020-06-16 09:55:04 -07001070 dlc_lib.DLC_IMAGE),
Andrewc7e1c6b2020-02-27 16:03:53 -08001071 mode='rsync')
Andrew5743d382020-06-16 09:55:04 -07001072 device.run(['cp', os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1073 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE)])
Andrewc7e1c6b2020-02-27 16:03:53 -08001074
1075 # Set the proper perms and ownership so dlcservice can access the image.
1076 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1077 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001078
Andrew67b5fa72020-02-05 14:14:48 -08001079 # Copy metadata to device.
Andrew5743d382020-06-16 09:55:04 -07001080 dest_mata_dir = os.path.join('/', dlc_lib.DLC_META_DIR, dlc_id,
1081 dlc_package)
Andrew67b5fa72020-02-05 14:14:48 -08001082 device.run(['mkdir', '-p', dest_mata_dir])
Andrew5743d382020-06-16 09:55:04 -07001083 src_meta_dir = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1084 dlc_package, dlc_lib.DLC_TMP_META_DIR)
Andrew67b5fa72020-02-05 14:14:48 -08001085 device.CopyToDevice(src_meta_dir + '/',
1086 dest_mata_dir,
1087 mode='rsync',
1088 recursive=True,
1089 remote_sudo=True)
1090
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001091
1092def _GetDLCInfo(device, pkg_path, from_dut):
1093 """Returns information of a DLC given its package path.
1094
1095 Args:
1096 device: commandline.Device object; None to use the default device.
1097 pkg_path: path to the package.
1098 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1099 info from host.
1100
1101 Returns:
1102 A tuple (dlc_id, dlc_package).
1103 """
1104 environment_content = ''
1105 if from_dut:
1106 # On DUT, |pkg_path| is the directory which contains environment file.
1107 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001108 try:
1109 environment_data = device.CatFile(
1110 environment_path, max_size=None, encoding=None)
1111 except remote_access.CatFileError:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001112 # The package is not installed on DUT yet. Skip extracting info.
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001113 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001114 else:
1115 # On host, pkg_path is tbz2 file which contains environment file.
1116 # Extract the metadata of the package file.
1117 data = portage.xpak.tbz2(pkg_path).get_data()
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001118 environment_data = data[_ENVIRONMENT_FILENAME.encode('utf-8')]
1119
1120 # Extract the environment metadata.
1121 environment_content = bz2.decompress(environment_data)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001122
1123 with tempfile.NamedTemporaryFile() as f:
1124 # Dumps content into a file so we can use osutils.SourceEnvironment.
1125 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001126 osutils.WriteFile(path, environment_content, mode='wb')
Andrew67b5fa72020-02-05 14:14:48 -08001127 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE,
1128 _DLC_ENABLED))
1129
1130 dlc_enabled = content.get(_DLC_ENABLED)
1131 if dlc_enabled is not None and (dlc_enabled is False or
1132 str(dlc_enabled) == 'false'):
1133 logging.info('Installing DLC in rootfs.')
1134 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001135 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001136
1137
Gilad Arnolda0a98062015-07-07 08:34:27 -07001138def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1139 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1140 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1141 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001142 """Deploys packages to a device.
1143
1144 Args:
David Pursell2e773382015-04-03 14:30:47 -07001145 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001146 packages: List of packages (strings) to deploy to device.
1147 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001148 emerge: True to emerge package, False to unmerge.
1149 update: Check installed version on device.
1150 deep: Install dependencies also. Implies |update|.
1151 deep_rev: Install reverse dependencies. Implies |deep|.
1152 clean_binpkg: Clean outdated binary packages.
1153 root: Package installation root path.
1154 strip: Run strip_package to filter out preset paths in the package.
1155 emerge_args: Extra arguments to pass to emerge.
1156 ssh_private_key: Path to an SSH private key file; None to use test keys.
1157 ping: True to ping the device before trying to connect.
1158 force: Ignore sanity checks and prompts.
1159 dry_run: Print deployment plan but do not deploy anything.
1160
1161 Raises:
1162 ValueError: Invalid parameter or parameter combination.
1163 DeployError: Unrecoverable failure during deploy.
1164 """
1165 if deep_rev:
1166 deep = True
1167 if deep:
1168 update = True
1169
Gilad Arnolda0a98062015-07-07 08:34:27 -07001170 if not packages:
1171 raise DeployError('No packages provided, nothing to deploy.')
1172
David Pursell9476bf42015-03-30 13:34:27 -07001173 if update and not emerge:
1174 raise ValueError('Cannot update and unmerge.')
1175
David Pursell2e773382015-04-03 14:30:47 -07001176 if device:
1177 hostname, username, port = device.hostname, device.username, device.port
1178 else:
1179 hostname, username, port = None, None, None
1180
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001181 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001182 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001183 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001184 # Somewhat confusing to clobber, but here we are.
1185 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001186 with remote_access.ChromiumOSDeviceHandler(
1187 hostname, port=port, username=username, private_key=ssh_private_key,
1188 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001189 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001190
Gilad Arnolda0a98062015-07-07 08:34:27 -07001191 board = cros_build_lib.GetBoard(device_board=device.board,
1192 override_board=board)
1193 if not force and board != device.board:
1194 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001195 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001196
Mike Frysinger06a51c82021-04-06 11:39:17 -04001197 sysroot = build_target_lib.get_default_sysroot_path(board)
David Pursell9476bf42015-03-30 13:34:27 -07001198
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001199 # Don't bother trying to clean for unmerges. We won't use the local db,
1200 # and it just slows things down for the user.
1201 if emerge and clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001202 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001203 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001204
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001205 # Remount rootfs as writable if necessary.
1206 if not device.MountRootfsReadWrite():
1207 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001208
1209 # Obtain list of packages to upgrade/remove.
1210 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001211 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001212 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001213 if emerge:
1214 action_str = 'emerge'
1215 else:
1216 pkgs.reverse()
1217 action_str = 'unmerge'
1218
1219 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001220 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001221 return
1222
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001223 # Warn when the user installs & didn't `cros workon start`.
1224 if emerge:
Brian Norris2eee8892021-04-06 16:23:23 -07001225 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(use_all=True)
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001226 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1227 for package in listed:
1228 cp = package_info.SplitCPV(package).cp
Brian Norris2eee8892021-04-06 16:23:23 -07001229 if cp in all_workon and cp not in worked_on_cps:
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001230 logging.warning(
1231 'Are you intentionally deploying unmodified packages, or did '
1232 'you forget to run `cros workon --board=$BOARD start %s`?', cp)
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +09001233
Ralph Nathane01ccf12015-04-16 10:40:32 -07001234 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001235 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001236 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001237
1238 if dry_run or not _ConfirmDeploy(num_updates):
1239 return
1240
Ralph Nathane01ccf12015-04-16 10:40:32 -07001241 # Select function (emerge or unmerge) and bind args.
1242 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001243 func = functools.partial(_EmergePackages, pkgs, device, strip,
Andrew06a5f812020-01-23 08:08:32 -08001244 sysroot, root, board, emerge_args)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001245 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001246 func = functools.partial(_UnmergePackages, pkgs, device, root,
1247 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001248
1249 # Call the function with the progress bar or with normal output.
1250 if command.UseProgressBar():
Mike Frysinger63d35512021-01-26 23:16:13 -05001251 op = BrilloDeployOperation(emerge)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001252 op.Run(func, log_level=logging.DEBUG)
1253 else:
1254 func()
David Pursell9476bf42015-03-30 13:34:27 -07001255
Ben Pastene5f03b052019-08-12 18:03:24 -07001256 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001257 if sum(x.count('selinux-policy') for x in pkgs):
1258 logging.warning(
1259 'Deploying SELinux policy will not take effect until reboot. '
Ian Barkley-Yeung6b2d8672020-08-13 18:58:10 -07001260 'SELinux policy is loaded by init. Also, changing the security '
1261 'contexts (labels) of a file will require building a new image '
1262 'and flashing the image onto the device.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001263
Mike Frysinger63d35512021-01-26 23:16:13 -05001264 # This message is read by BrilloDeployOperation.
David Pursell9476bf42015-03-30 13:34:27 -07001265 logging.warning('Please restart any updated services on the device, '
1266 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001267 except Exception:
1268 if lsb_release:
1269 lsb_entries = sorted(lsb_release.items())
1270 logging.info('Following are the LSB version details of the device:\n%s',
1271 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1272 raise