blob: 2c9cb1e11372ee21ca0b0a27cc5580fa659c1ac5 [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
Mike Frysinger06a51c82021-04-06 11:39:17 -040024from chromite.lib import build_target_lib
David Pursell9476bf42015-03-30 13:34:27 -070025from chromite.lib import cros_build_lib
26from chromite.lib import cros_logging as logging
Alex Klein18a60af2020-06-11 12:08:47 -060027from chromite.lib import dlc_lib
Ralph Nathane01ccf12015-04-16 10:40:32 -070028from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070029from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070030from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070031from chromite.lib import remote_access
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +090032from chromite.lib import workon_helper
Alex Klein18a60af2020-06-11 12:08:47 -060033from chromite.lib.parser import package_info
34
David Pursell9476bf42015-03-30 13:34:27 -070035try:
36 import portage
37except ImportError:
38 if cros_build_lib.IsInsideChroot():
39 raise
40
41
Mike Frysinger3f087aa2020-03-20 06:03:16 -040042assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
43
44
David Pursell9476bf42015-03-30 13:34:27 -070045_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
46# This is defined in src/platform/dev/builder.py
47_STRIPPED_PACKAGES_DIR = 'stripped-packages'
48
49_MAX_UPDATES_NUM = 10
50_MAX_UPDATES_WARNING = (
51 'You are about to update a large number of installed packages, which '
52 'might take a long time, fail midway, or leave the target in an '
53 'inconsistent state. It is highly recommended that you flash a new image '
54 'instead.')
55
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070056_DLC_ID = 'DLC_ID'
57_DLC_PACKAGE = 'DLC_PACKAGE'
Andrew67b5fa72020-02-05 14:14:48 -080058_DLC_ENABLED = 'DLC_ENABLED'
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070059_ENVIRONMENT_FILENAME = 'environment.bz2'
60_DLC_INSTALL_ROOT = '/var/cache/dlc'
61
David Pursell9476bf42015-03-30 13:34:27 -070062
63class DeployError(Exception):
64 """Thrown when an unrecoverable error is encountered during deploy."""
65
66
Ralph Nathane01ccf12015-04-16 10:40:32 -070067class BrilloDeployOperation(operation.ProgressBarOperation):
68 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070069 # These two variables are used to validate the output in the VM integration
70 # tests. Changes to the output must be reflected here.
Mike Frysinger63d35512021-01-26 23:16:13 -050071 MERGE_EVENTS = (
72 'Preparing local packages',
73 'NOTICE: Copying binpkgs',
74 'NOTICE: Installing',
75 'been installed.',
76 'Please restart any updated',
77 )
Mike Frysinger22bb5502021-01-29 13:05:46 -050078 UNMERGE_EVENTS = (
79 'NOTICE: Unmerging',
80 'been uninstalled.',
81 'Please restart any updated',
82 )
Ralph Nathane01ccf12015-04-16 10:40:32 -070083
Mike Frysinger63d35512021-01-26 23:16:13 -050084 def __init__(self, emerge):
Ralph Nathane01ccf12015-04-16 10:40:32 -070085 """Construct BrilloDeployOperation object.
86
87 Args:
Ralph Nathane01ccf12015-04-16 10:40:32 -070088 emerge: True if emerge, False is unmerge.
89 """
90 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070091 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070092 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070093 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070094 self._events = self.UNMERGE_EVENTS
Mike Frysinger63d35512021-01-26 23:16:13 -050095 self._total = len(self._events)
Ralph Nathane01ccf12015-04-16 10:40:32 -070096 self._completed = 0
97
Ralph Nathandc14ed92015-04-22 11:17:40 -070098 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070099 """Parse the output of brillo deploy to update a progress bar."""
100 stdout = self._stdout.read()
101 stderr = self._stderr.read()
102 output = stdout + stderr
103 for event in self._events:
104 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -0400105 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700106
107
David Pursell9476bf42015-03-30 13:34:27 -0700108class _InstallPackageScanner(object):
109 """Finds packages that need to be installed on a target device.
110
111 Scans the sysroot bintree, beginning with a user-provided list of packages,
112 to find all packages that need to be installed. If so instructed,
113 transitively scans forward (mandatory) and backward (optional) dependencies
114 as well. A package will be installed if missing on the target (mandatory
115 packages only), or it will be updated if its sysroot version and build time
116 are different from the target. Common usage:
117
118 pkg_scanner = _InstallPackageScanner(sysroot)
119 pkgs = pkg_scanner.Run(...)
120 """
121
122 class VartreeError(Exception):
123 """An error in the processing of the installed packages tree."""
124
125 class BintreeError(Exception):
126 """An error in the processing of the source binpkgs tree."""
127
128 class PkgInfo(object):
129 """A record containing package information."""
130
131 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
132
133 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
134 self.cpv = cpv
135 self.build_time = build_time
136 self.rdeps_raw = rdeps_raw
137 self.rdeps = set() if rdeps is None else rdeps
138 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
139
140 # Python snippet for dumping vartree info on the target. Instantiate using
141 # _GetVartreeSnippet().
142 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700143import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700144import os
145import portage
146
147# Normalize the path to match what portage will index.
148target_root = os.path.normpath('%(root)s')
149if not target_root.endswith('/'):
150 target_root += '/'
151trees = portage.create_trees(target_root=target_root, config_root='/')
152vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700153pkg_info = []
154for cpv in vartree.dbapi.cpv_all():
155 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
156 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
157 pkg_info.append((cpv, slot, rdep_raw, build_time))
158
159print(json.dumps(pkg_info))
160"""
161
162 def __init__(self, sysroot):
163 self.sysroot = sysroot
164 # Members containing the sysroot (binpkg) and target (installed) package DB.
165 self.target_db = None
166 self.binpkgs_db = None
167 # Members for managing the dependency resolution work queue.
168 self.queue = None
169 self.seen = None
170 self.listed = None
171
172 @staticmethod
173 def _GetCP(cpv):
174 """Returns the CP value for a given CPV string."""
Alex Klein9742cb62020-10-12 19:22:10 +0000175 attrs = package_info.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600176 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700177 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600178 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700179
180 @staticmethod
181 def _InDB(cp, slot, db):
182 """Returns whether CP and slot are found in a database (if provided)."""
183 cp_slots = db.get(cp) if db else None
184 return cp_slots is not None and (not slot or slot in cp_slots)
185
186 @staticmethod
187 def _AtomStr(cp, slot):
188 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
189 return '%s:%s' % (cp, slot) if slot else cp
190
191 @classmethod
192 def _GetVartreeSnippet(cls, root='/'):
193 """Returns a code snippet for dumping the vartree on the target.
194
195 Args:
196 root: The installation root.
197
198 Returns:
199 The said code snippet (string) with parameters filled in.
200 """
201 return cls._GET_VARTREE % {'root': root}
202
203 @classmethod
204 def _StripDepAtom(cls, dep_atom, installed_db=None):
205 """Strips a dependency atom and returns a (CP, slot) pair."""
206 # TODO(garnold) This is a gross simplification of ebuild dependency
207 # semantics, stripping and ignoring various qualifiers (versions, slots,
208 # USE flag, negation) and will likely need to be fixed. chromium:447366.
209
210 # Ignore unversioned blockers, leaving them for the user to resolve.
211 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
212 return None, None
213
214 cp = dep_atom
215 slot = None
216 require_installed = False
217
218 # Versioned blockers should be updated, but only if already installed.
219 # These are often used for forcing cascaded updates of multiple packages,
220 # so we're treating them as ordinary constraints with hopes that it'll lead
221 # to the desired result.
222 if cp.startswith('!'):
223 cp = cp.lstrip('!')
224 require_installed = True
225
226 # Remove USE flags.
227 if '[' in cp:
228 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
229
230 # Separate the slot qualifier and strip off subslots.
231 if ':' in cp:
232 cp, slot = cp.split(':')
233 for delim in ('/', '='):
234 slot = slot.split(delim, 1)[0]
235
236 # Strip version wildcards (right), comparators (left).
237 cp = cp.rstrip('*')
238 cp = cp.lstrip('<=>~')
239
240 # Turn into CP form.
241 cp = cls._GetCP(cp)
242
243 if require_installed and not cls._InDB(cp, None, installed_db):
244 return None, None
245
246 return cp, slot
247
248 @classmethod
249 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
250 """Resolves and returns a list of dependencies from a dependency string.
251
252 This parses a dependency string and returns a list of package names and
253 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
254 resolving disjunctive deps, we include all choices that are fully present
255 in |installed_db|. If none is present, we choose an arbitrary one that is
256 available.
257
258 Args:
259 dep_str: A raw dependency string.
260 installed_db: A database of installed packages.
261 avail_db: A database of packages available for installation.
262
263 Returns:
264 A list of pairs (CP, slot).
265
266 Raises:
267 ValueError: the dependencies string is malformed.
268 """
269 def ProcessSubDeps(dep_exp, disjunct):
270 """Parses and processes a dependency (sub)expression."""
271 deps = set()
272 default_deps = set()
273 sub_disjunct = False
274 for dep_sub_exp in dep_exp:
275 sub_deps = set()
276
277 if isinstance(dep_sub_exp, (list, tuple)):
278 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
279 sub_disjunct = False
280 elif sub_disjunct:
281 raise ValueError('Malformed disjunctive operation in deps')
282 elif dep_sub_exp == '||':
283 sub_disjunct = True
284 elif dep_sub_exp.endswith('?'):
285 raise ValueError('Dependencies contain a conditional')
286 else:
287 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
288 if cp:
289 sub_deps = set([(cp, slot)])
290 elif disjunct:
291 raise ValueError('Atom in disjunct ignored')
292
293 # Handle sub-deps of a disjunctive expression.
294 if disjunct:
295 # Make the first available choice the default, for use in case that
296 # no option is installed.
297 if (not default_deps and avail_db is not None and
298 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
299 default_deps = sub_deps
300
301 # If not all sub-deps are installed, then don't consider them.
302 if not all([cls._InDB(cp, slot, installed_db)
303 for cp, slot in sub_deps]):
304 sub_deps = set()
305
306 deps.update(sub_deps)
307
308 return deps or default_deps
309
310 try:
311 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
312 except portage.exception.InvalidDependString as e:
313 raise ValueError('Invalid dep string: %s' % e)
314 except ValueError as e:
315 raise ValueError('%s: %s' % (e, dep_str))
316
317 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
318 installed_db=None):
319 """Returns a database of packages given a list of CPV info.
320
321 Args:
322 cpv_info: A list of tuples containing package CPV and attributes.
323 process_rdeps: Whether to populate forward dependencies.
324 process_rev_rdeps: Whether to populate reverse dependencies.
325 installed_db: A database of installed packages for filtering disjunctive
326 choices against; if None, using own built database.
327
328 Returns:
329 A map from CP values to another dictionary that maps slots to package
330 attribute tuples. Tuples contain a CPV value (string), build time
331 (string), runtime dependencies (set), and reverse dependencies (set,
332 empty if not populated).
333
334 Raises:
335 ValueError: If more than one CPV occupies a single slot.
336 """
337 db = {}
338 logging.debug('Populating package DB...')
339 for cpv, slot, rdeps_raw, build_time in cpv_info:
340 cp = self._GetCP(cpv)
341 cp_slots = db.setdefault(cp, dict())
342 if slot in cp_slots:
343 raise ValueError('More than one package found for %s' %
344 self._AtomStr(cp, slot))
345 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
346 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
347 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
348
349 avail_db = db
350 if installed_db is None:
351 installed_db = db
352 avail_db = None
353
354 # Add approximate forward dependencies.
355 if process_rdeps:
356 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400357 for cp, cp_slots in db.items():
358 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700359 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
360 installed_db, avail_db))
361 logging.debug(' %s (%s) processed rdeps: %s',
362 self._AtomStr(cp, slot), pkg_info.cpv,
363 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
364 for rdep_cp, rdep_slot in pkg_info.rdeps]))
365
366 # Add approximate reverse dependencies (optional).
367 if process_rev_rdeps:
368 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400369 for cp, cp_slots in db.items():
370 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700371 for rdep_cp, rdep_slot in pkg_info.rdeps:
372 to_slots = db.get(rdep_cp)
373 if not to_slots:
374 continue
375
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400376 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700377 if rdep_slot and to_slot != rdep_slot:
378 continue
379 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
380 self._AtomStr(cp, slot), pkg_info.cpv,
381 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
382 to_pkg_info.rev_rdeps.add((cp, slot))
383
384 return db
385
386 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
387 """Initializes a dictionary of packages installed on |device|."""
388 get_vartree_script = self._GetVartreeSnippet(root)
389 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400390 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700391 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700392 except cros_build_lib.RunCommandError as e:
393 logging.error('Cannot get target vartree:\n%s', e.result.error)
394 raise
395
396 try:
397 self.target_db = self._BuildDB(json.loads(result.output),
398 process_rdeps, process_rev_rdeps)
399 except ValueError as e:
400 raise self.VartreeError(str(e))
401
402 def _InitBinpkgDB(self, process_rdeps):
403 """Initializes a dictionary of binary packages for updating the target."""
404 # Get build root trees; portage indexes require a trailing '/'.
405 build_root = os.path.join(self.sysroot, '')
406 trees = portage.create_trees(target_root=build_root, config_root=build_root)
407 bintree = trees[build_root]['bintree']
408 binpkgs_info = []
409 for cpv in bintree.dbapi.cpv_all():
410 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
411 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
412 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
413
414 try:
415 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
416 installed_db=self.target_db)
417 except ValueError as e:
418 raise self.BintreeError(str(e))
419
420 def _InitDepQueue(self):
421 """Initializes the dependency work queue."""
422 self.queue = set()
423 self.seen = {}
424 self.listed = set()
425
426 def _EnqDep(self, dep, listed, optional):
427 """Enqueues a dependency if not seen before or if turned non-optional."""
428 if dep in self.seen and (optional or not self.seen[dep]):
429 return False
430
431 self.queue.add(dep)
432 self.seen[dep] = optional
433 if listed:
434 self.listed.add(dep)
435 return True
436
437 def _DeqDep(self):
438 """Dequeues and returns a dependency, its listed and optional flags.
439
440 This returns listed packages first, if any are present, to ensure that we
441 correctly mark them as such when they are first being processed.
442 """
443 if self.listed:
444 dep = self.listed.pop()
445 self.queue.remove(dep)
446 listed = True
447 else:
448 dep = self.queue.pop()
449 listed = False
450
451 return dep, listed, self.seen[dep]
452
453 def _FindPackageMatches(self, cpv_pattern):
454 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
455
456 This is breaking |cpv_pattern| into its C, P and V components, each of
457 which may or may not be present or contain wildcards. It then scans the
458 binpkgs database to find all atoms that match these components, returning a
459 list of CP and slot qualifier. When the pattern does not specify a version,
460 or when a CP has only one slot in the binpkgs database, we omit the slot
461 qualifier in the result.
462
463 Args:
464 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
465
466 Returns:
467 A list of (CPV, slot) pairs of packages in the binpkgs database that
468 match the pattern.
469 """
Alex Klein9742cb62020-10-12 19:22:10 +0000470 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
David Pursell9476bf42015-03-30 13:34:27 -0700471 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
472 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400473 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700474 if not fnmatch.fnmatchcase(cp, cp_pattern):
475 continue
476
477 # If no version attribute was given or there's only one slot, omit the
478 # slot qualifier.
Alex Klein9742cb62020-10-12 19:22:10 +0000479 if not attrs.version or len(cp_slots) == 1:
David Pursell9476bf42015-03-30 13:34:27 -0700480 matches.append((cp, None))
481 else:
Alex Klein9742cb62020-10-12 19:22:10 +0000482 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400483 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700484 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
485 matches.append((cp, slot))
486
487 return matches
488
489 def _FindPackage(self, pkg):
490 """Returns the (CP, slot) pair for a package matching |pkg|.
491
492 Args:
493 pkg: Path to a binary package or a (partial) package CPV specifier.
494
495 Returns:
496 A (CP, slot) pair for the given package; slot may be None (unspecified).
497
498 Raises:
499 ValueError: if |pkg| is not a binpkg file nor does it match something
500 that's in the bintree.
501 """
502 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
503 package = os.path.basename(os.path.splitext(pkg)[0])
504 category = os.path.basename(os.path.dirname(pkg))
505 return self._GetCP(os.path.join(category, package)), None
506
507 matches = self._FindPackageMatches(pkg)
508 if not matches:
509 raise ValueError('No package found for %s' % pkg)
510
511 idx = 0
512 if len(matches) > 1:
513 # Ask user to pick among multiple matches.
514 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
515 ['%s:%s' % (cp, slot) if slot else cp
516 for cp, slot in matches])
517
518 return matches[idx]
519
520 def _NeedsInstall(self, cpv, slot, build_time, optional):
521 """Returns whether a package needs to be installed on the target.
522
523 Args:
524 cpv: Fully qualified CPV (string) of the package.
525 slot: Slot identifier (string).
526 build_time: The BUILT_TIME value (string) of the binpkg.
527 optional: Whether package is optional on the target.
528
529 Returns:
530 A tuple (install, update) indicating whether to |install| the package and
531 whether it is an |update| to an existing package.
532
533 Raises:
534 ValueError: if slot is not provided.
535 """
536 # If not checking installed packages, always install.
537 if not self.target_db:
538 return True, False
539
540 cp = self._GetCP(cpv)
541 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
542 if target_pkg_info is not None:
543 if cpv != target_pkg_info.cpv:
Alex Klein9742cb62020-10-12 19:22:10 +0000544 attrs = package_info.SplitCPV(cpv)
545 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
David Pursell9476bf42015-03-30 13:34:27 -0700546 logging.debug('Updating %s: version (%s) different on target (%s)',
Alex Klein9742cb62020-10-12 19:22:10 +0000547 cp, attrs.version, target_attrs.version)
David Pursell9476bf42015-03-30 13:34:27 -0700548 return True, True
549
550 if build_time != target_pkg_info.build_time:
551 logging.debug('Updating %s: build time (%s) different on target (%s)',
552 cpv, build_time, target_pkg_info.build_time)
553 return True, True
554
555 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
556 cp, target_pkg_info.cpv, target_pkg_info.build_time)
557 return False, False
558
559 if optional:
560 logging.debug('Not installing %s: missing on target but optional', cp)
561 return False, False
562
563 logging.debug('Installing %s: missing on target and non-optional (%s)',
564 cp, cpv)
565 return True, False
566
567 def _ProcessDeps(self, deps, reverse):
568 """Enqueues dependencies for processing.
569
570 Args:
571 deps: List of dependencies to enqueue.
572 reverse: Whether these are reverse dependencies.
573 """
574 if not deps:
575 return
576
577 logging.debug('Processing %d %s dep(s)...', len(deps),
578 'reverse' if reverse else 'forward')
579 num_already_seen = 0
580 for dep in deps:
581 if self._EnqDep(dep, False, reverse):
582 logging.debug(' Queued dep %s', dep)
583 else:
584 num_already_seen += 1
585
586 if num_already_seen:
587 logging.debug('%d dep(s) already seen', num_already_seen)
588
589 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
590 """Returns a dictionary of packages that need to be installed on the target.
591
592 Args:
593 process_rdeps: Whether to trace forward dependencies.
594 process_rev_rdeps: Whether to trace backward dependencies as well.
595
596 Returns:
597 A dictionary mapping CP values (string) to tuples containing a CPV
598 (string), a slot (string), a boolean indicating whether the package
599 was initially listed in the queue, and a boolean indicating whether this
600 is an update to an existing package.
601 """
602 installs = {}
603 while self.queue:
604 dep, listed, optional = self._DeqDep()
605 cp, required_slot = dep
606 if cp in installs:
607 logging.debug('Already updating %s', cp)
608 continue
609
610 cp_slots = self.binpkgs_db.get(cp, dict())
611 logging.debug('Checking packages matching %s%s%s...', cp,
612 ' (slot: %s)' % required_slot if required_slot else '',
613 ' (optional)' if optional else '')
614 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400615 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700616 if required_slot and slot != required_slot:
617 continue
618
619 num_processed += 1
620 logging.debug(' Checking %s...', pkg_info.cpv)
621
622 install, update = self._NeedsInstall(pkg_info.cpv, slot,
623 pkg_info.build_time, optional)
624 if not install:
625 continue
626
627 installs[cp] = (pkg_info.cpv, slot, listed, update)
628
629 # Add forward and backward runtime dependencies to queue.
630 if process_rdeps:
631 self._ProcessDeps(pkg_info.rdeps, False)
632 if process_rev_rdeps:
633 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
634 if target_pkg_info:
635 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
636
637 if num_processed == 0:
638 logging.warning('No qualified bintree package corresponding to %s', cp)
639
640 return installs
641
642 def _SortInstalls(self, installs):
643 """Returns a sorted list of packages to install.
644
645 Performs a topological sort based on dependencies found in the binary
646 package database.
647
648 Args:
649 installs: Dictionary of packages to install indexed by CP.
650
651 Returns:
652 A list of package CPVs (string).
653
654 Raises:
655 ValueError: If dependency graph contains a cycle.
656 """
657 not_visited = set(installs.keys())
658 curr_path = []
659 sorted_installs = []
660
661 def SortFrom(cp):
662 """Traverses dependencies recursively, emitting nodes in reverse order."""
663 cpv, slot, _, _ = installs[cp]
664 if cpv in curr_path:
665 raise ValueError('Dependencies contain a cycle: %s -> %s' %
666 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
667 curr_path.append(cpv)
668 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
669 if rdep_cp in not_visited:
670 not_visited.remove(rdep_cp)
671 SortFrom(rdep_cp)
672
673 sorted_installs.append(cpv)
674 curr_path.pop()
675
676 # So long as there's more packages, keep expanding dependency paths.
677 while not_visited:
678 SortFrom(not_visited.pop())
679
680 return sorted_installs
681
682 def _EnqListedPkg(self, pkg):
683 """Finds and enqueues a listed package."""
684 cp, slot = self._FindPackage(pkg)
685 if cp not in self.binpkgs_db:
686 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
687 self._EnqDep((cp, slot), True, False)
688
689 def _EnqInstalledPkgs(self):
690 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400691 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700692 target_cp_slots = self.target_db.get(cp)
693 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400694 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700695 if slot in target_cp_slots:
696 self._EnqDep((cp, slot), True, False)
697
698 def Run(self, device, root, listed_pkgs, update, process_rdeps,
699 process_rev_rdeps):
700 """Computes the list of packages that need to be installed on a target.
701
702 Args:
703 device: Target handler object.
704 root: Package installation root.
705 listed_pkgs: Package names/files listed by the user.
706 update: Whether to read the target's installed package database.
707 process_rdeps: Whether to trace forward dependencies.
708 process_rev_rdeps: Whether to trace backward dependencies as well.
709
710 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700711 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
712 list of package CPVs (string) to install on the target in an order that
713 satisfies their inter-dependencies, |listed| the subset that was
714 requested by the user, and |num_updates| the number of packages being
715 installed over preexisting versions. Note that installation order should
716 be reversed for removal, |install_attrs| is a dictionary mapping a package
717 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700718 """
719 if process_rev_rdeps and not process_rdeps:
720 raise ValueError('Must processing forward deps when processing rev deps')
721 if process_rdeps and not update:
722 raise ValueError('Must check installed packages when processing deps')
723
724 if update:
725 logging.info('Initializing target intalled packages database...')
726 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
727
728 logging.info('Initializing binary packages database...')
729 self._InitBinpkgDB(process_rdeps)
730
731 logging.info('Finding listed package(s)...')
732 self._InitDepQueue()
733 for pkg in listed_pkgs:
734 if pkg == '@installed':
735 if not update:
736 raise ValueError(
737 'Must check installed packages when updating all of them.')
738 self._EnqInstalledPkgs()
739 else:
740 self._EnqListedPkg(pkg)
741
742 logging.info('Computing set of packages to install...')
743 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
744
745 num_updates = 0
746 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400747 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700748 if listed:
749 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400750 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700751 num_updates += 1
752
753 logging.info('Processed %d package(s), %d will be installed, %d are '
754 'updating existing packages',
755 len(self.seen), len(installs), num_updates)
756
757 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700758
759 install_attrs = {}
760 for pkg in sorted_installs:
Mike Frysingerada2d1c2020-03-20 05:02:06 -0400761 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700762 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
763 install_attrs[pkg] = {}
764 if dlc_id and dlc_package:
765 install_attrs[pkg][_DLC_ID] = dlc_id
766
767 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700768
769
Mike Frysinger63d35512021-01-26 23:16:13 -0500770def _Emerge(device, pkg_paths, root, extra_args=None):
771 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -0700772
773 Args:
774 device: A ChromiumOSDevice object.
Mike Frysinger63d35512021-01-26 23:16:13 -0500775 pkg_paths: (Local) paths to binary packages.
David Pursell9476bf42015-03-30 13:34:27 -0700776 root: Package installation root path.
777 extra_args: Extra arguments to pass to emerge.
778
779 Raises:
780 DeployError: Unrecoverable error during emerge.
781 """
Mike Frysinger63d35512021-01-26 23:16:13 -0500782 def path_to_name(pkg_path):
783 return os.path.basename(pkg_path)
784 def path_to_category(pkg_path):
785 return os.path.basename(os.path.dirname(pkg_path))
786
787 pkg_names = ', '.join(path_to_name(x) for x in pkg_paths)
788
David Pursell9476bf42015-03-30 13:34:27 -0700789 pkgroot = os.path.join(device.work_dir, 'packages')
Mike Frysinger15a4e012015-05-21 22:18:45 -0400790 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
791 # Clean out the dirs first if we had a previous emerge on the device so as to
792 # free up space for this emerge. The last emerge gets implicitly cleaned up
793 # when the device connection deletes its work_dir.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400794 device.run(
Mike Frysinger63d35512021-01-26 23:16:13 -0500795 f'cd {device.work_dir} && '
796 f'rm -rf packages portage-tmp && '
797 f'mkdir -p portage-tmp packages && '
798 f'cd packages && '
799 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
800 shell=True, remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700801
David Pursell9476bf42015-03-30 13:34:27 -0700802 logging.info('Use portage temp dir %s', portage_tmpdir)
803
Ralph Nathane01ccf12015-04-16 10:40:32 -0700804 # This message is read by BrilloDeployOperation.
Mike Frysinger63d35512021-01-26 23:16:13 -0500805 logging.notice('Copying binpkgs to device.')
806 for pkg_path in pkg_paths:
807 pkg_name = path_to_name(pkg_path)
808 logging.info('Copying %s', pkg_name)
809 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
810 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True,
811 compress=False)
812
813 # This message is read by BrilloDeployOperation.
814 logging.notice('Installing: %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700815
816 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
817 # chromeos-base packages will be skipped due to the configuration
818 # in /etc/protage/make.profile/package.provided. However, there is
819 # a known bug that /usr/local/etc/portage is not setup properly
820 # (crbug.com/312041). This does not affect `cros deploy` because
821 # we do not use the preset PKGDIR.
822 extra_env = {
823 'FEATURES': '-sandbox',
824 'PKGDIR': pkgroot,
825 'PORTAGE_CONFIGROOT': '/usr/local',
826 'PORTAGE_TMPDIR': portage_tmpdir,
827 'PORTDIR': device.work_dir,
828 'CONFIG_PROTECT': '-*',
829 }
Mike Frysinger63d35512021-01-26 23:16:13 -0500830
Alex Kleinaaddc932020-01-30 15:02:24 -0700831 # --ignore-built-slot-operator-deps because we don't rebuild everything.
832 # It can cause errors, but that's expected with cros deploy since it's just a
833 # best effort to prevent developers avoid rebuilding an image every time.
Mike Frysinger63d35512021-01-26 23:16:13 -0500834 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', '--root',
835 root] + [os.path.join(pkgroot, *x.split('/')[-2:]) for x in pkg_paths]
David Pursell9476bf42015-03-30 13:34:27 -0700836 if extra_args:
837 cmd.append(extra_args)
838
Alex Kleinaaddc932020-01-30 15:02:24 -0700839 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
840 'packages built against the old version may not be able to '
841 'load the new .so. This is expected, and you will just need '
842 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700843 try:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400844 result = device.run(cmd, extra_env=extra_env, remote_sudo=True,
845 capture_output=True, debug_level=logging.INFO)
Greg Kerrb96c02c2019-02-08 14:32:41 -0800846
847 pattern = ('A requested package will not be merged because '
848 'it is listed in package.provided')
849 output = result.error.replace('\n', ' ').replace('\r', '')
850 if pattern in output:
851 error = ('Package failed to emerge: %s\n'
852 'Remove %s from /etc/portage/make.profile/'
853 'package.provided/chromeos-base.packages\n'
854 '(also see crbug.com/920140 for more context)\n'
855 % (pattern, pkg_name))
856 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700857 except Exception:
Mike Frysinger63d35512021-01-26 23:16:13 -0500858 logging.error('Failed to emerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700859 raise
860 else:
Mike Frysinger63d35512021-01-26 23:16:13 -0500861 # This message is read by BrilloDeployOperation.
862 logging.notice('Packages have been installed.')
David Pursell9476bf42015-03-30 13:34:27 -0700863
864
Qijiang Fand5958192019-07-26 12:32:36 +0900865def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800866 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900867
868 This reads the tarball from pkgpath, and calls restorecon on device to
869 restore SELinux context for files listed in the tarball, assuming those files
870 are installed to /
871
872 Args:
873 device: a ChromiumOSDevice object
874 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900875 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900876 """
Qijiang Fan8a945032019-04-25 20:53:29 +0900877 pkgroot = os.path.join(device.work_dir, 'packages')
878 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
879 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
880 # Testing shows restorecon splits on newlines instead of spaces.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400881 device.run(
Qijiang Fand5958192019-07-26 12:32:36 +0900882 ['cd', root, '&&',
883 'tar', 'tf', pkgpath_device, '|',
884 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900885 remote_sudo=True)
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900886
887
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700888def _GetPackagesByCPV(cpvs, strip, sysroot):
889 """Returns paths to binary packages corresponding to |cpvs|.
890
891 Args:
Alex Klein9742cb62020-10-12 19:22:10 +0000892 cpvs: List of CPV components given by package_info.SplitCPV().
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700893 strip: True to run strip_package.
894 sysroot: Sysroot path.
895
896 Returns:
897 List of paths corresponding to |cpvs|.
898
899 Raises:
900 DeployError: If a package is missing.
901 """
902 packages_dir = None
903 if strip:
904 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400905 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700906 ['strip_package', '--sysroot', sysroot] +
Alex Klein9742cb62020-10-12 19:22:10 +0000907 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700908 packages_dir = _STRIPPED_PACKAGES_DIR
909 except cros_build_lib.RunCommandError:
910 logging.error('Cannot strip packages %s',
Alex Klein9742cb62020-10-12 19:22:10 +0000911 ' '.join([str(cpv) for cpv in cpvs]))
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700912 raise
913
914 paths = []
915 for cpv in cpvs:
916 path = portage_util.GetBinaryPackagePath(
917 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
918 packages_dir=packages_dir)
919 if not path:
920 raise DeployError('Missing package %s.' % cpv)
921 paths.append(path)
922
923 return paths
924
925
926def _GetPackagesPaths(pkgs, strip, sysroot):
927 """Returns paths to binary |pkgs|.
928
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700929 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700930 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700931 strip: Whether or not to run strip_package for CPV packages.
932 sysroot: The sysroot path.
933
934 Returns:
935 List of paths corresponding to |pkgs|.
936 """
Alex Klein9742cb62020-10-12 19:22:10 +0000937 cpvs = [package_info.SplitCPV(p) for p in pkgs]
Ned Nguyend0db4072019-02-22 14:19:21 -0700938 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700939
940
Mike Frysinger22bb5502021-01-29 13:05:46 -0500941def _Unmerge(device, pkgs, root):
942 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -0700943
944 Args:
945 device: A RemoteDevice object.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500946 pkgs: Package names.
David Pursell9476bf42015-03-30 13:34:27 -0700947 root: Package installation root path.
948 """
Mike Frysinger22bb5502021-01-29 13:05:46 -0500949 pkg_names = ', '.join(os.path.basename(x) for x in pkgs)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700950 # This message is read by BrilloDeployOperation.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500951 logging.notice('Unmerging %s.', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700952 cmd = ['qmerge', '--yes']
953 # Check if qmerge is available on the device. If not, use emerge.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400954 if device.run(['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700955 cmd = ['emerge']
956
Mike Frysinger22bb5502021-01-29 13:05:46 -0500957 cmd += ['--unmerge', '--root', root]
958 cmd.extend('f={x}' for x in pkgs)
David Pursell9476bf42015-03-30 13:34:27 -0700959 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700960 # Always showing the emerge output for clarity.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400961 device.run(cmd, capture_output=False, remote_sudo=True,
962 debug_level=logging.INFO)
David Pursell9476bf42015-03-30 13:34:27 -0700963 except Exception:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500964 logging.error('Failed to unmerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700965 raise
966 else:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500967 # This message is read by BrilloDeployOperation.
968 logging.notice('Packages have been uninstalled.')
David Pursell9476bf42015-03-30 13:34:27 -0700969
970
971def _ConfirmDeploy(num_updates):
972 """Returns whether we can continue deployment."""
973 if num_updates > _MAX_UPDATES_NUM:
974 logging.warning(_MAX_UPDATES_WARNING)
975 return cros_build_lib.BooleanPrompt(default=False)
976
977 return True
978
979
Andrew06a5f812020-01-23 08:08:32 -0800980def _EmergePackages(pkgs, device, strip, sysroot, root, board, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800981 """Call _Emerge for each package in pkgs."""
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -0500982 if device.IsSELinuxAvailable():
983 enforced = device.IsSELinuxEnforced()
984 if enforced:
985 device.run(['setenforce', '0'])
986 else:
987 enforced = False
988
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700989 dlc_deployed = False
Mike Frysinger63d35512021-01-26 23:16:13 -0500990 # This message is read by BrilloDeployOperation.
991 logging.info('Preparing local packages for transfer.')
992 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
993 # Install all the packages in one pass so inter-package blockers work.
994 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
995 logging.info('Updating SELinux settings & DLC images.')
996 for pkg_path in pkg_paths:
Ben Pastene5f03b052019-08-12 18:03:24 -0700997 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900998 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800999
1000 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
1001 if dlc_id and dlc_package:
Andrew06a5f812020-01-23 08:08:32 -08001002 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001003 dlc_deployed = True
Mike Frysinger5f4c2742021-02-08 14:37:23 -05001004
1005 if dlc_deployed:
1006 # Clean up empty directories created by emerging DLCs.
1007 device.run(['test', '-d', '/build/rootfs', '&&', 'rmdir',
1008 '--ignore-fail-on-non-empty', '/build/rootfs', '/build'],
1009 check=False)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001010
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001011 if enforced:
1012 device.run(['setenforce', '1'])
1013
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001014 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1015 # we installed new DLC images).
1016 if dlc_deployed:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001017 device.run(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001018
1019
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001020def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001021 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001022 dlc_uninstalled = False
Mike Frysinger22bb5502021-01-29 13:05:46 -05001023 _Unmerge(device, pkgs, root)
1024 logging.info('Cleaning up DLC images.')
Ralph Nathane01ccf12015-04-16 10:40:32 -07001025 for pkg in pkgs:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001026 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1027 dlc_uninstalled = True
1028
1029 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1030 # uninstalled DLC images).
1031 if dlc_uninstalled:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001032 device.run(['restart', 'dlcservice'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001033
1034
1035def _UninstallDLCImage(device, pkg_attrs):
1036 """Uninstall a DLC image."""
1037 if _DLC_ID in pkg_attrs:
1038 dlc_id = pkg_attrs[_DLC_ID]
1039 logging.notice('Uninstalling DLC image for %s', dlc_id)
1040
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001041 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001042 return True
1043 else:
1044 logging.debug('DLC_ID not found in package')
1045 return False
1046
1047
Andrew06a5f812020-01-23 08:08:32 -08001048def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001049 """Deploy (install and mount) a DLC image."""
Andrew67b5fa72020-02-05 14:14:48 -08001050 # Build the DLC image if the image is outdated or doesn't exist.
Andrew5743d382020-06-16 09:55:04 -07001051 dlc_lib.InstallDlcImages(sysroot=sysroot, dlc_id=dlc_id, board=board)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001052
Andrewc7e1c6b2020-02-27 16:03:53 -08001053 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1054 try:
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001055 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Andrewc7e1c6b2020-02-27 16:03:53 -08001056 except cros_build_lib.RunCommandError as e:
1057 logging.info('Failed to uninstall DLC:%s. Continue anyway.',
1058 e.result.error)
1059 except Exception:
1060 logging.error('Failed to uninstall DLC.')
1061 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001062
Andrewc7e1c6b2020-02-27 16:03:53 -08001063 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1064 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1065 # location.
1066 logging.notice('Deploy the DLC image for %s', dlc_id)
Andrew5743d382020-06-16 09:55:04 -07001067 dlc_img_path_src = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1068 dlc_package, dlc_lib.DLC_IMAGE)
Andrewc7e1c6b2020-02-27 16:03:53 -08001069 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1070 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1071 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1072 # Create directories for DLC images.
1073 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1074 # Copy images to the destination directories.
1075 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
Andrew5743d382020-06-16 09:55:04 -07001076 dlc_lib.DLC_IMAGE),
Andrewc7e1c6b2020-02-27 16:03:53 -08001077 mode='rsync')
Andrew5743d382020-06-16 09:55:04 -07001078 device.run(['cp', os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1079 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE)])
Andrewc7e1c6b2020-02-27 16:03:53 -08001080
1081 # Set the proper perms and ownership so dlcservice can access the image.
1082 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1083 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001084
Andrew67b5fa72020-02-05 14:14:48 -08001085 # Copy metadata to device.
Andrew5743d382020-06-16 09:55:04 -07001086 dest_mata_dir = os.path.join('/', dlc_lib.DLC_META_DIR, dlc_id,
1087 dlc_package)
Andrew67b5fa72020-02-05 14:14:48 -08001088 device.run(['mkdir', '-p', dest_mata_dir])
Andrew5743d382020-06-16 09:55:04 -07001089 src_meta_dir = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1090 dlc_package, dlc_lib.DLC_TMP_META_DIR)
Andrew67b5fa72020-02-05 14:14:48 -08001091 device.CopyToDevice(src_meta_dir + '/',
1092 dest_mata_dir,
1093 mode='rsync',
1094 recursive=True,
1095 remote_sudo=True)
1096
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001097
1098def _GetDLCInfo(device, pkg_path, from_dut):
1099 """Returns information of a DLC given its package path.
1100
1101 Args:
1102 device: commandline.Device object; None to use the default device.
1103 pkg_path: path to the package.
1104 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1105 info from host.
1106
1107 Returns:
1108 A tuple (dlc_id, dlc_package).
1109 """
1110 environment_content = ''
1111 if from_dut:
1112 # On DUT, |pkg_path| is the directory which contains environment file.
1113 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001114 try:
1115 environment_data = device.CatFile(
1116 environment_path, max_size=None, encoding=None)
1117 except remote_access.CatFileError:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001118 # The package is not installed on DUT yet. Skip extracting info.
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001119 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001120 else:
1121 # On host, pkg_path is tbz2 file which contains environment file.
1122 # Extract the metadata of the package file.
1123 data = portage.xpak.tbz2(pkg_path).get_data()
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001124 environment_data = data[_ENVIRONMENT_FILENAME.encode('utf-8')]
1125
1126 # Extract the environment metadata.
1127 environment_content = bz2.decompress(environment_data)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001128
1129 with tempfile.NamedTemporaryFile() as f:
1130 # Dumps content into a file so we can use osutils.SourceEnvironment.
1131 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001132 osutils.WriteFile(path, environment_content, mode='wb')
Andrew67b5fa72020-02-05 14:14:48 -08001133 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE,
1134 _DLC_ENABLED))
1135
1136 dlc_enabled = content.get(_DLC_ENABLED)
1137 if dlc_enabled is not None and (dlc_enabled is False or
1138 str(dlc_enabled) == 'false'):
1139 logging.info('Installing DLC in rootfs.')
1140 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001141 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001142
1143
Gilad Arnolda0a98062015-07-07 08:34:27 -07001144def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1145 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1146 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1147 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001148 """Deploys packages to a device.
1149
1150 Args:
David Pursell2e773382015-04-03 14:30:47 -07001151 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001152 packages: List of packages (strings) to deploy to device.
1153 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001154 emerge: True to emerge package, False to unmerge.
1155 update: Check installed version on device.
1156 deep: Install dependencies also. Implies |update|.
1157 deep_rev: Install reverse dependencies. Implies |deep|.
1158 clean_binpkg: Clean outdated binary packages.
1159 root: Package installation root path.
1160 strip: Run strip_package to filter out preset paths in the package.
1161 emerge_args: Extra arguments to pass to emerge.
1162 ssh_private_key: Path to an SSH private key file; None to use test keys.
1163 ping: True to ping the device before trying to connect.
1164 force: Ignore sanity checks and prompts.
1165 dry_run: Print deployment plan but do not deploy anything.
1166
1167 Raises:
1168 ValueError: Invalid parameter or parameter combination.
1169 DeployError: Unrecoverable failure during deploy.
1170 """
1171 if deep_rev:
1172 deep = True
1173 if deep:
1174 update = True
1175
Gilad Arnolda0a98062015-07-07 08:34:27 -07001176 if not packages:
1177 raise DeployError('No packages provided, nothing to deploy.')
1178
David Pursell9476bf42015-03-30 13:34:27 -07001179 if update and not emerge:
1180 raise ValueError('Cannot update and unmerge.')
1181
David Pursell2e773382015-04-03 14:30:47 -07001182 if device:
1183 hostname, username, port = device.hostname, device.username, device.port
1184 else:
1185 hostname, username, port = None, None, None
1186
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001187 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001188 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001189 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001190 # Somewhat confusing to clobber, but here we are.
1191 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001192 with remote_access.ChromiumOSDeviceHandler(
1193 hostname, port=port, username=username, private_key=ssh_private_key,
1194 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001195 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001196
Gilad Arnolda0a98062015-07-07 08:34:27 -07001197 board = cros_build_lib.GetBoard(device_board=device.board,
1198 override_board=board)
1199 if not force and board != device.board:
1200 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001201 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001202
Mike Frysinger06a51c82021-04-06 11:39:17 -04001203 sysroot = build_target_lib.get_default_sysroot_path(board)
David Pursell9476bf42015-03-30 13:34:27 -07001204
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001205 # Don't bother trying to clean for unmerges. We won't use the local db,
1206 # and it just slows things down for the user.
1207 if emerge and clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001208 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001209 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001210
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001211 # Remount rootfs as writable if necessary.
1212 if not device.MountRootfsReadWrite():
1213 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001214
1215 # Obtain list of packages to upgrade/remove.
1216 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001217 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001218 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001219 if emerge:
1220 action_str = 'emerge'
1221 else:
1222 pkgs.reverse()
1223 action_str = 'unmerge'
1224
1225 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001226 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001227 return
1228
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001229 # Warn when the user installs & didn't `cros workon start`.
1230 if emerge:
Brian Norris2eee8892021-04-06 16:23:23 -07001231 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(use_all=True)
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001232 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1233 for package in listed:
1234 cp = package_info.SplitCPV(package).cp
Brian Norris2eee8892021-04-06 16:23:23 -07001235 if cp in all_workon and cp not in worked_on_cps:
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001236 logging.warning(
1237 'Are you intentionally deploying unmodified packages, or did '
1238 'you forget to run `cros workon --board=$BOARD start %s`?', cp)
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +09001239
Ralph Nathane01ccf12015-04-16 10:40:32 -07001240 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001241 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001242 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001243
1244 if dry_run or not _ConfirmDeploy(num_updates):
1245 return
1246
Ralph Nathane01ccf12015-04-16 10:40:32 -07001247 # Select function (emerge or unmerge) and bind args.
1248 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001249 func = functools.partial(_EmergePackages, pkgs, device, strip,
Andrew06a5f812020-01-23 08:08:32 -08001250 sysroot, root, board, emerge_args)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001251 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001252 func = functools.partial(_UnmergePackages, pkgs, device, root,
1253 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001254
1255 # Call the function with the progress bar or with normal output.
1256 if command.UseProgressBar():
Mike Frysinger63d35512021-01-26 23:16:13 -05001257 op = BrilloDeployOperation(emerge)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001258 op.Run(func, log_level=logging.DEBUG)
1259 else:
1260 func()
David Pursell9476bf42015-03-30 13:34:27 -07001261
Ben Pastene5f03b052019-08-12 18:03:24 -07001262 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001263 if sum(x.count('selinux-policy') for x in pkgs):
1264 logging.warning(
1265 'Deploying SELinux policy will not take effect until reboot. '
Ian Barkley-Yeung6b2d8672020-08-13 18:58:10 -07001266 'SELinux policy is loaded by init. Also, changing the security '
1267 'contexts (labels) of a file will require building a new image '
1268 'and flashing the image onto the device.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001269
Mike Frysinger63d35512021-01-26 23:16:13 -05001270 # This message is read by BrilloDeployOperation.
David Pursell9476bf42015-03-30 13:34:27 -07001271 logging.warning('Please restart any updated services on the device, '
1272 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001273 except Exception:
1274 if lsb_release:
1275 lsb_entries = sorted(lsb_release.items())
1276 logging.info('Following are the LSB version details of the device:\n%s',
1277 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1278 raise