blob: 058bcfa3535e3d12000a00b1b325e577b3b70a78 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
David Pursell9476bf42015-03-30 13:34:27 -07002# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Alex Kleinaaddc932020-01-30 15:02:24 -07006"""Deploy packages onto a target device.
7
8Integration tests for this file can be found at cli/cros/tests/cros_vm_tests.py.
9See that file for more information.
10"""
David Pursell9476bf42015-03-30 13:34:27 -070011
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040012from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -070013from __future__ import print_function
14
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070015import bz2
David Pursell9476bf42015-03-30 13:34:27 -070016import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070017import functools
David Pursell9476bf42015-03-30 13:34:27 -070018import json
19import os
Mike Frysinger3f087aa2020-03-20 06:03:16 -040020import sys
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070021import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070022
Ralph Nathane01ccf12015-04-16 10:40:32 -070023from chromite.cli import command
David Pursell9476bf42015-03-30 13:34:27 -070024from chromite.lib import cros_build_lib
25from chromite.lib import cros_logging as logging
Alex Klein18a60af2020-06-11 12:08:47 -060026from chromite.lib import dlc_lib
Ralph Nathane01ccf12015-04-16 10:40:32 -070027from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070028from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070029from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070030from chromite.lib import remote_access
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +090031from chromite.lib import workon_helper
Alex Klein18a60af2020-06-11 12:08:47 -060032from chromite.lib.parser import package_info
33
David Pursell9476bf42015-03-30 13:34:27 -070034try:
35 import portage
36except ImportError:
37 if cros_build_lib.IsInsideChroot():
38 raise
39
40
Mike Frysinger3f087aa2020-03-20 06:03:16 -040041assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
42
43
David Pursell9476bf42015-03-30 13:34:27 -070044_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
45# This is defined in src/platform/dev/builder.py
46_STRIPPED_PACKAGES_DIR = 'stripped-packages'
47
48_MAX_UPDATES_NUM = 10
49_MAX_UPDATES_WARNING = (
50 'You are about to update a large number of installed packages, which '
51 'might take a long time, fail midway, or leave the target in an '
52 'inconsistent state. It is highly recommended that you flash a new image '
53 'instead.')
54
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070055_DLC_ID = 'DLC_ID'
56_DLC_PACKAGE = 'DLC_PACKAGE'
Andrew67b5fa72020-02-05 14:14:48 -080057_DLC_ENABLED = 'DLC_ENABLED'
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070058_ENVIRONMENT_FILENAME = 'environment.bz2'
59_DLC_INSTALL_ROOT = '/var/cache/dlc'
60
David Pursell9476bf42015-03-30 13:34:27 -070061
62class DeployError(Exception):
63 """Thrown when an unrecoverable error is encountered during deploy."""
64
65
Ralph Nathane01ccf12015-04-16 10:40:32 -070066class BrilloDeployOperation(operation.ProgressBarOperation):
67 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070068 # These two variables are used to validate the output in the VM integration
69 # tests. Changes to the output must be reflected here.
Mike Frysinger63d35512021-01-26 23:16:13 -050070 MERGE_EVENTS = (
71 'Preparing local packages',
72 'NOTICE: Copying binpkgs',
73 'NOTICE: Installing',
74 'been installed.',
75 'Please restart any updated',
76 )
Mike Frysinger22bb5502021-01-29 13:05:46 -050077 UNMERGE_EVENTS = (
78 'NOTICE: Unmerging',
79 'been uninstalled.',
80 'Please restart any updated',
81 )
Ralph Nathane01ccf12015-04-16 10:40:32 -070082
Mike Frysinger63d35512021-01-26 23:16:13 -050083 def __init__(self, emerge):
Ralph Nathane01ccf12015-04-16 10:40:32 -070084 """Construct BrilloDeployOperation object.
85
86 Args:
Ralph Nathane01ccf12015-04-16 10:40:32 -070087 emerge: True if emerge, False is unmerge.
88 """
89 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070090 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070091 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070092 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070093 self._events = self.UNMERGE_EVENTS
Mike Frysinger63d35512021-01-26 23:16:13 -050094 self._total = len(self._events)
Ralph Nathane01ccf12015-04-16 10:40:32 -070095 self._completed = 0
96
Ralph Nathandc14ed92015-04-22 11:17:40 -070097 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070098 """Parse the output of brillo deploy to update a progress bar."""
99 stdout = self._stdout.read()
100 stderr = self._stderr.read()
101 output = stdout + stderr
102 for event in self._events:
103 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -0400104 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700105
106
David Pursell9476bf42015-03-30 13:34:27 -0700107class _InstallPackageScanner(object):
108 """Finds packages that need to be installed on a target device.
109
110 Scans the sysroot bintree, beginning with a user-provided list of packages,
111 to find all packages that need to be installed. If so instructed,
112 transitively scans forward (mandatory) and backward (optional) dependencies
113 as well. A package will be installed if missing on the target (mandatory
114 packages only), or it will be updated if its sysroot version and build time
115 are different from the target. Common usage:
116
117 pkg_scanner = _InstallPackageScanner(sysroot)
118 pkgs = pkg_scanner.Run(...)
119 """
120
121 class VartreeError(Exception):
122 """An error in the processing of the installed packages tree."""
123
124 class BintreeError(Exception):
125 """An error in the processing of the source binpkgs tree."""
126
127 class PkgInfo(object):
128 """A record containing package information."""
129
130 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
131
132 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
133 self.cpv = cpv
134 self.build_time = build_time
135 self.rdeps_raw = rdeps_raw
136 self.rdeps = set() if rdeps is None else rdeps
137 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
138
139 # Python snippet for dumping vartree info on the target. Instantiate using
140 # _GetVartreeSnippet().
141 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700142import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700143import os
144import portage
145
146# Normalize the path to match what portage will index.
147target_root = os.path.normpath('%(root)s')
148if not target_root.endswith('/'):
149 target_root += '/'
150trees = portage.create_trees(target_root=target_root, config_root='/')
151vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700152pkg_info = []
153for cpv in vartree.dbapi.cpv_all():
154 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
155 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
156 pkg_info.append((cpv, slot, rdep_raw, build_time))
157
158print(json.dumps(pkg_info))
159"""
160
161 def __init__(self, sysroot):
162 self.sysroot = sysroot
163 # Members containing the sysroot (binpkg) and target (installed) package DB.
164 self.target_db = None
165 self.binpkgs_db = None
166 # Members for managing the dependency resolution work queue.
167 self.queue = None
168 self.seen = None
169 self.listed = None
170
171 @staticmethod
172 def _GetCP(cpv):
173 """Returns the CP value for a given CPV string."""
Alex Klein9742cb62020-10-12 19:22:10 +0000174 attrs = package_info.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600175 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700176 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600177 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700178
179 @staticmethod
180 def _InDB(cp, slot, db):
181 """Returns whether CP and slot are found in a database (if provided)."""
182 cp_slots = db.get(cp) if db else None
183 return cp_slots is not None and (not slot or slot in cp_slots)
184
185 @staticmethod
186 def _AtomStr(cp, slot):
187 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
188 return '%s:%s' % (cp, slot) if slot else cp
189
190 @classmethod
191 def _GetVartreeSnippet(cls, root='/'):
192 """Returns a code snippet for dumping the vartree on the target.
193
194 Args:
195 root: The installation root.
196
197 Returns:
198 The said code snippet (string) with parameters filled in.
199 """
200 return cls._GET_VARTREE % {'root': root}
201
202 @classmethod
203 def _StripDepAtom(cls, dep_atom, installed_db=None):
204 """Strips a dependency atom and returns a (CP, slot) pair."""
205 # TODO(garnold) This is a gross simplification of ebuild dependency
206 # semantics, stripping and ignoring various qualifiers (versions, slots,
207 # USE flag, negation) and will likely need to be fixed. chromium:447366.
208
209 # Ignore unversioned blockers, leaving them for the user to resolve.
210 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
211 return None, None
212
213 cp = dep_atom
214 slot = None
215 require_installed = False
216
217 # Versioned blockers should be updated, but only if already installed.
218 # These are often used for forcing cascaded updates of multiple packages,
219 # so we're treating them as ordinary constraints with hopes that it'll lead
220 # to the desired result.
221 if cp.startswith('!'):
222 cp = cp.lstrip('!')
223 require_installed = True
224
225 # Remove USE flags.
226 if '[' in cp:
227 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
228
229 # Separate the slot qualifier and strip off subslots.
230 if ':' in cp:
231 cp, slot = cp.split(':')
232 for delim in ('/', '='):
233 slot = slot.split(delim, 1)[0]
234
235 # Strip version wildcards (right), comparators (left).
236 cp = cp.rstrip('*')
237 cp = cp.lstrip('<=>~')
238
239 # Turn into CP form.
240 cp = cls._GetCP(cp)
241
242 if require_installed and not cls._InDB(cp, None, installed_db):
243 return None, None
244
245 return cp, slot
246
247 @classmethod
248 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
249 """Resolves and returns a list of dependencies from a dependency string.
250
251 This parses a dependency string and returns a list of package names and
252 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
253 resolving disjunctive deps, we include all choices that are fully present
254 in |installed_db|. If none is present, we choose an arbitrary one that is
255 available.
256
257 Args:
258 dep_str: A raw dependency string.
259 installed_db: A database of installed packages.
260 avail_db: A database of packages available for installation.
261
262 Returns:
263 A list of pairs (CP, slot).
264
265 Raises:
266 ValueError: the dependencies string is malformed.
267 """
268 def ProcessSubDeps(dep_exp, disjunct):
269 """Parses and processes a dependency (sub)expression."""
270 deps = set()
271 default_deps = set()
272 sub_disjunct = False
273 for dep_sub_exp in dep_exp:
274 sub_deps = set()
275
276 if isinstance(dep_sub_exp, (list, tuple)):
277 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
278 sub_disjunct = False
279 elif sub_disjunct:
280 raise ValueError('Malformed disjunctive operation in deps')
281 elif dep_sub_exp == '||':
282 sub_disjunct = True
283 elif dep_sub_exp.endswith('?'):
284 raise ValueError('Dependencies contain a conditional')
285 else:
286 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
287 if cp:
288 sub_deps = set([(cp, slot)])
289 elif disjunct:
290 raise ValueError('Atom in disjunct ignored')
291
292 # Handle sub-deps of a disjunctive expression.
293 if disjunct:
294 # Make the first available choice the default, for use in case that
295 # no option is installed.
296 if (not default_deps and avail_db is not None and
297 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
298 default_deps = sub_deps
299
300 # If not all sub-deps are installed, then don't consider them.
301 if not all([cls._InDB(cp, slot, installed_db)
302 for cp, slot in sub_deps]):
303 sub_deps = set()
304
305 deps.update(sub_deps)
306
307 return deps or default_deps
308
309 try:
310 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
311 except portage.exception.InvalidDependString as e:
312 raise ValueError('Invalid dep string: %s' % e)
313 except ValueError as e:
314 raise ValueError('%s: %s' % (e, dep_str))
315
316 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
317 installed_db=None):
318 """Returns a database of packages given a list of CPV info.
319
320 Args:
321 cpv_info: A list of tuples containing package CPV and attributes.
322 process_rdeps: Whether to populate forward dependencies.
323 process_rev_rdeps: Whether to populate reverse dependencies.
324 installed_db: A database of installed packages for filtering disjunctive
325 choices against; if None, using own built database.
326
327 Returns:
328 A map from CP values to another dictionary that maps slots to package
329 attribute tuples. Tuples contain a CPV value (string), build time
330 (string), runtime dependencies (set), and reverse dependencies (set,
331 empty if not populated).
332
333 Raises:
334 ValueError: If more than one CPV occupies a single slot.
335 """
336 db = {}
337 logging.debug('Populating package DB...')
338 for cpv, slot, rdeps_raw, build_time in cpv_info:
339 cp = self._GetCP(cpv)
340 cp_slots = db.setdefault(cp, dict())
341 if slot in cp_slots:
342 raise ValueError('More than one package found for %s' %
343 self._AtomStr(cp, slot))
344 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
345 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
346 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
347
348 avail_db = db
349 if installed_db is None:
350 installed_db = db
351 avail_db = None
352
353 # Add approximate forward dependencies.
354 if process_rdeps:
355 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400356 for cp, cp_slots in db.items():
357 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700358 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
359 installed_db, avail_db))
360 logging.debug(' %s (%s) processed rdeps: %s',
361 self._AtomStr(cp, slot), pkg_info.cpv,
362 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
363 for rdep_cp, rdep_slot in pkg_info.rdeps]))
364
365 # Add approximate reverse dependencies (optional).
366 if process_rev_rdeps:
367 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400368 for cp, cp_slots in db.items():
369 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700370 for rdep_cp, rdep_slot in pkg_info.rdeps:
371 to_slots = db.get(rdep_cp)
372 if not to_slots:
373 continue
374
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400375 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700376 if rdep_slot and to_slot != rdep_slot:
377 continue
378 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
379 self._AtomStr(cp, slot), pkg_info.cpv,
380 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
381 to_pkg_info.rev_rdeps.add((cp, slot))
382
383 return db
384
385 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
386 """Initializes a dictionary of packages installed on |device|."""
387 get_vartree_script = self._GetVartreeSnippet(root)
388 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400389 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700390 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700391 except cros_build_lib.RunCommandError as e:
392 logging.error('Cannot get target vartree:\n%s', e.result.error)
393 raise
394
395 try:
396 self.target_db = self._BuildDB(json.loads(result.output),
397 process_rdeps, process_rev_rdeps)
398 except ValueError as e:
399 raise self.VartreeError(str(e))
400
401 def _InitBinpkgDB(self, process_rdeps):
402 """Initializes a dictionary of binary packages for updating the target."""
403 # Get build root trees; portage indexes require a trailing '/'.
404 build_root = os.path.join(self.sysroot, '')
405 trees = portage.create_trees(target_root=build_root, config_root=build_root)
406 bintree = trees[build_root]['bintree']
407 binpkgs_info = []
408 for cpv in bintree.dbapi.cpv_all():
409 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
410 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
411 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
412
413 try:
414 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
415 installed_db=self.target_db)
416 except ValueError as e:
417 raise self.BintreeError(str(e))
418
419 def _InitDepQueue(self):
420 """Initializes the dependency work queue."""
421 self.queue = set()
422 self.seen = {}
423 self.listed = set()
424
425 def _EnqDep(self, dep, listed, optional):
426 """Enqueues a dependency if not seen before or if turned non-optional."""
427 if dep in self.seen and (optional or not self.seen[dep]):
428 return False
429
430 self.queue.add(dep)
431 self.seen[dep] = optional
432 if listed:
433 self.listed.add(dep)
434 return True
435
436 def _DeqDep(self):
437 """Dequeues and returns a dependency, its listed and optional flags.
438
439 This returns listed packages first, if any are present, to ensure that we
440 correctly mark them as such when they are first being processed.
441 """
442 if self.listed:
443 dep = self.listed.pop()
444 self.queue.remove(dep)
445 listed = True
446 else:
447 dep = self.queue.pop()
448 listed = False
449
450 return dep, listed, self.seen[dep]
451
452 def _FindPackageMatches(self, cpv_pattern):
453 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
454
455 This is breaking |cpv_pattern| into its C, P and V components, each of
456 which may or may not be present or contain wildcards. It then scans the
457 binpkgs database to find all atoms that match these components, returning a
458 list of CP and slot qualifier. When the pattern does not specify a version,
459 or when a CP has only one slot in the binpkgs database, we omit the slot
460 qualifier in the result.
461
462 Args:
463 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
464
465 Returns:
466 A list of (CPV, slot) pairs of packages in the binpkgs database that
467 match the pattern.
468 """
Alex Klein9742cb62020-10-12 19:22:10 +0000469 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
David Pursell9476bf42015-03-30 13:34:27 -0700470 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
471 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400472 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700473 if not fnmatch.fnmatchcase(cp, cp_pattern):
474 continue
475
476 # If no version attribute was given or there's only one slot, omit the
477 # slot qualifier.
Alex Klein9742cb62020-10-12 19:22:10 +0000478 if not attrs.version or len(cp_slots) == 1:
David Pursell9476bf42015-03-30 13:34:27 -0700479 matches.append((cp, None))
480 else:
Alex Klein9742cb62020-10-12 19:22:10 +0000481 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400482 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700483 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
484 matches.append((cp, slot))
485
486 return matches
487
488 def _FindPackage(self, pkg):
489 """Returns the (CP, slot) pair for a package matching |pkg|.
490
491 Args:
492 pkg: Path to a binary package or a (partial) package CPV specifier.
493
494 Returns:
495 A (CP, slot) pair for the given package; slot may be None (unspecified).
496
497 Raises:
498 ValueError: if |pkg| is not a binpkg file nor does it match something
499 that's in the bintree.
500 """
501 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
502 package = os.path.basename(os.path.splitext(pkg)[0])
503 category = os.path.basename(os.path.dirname(pkg))
504 return self._GetCP(os.path.join(category, package)), None
505
506 matches = self._FindPackageMatches(pkg)
507 if not matches:
508 raise ValueError('No package found for %s' % pkg)
509
510 idx = 0
511 if len(matches) > 1:
512 # Ask user to pick among multiple matches.
513 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
514 ['%s:%s' % (cp, slot) if slot else cp
515 for cp, slot in matches])
516
517 return matches[idx]
518
519 def _NeedsInstall(self, cpv, slot, build_time, optional):
520 """Returns whether a package needs to be installed on the target.
521
522 Args:
523 cpv: Fully qualified CPV (string) of the package.
524 slot: Slot identifier (string).
525 build_time: The BUILT_TIME value (string) of the binpkg.
526 optional: Whether package is optional on the target.
527
528 Returns:
529 A tuple (install, update) indicating whether to |install| the package and
530 whether it is an |update| to an existing package.
531
532 Raises:
533 ValueError: if slot is not provided.
534 """
535 # If not checking installed packages, always install.
536 if not self.target_db:
537 return True, False
538
539 cp = self._GetCP(cpv)
540 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
541 if target_pkg_info is not None:
542 if cpv != target_pkg_info.cpv:
Alex Klein9742cb62020-10-12 19:22:10 +0000543 attrs = package_info.SplitCPV(cpv)
544 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
David Pursell9476bf42015-03-30 13:34:27 -0700545 logging.debug('Updating %s: version (%s) different on target (%s)',
Alex Klein9742cb62020-10-12 19:22:10 +0000546 cp, attrs.version, target_attrs.version)
David Pursell9476bf42015-03-30 13:34:27 -0700547 return True, True
548
549 if build_time != target_pkg_info.build_time:
550 logging.debug('Updating %s: build time (%s) different on target (%s)',
551 cpv, build_time, target_pkg_info.build_time)
552 return True, True
553
554 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
555 cp, target_pkg_info.cpv, target_pkg_info.build_time)
556 return False, False
557
558 if optional:
559 logging.debug('Not installing %s: missing on target but optional', cp)
560 return False, False
561
562 logging.debug('Installing %s: missing on target and non-optional (%s)',
563 cp, cpv)
564 return True, False
565
566 def _ProcessDeps(self, deps, reverse):
567 """Enqueues dependencies for processing.
568
569 Args:
570 deps: List of dependencies to enqueue.
571 reverse: Whether these are reverse dependencies.
572 """
573 if not deps:
574 return
575
576 logging.debug('Processing %d %s dep(s)...', len(deps),
577 'reverse' if reverse else 'forward')
578 num_already_seen = 0
579 for dep in deps:
580 if self._EnqDep(dep, False, reverse):
581 logging.debug(' Queued dep %s', dep)
582 else:
583 num_already_seen += 1
584
585 if num_already_seen:
586 logging.debug('%d dep(s) already seen', num_already_seen)
587
588 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
589 """Returns a dictionary of packages that need to be installed on the target.
590
591 Args:
592 process_rdeps: Whether to trace forward dependencies.
593 process_rev_rdeps: Whether to trace backward dependencies as well.
594
595 Returns:
596 A dictionary mapping CP values (string) to tuples containing a CPV
597 (string), a slot (string), a boolean indicating whether the package
598 was initially listed in the queue, and a boolean indicating whether this
599 is an update to an existing package.
600 """
601 installs = {}
602 while self.queue:
603 dep, listed, optional = self._DeqDep()
604 cp, required_slot = dep
605 if cp in installs:
606 logging.debug('Already updating %s', cp)
607 continue
608
609 cp_slots = self.binpkgs_db.get(cp, dict())
610 logging.debug('Checking packages matching %s%s%s...', cp,
611 ' (slot: %s)' % required_slot if required_slot else '',
612 ' (optional)' if optional else '')
613 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400614 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700615 if required_slot and slot != required_slot:
616 continue
617
618 num_processed += 1
619 logging.debug(' Checking %s...', pkg_info.cpv)
620
621 install, update = self._NeedsInstall(pkg_info.cpv, slot,
622 pkg_info.build_time, optional)
623 if not install:
624 continue
625
626 installs[cp] = (pkg_info.cpv, slot, listed, update)
627
628 # Add forward and backward runtime dependencies to queue.
629 if process_rdeps:
630 self._ProcessDeps(pkg_info.rdeps, False)
631 if process_rev_rdeps:
632 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
633 if target_pkg_info:
634 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
635
636 if num_processed == 0:
637 logging.warning('No qualified bintree package corresponding to %s', cp)
638
639 return installs
640
641 def _SortInstalls(self, installs):
642 """Returns a sorted list of packages to install.
643
644 Performs a topological sort based on dependencies found in the binary
645 package database.
646
647 Args:
648 installs: Dictionary of packages to install indexed by CP.
649
650 Returns:
651 A list of package CPVs (string).
652
653 Raises:
654 ValueError: If dependency graph contains a cycle.
655 """
656 not_visited = set(installs.keys())
657 curr_path = []
658 sorted_installs = []
659
660 def SortFrom(cp):
661 """Traverses dependencies recursively, emitting nodes in reverse order."""
662 cpv, slot, _, _ = installs[cp]
663 if cpv in curr_path:
664 raise ValueError('Dependencies contain a cycle: %s -> %s' %
665 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
666 curr_path.append(cpv)
667 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
668 if rdep_cp in not_visited:
669 not_visited.remove(rdep_cp)
670 SortFrom(rdep_cp)
671
672 sorted_installs.append(cpv)
673 curr_path.pop()
674
675 # So long as there's more packages, keep expanding dependency paths.
676 while not_visited:
677 SortFrom(not_visited.pop())
678
679 return sorted_installs
680
681 def _EnqListedPkg(self, pkg):
682 """Finds and enqueues a listed package."""
683 cp, slot = self._FindPackage(pkg)
684 if cp not in self.binpkgs_db:
685 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
686 self._EnqDep((cp, slot), True, False)
687
688 def _EnqInstalledPkgs(self):
689 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400690 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700691 target_cp_slots = self.target_db.get(cp)
692 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400693 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700694 if slot in target_cp_slots:
695 self._EnqDep((cp, slot), True, False)
696
697 def Run(self, device, root, listed_pkgs, update, process_rdeps,
698 process_rev_rdeps):
699 """Computes the list of packages that need to be installed on a target.
700
701 Args:
702 device: Target handler object.
703 root: Package installation root.
704 listed_pkgs: Package names/files listed by the user.
705 update: Whether to read the target's installed package database.
706 process_rdeps: Whether to trace forward dependencies.
707 process_rev_rdeps: Whether to trace backward dependencies as well.
708
709 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700710 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
711 list of package CPVs (string) to install on the target in an order that
712 satisfies their inter-dependencies, |listed| the subset that was
713 requested by the user, and |num_updates| the number of packages being
714 installed over preexisting versions. Note that installation order should
715 be reversed for removal, |install_attrs| is a dictionary mapping a package
716 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700717 """
718 if process_rev_rdeps and not process_rdeps:
719 raise ValueError('Must processing forward deps when processing rev deps')
720 if process_rdeps and not update:
721 raise ValueError('Must check installed packages when processing deps')
722
723 if update:
724 logging.info('Initializing target intalled packages database...')
725 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
726
727 logging.info('Initializing binary packages database...')
728 self._InitBinpkgDB(process_rdeps)
729
730 logging.info('Finding listed package(s)...')
731 self._InitDepQueue()
732 for pkg in listed_pkgs:
733 if pkg == '@installed':
734 if not update:
735 raise ValueError(
736 'Must check installed packages when updating all of them.')
737 self._EnqInstalledPkgs()
738 else:
739 self._EnqListedPkg(pkg)
740
741 logging.info('Computing set of packages to install...')
742 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
743
744 num_updates = 0
745 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400746 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700747 if listed:
748 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400749 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700750 num_updates += 1
751
752 logging.info('Processed %d package(s), %d will be installed, %d are '
753 'updating existing packages',
754 len(self.seen), len(installs), num_updates)
755
756 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700757
758 install_attrs = {}
759 for pkg in sorted_installs:
Mike Frysingerada2d1c2020-03-20 05:02:06 -0400760 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700761 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
762 install_attrs[pkg] = {}
763 if dlc_id and dlc_package:
764 install_attrs[pkg][_DLC_ID] = dlc_id
765
766 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700767
768
Mike Frysinger63d35512021-01-26 23:16:13 -0500769def _Emerge(device, pkg_paths, root, extra_args=None):
770 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -0700771
772 Args:
773 device: A ChromiumOSDevice object.
Mike Frysinger63d35512021-01-26 23:16:13 -0500774 pkg_paths: (Local) paths to binary packages.
David Pursell9476bf42015-03-30 13:34:27 -0700775 root: Package installation root path.
776 extra_args: Extra arguments to pass to emerge.
777
778 Raises:
779 DeployError: Unrecoverable error during emerge.
780 """
Mike Frysinger63d35512021-01-26 23:16:13 -0500781 def path_to_name(pkg_path):
782 return os.path.basename(pkg_path)
783 def path_to_category(pkg_path):
784 return os.path.basename(os.path.dirname(pkg_path))
785
786 pkg_names = ', '.join(path_to_name(x) for x in pkg_paths)
787
David Pursell9476bf42015-03-30 13:34:27 -0700788 pkgroot = os.path.join(device.work_dir, 'packages')
Mike Frysinger15a4e012015-05-21 22:18:45 -0400789 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
790 # Clean out the dirs first if we had a previous emerge on the device so as to
791 # free up space for this emerge. The last emerge gets implicitly cleaned up
792 # when the device connection deletes its work_dir.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400793 device.run(
Mike Frysinger63d35512021-01-26 23:16:13 -0500794 f'cd {device.work_dir} && '
795 f'rm -rf packages portage-tmp && '
796 f'mkdir -p portage-tmp packages && '
797 f'cd packages && '
798 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
799 shell=True, remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700800
David Pursell9476bf42015-03-30 13:34:27 -0700801 logging.info('Use portage temp dir %s', portage_tmpdir)
802
Ralph Nathane01ccf12015-04-16 10:40:32 -0700803 # This message is read by BrilloDeployOperation.
Mike Frysinger63d35512021-01-26 23:16:13 -0500804 logging.notice('Copying binpkgs to device.')
805 for pkg_path in pkg_paths:
806 pkg_name = path_to_name(pkg_path)
807 logging.info('Copying %s', pkg_name)
808 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
809 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True,
810 compress=False)
811
812 # This message is read by BrilloDeployOperation.
813 logging.notice('Installing: %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700814
815 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
816 # chromeos-base packages will be skipped due to the configuration
817 # in /etc/protage/make.profile/package.provided. However, there is
818 # a known bug that /usr/local/etc/portage is not setup properly
819 # (crbug.com/312041). This does not affect `cros deploy` because
820 # we do not use the preset PKGDIR.
821 extra_env = {
822 'FEATURES': '-sandbox',
823 'PKGDIR': pkgroot,
824 'PORTAGE_CONFIGROOT': '/usr/local',
825 'PORTAGE_TMPDIR': portage_tmpdir,
826 'PORTDIR': device.work_dir,
827 'CONFIG_PROTECT': '-*',
828 }
Mike Frysinger63d35512021-01-26 23:16:13 -0500829
Alex Kleinaaddc932020-01-30 15:02:24 -0700830 # --ignore-built-slot-operator-deps because we don't rebuild everything.
831 # It can cause errors, but that's expected with cros deploy since it's just a
832 # best effort to prevent developers avoid rebuilding an image every time.
Mike Frysinger63d35512021-01-26 23:16:13 -0500833 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', '--root',
834 root] + [os.path.join(pkgroot, *x.split('/')[-2:]) for x in pkg_paths]
David Pursell9476bf42015-03-30 13:34:27 -0700835 if extra_args:
836 cmd.append(extra_args)
837
Alex Kleinaaddc932020-01-30 15:02:24 -0700838 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
839 'packages built against the old version may not be able to '
840 'load the new .so. This is expected, and you will just need '
841 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700842 try:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400843 result = device.run(cmd, extra_env=extra_env, remote_sudo=True,
844 capture_output=True, debug_level=logging.INFO)
Greg Kerrb96c02c2019-02-08 14:32:41 -0800845
846 pattern = ('A requested package will not be merged because '
847 'it is listed in package.provided')
848 output = result.error.replace('\n', ' ').replace('\r', '')
849 if pattern in output:
850 error = ('Package failed to emerge: %s\n'
851 'Remove %s from /etc/portage/make.profile/'
852 'package.provided/chromeos-base.packages\n'
853 '(also see crbug.com/920140 for more context)\n'
854 % (pattern, pkg_name))
855 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700856 except Exception:
Mike Frysinger63d35512021-01-26 23:16:13 -0500857 logging.error('Failed to emerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700858 raise
859 else:
Mike Frysinger63d35512021-01-26 23:16:13 -0500860 # This message is read by BrilloDeployOperation.
861 logging.notice('Packages have been installed.')
David Pursell9476bf42015-03-30 13:34:27 -0700862
863
Qijiang Fand5958192019-07-26 12:32:36 +0900864def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800865 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900866
867 This reads the tarball from pkgpath, and calls restorecon on device to
868 restore SELinux context for files listed in the tarball, assuming those files
869 are installed to /
870
871 Args:
872 device: a ChromiumOSDevice object
873 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900874 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900875 """
Qijiang Fan8a945032019-04-25 20:53:29 +0900876 pkgroot = os.path.join(device.work_dir, 'packages')
877 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
878 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
879 # Testing shows restorecon splits on newlines instead of spaces.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400880 device.run(
Qijiang Fand5958192019-07-26 12:32:36 +0900881 ['cd', root, '&&',
882 'tar', 'tf', pkgpath_device, '|',
883 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900884 remote_sudo=True)
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900885
886
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700887def _GetPackagesByCPV(cpvs, strip, sysroot):
888 """Returns paths to binary packages corresponding to |cpvs|.
889
890 Args:
Alex Klein9742cb62020-10-12 19:22:10 +0000891 cpvs: List of CPV components given by package_info.SplitCPV().
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700892 strip: True to run strip_package.
893 sysroot: Sysroot path.
894
895 Returns:
896 List of paths corresponding to |cpvs|.
897
898 Raises:
899 DeployError: If a package is missing.
900 """
901 packages_dir = None
902 if strip:
903 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400904 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700905 ['strip_package', '--sysroot', sysroot] +
Alex Klein9742cb62020-10-12 19:22:10 +0000906 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700907 packages_dir = _STRIPPED_PACKAGES_DIR
908 except cros_build_lib.RunCommandError:
909 logging.error('Cannot strip packages %s',
Alex Klein9742cb62020-10-12 19:22:10 +0000910 ' '.join([str(cpv) for cpv in cpvs]))
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700911 raise
912
913 paths = []
914 for cpv in cpvs:
915 path = portage_util.GetBinaryPackagePath(
916 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
917 packages_dir=packages_dir)
918 if not path:
919 raise DeployError('Missing package %s.' % cpv)
920 paths.append(path)
921
922 return paths
923
924
925def _GetPackagesPaths(pkgs, strip, sysroot):
926 """Returns paths to binary |pkgs|.
927
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700928 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700929 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700930 strip: Whether or not to run strip_package for CPV packages.
931 sysroot: The sysroot path.
932
933 Returns:
934 List of paths corresponding to |pkgs|.
935 """
Alex Klein9742cb62020-10-12 19:22:10 +0000936 cpvs = [package_info.SplitCPV(p) for p in pkgs]
Ned Nguyend0db4072019-02-22 14:19:21 -0700937 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700938
939
Mike Frysinger22bb5502021-01-29 13:05:46 -0500940def _Unmerge(device, pkgs, root):
941 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -0700942
943 Args:
944 device: A RemoteDevice object.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500945 pkgs: Package names.
David Pursell9476bf42015-03-30 13:34:27 -0700946 root: Package installation root path.
947 """
Mike Frysinger22bb5502021-01-29 13:05:46 -0500948 pkg_names = ', '.join(os.path.basename(x) for x in pkgs)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700949 # This message is read by BrilloDeployOperation.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500950 logging.notice('Unmerging %s.', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700951 cmd = ['qmerge', '--yes']
952 # Check if qmerge is available on the device. If not, use emerge.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400953 if device.run(['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700954 cmd = ['emerge']
955
Mike Frysinger22bb5502021-01-29 13:05:46 -0500956 cmd += ['--unmerge', '--root', root]
957 cmd.extend('f={x}' for x in pkgs)
David Pursell9476bf42015-03-30 13:34:27 -0700958 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700959 # Always showing the emerge output for clarity.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400960 device.run(cmd, capture_output=False, remote_sudo=True,
961 debug_level=logging.INFO)
David Pursell9476bf42015-03-30 13:34:27 -0700962 except Exception:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500963 logging.error('Failed to unmerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700964 raise
965 else:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500966 # This message is read by BrilloDeployOperation.
967 logging.notice('Packages have been uninstalled.')
David Pursell9476bf42015-03-30 13:34:27 -0700968
969
970def _ConfirmDeploy(num_updates):
971 """Returns whether we can continue deployment."""
972 if num_updates > _MAX_UPDATES_NUM:
973 logging.warning(_MAX_UPDATES_WARNING)
974 return cros_build_lib.BooleanPrompt(default=False)
975
976 return True
977
978
Andrew06a5f812020-01-23 08:08:32 -0800979def _EmergePackages(pkgs, device, strip, sysroot, root, board, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800980 """Call _Emerge for each package in pkgs."""
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -0500981 if device.IsSELinuxAvailable():
982 enforced = device.IsSELinuxEnforced()
983 if enforced:
984 device.run(['setenforce', '0'])
985 else:
986 enforced = False
987
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700988 dlc_deployed = False
Mike Frysinger63d35512021-01-26 23:16:13 -0500989 # This message is read by BrilloDeployOperation.
990 logging.info('Preparing local packages for transfer.')
991 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
992 # Install all the packages in one pass so inter-package blockers work.
993 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
994 logging.info('Updating SELinux settings & DLC images.')
995 for pkg_path in pkg_paths:
Ben Pastene5f03b052019-08-12 18:03:24 -0700996 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900997 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800998
999 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
1000 if dlc_id and dlc_package:
Andrew06a5f812020-01-23 08:08:32 -08001001 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001002 dlc_deployed = True
Mike Frysinger5f4c2742021-02-08 14:37:23 -05001003
1004 if dlc_deployed:
1005 # Clean up empty directories created by emerging DLCs.
1006 device.run(['test', '-d', '/build/rootfs', '&&', 'rmdir',
1007 '--ignore-fail-on-non-empty', '/build/rootfs', '/build'],
1008 check=False)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001009
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001010 if enforced:
1011 device.run(['setenforce', '1'])
1012
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001013 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1014 # we installed new DLC images).
1015 if dlc_deployed:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001016 device.run(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001017
1018
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001019def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001020 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001021 dlc_uninstalled = False
Mike Frysinger22bb5502021-01-29 13:05:46 -05001022 _Unmerge(device, pkgs, root)
1023 logging.info('Cleaning up DLC images.')
Ralph Nathane01ccf12015-04-16 10:40:32 -07001024 for pkg in pkgs:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001025 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1026 dlc_uninstalled = True
1027
1028 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1029 # uninstalled DLC images).
1030 if dlc_uninstalled:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001031 device.run(['restart', 'dlcservice'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001032
1033
1034def _UninstallDLCImage(device, pkg_attrs):
1035 """Uninstall a DLC image."""
1036 if _DLC_ID in pkg_attrs:
1037 dlc_id = pkg_attrs[_DLC_ID]
1038 logging.notice('Uninstalling DLC image for %s', dlc_id)
1039
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001040 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001041 return True
1042 else:
1043 logging.debug('DLC_ID not found in package')
1044 return False
1045
1046
Andrew06a5f812020-01-23 08:08:32 -08001047def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001048 """Deploy (install and mount) a DLC image."""
Andrew67b5fa72020-02-05 14:14:48 -08001049 # Build the DLC image if the image is outdated or doesn't exist.
Andrew5743d382020-06-16 09:55:04 -07001050 dlc_lib.InstallDlcImages(sysroot=sysroot, dlc_id=dlc_id, board=board)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001051
Andrewc7e1c6b2020-02-27 16:03:53 -08001052 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1053 try:
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001054 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Andrewc7e1c6b2020-02-27 16:03:53 -08001055 except cros_build_lib.RunCommandError as e:
1056 logging.info('Failed to uninstall DLC:%s. Continue anyway.',
1057 e.result.error)
1058 except Exception:
1059 logging.error('Failed to uninstall DLC.')
1060 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001061
Andrewc7e1c6b2020-02-27 16:03:53 -08001062 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1063 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1064 # location.
1065 logging.notice('Deploy the DLC image for %s', dlc_id)
Andrew5743d382020-06-16 09:55:04 -07001066 dlc_img_path_src = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1067 dlc_package, dlc_lib.DLC_IMAGE)
Andrewc7e1c6b2020-02-27 16:03:53 -08001068 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1069 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1070 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1071 # Create directories for DLC images.
1072 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1073 # Copy images to the destination directories.
1074 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
Andrew5743d382020-06-16 09:55:04 -07001075 dlc_lib.DLC_IMAGE),
Andrewc7e1c6b2020-02-27 16:03:53 -08001076 mode='rsync')
Andrew5743d382020-06-16 09:55:04 -07001077 device.run(['cp', os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1078 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE)])
Andrewc7e1c6b2020-02-27 16:03:53 -08001079
1080 # Set the proper perms and ownership so dlcservice can access the image.
1081 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1082 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001083
Andrew67b5fa72020-02-05 14:14:48 -08001084 # Copy metadata to device.
Andrew5743d382020-06-16 09:55:04 -07001085 dest_mata_dir = os.path.join('/', dlc_lib.DLC_META_DIR, dlc_id,
1086 dlc_package)
Andrew67b5fa72020-02-05 14:14:48 -08001087 device.run(['mkdir', '-p', dest_mata_dir])
Andrew5743d382020-06-16 09:55:04 -07001088 src_meta_dir = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1089 dlc_package, dlc_lib.DLC_TMP_META_DIR)
Andrew67b5fa72020-02-05 14:14:48 -08001090 device.CopyToDevice(src_meta_dir + '/',
1091 dest_mata_dir,
1092 mode='rsync',
1093 recursive=True,
1094 remote_sudo=True)
1095
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001096
1097def _GetDLCInfo(device, pkg_path, from_dut):
1098 """Returns information of a DLC given its package path.
1099
1100 Args:
1101 device: commandline.Device object; None to use the default device.
1102 pkg_path: path to the package.
1103 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1104 info from host.
1105
1106 Returns:
1107 A tuple (dlc_id, dlc_package).
1108 """
1109 environment_content = ''
1110 if from_dut:
1111 # On DUT, |pkg_path| is the directory which contains environment file.
1112 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001113 try:
1114 environment_data = device.CatFile(
1115 environment_path, max_size=None, encoding=None)
1116 except remote_access.CatFileError:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001117 # The package is not installed on DUT yet. Skip extracting info.
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001118 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001119 else:
1120 # On host, pkg_path is tbz2 file which contains environment file.
1121 # Extract the metadata of the package file.
1122 data = portage.xpak.tbz2(pkg_path).get_data()
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001123 environment_data = data[_ENVIRONMENT_FILENAME.encode('utf-8')]
1124
1125 # Extract the environment metadata.
1126 environment_content = bz2.decompress(environment_data)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001127
1128 with tempfile.NamedTemporaryFile() as f:
1129 # Dumps content into a file so we can use osutils.SourceEnvironment.
1130 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001131 osutils.WriteFile(path, environment_content, mode='wb')
Andrew67b5fa72020-02-05 14:14:48 -08001132 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE,
1133 _DLC_ENABLED))
1134
1135 dlc_enabled = content.get(_DLC_ENABLED)
1136 if dlc_enabled is not None and (dlc_enabled is False or
1137 str(dlc_enabled) == 'false'):
1138 logging.info('Installing DLC in rootfs.')
1139 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001140 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001141
1142
Gilad Arnolda0a98062015-07-07 08:34:27 -07001143def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1144 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1145 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1146 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001147 """Deploys packages to a device.
1148
1149 Args:
David Pursell2e773382015-04-03 14:30:47 -07001150 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001151 packages: List of packages (strings) to deploy to device.
1152 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001153 emerge: True to emerge package, False to unmerge.
1154 update: Check installed version on device.
1155 deep: Install dependencies also. Implies |update|.
1156 deep_rev: Install reverse dependencies. Implies |deep|.
1157 clean_binpkg: Clean outdated binary packages.
1158 root: Package installation root path.
1159 strip: Run strip_package to filter out preset paths in the package.
1160 emerge_args: Extra arguments to pass to emerge.
1161 ssh_private_key: Path to an SSH private key file; None to use test keys.
1162 ping: True to ping the device before trying to connect.
1163 force: Ignore sanity checks and prompts.
1164 dry_run: Print deployment plan but do not deploy anything.
1165
1166 Raises:
1167 ValueError: Invalid parameter or parameter combination.
1168 DeployError: Unrecoverable failure during deploy.
1169 """
1170 if deep_rev:
1171 deep = True
1172 if deep:
1173 update = True
1174
Gilad Arnolda0a98062015-07-07 08:34:27 -07001175 if not packages:
1176 raise DeployError('No packages provided, nothing to deploy.')
1177
David Pursell9476bf42015-03-30 13:34:27 -07001178 if update and not emerge:
1179 raise ValueError('Cannot update and unmerge.')
1180
David Pursell2e773382015-04-03 14:30:47 -07001181 if device:
1182 hostname, username, port = device.hostname, device.username, device.port
1183 else:
1184 hostname, username, port = None, None, None
1185
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001186 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001187 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001188 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001189 # Somewhat confusing to clobber, but here we are.
1190 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001191 with remote_access.ChromiumOSDeviceHandler(
1192 hostname, port=port, username=username, private_key=ssh_private_key,
1193 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001194 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001195
Gilad Arnolda0a98062015-07-07 08:34:27 -07001196 board = cros_build_lib.GetBoard(device_board=device.board,
1197 override_board=board)
1198 if not force and board != device.board:
1199 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001200 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001201
Gilad Arnolda0a98062015-07-07 08:34:27 -07001202 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001203
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001204 # Don't bother trying to clean for unmerges. We won't use the local db,
1205 # and it just slows things down for the user.
1206 if emerge and clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001207 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001208 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001209
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001210 # Remount rootfs as writable if necessary.
1211 if not device.MountRootfsReadWrite():
1212 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001213
1214 # Obtain list of packages to upgrade/remove.
1215 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001216 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001217 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001218 if emerge:
1219 action_str = 'emerge'
1220 else:
1221 pkgs.reverse()
1222 action_str = 'unmerge'
1223
1224 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001225 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001226 return
1227
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001228 # Warn when the user installs & didn't `cros workon start`.
1229 if emerge:
1230 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1231 for package in listed:
1232 cp = package_info.SplitCPV(package).cp
1233 if cp not in worked_on_cps:
1234 logging.warning(
1235 'Are you intentionally deploying unmodified packages, or did '
1236 'you forget to run `cros workon --board=$BOARD start %s`?', cp)
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +09001237
Ralph Nathane01ccf12015-04-16 10:40:32 -07001238 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001239 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001240 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001241
1242 if dry_run or not _ConfirmDeploy(num_updates):
1243 return
1244
Ralph Nathane01ccf12015-04-16 10:40:32 -07001245 # Select function (emerge or unmerge) and bind args.
1246 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001247 func = functools.partial(_EmergePackages, pkgs, device, strip,
Andrew06a5f812020-01-23 08:08:32 -08001248 sysroot, root, board, emerge_args)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001249 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001250 func = functools.partial(_UnmergePackages, pkgs, device, root,
1251 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001252
1253 # Call the function with the progress bar or with normal output.
1254 if command.UseProgressBar():
Mike Frysinger63d35512021-01-26 23:16:13 -05001255 op = BrilloDeployOperation(emerge)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001256 op.Run(func, log_level=logging.DEBUG)
1257 else:
1258 func()
David Pursell9476bf42015-03-30 13:34:27 -07001259
Ben Pastene5f03b052019-08-12 18:03:24 -07001260 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001261 if sum(x.count('selinux-policy') for x in pkgs):
1262 logging.warning(
1263 'Deploying SELinux policy will not take effect until reboot. '
Ian Barkley-Yeung6b2d8672020-08-13 18:58:10 -07001264 'SELinux policy is loaded by init. Also, changing the security '
1265 'contexts (labels) of a file will require building a new image '
1266 'and flashing the image onto the device.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001267
Mike Frysinger63d35512021-01-26 23:16:13 -05001268 # This message is read by BrilloDeployOperation.
David Pursell9476bf42015-03-30 13:34:27 -07001269 logging.warning('Please restart any updated services on the device, '
1270 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001271 except Exception:
1272 if lsb_release:
1273 lsb_entries = sorted(lsb_release.items())
1274 logging.info('Following are the LSB version details of the device:\n%s',
1275 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1276 raise