blob: 10801cd3fbb9cbe18c6833f53de9cdaabd13079f [file] [log] [blame]
David Pursell9476bf42015-03-30 13:34:27 -07001# Copyright 2015 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Alex Kleinaaddc932020-01-30 15:02:24 -07005"""Deploy packages onto a target device.
6
7Integration tests for this file can be found at cli/cros/tests/cros_vm_tests.py.
8See that file for more information.
9"""
David Pursell9476bf42015-03-30 13:34:27 -070010
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040011from __future__ import division
David Pursell9476bf42015-03-30 13:34:27 -070012
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070013import bz2
David Pursell9476bf42015-03-30 13:34:27 -070014import fnmatch
Ralph Nathane01ccf12015-04-16 10:40:32 -070015import functools
David Pursell9476bf42015-03-30 13:34:27 -070016import json
Chris McDonald14ac61d2021-07-21 11:49:56 -060017import logging
David Pursell9476bf42015-03-30 13:34:27 -070018import os
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070019import tempfile
David Pursell9476bf42015-03-30 13:34:27 -070020
Ralph Nathane01ccf12015-04-16 10:40:32 -070021from chromite.cli import command
Mike Frysinger06a51c82021-04-06 11:39:17 -040022from chromite.lib import build_target_lib
David Pursell9476bf42015-03-30 13:34:27 -070023from chromite.lib import cros_build_lib
Alex Klein18a60af2020-06-11 12:08:47 -060024from chromite.lib import dlc_lib
Ralph Nathane01ccf12015-04-16 10:40:32 -070025from chromite.lib import operation
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070026from chromite.lib import osutils
David Pursell9476bf42015-03-30 13:34:27 -070027from chromite.lib import portage_util
David Pursell9476bf42015-03-30 13:34:27 -070028from chromite.lib import remote_access
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +090029from chromite.lib import workon_helper
Alex Klein18a60af2020-06-11 12:08:47 -060030from chromite.lib.parser import package_info
31
Chris McDonald14ac61d2021-07-21 11:49:56 -060032
David Pursell9476bf42015-03-30 13:34:27 -070033try:
34 import portage
35except ImportError:
36 if cros_build_lib.IsInsideChroot():
37 raise
38
39
40_DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
41# This is defined in src/platform/dev/builder.py
42_STRIPPED_PACKAGES_DIR = 'stripped-packages'
43
44_MAX_UPDATES_NUM = 10
45_MAX_UPDATES_WARNING = (
46 'You are about to update a large number of installed packages, which '
47 'might take a long time, fail midway, or leave the target in an '
48 'inconsistent state. It is highly recommended that you flash a new image '
49 'instead.')
50
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070051_DLC_ID = 'DLC_ID'
52_DLC_PACKAGE = 'DLC_PACKAGE'
Andrew67b5fa72020-02-05 14:14:48 -080053_DLC_ENABLED = 'DLC_ENABLED'
Xiaochu Liu2726e7c2019-07-18 10:28:10 -070054_ENVIRONMENT_FILENAME = 'environment.bz2'
55_DLC_INSTALL_ROOT = '/var/cache/dlc'
56
David Pursell9476bf42015-03-30 13:34:27 -070057
58class DeployError(Exception):
59 """Thrown when an unrecoverable error is encountered during deploy."""
60
61
Ralph Nathane01ccf12015-04-16 10:40:32 -070062class BrilloDeployOperation(operation.ProgressBarOperation):
63 """ProgressBarOperation specific for brillo deploy."""
Alex Kleinaaddc932020-01-30 15:02:24 -070064 # These two variables are used to validate the output in the VM integration
65 # tests. Changes to the output must be reflected here.
Mike Frysinger63d35512021-01-26 23:16:13 -050066 MERGE_EVENTS = (
67 'Preparing local packages',
68 'NOTICE: Copying binpkgs',
69 'NOTICE: Installing',
70 'been installed.',
71 'Please restart any updated',
72 )
Mike Frysinger22bb5502021-01-29 13:05:46 -050073 UNMERGE_EVENTS = (
74 'NOTICE: Unmerging',
75 'been uninstalled.',
76 'Please restart any updated',
77 )
Ralph Nathane01ccf12015-04-16 10:40:32 -070078
Mike Frysinger63d35512021-01-26 23:16:13 -050079 def __init__(self, emerge):
Ralph Nathane01ccf12015-04-16 10:40:32 -070080 """Construct BrilloDeployOperation object.
81
82 Args:
Ralph Nathane01ccf12015-04-16 10:40:32 -070083 emerge: True if emerge, False is unmerge.
84 """
Jae Hoon Kimad176b82021-07-26 19:29:29 +000085 super().__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070086 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070087 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070088 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070089 self._events = self.UNMERGE_EVENTS
Mike Frysinger63d35512021-01-26 23:16:13 -050090 self._total = len(self._events)
Ralph Nathane01ccf12015-04-16 10:40:32 -070091 self._completed = 0
92
Ralph Nathandc14ed92015-04-22 11:17:40 -070093 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070094 """Parse the output of brillo deploy to update a progress bar."""
95 stdout = self._stdout.read()
96 stderr = self._stderr.read()
97 output = stdout + stderr
98 for event in self._events:
99 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -0400100 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700101
102
David Pursell9476bf42015-03-30 13:34:27 -0700103class _InstallPackageScanner(object):
104 """Finds packages that need to be installed on a target device.
105
106 Scans the sysroot bintree, beginning with a user-provided list of packages,
107 to find all packages that need to be installed. If so instructed,
108 transitively scans forward (mandatory) and backward (optional) dependencies
109 as well. A package will be installed if missing on the target (mandatory
110 packages only), or it will be updated if its sysroot version and build time
111 are different from the target. Common usage:
112
113 pkg_scanner = _InstallPackageScanner(sysroot)
114 pkgs = pkg_scanner.Run(...)
115 """
116
117 class VartreeError(Exception):
118 """An error in the processing of the installed packages tree."""
119
120 class BintreeError(Exception):
121 """An error in the processing of the source binpkgs tree."""
122
123 class PkgInfo(object):
124 """A record containing package information."""
125
126 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
127
128 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
129 self.cpv = cpv
130 self.build_time = build_time
131 self.rdeps_raw = rdeps_raw
132 self.rdeps = set() if rdeps is None else rdeps
133 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
134
135 # Python snippet for dumping vartree info on the target. Instantiate using
136 # _GetVartreeSnippet().
137 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700138import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700139import os
140import portage
141
142# Normalize the path to match what portage will index.
143target_root = os.path.normpath('%(root)s')
144if not target_root.endswith('/'):
145 target_root += '/'
146trees = portage.create_trees(target_root=target_root, config_root='/')
147vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700148pkg_info = []
149for cpv in vartree.dbapi.cpv_all():
150 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
151 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
152 pkg_info.append((cpv, slot, rdep_raw, build_time))
153
154print(json.dumps(pkg_info))
155"""
156
157 def __init__(self, sysroot):
158 self.sysroot = sysroot
159 # Members containing the sysroot (binpkg) and target (installed) package DB.
160 self.target_db = None
161 self.binpkgs_db = None
162 # Members for managing the dependency resolution work queue.
163 self.queue = None
164 self.seen = None
165 self.listed = None
166
167 @staticmethod
168 def _GetCP(cpv):
169 """Returns the CP value for a given CPV string."""
Alex Klein9742cb62020-10-12 19:22:10 +0000170 attrs = package_info.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600171 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700172 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600173 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700174
175 @staticmethod
176 def _InDB(cp, slot, db):
177 """Returns whether CP and slot are found in a database (if provided)."""
178 cp_slots = db.get(cp) if db else None
179 return cp_slots is not None and (not slot or slot in cp_slots)
180
181 @staticmethod
182 def _AtomStr(cp, slot):
183 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
184 return '%s:%s' % (cp, slot) if slot else cp
185
186 @classmethod
187 def _GetVartreeSnippet(cls, root='/'):
188 """Returns a code snippet for dumping the vartree on the target.
189
190 Args:
191 root: The installation root.
192
193 Returns:
194 The said code snippet (string) with parameters filled in.
195 """
196 return cls._GET_VARTREE % {'root': root}
197
198 @classmethod
199 def _StripDepAtom(cls, dep_atom, installed_db=None):
200 """Strips a dependency atom and returns a (CP, slot) pair."""
201 # TODO(garnold) This is a gross simplification of ebuild dependency
202 # semantics, stripping and ignoring various qualifiers (versions, slots,
203 # USE flag, negation) and will likely need to be fixed. chromium:447366.
204
205 # Ignore unversioned blockers, leaving them for the user to resolve.
206 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
207 return None, None
208
209 cp = dep_atom
210 slot = None
211 require_installed = False
212
213 # Versioned blockers should be updated, but only if already installed.
214 # These are often used for forcing cascaded updates of multiple packages,
215 # so we're treating them as ordinary constraints with hopes that it'll lead
216 # to the desired result.
217 if cp.startswith('!'):
218 cp = cp.lstrip('!')
219 require_installed = True
220
221 # Remove USE flags.
222 if '[' in cp:
223 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
224
225 # Separate the slot qualifier and strip off subslots.
226 if ':' in cp:
227 cp, slot = cp.split(':')
228 for delim in ('/', '='):
229 slot = slot.split(delim, 1)[0]
230
231 # Strip version wildcards (right), comparators (left).
232 cp = cp.rstrip('*')
233 cp = cp.lstrip('<=>~')
234
235 # Turn into CP form.
236 cp = cls._GetCP(cp)
237
238 if require_installed and not cls._InDB(cp, None, installed_db):
239 return None, None
240
241 return cp, slot
242
243 @classmethod
244 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
245 """Resolves and returns a list of dependencies from a dependency string.
246
247 This parses a dependency string and returns a list of package names and
248 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
249 resolving disjunctive deps, we include all choices that are fully present
250 in |installed_db|. If none is present, we choose an arbitrary one that is
251 available.
252
253 Args:
254 dep_str: A raw dependency string.
255 installed_db: A database of installed packages.
256 avail_db: A database of packages available for installation.
257
258 Returns:
259 A list of pairs (CP, slot).
260
261 Raises:
262 ValueError: the dependencies string is malformed.
263 """
264 def ProcessSubDeps(dep_exp, disjunct):
265 """Parses and processes a dependency (sub)expression."""
266 deps = set()
267 default_deps = set()
268 sub_disjunct = False
269 for dep_sub_exp in dep_exp:
270 sub_deps = set()
271
272 if isinstance(dep_sub_exp, (list, tuple)):
273 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
274 sub_disjunct = False
275 elif sub_disjunct:
276 raise ValueError('Malformed disjunctive operation in deps')
277 elif dep_sub_exp == '||':
278 sub_disjunct = True
279 elif dep_sub_exp.endswith('?'):
280 raise ValueError('Dependencies contain a conditional')
281 else:
282 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
283 if cp:
284 sub_deps = set([(cp, slot)])
285 elif disjunct:
286 raise ValueError('Atom in disjunct ignored')
287
288 # Handle sub-deps of a disjunctive expression.
289 if disjunct:
290 # Make the first available choice the default, for use in case that
291 # no option is installed.
292 if (not default_deps and avail_db is not None and
293 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
294 default_deps = sub_deps
295
296 # If not all sub-deps are installed, then don't consider them.
297 if not all([cls._InDB(cp, slot, installed_db)
298 for cp, slot in sub_deps]):
299 sub_deps = set()
300
301 deps.update(sub_deps)
302
303 return deps or default_deps
304
305 try:
306 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
307 except portage.exception.InvalidDependString as e:
308 raise ValueError('Invalid dep string: %s' % e)
309 except ValueError as e:
310 raise ValueError('%s: %s' % (e, dep_str))
311
312 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
313 installed_db=None):
314 """Returns a database of packages given a list of CPV info.
315
316 Args:
317 cpv_info: A list of tuples containing package CPV and attributes.
318 process_rdeps: Whether to populate forward dependencies.
319 process_rev_rdeps: Whether to populate reverse dependencies.
320 installed_db: A database of installed packages for filtering disjunctive
321 choices against; if None, using own built database.
322
323 Returns:
324 A map from CP values to another dictionary that maps slots to package
325 attribute tuples. Tuples contain a CPV value (string), build time
326 (string), runtime dependencies (set), and reverse dependencies (set,
327 empty if not populated).
328
329 Raises:
330 ValueError: If more than one CPV occupies a single slot.
331 """
332 db = {}
333 logging.debug('Populating package DB...')
334 for cpv, slot, rdeps_raw, build_time in cpv_info:
335 cp = self._GetCP(cpv)
336 cp_slots = db.setdefault(cp, dict())
337 if slot in cp_slots:
338 raise ValueError('More than one package found for %s' %
339 self._AtomStr(cp, slot))
340 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
341 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
342 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
343
344 avail_db = db
345 if installed_db is None:
346 installed_db = db
347 avail_db = None
348
349 # Add approximate forward dependencies.
350 if process_rdeps:
351 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400352 for cp, cp_slots in db.items():
353 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700354 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
355 installed_db, avail_db))
356 logging.debug(' %s (%s) processed rdeps: %s',
357 self._AtomStr(cp, slot), pkg_info.cpv,
358 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
359 for rdep_cp, rdep_slot in pkg_info.rdeps]))
360
361 # Add approximate reverse dependencies (optional).
362 if process_rev_rdeps:
363 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400364 for cp, cp_slots in db.items():
365 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700366 for rdep_cp, rdep_slot in pkg_info.rdeps:
367 to_slots = db.get(rdep_cp)
368 if not to_slots:
369 continue
370
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400371 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700372 if rdep_slot and to_slot != rdep_slot:
373 continue
374 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
375 self._AtomStr(cp, slot), pkg_info.cpv,
376 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
377 to_pkg_info.rev_rdeps.add((cp, slot))
378
379 return db
380
381 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
382 """Initializes a dictionary of packages installed on |device|."""
383 get_vartree_script = self._GetVartreeSnippet(root)
384 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400385 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700386 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700387 except cros_build_lib.RunCommandError as e:
388 logging.error('Cannot get target vartree:\n%s', e.result.error)
389 raise
390
391 try:
392 self.target_db = self._BuildDB(json.loads(result.output),
393 process_rdeps, process_rev_rdeps)
394 except ValueError as e:
395 raise self.VartreeError(str(e))
396
397 def _InitBinpkgDB(self, process_rdeps):
398 """Initializes a dictionary of binary packages for updating the target."""
399 # Get build root trees; portage indexes require a trailing '/'.
400 build_root = os.path.join(self.sysroot, '')
401 trees = portage.create_trees(target_root=build_root, config_root=build_root)
402 bintree = trees[build_root]['bintree']
403 binpkgs_info = []
404 for cpv in bintree.dbapi.cpv_all():
405 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
406 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
407 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
408
409 try:
410 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
411 installed_db=self.target_db)
412 except ValueError as e:
413 raise self.BintreeError(str(e))
414
415 def _InitDepQueue(self):
416 """Initializes the dependency work queue."""
417 self.queue = set()
418 self.seen = {}
419 self.listed = set()
420
421 def _EnqDep(self, dep, listed, optional):
422 """Enqueues a dependency if not seen before or if turned non-optional."""
423 if dep in self.seen and (optional or not self.seen[dep]):
424 return False
425
426 self.queue.add(dep)
427 self.seen[dep] = optional
428 if listed:
429 self.listed.add(dep)
430 return True
431
432 def _DeqDep(self):
433 """Dequeues and returns a dependency, its listed and optional flags.
434
435 This returns listed packages first, if any are present, to ensure that we
436 correctly mark them as such when they are first being processed.
437 """
438 if self.listed:
439 dep = self.listed.pop()
440 self.queue.remove(dep)
441 listed = True
442 else:
443 dep = self.queue.pop()
444 listed = False
445
446 return dep, listed, self.seen[dep]
447
448 def _FindPackageMatches(self, cpv_pattern):
449 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
450
451 This is breaking |cpv_pattern| into its C, P and V components, each of
452 which may or may not be present or contain wildcards. It then scans the
453 binpkgs database to find all atoms that match these components, returning a
454 list of CP and slot qualifier. When the pattern does not specify a version,
455 or when a CP has only one slot in the binpkgs database, we omit the slot
456 qualifier in the result.
457
458 Args:
459 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
460
461 Returns:
462 A list of (CPV, slot) pairs of packages in the binpkgs database that
463 match the pattern.
464 """
Alex Klein9742cb62020-10-12 19:22:10 +0000465 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
David Pursell9476bf42015-03-30 13:34:27 -0700466 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
467 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400468 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700469 if not fnmatch.fnmatchcase(cp, cp_pattern):
470 continue
471
472 # If no version attribute was given or there's only one slot, omit the
473 # slot qualifier.
Alex Klein9742cb62020-10-12 19:22:10 +0000474 if not attrs.version or len(cp_slots) == 1:
David Pursell9476bf42015-03-30 13:34:27 -0700475 matches.append((cp, None))
476 else:
Alex Klein9742cb62020-10-12 19:22:10 +0000477 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400478 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700479 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
480 matches.append((cp, slot))
481
482 return matches
483
484 def _FindPackage(self, pkg):
485 """Returns the (CP, slot) pair for a package matching |pkg|.
486
487 Args:
488 pkg: Path to a binary package or a (partial) package CPV specifier.
489
490 Returns:
491 A (CP, slot) pair for the given package; slot may be None (unspecified).
492
493 Raises:
494 ValueError: if |pkg| is not a binpkg file nor does it match something
495 that's in the bintree.
496 """
497 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
498 package = os.path.basename(os.path.splitext(pkg)[0])
499 category = os.path.basename(os.path.dirname(pkg))
500 return self._GetCP(os.path.join(category, package)), None
501
502 matches = self._FindPackageMatches(pkg)
503 if not matches:
504 raise ValueError('No package found for %s' % pkg)
505
506 idx = 0
507 if len(matches) > 1:
508 # Ask user to pick among multiple matches.
509 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
510 ['%s:%s' % (cp, slot) if slot else cp
511 for cp, slot in matches])
512
513 return matches[idx]
514
515 def _NeedsInstall(self, cpv, slot, build_time, optional):
516 """Returns whether a package needs to be installed on the target.
517
518 Args:
519 cpv: Fully qualified CPV (string) of the package.
520 slot: Slot identifier (string).
521 build_time: The BUILT_TIME value (string) of the binpkg.
522 optional: Whether package is optional on the target.
523
524 Returns:
525 A tuple (install, update) indicating whether to |install| the package and
526 whether it is an |update| to an existing package.
527
528 Raises:
529 ValueError: if slot is not provided.
530 """
531 # If not checking installed packages, always install.
532 if not self.target_db:
533 return True, False
534
535 cp = self._GetCP(cpv)
536 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
537 if target_pkg_info is not None:
538 if cpv != target_pkg_info.cpv:
Alex Klein9742cb62020-10-12 19:22:10 +0000539 attrs = package_info.SplitCPV(cpv)
540 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
David Pursell9476bf42015-03-30 13:34:27 -0700541 logging.debug('Updating %s: version (%s) different on target (%s)',
Alex Klein9742cb62020-10-12 19:22:10 +0000542 cp, attrs.version, target_attrs.version)
David Pursell9476bf42015-03-30 13:34:27 -0700543 return True, True
544
545 if build_time != target_pkg_info.build_time:
546 logging.debug('Updating %s: build time (%s) different on target (%s)',
547 cpv, build_time, target_pkg_info.build_time)
548 return True, True
549
550 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
551 cp, target_pkg_info.cpv, target_pkg_info.build_time)
552 return False, False
553
554 if optional:
555 logging.debug('Not installing %s: missing on target but optional', cp)
556 return False, False
557
558 logging.debug('Installing %s: missing on target and non-optional (%s)',
559 cp, cpv)
560 return True, False
561
562 def _ProcessDeps(self, deps, reverse):
563 """Enqueues dependencies for processing.
564
565 Args:
566 deps: List of dependencies to enqueue.
567 reverse: Whether these are reverse dependencies.
568 """
569 if not deps:
570 return
571
572 logging.debug('Processing %d %s dep(s)...', len(deps),
573 'reverse' if reverse else 'forward')
574 num_already_seen = 0
575 for dep in deps:
576 if self._EnqDep(dep, False, reverse):
577 logging.debug(' Queued dep %s', dep)
578 else:
579 num_already_seen += 1
580
581 if num_already_seen:
582 logging.debug('%d dep(s) already seen', num_already_seen)
583
584 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
585 """Returns a dictionary of packages that need to be installed on the target.
586
587 Args:
588 process_rdeps: Whether to trace forward dependencies.
589 process_rev_rdeps: Whether to trace backward dependencies as well.
590
591 Returns:
592 A dictionary mapping CP values (string) to tuples containing a CPV
593 (string), a slot (string), a boolean indicating whether the package
594 was initially listed in the queue, and a boolean indicating whether this
595 is an update to an existing package.
596 """
597 installs = {}
598 while self.queue:
599 dep, listed, optional = self._DeqDep()
600 cp, required_slot = dep
601 if cp in installs:
602 logging.debug('Already updating %s', cp)
603 continue
604
605 cp_slots = self.binpkgs_db.get(cp, dict())
606 logging.debug('Checking packages matching %s%s%s...', cp,
607 ' (slot: %s)' % required_slot if required_slot else '',
608 ' (optional)' if optional else '')
609 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400610 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700611 if required_slot and slot != required_slot:
612 continue
613
614 num_processed += 1
615 logging.debug(' Checking %s...', pkg_info.cpv)
616
617 install, update = self._NeedsInstall(pkg_info.cpv, slot,
618 pkg_info.build_time, optional)
619 if not install:
620 continue
621
622 installs[cp] = (pkg_info.cpv, slot, listed, update)
623
624 # Add forward and backward runtime dependencies to queue.
625 if process_rdeps:
626 self._ProcessDeps(pkg_info.rdeps, False)
627 if process_rev_rdeps:
628 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
629 if target_pkg_info:
630 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
631
632 if num_processed == 0:
633 logging.warning('No qualified bintree package corresponding to %s', cp)
634
635 return installs
636
637 def _SortInstalls(self, installs):
638 """Returns a sorted list of packages to install.
639
640 Performs a topological sort based on dependencies found in the binary
641 package database.
642
643 Args:
644 installs: Dictionary of packages to install indexed by CP.
645
646 Returns:
647 A list of package CPVs (string).
648
649 Raises:
650 ValueError: If dependency graph contains a cycle.
651 """
652 not_visited = set(installs.keys())
653 curr_path = []
654 sorted_installs = []
655
656 def SortFrom(cp):
657 """Traverses dependencies recursively, emitting nodes in reverse order."""
658 cpv, slot, _, _ = installs[cp]
659 if cpv in curr_path:
660 raise ValueError('Dependencies contain a cycle: %s -> %s' %
661 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
662 curr_path.append(cpv)
663 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
664 if rdep_cp in not_visited:
665 not_visited.remove(rdep_cp)
666 SortFrom(rdep_cp)
667
668 sorted_installs.append(cpv)
669 curr_path.pop()
670
671 # So long as there's more packages, keep expanding dependency paths.
672 while not_visited:
673 SortFrom(not_visited.pop())
674
675 return sorted_installs
676
677 def _EnqListedPkg(self, pkg):
678 """Finds and enqueues a listed package."""
679 cp, slot = self._FindPackage(pkg)
680 if cp not in self.binpkgs_db:
681 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
682 self._EnqDep((cp, slot), True, False)
683
684 def _EnqInstalledPkgs(self):
685 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400686 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700687 target_cp_slots = self.target_db.get(cp)
688 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400689 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700690 if slot in target_cp_slots:
691 self._EnqDep((cp, slot), True, False)
692
693 def Run(self, device, root, listed_pkgs, update, process_rdeps,
694 process_rev_rdeps):
695 """Computes the list of packages that need to be installed on a target.
696
697 Args:
698 device: Target handler object.
699 root: Package installation root.
700 listed_pkgs: Package names/files listed by the user.
701 update: Whether to read the target's installed package database.
702 process_rdeps: Whether to trace forward dependencies.
703 process_rev_rdeps: Whether to trace backward dependencies as well.
704
705 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700706 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
707 list of package CPVs (string) to install on the target in an order that
708 satisfies their inter-dependencies, |listed| the subset that was
709 requested by the user, and |num_updates| the number of packages being
710 installed over preexisting versions. Note that installation order should
711 be reversed for removal, |install_attrs| is a dictionary mapping a package
712 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700713 """
714 if process_rev_rdeps and not process_rdeps:
715 raise ValueError('Must processing forward deps when processing rev deps')
716 if process_rdeps and not update:
717 raise ValueError('Must check installed packages when processing deps')
718
719 if update:
720 logging.info('Initializing target intalled packages database...')
721 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
722
723 logging.info('Initializing binary packages database...')
724 self._InitBinpkgDB(process_rdeps)
725
726 logging.info('Finding listed package(s)...')
727 self._InitDepQueue()
728 for pkg in listed_pkgs:
729 if pkg == '@installed':
730 if not update:
731 raise ValueError(
732 'Must check installed packages when updating all of them.')
733 self._EnqInstalledPkgs()
734 else:
735 self._EnqListedPkg(pkg)
736
737 logging.info('Computing set of packages to install...')
738 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
739
740 num_updates = 0
741 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400742 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700743 if listed:
744 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400745 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700746 num_updates += 1
747
748 logging.info('Processed %d package(s), %d will be installed, %d are '
749 'updating existing packages',
750 len(self.seen), len(installs), num_updates)
751
752 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700753
754 install_attrs = {}
755 for pkg in sorted_installs:
Mike Frysingerada2d1c2020-03-20 05:02:06 -0400756 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700757 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
758 install_attrs[pkg] = {}
759 if dlc_id and dlc_package:
760 install_attrs[pkg][_DLC_ID] = dlc_id
761
762 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700763
764
Mike Frysinger63d35512021-01-26 23:16:13 -0500765def _Emerge(device, pkg_paths, root, extra_args=None):
766 """Copies |pkg_paths| to |device| and emerges them.
David Pursell9476bf42015-03-30 13:34:27 -0700767
768 Args:
769 device: A ChromiumOSDevice object.
Mike Frysinger63d35512021-01-26 23:16:13 -0500770 pkg_paths: (Local) paths to binary packages.
David Pursell9476bf42015-03-30 13:34:27 -0700771 root: Package installation root path.
772 extra_args: Extra arguments to pass to emerge.
773
774 Raises:
775 DeployError: Unrecoverable error during emerge.
776 """
Mike Frysinger63d35512021-01-26 23:16:13 -0500777 def path_to_name(pkg_path):
778 return os.path.basename(pkg_path)
779 def path_to_category(pkg_path):
780 return os.path.basename(os.path.dirname(pkg_path))
781
782 pkg_names = ', '.join(path_to_name(x) for x in pkg_paths)
783
David Pursell9476bf42015-03-30 13:34:27 -0700784 pkgroot = os.path.join(device.work_dir, 'packages')
Mike Frysinger15a4e012015-05-21 22:18:45 -0400785 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
786 # Clean out the dirs first if we had a previous emerge on the device so as to
787 # free up space for this emerge. The last emerge gets implicitly cleaned up
788 # when the device connection deletes its work_dir.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400789 device.run(
Mike Frysinger63d35512021-01-26 23:16:13 -0500790 f'cd {device.work_dir} && '
791 f'rm -rf packages portage-tmp && '
792 f'mkdir -p portage-tmp packages && '
793 f'cd packages && '
794 f'mkdir -p {" ".join(set(path_to_category(x) for x in pkg_paths))}',
795 shell=True, remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700796
David Pursell9476bf42015-03-30 13:34:27 -0700797 logging.info('Use portage temp dir %s', portage_tmpdir)
798
Ralph Nathane01ccf12015-04-16 10:40:32 -0700799 # This message is read by BrilloDeployOperation.
Mike Frysinger63d35512021-01-26 23:16:13 -0500800 logging.notice('Copying binpkgs to device.')
801 for pkg_path in pkg_paths:
802 pkg_name = path_to_name(pkg_path)
803 logging.info('Copying %s', pkg_name)
804 pkg_dir = os.path.join(pkgroot, path_to_category(pkg_path))
805 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True,
806 compress=False)
807
808 # This message is read by BrilloDeployOperation.
809 logging.notice('Installing: %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700810
811 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
812 # chromeos-base packages will be skipped due to the configuration
813 # in /etc/protage/make.profile/package.provided. However, there is
814 # a known bug that /usr/local/etc/portage is not setup properly
815 # (crbug.com/312041). This does not affect `cros deploy` because
816 # we do not use the preset PKGDIR.
817 extra_env = {
818 'FEATURES': '-sandbox',
819 'PKGDIR': pkgroot,
820 'PORTAGE_CONFIGROOT': '/usr/local',
821 'PORTAGE_TMPDIR': portage_tmpdir,
822 'PORTDIR': device.work_dir,
823 'CONFIG_PROTECT': '-*',
824 }
Mike Frysinger63d35512021-01-26 23:16:13 -0500825
Alex Kleinaaddc932020-01-30 15:02:24 -0700826 # --ignore-built-slot-operator-deps because we don't rebuild everything.
827 # It can cause errors, but that's expected with cros deploy since it's just a
828 # best effort to prevent developers avoid rebuilding an image every time.
Mike Frysinger63d35512021-01-26 23:16:13 -0500829 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', '--root',
830 root] + [os.path.join(pkgroot, *x.split('/')[-2:]) for x in pkg_paths]
David Pursell9476bf42015-03-30 13:34:27 -0700831 if extra_args:
832 cmd.append(extra_args)
833
Alex Kleinaaddc932020-01-30 15:02:24 -0700834 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
835 'packages built against the old version may not be able to '
836 'load the new .so. This is expected, and you will just need '
837 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700838 try:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400839 result = device.run(cmd, extra_env=extra_env, remote_sudo=True,
840 capture_output=True, debug_level=logging.INFO)
Greg Kerrb96c02c2019-02-08 14:32:41 -0800841
842 pattern = ('A requested package will not be merged because '
843 'it is listed in package.provided')
844 output = result.error.replace('\n', ' ').replace('\r', '')
845 if pattern in output:
846 error = ('Package failed to emerge: %s\n'
847 'Remove %s from /etc/portage/make.profile/'
848 'package.provided/chromeos-base.packages\n'
849 '(also see crbug.com/920140 for more context)\n'
850 % (pattern, pkg_name))
851 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700852 except Exception:
Mike Frysinger63d35512021-01-26 23:16:13 -0500853 logging.error('Failed to emerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700854 raise
855 else:
Mike Frysinger63d35512021-01-26 23:16:13 -0500856 # This message is read by BrilloDeployOperation.
857 logging.notice('Packages have been installed.')
David Pursell9476bf42015-03-30 13:34:27 -0700858
859
Qijiang Fand5958192019-07-26 12:32:36 +0900860def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800861 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900862
863 This reads the tarball from pkgpath, and calls restorecon on device to
864 restore SELinux context for files listed in the tarball, assuming those files
865 are installed to /
866
867 Args:
868 device: a ChromiumOSDevice object
869 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900870 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900871 """
Qijiang Fan8a945032019-04-25 20:53:29 +0900872 pkgroot = os.path.join(device.work_dir, 'packages')
873 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
874 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
875 # Testing shows restorecon splits on newlines instead of spaces.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400876 device.run(
Qijiang Fand5958192019-07-26 12:32:36 +0900877 ['cd', root, '&&',
878 'tar', 'tf', pkgpath_device, '|',
879 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900880 remote_sudo=True)
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900881
882
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700883def _GetPackagesByCPV(cpvs, strip, sysroot):
884 """Returns paths to binary packages corresponding to |cpvs|.
885
886 Args:
Alex Klein9742cb62020-10-12 19:22:10 +0000887 cpvs: List of CPV components given by package_info.SplitCPV().
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700888 strip: True to run strip_package.
889 sysroot: Sysroot path.
890
891 Returns:
892 List of paths corresponding to |cpvs|.
893
894 Raises:
895 DeployError: If a package is missing.
896 """
897 packages_dir = None
898 if strip:
899 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400900 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700901 ['strip_package', '--sysroot', sysroot] +
Alex Klein9742cb62020-10-12 19:22:10 +0000902 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700903 packages_dir = _STRIPPED_PACKAGES_DIR
904 except cros_build_lib.RunCommandError:
905 logging.error('Cannot strip packages %s',
Alex Klein9742cb62020-10-12 19:22:10 +0000906 ' '.join([str(cpv) for cpv in cpvs]))
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700907 raise
908
909 paths = []
910 for cpv in cpvs:
911 path = portage_util.GetBinaryPackagePath(
912 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
913 packages_dir=packages_dir)
914 if not path:
915 raise DeployError('Missing package %s.' % cpv)
916 paths.append(path)
917
918 return paths
919
920
921def _GetPackagesPaths(pkgs, strip, sysroot):
922 """Returns paths to binary |pkgs|.
923
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700924 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700925 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700926 strip: Whether or not to run strip_package for CPV packages.
927 sysroot: The sysroot path.
928
929 Returns:
930 List of paths corresponding to |pkgs|.
931 """
Alex Klein9742cb62020-10-12 19:22:10 +0000932 cpvs = [package_info.SplitCPV(p) for p in pkgs]
Ned Nguyend0db4072019-02-22 14:19:21 -0700933 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700934
935
Mike Frysinger22bb5502021-01-29 13:05:46 -0500936def _Unmerge(device, pkgs, root):
937 """Unmerges |pkgs| on |device|.
David Pursell9476bf42015-03-30 13:34:27 -0700938
939 Args:
940 device: A RemoteDevice object.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500941 pkgs: Package names.
David Pursell9476bf42015-03-30 13:34:27 -0700942 root: Package installation root path.
943 """
Mike Frysinger22bb5502021-01-29 13:05:46 -0500944 pkg_names = ', '.join(os.path.basename(x) for x in pkgs)
Ralph Nathane01ccf12015-04-16 10:40:32 -0700945 # This message is read by BrilloDeployOperation.
Mike Frysinger22bb5502021-01-29 13:05:46 -0500946 logging.notice('Unmerging %s.', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700947 cmd = ['qmerge', '--yes']
948 # Check if qmerge is available on the device. If not, use emerge.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400949 if device.run(['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700950 cmd = ['emerge']
951
Mike Frysinger22bb5502021-01-29 13:05:46 -0500952 cmd += ['--unmerge', '--root', root]
953 cmd.extend('f={x}' for x in pkgs)
David Pursell9476bf42015-03-30 13:34:27 -0700954 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700955 # Always showing the emerge output for clarity.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400956 device.run(cmd, capture_output=False, remote_sudo=True,
957 debug_level=logging.INFO)
David Pursell9476bf42015-03-30 13:34:27 -0700958 except Exception:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500959 logging.error('Failed to unmerge packages %s', pkg_names)
David Pursell9476bf42015-03-30 13:34:27 -0700960 raise
961 else:
Mike Frysinger22bb5502021-01-29 13:05:46 -0500962 # This message is read by BrilloDeployOperation.
963 logging.notice('Packages have been uninstalled.')
David Pursell9476bf42015-03-30 13:34:27 -0700964
965
966def _ConfirmDeploy(num_updates):
967 """Returns whether we can continue deployment."""
968 if num_updates > _MAX_UPDATES_NUM:
969 logging.warning(_MAX_UPDATES_WARNING)
970 return cros_build_lib.BooleanPrompt(default=False)
971
972 return True
973
974
Andrew06a5f812020-01-23 08:08:32 -0800975def _EmergePackages(pkgs, device, strip, sysroot, root, board, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800976 """Call _Emerge for each package in pkgs."""
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -0500977 if device.IsSELinuxAvailable():
978 enforced = device.IsSELinuxEnforced()
979 if enforced:
980 device.run(['setenforce', '0'])
981 else:
982 enforced = False
983
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700984 dlc_deployed = False
Mike Frysinger63d35512021-01-26 23:16:13 -0500985 # This message is read by BrilloDeployOperation.
986 logging.info('Preparing local packages for transfer.')
987 pkg_paths = _GetPackagesPaths(pkgs, strip, sysroot)
988 # Install all the packages in one pass so inter-package blockers work.
989 _Emerge(device, pkg_paths, root, extra_args=emerge_args)
990 logging.info('Updating SELinux settings & DLC images.')
991 for pkg_path in pkg_paths:
Ben Pastene5f03b052019-08-12 18:03:24 -0700992 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900993 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800994
995 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
996 if dlc_id and dlc_package:
Andrew06a5f812020-01-23 08:08:32 -0800997 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700998 dlc_deployed = True
Mike Frysinger5f4c2742021-02-08 14:37:23 -0500999
1000 if dlc_deployed:
1001 # Clean up empty directories created by emerging DLCs.
1002 device.run(['test', '-d', '/build/rootfs', '&&', 'rmdir',
1003 '--ignore-fail-on-non-empty', '/build/rootfs', '/build'],
1004 check=False)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001005
Mike Frysinger4eb5f4e2021-01-26 21:48:37 -05001006 if enforced:
1007 device.run(['setenforce', '1'])
1008
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001009 # Restart dlcservice so it picks up the newly installed DLC modules (in case
1010 # we installed new DLC images).
1011 if dlc_deployed:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001012 device.run(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -07001013
1014
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001015def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001016 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001017 dlc_uninstalled = False
Mike Frysinger22bb5502021-01-29 13:05:46 -05001018 _Unmerge(device, pkgs, root)
1019 logging.info('Cleaning up DLC images.')
Ralph Nathane01ccf12015-04-16 10:40:32 -07001020 for pkg in pkgs:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001021 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
1022 dlc_uninstalled = True
1023
1024 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
1025 # uninstalled DLC images).
1026 if dlc_uninstalled:
Mike Frysinger3459bf52020-03-31 00:52:11 -04001027 device.run(['restart', 'dlcservice'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001028
1029
1030def _UninstallDLCImage(device, pkg_attrs):
1031 """Uninstall a DLC image."""
1032 if _DLC_ID in pkg_attrs:
1033 dlc_id = pkg_attrs[_DLC_ID]
1034 logging.notice('Uninstalling DLC image for %s', dlc_id)
1035
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001036 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001037 return True
1038 else:
1039 logging.debug('DLC_ID not found in package')
1040 return False
1041
1042
Andrew06a5f812020-01-23 08:08:32 -08001043def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001044 """Deploy (install and mount) a DLC image."""
Andrew67b5fa72020-02-05 14:14:48 -08001045 # Build the DLC image if the image is outdated or doesn't exist.
Andrew5743d382020-06-16 09:55:04 -07001046 dlc_lib.InstallDlcImages(sysroot=sysroot, dlc_id=dlc_id, board=board)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001047
Andrewc7e1c6b2020-02-27 16:03:53 -08001048 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1049 try:
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001050 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Andrewc7e1c6b2020-02-27 16:03:53 -08001051 except cros_build_lib.RunCommandError as e:
1052 logging.info('Failed to uninstall DLC:%s. Continue anyway.',
1053 e.result.error)
1054 except Exception:
1055 logging.error('Failed to uninstall DLC.')
1056 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001057
Andrewc7e1c6b2020-02-27 16:03:53 -08001058 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1059 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1060 # location.
1061 logging.notice('Deploy the DLC image for %s', dlc_id)
Andrew5743d382020-06-16 09:55:04 -07001062 dlc_img_path_src = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1063 dlc_package, dlc_lib.DLC_IMAGE)
Andrewc7e1c6b2020-02-27 16:03:53 -08001064 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1065 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1066 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1067 # Create directories for DLC images.
1068 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1069 # Copy images to the destination directories.
1070 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
Andrew5743d382020-06-16 09:55:04 -07001071 dlc_lib.DLC_IMAGE),
Andrewc7e1c6b2020-02-27 16:03:53 -08001072 mode='rsync')
Andrew5743d382020-06-16 09:55:04 -07001073 device.run(['cp', os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1074 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE)])
Andrewc7e1c6b2020-02-27 16:03:53 -08001075
1076 # Set the proper perms and ownership so dlcservice can access the image.
1077 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1078 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001079
Andrew67b5fa72020-02-05 14:14:48 -08001080 # Copy metadata to device.
Andrew5743d382020-06-16 09:55:04 -07001081 dest_mata_dir = os.path.join('/', dlc_lib.DLC_META_DIR, dlc_id,
1082 dlc_package)
Andrew67b5fa72020-02-05 14:14:48 -08001083 device.run(['mkdir', '-p', dest_mata_dir])
Andrew5743d382020-06-16 09:55:04 -07001084 src_meta_dir = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1085 dlc_package, dlc_lib.DLC_TMP_META_DIR)
Andrew67b5fa72020-02-05 14:14:48 -08001086 device.CopyToDevice(src_meta_dir + '/',
1087 dest_mata_dir,
1088 mode='rsync',
1089 recursive=True,
1090 remote_sudo=True)
1091
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001092
1093def _GetDLCInfo(device, pkg_path, from_dut):
1094 """Returns information of a DLC given its package path.
1095
1096 Args:
1097 device: commandline.Device object; None to use the default device.
1098 pkg_path: path to the package.
1099 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1100 info from host.
1101
1102 Returns:
1103 A tuple (dlc_id, dlc_package).
1104 """
1105 environment_content = ''
1106 if from_dut:
1107 # On DUT, |pkg_path| is the directory which contains environment file.
1108 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001109 try:
1110 environment_data = device.CatFile(
1111 environment_path, max_size=None, encoding=None)
1112 except remote_access.CatFileError:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001113 # The package is not installed on DUT yet. Skip extracting info.
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001114 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001115 else:
1116 # On host, pkg_path is tbz2 file which contains environment file.
1117 # Extract the metadata of the package file.
1118 data = portage.xpak.tbz2(pkg_path).get_data()
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001119 environment_data = data[_ENVIRONMENT_FILENAME.encode('utf-8')]
1120
1121 # Extract the environment metadata.
1122 environment_content = bz2.decompress(environment_data)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001123
1124 with tempfile.NamedTemporaryFile() as f:
1125 # Dumps content into a file so we can use osutils.SourceEnvironment.
1126 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001127 osutils.WriteFile(path, environment_content, mode='wb')
Andrew67b5fa72020-02-05 14:14:48 -08001128 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE,
1129 _DLC_ENABLED))
1130
1131 dlc_enabled = content.get(_DLC_ENABLED)
1132 if dlc_enabled is not None and (dlc_enabled is False or
1133 str(dlc_enabled) == 'false'):
1134 logging.info('Installing DLC in rootfs.')
1135 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001136 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001137
1138
Gilad Arnolda0a98062015-07-07 08:34:27 -07001139def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1140 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1141 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1142 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001143 """Deploys packages to a device.
1144
1145 Args:
David Pursell2e773382015-04-03 14:30:47 -07001146 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001147 packages: List of packages (strings) to deploy to device.
1148 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001149 emerge: True to emerge package, False to unmerge.
1150 update: Check installed version on device.
1151 deep: Install dependencies also. Implies |update|.
1152 deep_rev: Install reverse dependencies. Implies |deep|.
1153 clean_binpkg: Clean outdated binary packages.
1154 root: Package installation root path.
1155 strip: Run strip_package to filter out preset paths in the package.
1156 emerge_args: Extra arguments to pass to emerge.
1157 ssh_private_key: Path to an SSH private key file; None to use test keys.
1158 ping: True to ping the device before trying to connect.
1159 force: Ignore sanity checks and prompts.
1160 dry_run: Print deployment plan but do not deploy anything.
1161
1162 Raises:
1163 ValueError: Invalid parameter or parameter combination.
1164 DeployError: Unrecoverable failure during deploy.
1165 """
1166 if deep_rev:
1167 deep = True
1168 if deep:
1169 update = True
1170
Gilad Arnolda0a98062015-07-07 08:34:27 -07001171 if not packages:
1172 raise DeployError('No packages provided, nothing to deploy.')
1173
David Pursell9476bf42015-03-30 13:34:27 -07001174 if update and not emerge:
1175 raise ValueError('Cannot update and unmerge.')
1176
David Pursell2e773382015-04-03 14:30:47 -07001177 if device:
1178 hostname, username, port = device.hostname, device.username, device.port
1179 else:
1180 hostname, username, port = None, None, None
1181
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001182 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001183 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001184 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001185 # Somewhat confusing to clobber, but here we are.
1186 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001187 with remote_access.ChromiumOSDeviceHandler(
1188 hostname, port=port, username=username, private_key=ssh_private_key,
1189 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001190 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001191
Gilad Arnolda0a98062015-07-07 08:34:27 -07001192 board = cros_build_lib.GetBoard(device_board=device.board,
1193 override_board=board)
1194 if not force and board != device.board:
1195 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001196 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001197
Mike Frysinger06a51c82021-04-06 11:39:17 -04001198 sysroot = build_target_lib.get_default_sysroot_path(board)
David Pursell9476bf42015-03-30 13:34:27 -07001199
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001200 # Don't bother trying to clean for unmerges. We won't use the local db,
1201 # and it just slows things down for the user.
1202 if emerge and clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001203 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001204 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001205
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001206 # Remount rootfs as writable if necessary.
1207 if not device.MountRootfsReadWrite():
1208 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001209
1210 # Obtain list of packages to upgrade/remove.
1211 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001212 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001213 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001214 if emerge:
1215 action_str = 'emerge'
1216 else:
1217 pkgs.reverse()
1218 action_str = 'unmerge'
1219
1220 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001221 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001222 return
1223
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001224 # Warn when the user installs & didn't `cros workon start`.
1225 if emerge:
Brian Norris2eee8892021-04-06 16:23:23 -07001226 all_workon = workon_helper.WorkonHelper(sysroot).ListAtoms(use_all=True)
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001227 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1228 for package in listed:
1229 cp = package_info.SplitCPV(package).cp
Brian Norris2eee8892021-04-06 16:23:23 -07001230 if cp in all_workon and cp not in worked_on_cps:
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001231 logging.warning(
1232 'Are you intentionally deploying unmodified packages, or did '
1233 'you forget to run `cros workon --board=$BOARD start %s`?', cp)
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +09001234
Ralph Nathane01ccf12015-04-16 10:40:32 -07001235 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001236 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001237 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001238
1239 if dry_run or not _ConfirmDeploy(num_updates):
1240 return
1241
Ralph Nathane01ccf12015-04-16 10:40:32 -07001242 # Select function (emerge or unmerge) and bind args.
1243 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001244 func = functools.partial(_EmergePackages, pkgs, device, strip,
Andrew06a5f812020-01-23 08:08:32 -08001245 sysroot, root, board, emerge_args)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001246 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001247 func = functools.partial(_UnmergePackages, pkgs, device, root,
1248 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001249
1250 # Call the function with the progress bar or with normal output.
1251 if command.UseProgressBar():
Mike Frysinger63d35512021-01-26 23:16:13 -05001252 op = BrilloDeployOperation(emerge)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001253 op.Run(func, log_level=logging.DEBUG)
1254 else:
1255 func()
David Pursell9476bf42015-03-30 13:34:27 -07001256
Ben Pastene5f03b052019-08-12 18:03:24 -07001257 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001258 if sum(x.count('selinux-policy') for x in pkgs):
1259 logging.warning(
1260 'Deploying SELinux policy will not take effect until reboot. '
Ian Barkley-Yeung6b2d8672020-08-13 18:58:10 -07001261 'SELinux policy is loaded by init. Also, changing the security '
1262 'contexts (labels) of a file will require building a new image '
1263 'and flashing the image onto the device.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001264
Mike Frysinger63d35512021-01-26 23:16:13 -05001265 # This message is read by BrilloDeployOperation.
David Pursell9476bf42015-03-30 13:34:27 -07001266 logging.warning('Please restart any updated services on the device, '
1267 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001268 except Exception:
1269 if lsb_release:
1270 lsb_entries = sorted(lsb_release.items())
1271 logging.info('Following are the LSB version details of the device:\n%s',
1272 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1273 raise