blob: 1183eb56ccf304b6904df51473a1830cfd11f5f1 [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.
70 MERGE_EVENTS = ['NOTICE: Copying', 'NOTICE: Installing', 'WARNING: Ignoring',
Achuith Bhandarkar0487c312019-04-22 12:19:25 -070071 'emerge --usepkg', 'has been installed.']
Ralph Nathan90475a12015-05-20 13:19:01 -070072 UNMERGE_EVENTS = ['NOTICE: Unmerging', 'has been uninstalled.']
Ralph Nathane01ccf12015-04-16 10:40:32 -070073
74 def __init__(self, pkg_count, emerge):
75 """Construct BrilloDeployOperation object.
76
77 Args:
78 pkg_count: number of packages being built.
79 emerge: True if emerge, False is unmerge.
80 """
81 super(BrilloDeployOperation, self).__init__()
Ralph Nathane01ccf12015-04-16 10:40:32 -070082 if emerge:
Ralph Nathan90475a12015-05-20 13:19:01 -070083 self._events = self.MERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070084 else:
Ralph Nathan90475a12015-05-20 13:19:01 -070085 self._events = self.UNMERGE_EVENTS
Ralph Nathane01ccf12015-04-16 10:40:32 -070086 self._total = pkg_count * len(self._events)
87 self._completed = 0
88
Ralph Nathandc14ed92015-04-22 11:17:40 -070089 def ParseOutput(self, output=None):
Ralph Nathane01ccf12015-04-16 10:40:32 -070090 """Parse the output of brillo deploy to update a progress bar."""
91 stdout = self._stdout.read()
92 stderr = self._stderr.read()
93 output = stdout + stderr
94 for event in self._events:
95 self._completed += output.count(event)
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040096 self.ProgressBar(self._completed / self._total)
Ralph Nathane01ccf12015-04-16 10:40:32 -070097
98
David Pursell9476bf42015-03-30 13:34:27 -070099class _InstallPackageScanner(object):
100 """Finds packages that need to be installed on a target device.
101
102 Scans the sysroot bintree, beginning with a user-provided list of packages,
103 to find all packages that need to be installed. If so instructed,
104 transitively scans forward (mandatory) and backward (optional) dependencies
105 as well. A package will be installed if missing on the target (mandatory
106 packages only), or it will be updated if its sysroot version and build time
107 are different from the target. Common usage:
108
109 pkg_scanner = _InstallPackageScanner(sysroot)
110 pkgs = pkg_scanner.Run(...)
111 """
112
113 class VartreeError(Exception):
114 """An error in the processing of the installed packages tree."""
115
116 class BintreeError(Exception):
117 """An error in the processing of the source binpkgs tree."""
118
119 class PkgInfo(object):
120 """A record containing package information."""
121
122 __slots__ = ('cpv', 'build_time', 'rdeps_raw', 'rdeps', 'rev_rdeps')
123
124 def __init__(self, cpv, build_time, rdeps_raw, rdeps=None, rev_rdeps=None):
125 self.cpv = cpv
126 self.build_time = build_time
127 self.rdeps_raw = rdeps_raw
128 self.rdeps = set() if rdeps is None else rdeps
129 self.rev_rdeps = set() if rev_rdeps is None else rev_rdeps
130
131 # Python snippet for dumping vartree info on the target. Instantiate using
132 # _GetVartreeSnippet().
133 _GET_VARTREE = """
David Pursell9476bf42015-03-30 13:34:27 -0700134import json
Gwendal Grignou99e6f532018-10-25 12:16:28 -0700135import os
136import portage
137
138# Normalize the path to match what portage will index.
139target_root = os.path.normpath('%(root)s')
140if not target_root.endswith('/'):
141 target_root += '/'
142trees = portage.create_trees(target_root=target_root, config_root='/')
143vartree = trees[target_root]['vartree']
David Pursell9476bf42015-03-30 13:34:27 -0700144pkg_info = []
145for cpv in vartree.dbapi.cpv_all():
146 slot, rdep_raw, build_time = vartree.dbapi.aux_get(
147 cpv, ('SLOT', 'RDEPEND', 'BUILD_TIME'))
148 pkg_info.append((cpv, slot, rdep_raw, build_time))
149
150print(json.dumps(pkg_info))
151"""
152
153 def __init__(self, sysroot):
154 self.sysroot = sysroot
155 # Members containing the sysroot (binpkg) and target (installed) package DB.
156 self.target_db = None
157 self.binpkgs_db = None
158 # Members for managing the dependency resolution work queue.
159 self.queue = None
160 self.seen = None
161 self.listed = None
162
163 @staticmethod
164 def _GetCP(cpv):
165 """Returns the CP value for a given CPV string."""
Alex Klein9742cb62020-10-12 19:22:10 +0000166 attrs = package_info.SplitCPV(cpv, strict=False)
Alex Klein9f93b482018-10-01 09:26:51 -0600167 if not attrs.cp:
David Pursell9476bf42015-03-30 13:34:27 -0700168 raise ValueError('Cannot get CP value for %s' % cpv)
Alex Klein9f93b482018-10-01 09:26:51 -0600169 return attrs.cp
David Pursell9476bf42015-03-30 13:34:27 -0700170
171 @staticmethod
172 def _InDB(cp, slot, db):
173 """Returns whether CP and slot are found in a database (if provided)."""
174 cp_slots = db.get(cp) if db else None
175 return cp_slots is not None and (not slot or slot in cp_slots)
176
177 @staticmethod
178 def _AtomStr(cp, slot):
179 """Returns 'CP:slot' if slot is non-empty, else just 'CP'."""
180 return '%s:%s' % (cp, slot) if slot else cp
181
182 @classmethod
183 def _GetVartreeSnippet(cls, root='/'):
184 """Returns a code snippet for dumping the vartree on the target.
185
186 Args:
187 root: The installation root.
188
189 Returns:
190 The said code snippet (string) with parameters filled in.
191 """
192 return cls._GET_VARTREE % {'root': root}
193
194 @classmethod
195 def _StripDepAtom(cls, dep_atom, installed_db=None):
196 """Strips a dependency atom and returns a (CP, slot) pair."""
197 # TODO(garnold) This is a gross simplification of ebuild dependency
198 # semantics, stripping and ignoring various qualifiers (versions, slots,
199 # USE flag, negation) and will likely need to be fixed. chromium:447366.
200
201 # Ignore unversioned blockers, leaving them for the user to resolve.
202 if dep_atom[0] == '!' and dep_atom[1] not in '<=>~':
203 return None, None
204
205 cp = dep_atom
206 slot = None
207 require_installed = False
208
209 # Versioned blockers should be updated, but only if already installed.
210 # These are often used for forcing cascaded updates of multiple packages,
211 # so we're treating them as ordinary constraints with hopes that it'll lead
212 # to the desired result.
213 if cp.startswith('!'):
214 cp = cp.lstrip('!')
215 require_installed = True
216
217 # Remove USE flags.
218 if '[' in cp:
219 cp = cp[:cp.index('[')] + cp[cp.index(']') + 1:]
220
221 # Separate the slot qualifier and strip off subslots.
222 if ':' in cp:
223 cp, slot = cp.split(':')
224 for delim in ('/', '='):
225 slot = slot.split(delim, 1)[0]
226
227 # Strip version wildcards (right), comparators (left).
228 cp = cp.rstrip('*')
229 cp = cp.lstrip('<=>~')
230
231 # Turn into CP form.
232 cp = cls._GetCP(cp)
233
234 if require_installed and not cls._InDB(cp, None, installed_db):
235 return None, None
236
237 return cp, slot
238
239 @classmethod
240 def _ProcessDepStr(cls, dep_str, installed_db, avail_db):
241 """Resolves and returns a list of dependencies from a dependency string.
242
243 This parses a dependency string and returns a list of package names and
244 slots. Other atom qualifiers (version, sub-slot, block) are ignored. When
245 resolving disjunctive deps, we include all choices that are fully present
246 in |installed_db|. If none is present, we choose an arbitrary one that is
247 available.
248
249 Args:
250 dep_str: A raw dependency string.
251 installed_db: A database of installed packages.
252 avail_db: A database of packages available for installation.
253
254 Returns:
255 A list of pairs (CP, slot).
256
257 Raises:
258 ValueError: the dependencies string is malformed.
259 """
260 def ProcessSubDeps(dep_exp, disjunct):
261 """Parses and processes a dependency (sub)expression."""
262 deps = set()
263 default_deps = set()
264 sub_disjunct = False
265 for dep_sub_exp in dep_exp:
266 sub_deps = set()
267
268 if isinstance(dep_sub_exp, (list, tuple)):
269 sub_deps = ProcessSubDeps(dep_sub_exp, sub_disjunct)
270 sub_disjunct = False
271 elif sub_disjunct:
272 raise ValueError('Malformed disjunctive operation in deps')
273 elif dep_sub_exp == '||':
274 sub_disjunct = True
275 elif dep_sub_exp.endswith('?'):
276 raise ValueError('Dependencies contain a conditional')
277 else:
278 cp, slot = cls._StripDepAtom(dep_sub_exp, installed_db)
279 if cp:
280 sub_deps = set([(cp, slot)])
281 elif disjunct:
282 raise ValueError('Atom in disjunct ignored')
283
284 # Handle sub-deps of a disjunctive expression.
285 if disjunct:
286 # Make the first available choice the default, for use in case that
287 # no option is installed.
288 if (not default_deps and avail_db is not None and
289 all([cls._InDB(cp, slot, avail_db) for cp, slot in sub_deps])):
290 default_deps = sub_deps
291
292 # If not all sub-deps are installed, then don't consider them.
293 if not all([cls._InDB(cp, slot, installed_db)
294 for cp, slot in sub_deps]):
295 sub_deps = set()
296
297 deps.update(sub_deps)
298
299 return deps or default_deps
300
301 try:
302 return ProcessSubDeps(portage.dep.paren_reduce(dep_str), False)
303 except portage.exception.InvalidDependString as e:
304 raise ValueError('Invalid dep string: %s' % e)
305 except ValueError as e:
306 raise ValueError('%s: %s' % (e, dep_str))
307
308 def _BuildDB(self, cpv_info, process_rdeps, process_rev_rdeps,
309 installed_db=None):
310 """Returns a database of packages given a list of CPV info.
311
312 Args:
313 cpv_info: A list of tuples containing package CPV and attributes.
314 process_rdeps: Whether to populate forward dependencies.
315 process_rev_rdeps: Whether to populate reverse dependencies.
316 installed_db: A database of installed packages for filtering disjunctive
317 choices against; if None, using own built database.
318
319 Returns:
320 A map from CP values to another dictionary that maps slots to package
321 attribute tuples. Tuples contain a CPV value (string), build time
322 (string), runtime dependencies (set), and reverse dependencies (set,
323 empty if not populated).
324
325 Raises:
326 ValueError: If more than one CPV occupies a single slot.
327 """
328 db = {}
329 logging.debug('Populating package DB...')
330 for cpv, slot, rdeps_raw, build_time in cpv_info:
331 cp = self._GetCP(cpv)
332 cp_slots = db.setdefault(cp, dict())
333 if slot in cp_slots:
334 raise ValueError('More than one package found for %s' %
335 self._AtomStr(cp, slot))
336 logging.debug(' %s -> %s, built %s, raw rdeps: %s',
337 self._AtomStr(cp, slot), cpv, build_time, rdeps_raw)
338 cp_slots[slot] = self.PkgInfo(cpv, build_time, rdeps_raw)
339
340 avail_db = db
341 if installed_db is None:
342 installed_db = db
343 avail_db = None
344
345 # Add approximate forward dependencies.
346 if process_rdeps:
347 logging.debug('Populating forward dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400348 for cp, cp_slots in db.items():
349 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700350 pkg_info.rdeps.update(self._ProcessDepStr(pkg_info.rdeps_raw,
351 installed_db, avail_db))
352 logging.debug(' %s (%s) processed rdeps: %s',
353 self._AtomStr(cp, slot), pkg_info.cpv,
354 ' '.join([self._AtomStr(rdep_cp, rdep_slot)
355 for rdep_cp, rdep_slot in pkg_info.rdeps]))
356
357 # Add approximate reverse dependencies (optional).
358 if process_rev_rdeps:
359 logging.debug('Populating reverse dependencies...')
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400360 for cp, cp_slots in db.items():
361 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700362 for rdep_cp, rdep_slot in pkg_info.rdeps:
363 to_slots = db.get(rdep_cp)
364 if not to_slots:
365 continue
366
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400367 for to_slot, to_pkg_info in to_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700368 if rdep_slot and to_slot != rdep_slot:
369 continue
370 logging.debug(' %s (%s) added as rev rdep for %s (%s)',
371 self._AtomStr(cp, slot), pkg_info.cpv,
372 self._AtomStr(rdep_cp, to_slot), to_pkg_info.cpv)
373 to_pkg_info.rev_rdeps.add((cp, slot))
374
375 return db
376
377 def _InitTargetVarDB(self, device, root, process_rdeps, process_rev_rdeps):
378 """Initializes a dictionary of packages installed on |device|."""
379 get_vartree_script = self._GetVartreeSnippet(root)
380 try:
Mike Frysinger345666a2017-10-06 00:26:21 -0400381 result = device.GetAgent().RemoteSh(['python'], remote_sudo=True,
David Pursell67a82762015-04-30 17:26:59 -0700382 input=get_vartree_script)
David Pursell9476bf42015-03-30 13:34:27 -0700383 except cros_build_lib.RunCommandError as e:
384 logging.error('Cannot get target vartree:\n%s', e.result.error)
385 raise
386
387 try:
388 self.target_db = self._BuildDB(json.loads(result.output),
389 process_rdeps, process_rev_rdeps)
390 except ValueError as e:
391 raise self.VartreeError(str(e))
392
393 def _InitBinpkgDB(self, process_rdeps):
394 """Initializes a dictionary of binary packages for updating the target."""
395 # Get build root trees; portage indexes require a trailing '/'.
396 build_root = os.path.join(self.sysroot, '')
397 trees = portage.create_trees(target_root=build_root, config_root=build_root)
398 bintree = trees[build_root]['bintree']
399 binpkgs_info = []
400 for cpv in bintree.dbapi.cpv_all():
401 slot, rdep_raw, build_time = bintree.dbapi.aux_get(
402 cpv, ['SLOT', 'RDEPEND', 'BUILD_TIME'])
403 binpkgs_info.append((cpv, slot, rdep_raw, build_time))
404
405 try:
406 self.binpkgs_db = self._BuildDB(binpkgs_info, process_rdeps, False,
407 installed_db=self.target_db)
408 except ValueError as e:
409 raise self.BintreeError(str(e))
410
411 def _InitDepQueue(self):
412 """Initializes the dependency work queue."""
413 self.queue = set()
414 self.seen = {}
415 self.listed = set()
416
417 def _EnqDep(self, dep, listed, optional):
418 """Enqueues a dependency if not seen before or if turned non-optional."""
419 if dep in self.seen and (optional or not self.seen[dep]):
420 return False
421
422 self.queue.add(dep)
423 self.seen[dep] = optional
424 if listed:
425 self.listed.add(dep)
426 return True
427
428 def _DeqDep(self):
429 """Dequeues and returns a dependency, its listed and optional flags.
430
431 This returns listed packages first, if any are present, to ensure that we
432 correctly mark them as such when they are first being processed.
433 """
434 if self.listed:
435 dep = self.listed.pop()
436 self.queue.remove(dep)
437 listed = True
438 else:
439 dep = self.queue.pop()
440 listed = False
441
442 return dep, listed, self.seen[dep]
443
444 def _FindPackageMatches(self, cpv_pattern):
445 """Returns list of binpkg (CP, slot) pairs that match |cpv_pattern|.
446
447 This is breaking |cpv_pattern| into its C, P and V components, each of
448 which may or may not be present or contain wildcards. It then scans the
449 binpkgs database to find all atoms that match these components, returning a
450 list of CP and slot qualifier. When the pattern does not specify a version,
451 or when a CP has only one slot in the binpkgs database, we omit the slot
452 qualifier in the result.
453
454 Args:
455 cpv_pattern: A CPV pattern, potentially partial and/or having wildcards.
456
457 Returns:
458 A list of (CPV, slot) pairs of packages in the binpkgs database that
459 match the pattern.
460 """
Alex Klein9742cb62020-10-12 19:22:10 +0000461 attrs = package_info.SplitCPV(cpv_pattern, strict=False)
David Pursell9476bf42015-03-30 13:34:27 -0700462 cp_pattern = os.path.join(attrs.category or '*', attrs.package or '*')
463 matches = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400464 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700465 if not fnmatch.fnmatchcase(cp, cp_pattern):
466 continue
467
468 # If no version attribute was given or there's only one slot, omit the
469 # slot qualifier.
Alex Klein9742cb62020-10-12 19:22:10 +0000470 if not attrs.version or len(cp_slots) == 1:
David Pursell9476bf42015-03-30 13:34:27 -0700471 matches.append((cp, None))
472 else:
Alex Klein9742cb62020-10-12 19:22:10 +0000473 cpv_pattern = '%s-%s' % (cp, attrs.version)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400474 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700475 if fnmatch.fnmatchcase(pkg_info.cpv, cpv_pattern):
476 matches.append((cp, slot))
477
478 return matches
479
480 def _FindPackage(self, pkg):
481 """Returns the (CP, slot) pair for a package matching |pkg|.
482
483 Args:
484 pkg: Path to a binary package or a (partial) package CPV specifier.
485
486 Returns:
487 A (CP, slot) pair for the given package; slot may be None (unspecified).
488
489 Raises:
490 ValueError: if |pkg| is not a binpkg file nor does it match something
491 that's in the bintree.
492 """
493 if pkg.endswith('.tbz2') and os.path.isfile(pkg):
494 package = os.path.basename(os.path.splitext(pkg)[0])
495 category = os.path.basename(os.path.dirname(pkg))
496 return self._GetCP(os.path.join(category, package)), None
497
498 matches = self._FindPackageMatches(pkg)
499 if not matches:
500 raise ValueError('No package found for %s' % pkg)
501
502 idx = 0
503 if len(matches) > 1:
504 # Ask user to pick among multiple matches.
505 idx = cros_build_lib.GetChoice('Multiple matches found for %s: ' % pkg,
506 ['%s:%s' % (cp, slot) if slot else cp
507 for cp, slot in matches])
508
509 return matches[idx]
510
511 def _NeedsInstall(self, cpv, slot, build_time, optional):
512 """Returns whether a package needs to be installed on the target.
513
514 Args:
515 cpv: Fully qualified CPV (string) of the package.
516 slot: Slot identifier (string).
517 build_time: The BUILT_TIME value (string) of the binpkg.
518 optional: Whether package is optional on the target.
519
520 Returns:
521 A tuple (install, update) indicating whether to |install| the package and
522 whether it is an |update| to an existing package.
523
524 Raises:
525 ValueError: if slot is not provided.
526 """
527 # If not checking installed packages, always install.
528 if not self.target_db:
529 return True, False
530
531 cp = self._GetCP(cpv)
532 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
533 if target_pkg_info is not None:
534 if cpv != target_pkg_info.cpv:
Alex Klein9742cb62020-10-12 19:22:10 +0000535 attrs = package_info.SplitCPV(cpv)
536 target_attrs = package_info.SplitCPV(target_pkg_info.cpv)
David Pursell9476bf42015-03-30 13:34:27 -0700537 logging.debug('Updating %s: version (%s) different on target (%s)',
Alex Klein9742cb62020-10-12 19:22:10 +0000538 cp, attrs.version, target_attrs.version)
David Pursell9476bf42015-03-30 13:34:27 -0700539 return True, True
540
541 if build_time != target_pkg_info.build_time:
542 logging.debug('Updating %s: build time (%s) different on target (%s)',
543 cpv, build_time, target_pkg_info.build_time)
544 return True, True
545
546 logging.debug('Not updating %s: already up-to-date (%s, built %s)',
547 cp, target_pkg_info.cpv, target_pkg_info.build_time)
548 return False, False
549
550 if optional:
551 logging.debug('Not installing %s: missing on target but optional', cp)
552 return False, False
553
554 logging.debug('Installing %s: missing on target and non-optional (%s)',
555 cp, cpv)
556 return True, False
557
558 def _ProcessDeps(self, deps, reverse):
559 """Enqueues dependencies for processing.
560
561 Args:
562 deps: List of dependencies to enqueue.
563 reverse: Whether these are reverse dependencies.
564 """
565 if not deps:
566 return
567
568 logging.debug('Processing %d %s dep(s)...', len(deps),
569 'reverse' if reverse else 'forward')
570 num_already_seen = 0
571 for dep in deps:
572 if self._EnqDep(dep, False, reverse):
573 logging.debug(' Queued dep %s', dep)
574 else:
575 num_already_seen += 1
576
577 if num_already_seen:
578 logging.debug('%d dep(s) already seen', num_already_seen)
579
580 def _ComputeInstalls(self, process_rdeps, process_rev_rdeps):
581 """Returns a dictionary of packages that need to be installed on the target.
582
583 Args:
584 process_rdeps: Whether to trace forward dependencies.
585 process_rev_rdeps: Whether to trace backward dependencies as well.
586
587 Returns:
588 A dictionary mapping CP values (string) to tuples containing a CPV
589 (string), a slot (string), a boolean indicating whether the package
590 was initially listed in the queue, and a boolean indicating whether this
591 is an update to an existing package.
592 """
593 installs = {}
594 while self.queue:
595 dep, listed, optional = self._DeqDep()
596 cp, required_slot = dep
597 if cp in installs:
598 logging.debug('Already updating %s', cp)
599 continue
600
601 cp_slots = self.binpkgs_db.get(cp, dict())
602 logging.debug('Checking packages matching %s%s%s...', cp,
603 ' (slot: %s)' % required_slot if required_slot else '',
604 ' (optional)' if optional else '')
605 num_processed = 0
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400606 for slot, pkg_info in cp_slots.items():
David Pursell9476bf42015-03-30 13:34:27 -0700607 if required_slot and slot != required_slot:
608 continue
609
610 num_processed += 1
611 logging.debug(' Checking %s...', pkg_info.cpv)
612
613 install, update = self._NeedsInstall(pkg_info.cpv, slot,
614 pkg_info.build_time, optional)
615 if not install:
616 continue
617
618 installs[cp] = (pkg_info.cpv, slot, listed, update)
619
620 # Add forward and backward runtime dependencies to queue.
621 if process_rdeps:
622 self._ProcessDeps(pkg_info.rdeps, False)
623 if process_rev_rdeps:
624 target_pkg_info = self.target_db.get(cp, dict()).get(slot)
625 if target_pkg_info:
626 self._ProcessDeps(target_pkg_info.rev_rdeps, True)
627
628 if num_processed == 0:
629 logging.warning('No qualified bintree package corresponding to %s', cp)
630
631 return installs
632
633 def _SortInstalls(self, installs):
634 """Returns a sorted list of packages to install.
635
636 Performs a topological sort based on dependencies found in the binary
637 package database.
638
639 Args:
640 installs: Dictionary of packages to install indexed by CP.
641
642 Returns:
643 A list of package CPVs (string).
644
645 Raises:
646 ValueError: If dependency graph contains a cycle.
647 """
648 not_visited = set(installs.keys())
649 curr_path = []
650 sorted_installs = []
651
652 def SortFrom(cp):
653 """Traverses dependencies recursively, emitting nodes in reverse order."""
654 cpv, slot, _, _ = installs[cp]
655 if cpv in curr_path:
656 raise ValueError('Dependencies contain a cycle: %s -> %s' %
657 (' -> '.join(curr_path[curr_path.index(cpv):]), cpv))
658 curr_path.append(cpv)
659 for rdep_cp, _ in self.binpkgs_db[cp][slot].rdeps:
660 if rdep_cp in not_visited:
661 not_visited.remove(rdep_cp)
662 SortFrom(rdep_cp)
663
664 sorted_installs.append(cpv)
665 curr_path.pop()
666
667 # So long as there's more packages, keep expanding dependency paths.
668 while not_visited:
669 SortFrom(not_visited.pop())
670
671 return sorted_installs
672
673 def _EnqListedPkg(self, pkg):
674 """Finds and enqueues a listed package."""
675 cp, slot = self._FindPackage(pkg)
676 if cp not in self.binpkgs_db:
677 raise self.BintreeError('Package %s not found in binpkgs tree' % pkg)
678 self._EnqDep((cp, slot), True, False)
679
680 def _EnqInstalledPkgs(self):
681 """Enqueues all available binary packages that are already installed."""
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400682 for cp, cp_slots in self.binpkgs_db.items():
David Pursell9476bf42015-03-30 13:34:27 -0700683 target_cp_slots = self.target_db.get(cp)
684 if target_cp_slots:
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400685 for slot in cp_slots.keys():
David Pursell9476bf42015-03-30 13:34:27 -0700686 if slot in target_cp_slots:
687 self._EnqDep((cp, slot), True, False)
688
689 def Run(self, device, root, listed_pkgs, update, process_rdeps,
690 process_rev_rdeps):
691 """Computes the list of packages that need to be installed on a target.
692
693 Args:
694 device: Target handler object.
695 root: Package installation root.
696 listed_pkgs: Package names/files listed by the user.
697 update: Whether to read the target's installed package database.
698 process_rdeps: Whether to trace forward dependencies.
699 process_rev_rdeps: Whether to trace backward dependencies as well.
700
701 Returns:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700702 A tuple (sorted, listed, num_updates, install_attrs) where |sorted| is a
703 list of package CPVs (string) to install on the target in an order that
704 satisfies their inter-dependencies, |listed| the subset that was
705 requested by the user, and |num_updates| the number of packages being
706 installed over preexisting versions. Note that installation order should
707 be reversed for removal, |install_attrs| is a dictionary mapping a package
708 CPV (string) to some of its extracted environment attributes.
David Pursell9476bf42015-03-30 13:34:27 -0700709 """
710 if process_rev_rdeps and not process_rdeps:
711 raise ValueError('Must processing forward deps when processing rev deps')
712 if process_rdeps and not update:
713 raise ValueError('Must check installed packages when processing deps')
714
715 if update:
716 logging.info('Initializing target intalled packages database...')
717 self._InitTargetVarDB(device, root, process_rdeps, process_rev_rdeps)
718
719 logging.info('Initializing binary packages database...')
720 self._InitBinpkgDB(process_rdeps)
721
722 logging.info('Finding listed package(s)...')
723 self._InitDepQueue()
724 for pkg in listed_pkgs:
725 if pkg == '@installed':
726 if not update:
727 raise ValueError(
728 'Must check installed packages when updating all of them.')
729 self._EnqInstalledPkgs()
730 else:
731 self._EnqListedPkg(pkg)
732
733 logging.info('Computing set of packages to install...')
734 installs = self._ComputeInstalls(process_rdeps, process_rev_rdeps)
735
736 num_updates = 0
737 listed_installs = []
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400738 for cpv, _, listed, isupdate in installs.values():
David Pursell9476bf42015-03-30 13:34:27 -0700739 if listed:
740 listed_installs.append(cpv)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400741 if isupdate:
David Pursell9476bf42015-03-30 13:34:27 -0700742 num_updates += 1
743
744 logging.info('Processed %d package(s), %d will be installed, %d are '
745 'updating existing packages',
746 len(self.seen), len(installs), num_updates)
747
748 sorted_installs = self._SortInstalls(installs)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700749
750 install_attrs = {}
751 for pkg in sorted_installs:
Mike Frysingerada2d1c2020-03-20 05:02:06 -0400752 pkg_path = os.path.join(root, portage_util.VDB_PATH, pkg)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700753 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=True)
754 install_attrs[pkg] = {}
755 if dlc_id and dlc_package:
756 install_attrs[pkg][_DLC_ID] = dlc_id
757
758 return sorted_installs, listed_installs, num_updates, install_attrs
David Pursell9476bf42015-03-30 13:34:27 -0700759
760
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700761def _Emerge(device, pkg_path, root, extra_args=None):
David Pursell9476bf42015-03-30 13:34:27 -0700762 """Copies |pkg| to |device| and emerges it.
763
764 Args:
765 device: A ChromiumOSDevice object.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700766 pkg_path: A path to a binary package.
David Pursell9476bf42015-03-30 13:34:27 -0700767 root: Package installation root path.
768 extra_args: Extra arguments to pass to emerge.
769
770 Raises:
771 DeployError: Unrecoverable error during emerge.
772 """
David Pursell9476bf42015-03-30 13:34:27 -0700773 pkgroot = os.path.join(device.work_dir, 'packages')
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700774 pkg_name = os.path.basename(pkg_path)
775 pkg_dirname = os.path.basename(os.path.dirname(pkg_path))
David Pursell9476bf42015-03-30 13:34:27 -0700776 pkg_dir = os.path.join(pkgroot, pkg_dirname)
Mike Frysinger15a4e012015-05-21 22:18:45 -0400777 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
778 # Clean out the dirs first if we had a previous emerge on the device so as to
779 # free up space for this emerge. The last emerge gets implicitly cleaned up
780 # when the device connection deletes its work_dir.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400781 device.run(
Mike Frysinger15a4e012015-05-21 22:18:45 -0400782 ['rm', '-rf', pkg_dir, portage_tmpdir, '&&',
783 'mkdir', '-p', pkg_dir, portage_tmpdir], remote_sudo=True)
David Pursell9476bf42015-03-30 13:34:27 -0700784
Ralph Nathane01ccf12015-04-16 10:40:32 -0700785 # This message is read by BrilloDeployOperation.
786 logging.notice('Copying %s to device.', pkg_name)
Mike Frysingerc664c662021-01-27 01:11:59 -0500787 device.CopyToDevice(pkg_path, pkg_dir, mode='rsync', remote_sudo=True,
788 compress=False)
David Pursell9476bf42015-03-30 13:34:27 -0700789
David Pursell9476bf42015-03-30 13:34:27 -0700790 logging.info('Use portage temp dir %s', portage_tmpdir)
791
Ralph Nathane01ccf12015-04-16 10:40:32 -0700792 # This message is read by BrilloDeployOperation.
793 logging.notice('Installing %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700794 pkg_path = os.path.join(pkg_dir, pkg_name)
795
796 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
797 # chromeos-base packages will be skipped due to the configuration
798 # in /etc/protage/make.profile/package.provided. However, there is
799 # a known bug that /usr/local/etc/portage is not setup properly
800 # (crbug.com/312041). This does not affect `cros deploy` because
801 # we do not use the preset PKGDIR.
802 extra_env = {
803 'FEATURES': '-sandbox',
804 'PKGDIR': pkgroot,
805 'PORTAGE_CONFIGROOT': '/usr/local',
806 'PORTAGE_TMPDIR': portage_tmpdir,
807 'PORTDIR': device.work_dir,
808 'CONFIG_PROTECT': '-*',
809 }
Alex Kleinaaddc932020-01-30 15:02:24 -0700810 # --ignore-built-slot-operator-deps because we don't rebuild everything.
811 # It can cause errors, but that's expected with cros deploy since it's just a
812 # best effort to prevent developers avoid rebuilding an image every time.
813 cmd = ['emerge', '--usepkg', '--ignore-built-slot-operator-deps=y', pkg_path,
814 '--root=%s' % root]
David Pursell9476bf42015-03-30 13:34:27 -0700815 if extra_args:
816 cmd.append(extra_args)
817
Alex Kleinaaddc932020-01-30 15:02:24 -0700818 logging.warning('Ignoring slot dependencies! This may break things! e.g. '
819 'packages built against the old version may not be able to '
820 'load the new .so. This is expected, and you will just need '
821 'to build and flash a new image if you have problems.')
David Pursell9476bf42015-03-30 13:34:27 -0700822 try:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400823 result = device.run(cmd, extra_env=extra_env, remote_sudo=True,
824 capture_output=True, debug_level=logging.INFO)
Greg Kerrb96c02c2019-02-08 14:32:41 -0800825
826 pattern = ('A requested package will not be merged because '
827 'it is listed in package.provided')
828 output = result.error.replace('\n', ' ').replace('\r', '')
829 if pattern in output:
830 error = ('Package failed to emerge: %s\n'
831 'Remove %s from /etc/portage/make.profile/'
832 'package.provided/chromeos-base.packages\n'
833 '(also see crbug.com/920140 for more context)\n'
834 % (pattern, pkg_name))
835 cros_build_lib.Die(error)
David Pursell9476bf42015-03-30 13:34:27 -0700836 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700837 logging.error('Failed to emerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700838 raise
839 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700840 logging.notice('%s has been installed.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700841
842
Qijiang Fand5958192019-07-26 12:32:36 +0900843def _RestoreSELinuxContext(device, pkgpath, root):
Andrewc7e1c6b2020-02-27 16:03:53 -0800844 """Restore SELinux context for files in a given package.
Qijiang Fan8a945032019-04-25 20:53:29 +0900845
846 This reads the tarball from pkgpath, and calls restorecon on device to
847 restore SELinux context for files listed in the tarball, assuming those files
848 are installed to /
849
850 Args:
851 device: a ChromiumOSDevice object
852 pkgpath: path to tarball
Qijiang Fand5958192019-07-26 12:32:36 +0900853 root: Package installation root path.
Qijiang Fan8a945032019-04-25 20:53:29 +0900854 """
Ben Pastene5f03b052019-08-12 18:03:24 -0700855 enforced = device.IsSELinuxEnforced()
Qijiang Fan8a945032019-04-25 20:53:29 +0900856 if enforced:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400857 device.run(['setenforce', '0'])
Qijiang Fan8a945032019-04-25 20:53:29 +0900858 pkgroot = os.path.join(device.work_dir, 'packages')
859 pkg_dirname = os.path.basename(os.path.dirname(pkgpath))
860 pkgpath_device = os.path.join(pkgroot, pkg_dirname, os.path.basename(pkgpath))
861 # Testing shows restorecon splits on newlines instead of spaces.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400862 device.run(
Qijiang Fand5958192019-07-26 12:32:36 +0900863 ['cd', root, '&&',
864 'tar', 'tf', pkgpath_device, '|',
865 'restorecon', '-i', '-f', '-'],
Qijiang Fan8a945032019-04-25 20:53:29 +0900866 remote_sudo=True)
867 if enforced:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400868 device.run(['setenforce', '1'])
Qijiang Fan352d0eb2019-02-25 13:10:08 +0900869
870
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700871def _GetPackagesByCPV(cpvs, strip, sysroot):
872 """Returns paths to binary packages corresponding to |cpvs|.
873
874 Args:
Alex Klein9742cb62020-10-12 19:22:10 +0000875 cpvs: List of CPV components given by package_info.SplitCPV().
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700876 strip: True to run strip_package.
877 sysroot: Sysroot path.
878
879 Returns:
880 List of paths corresponding to |cpvs|.
881
882 Raises:
883 DeployError: If a package is missing.
884 """
885 packages_dir = None
886 if strip:
887 try:
Mike Frysinger45602c72019-09-22 02:15:11 -0400888 cros_build_lib.run(
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700889 ['strip_package', '--sysroot', sysroot] +
Alex Klein9742cb62020-10-12 19:22:10 +0000890 [cpv.cpf for cpv in cpvs])
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700891 packages_dir = _STRIPPED_PACKAGES_DIR
892 except cros_build_lib.RunCommandError:
893 logging.error('Cannot strip packages %s',
Alex Klein9742cb62020-10-12 19:22:10 +0000894 ' '.join([str(cpv) for cpv in cpvs]))
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700895 raise
896
897 paths = []
898 for cpv in cpvs:
899 path = portage_util.GetBinaryPackagePath(
900 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
901 packages_dir=packages_dir)
902 if not path:
903 raise DeployError('Missing package %s.' % cpv)
904 paths.append(path)
905
906 return paths
907
908
909def _GetPackagesPaths(pkgs, strip, sysroot):
910 """Returns paths to binary |pkgs|.
911
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700912 Args:
Ned Nguyend0db4072019-02-22 14:19:21 -0700913 pkgs: List of package CPVs string.
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700914 strip: Whether or not to run strip_package for CPV packages.
915 sysroot: The sysroot path.
916
917 Returns:
918 List of paths corresponding to |pkgs|.
919 """
Alex Klein9742cb62020-10-12 19:22:10 +0000920 cpvs = [package_info.SplitCPV(p) for p in pkgs]
Ned Nguyend0db4072019-02-22 14:19:21 -0700921 return _GetPackagesByCPV(cpvs, strip, sysroot)
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700922
923
David Pursell9476bf42015-03-30 13:34:27 -0700924def _Unmerge(device, pkg, root):
925 """Unmerges |pkg| on |device|.
926
927 Args:
928 device: A RemoteDevice object.
929 pkg: A package name.
930 root: Package installation root path.
931 """
Ralph Nathane01ccf12015-04-16 10:40:32 -0700932 pkg_name = os.path.basename(pkg)
933 # This message is read by BrilloDeployOperation.
934 logging.notice('Unmerging %s.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700935 cmd = ['qmerge', '--yes']
936 # Check if qmerge is available on the device. If not, use emerge.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400937 if device.run(['qmerge', '--version'], check=False).returncode != 0:
David Pursell9476bf42015-03-30 13:34:27 -0700938 cmd = ['emerge']
939
Mike Frysinger671ca642020-12-04 02:29:42 -0500940 cmd.extend(['--unmerge', f'={pkg}', '--root=%s' % root])
David Pursell9476bf42015-03-30 13:34:27 -0700941 try:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700942 # Always showing the emerge output for clarity.
Mike Frysinger3459bf52020-03-31 00:52:11 -0400943 device.run(cmd, capture_output=False, remote_sudo=True,
944 debug_level=logging.INFO)
David Pursell9476bf42015-03-30 13:34:27 -0700945 except Exception:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700946 logging.error('Failed to unmerge package %s', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700947 raise
948 else:
Ralph Nathane01ccf12015-04-16 10:40:32 -0700949 logging.notice('%s has been uninstalled.', pkg_name)
David Pursell9476bf42015-03-30 13:34:27 -0700950
951
952def _ConfirmDeploy(num_updates):
953 """Returns whether we can continue deployment."""
954 if num_updates > _MAX_UPDATES_NUM:
955 logging.warning(_MAX_UPDATES_WARNING)
956 return cros_build_lib.BooleanPrompt(default=False)
957
958 return True
959
960
Andrew06a5f812020-01-23 08:08:32 -0800961def _EmergePackages(pkgs, device, strip, sysroot, root, board, emerge_args):
Andrewc7e1c6b2020-02-27 16:03:53 -0800962 """Call _Emerge for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700963 dlc_deployed = False
Gilad Arnold0e1b1da2015-06-10 06:41:05 -0700964 for pkg_path in _GetPackagesPaths(pkgs, strip, sysroot):
965 _Emerge(device, pkg_path, root, extra_args=emerge_args)
Ben Pastene5f03b052019-08-12 18:03:24 -0700966 if device.IsSELinuxAvailable():
Qijiang Fand5958192019-07-26 12:32:36 +0900967 _RestoreSELinuxContext(device, pkg_path, root)
Andrewc7e1c6b2020-02-27 16:03:53 -0800968
969 dlc_id, dlc_package = _GetDLCInfo(device, pkg_path, from_dut=False)
970 if dlc_id and dlc_package:
Andrew06a5f812020-01-23 08:08:32 -0800971 _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700972 dlc_deployed = True
Andrew67b5fa72020-02-05 14:14:48 -0800973 # Clean up empty directories created by emerging DLCs.
974 device.run(['test', '-d', '/build/rootfs', '&&', 'rmdir',
975 '--ignore-fail-on-non-empty', '/build/rootfs', '/build'],
976 check=False)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700977
978 # Restart dlcservice so it picks up the newly installed DLC modules (in case
979 # we installed new DLC images).
980 if dlc_deployed:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400981 device.run(['restart', 'dlcservice'])
Ralph Nathane01ccf12015-04-16 10:40:32 -0700982
983
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700984def _UnmergePackages(pkgs, device, root, pkgs_attrs):
Ralph Nathane01ccf12015-04-16 10:40:32 -0700985 """Call _Unmege for each package in pkgs."""
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700986 dlc_uninstalled = False
Ralph Nathane01ccf12015-04-16 10:40:32 -0700987 for pkg in pkgs:
988 _Unmerge(device, pkg, root)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700989 if _UninstallDLCImage(device, pkgs_attrs[pkg]):
990 dlc_uninstalled = True
991
992 # Restart dlcservice so it picks up the uninstalled DLC modules (in case we
993 # uninstalled DLC images).
994 if dlc_uninstalled:
Mike Frysinger3459bf52020-03-31 00:52:11 -0400995 device.run(['restart', 'dlcservice'])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -0700996
997
998def _UninstallDLCImage(device, pkg_attrs):
999 """Uninstall a DLC image."""
1000 if _DLC_ID in pkg_attrs:
1001 dlc_id = pkg_attrs[_DLC_ID]
1002 logging.notice('Uninstalling DLC image for %s', dlc_id)
1003
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001004 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001005 return True
1006 else:
1007 logging.debug('DLC_ID not found in package')
1008 return False
1009
1010
Andrew06a5f812020-01-23 08:08:32 -08001011def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001012 """Deploy (install and mount) a DLC image."""
Andrew67b5fa72020-02-05 14:14:48 -08001013 # Build the DLC image if the image is outdated or doesn't exist.
Andrew5743d382020-06-16 09:55:04 -07001014 dlc_lib.InstallDlcImages(sysroot=sysroot, dlc_id=dlc_id, board=board)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001015
Andrewc7e1c6b2020-02-27 16:03:53 -08001016 logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
1017 try:
Jae Hoon Kim964ed7e2020-05-15 13:59:23 -07001018 device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
Andrewc7e1c6b2020-02-27 16:03:53 -08001019 except cros_build_lib.RunCommandError as e:
1020 logging.info('Failed to uninstall DLC:%s. Continue anyway.',
1021 e.result.error)
1022 except Exception:
1023 logging.error('Failed to uninstall DLC.')
1024 raise
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001025
Andrewc7e1c6b2020-02-27 16:03:53 -08001026 # TODO(andrewlassalle): Copy the DLC image to the preload location instead
1027 # of to dlc_a and dlc_b, and let dlcserive install the images to their final
1028 # location.
1029 logging.notice('Deploy the DLC image for %s', dlc_id)
Andrew5743d382020-06-16 09:55:04 -07001030 dlc_img_path_src = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1031 dlc_package, dlc_lib.DLC_IMAGE)
Andrewc7e1c6b2020-02-27 16:03:53 -08001032 dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
1033 dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
1034 dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
1035 # Create directories for DLC images.
1036 device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
1037 # Copy images to the destination directories.
1038 device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
Andrew5743d382020-06-16 09:55:04 -07001039 dlc_lib.DLC_IMAGE),
Andrewc7e1c6b2020-02-27 16:03:53 -08001040 mode='rsync')
Andrew5743d382020-06-16 09:55:04 -07001041 device.run(['cp', os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
1042 os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE)])
Andrewc7e1c6b2020-02-27 16:03:53 -08001043
1044 # Set the proper perms and ownership so dlcservice can access the image.
1045 device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
1046 device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001047
Andrew67b5fa72020-02-05 14:14:48 -08001048 # Copy metadata to device.
Andrew5743d382020-06-16 09:55:04 -07001049 dest_mata_dir = os.path.join('/', dlc_lib.DLC_META_DIR, dlc_id,
1050 dlc_package)
Andrew67b5fa72020-02-05 14:14:48 -08001051 device.run(['mkdir', '-p', dest_mata_dir])
Andrew5743d382020-06-16 09:55:04 -07001052 src_meta_dir = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
1053 dlc_package, dlc_lib.DLC_TMP_META_DIR)
Andrew67b5fa72020-02-05 14:14:48 -08001054 device.CopyToDevice(src_meta_dir + '/',
1055 dest_mata_dir,
1056 mode='rsync',
1057 recursive=True,
1058 remote_sudo=True)
1059
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001060
1061def _GetDLCInfo(device, pkg_path, from_dut):
1062 """Returns information of a DLC given its package path.
1063
1064 Args:
1065 device: commandline.Device object; None to use the default device.
1066 pkg_path: path to the package.
1067 from_dut: True if extracting DLC info from DUT, False if extracting DLC
1068 info from host.
1069
1070 Returns:
1071 A tuple (dlc_id, dlc_package).
1072 """
1073 environment_content = ''
1074 if from_dut:
1075 # On DUT, |pkg_path| is the directory which contains environment file.
1076 environment_path = os.path.join(pkg_path, _ENVIRONMENT_FILENAME)
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001077 try:
1078 environment_data = device.CatFile(
1079 environment_path, max_size=None, encoding=None)
1080 except remote_access.CatFileError:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001081 # The package is not installed on DUT yet. Skip extracting info.
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001082 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001083 else:
1084 # On host, pkg_path is tbz2 file which contains environment file.
1085 # Extract the metadata of the package file.
1086 data = portage.xpak.tbz2(pkg_path).get_data()
Mike Frysingeracd06cd2021-01-27 13:33:52 -05001087 environment_data = data[_ENVIRONMENT_FILENAME.encode('utf-8')]
1088
1089 # Extract the environment metadata.
1090 environment_content = bz2.decompress(environment_data)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001091
1092 with tempfile.NamedTemporaryFile() as f:
1093 # Dumps content into a file so we can use osutils.SourceEnvironment.
1094 path = os.path.realpath(f.name)
Woody Chowde57a322020-01-07 16:18:52 +09001095 osutils.WriteFile(path, environment_content, mode='wb')
Andrew67b5fa72020-02-05 14:14:48 -08001096 content = osutils.SourceEnvironment(path, (_DLC_ID, _DLC_PACKAGE,
1097 _DLC_ENABLED))
1098
1099 dlc_enabled = content.get(_DLC_ENABLED)
1100 if dlc_enabled is not None and (dlc_enabled is False or
1101 str(dlc_enabled) == 'false'):
1102 logging.info('Installing DLC in rootfs.')
1103 return None, None
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001104 return content.get(_DLC_ID), content.get(_DLC_PACKAGE)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001105
1106
Gilad Arnolda0a98062015-07-07 08:34:27 -07001107def Deploy(device, packages, board=None, emerge=True, update=False, deep=False,
1108 deep_rev=False, clean_binpkg=True, root='/', strip=True,
1109 emerge_args=None, ssh_private_key=None, ping=True, force=False,
1110 dry_run=False):
David Pursell9476bf42015-03-30 13:34:27 -07001111 """Deploys packages to a device.
1112
1113 Args:
David Pursell2e773382015-04-03 14:30:47 -07001114 device: commandline.Device object; None to use the default device.
David Pursell9476bf42015-03-30 13:34:27 -07001115 packages: List of packages (strings) to deploy to device.
1116 board: Board to use; None to automatically detect.
David Pursell9476bf42015-03-30 13:34:27 -07001117 emerge: True to emerge package, False to unmerge.
1118 update: Check installed version on device.
1119 deep: Install dependencies also. Implies |update|.
1120 deep_rev: Install reverse dependencies. Implies |deep|.
1121 clean_binpkg: Clean outdated binary packages.
1122 root: Package installation root path.
1123 strip: Run strip_package to filter out preset paths in the package.
1124 emerge_args: Extra arguments to pass to emerge.
1125 ssh_private_key: Path to an SSH private key file; None to use test keys.
1126 ping: True to ping the device before trying to connect.
1127 force: Ignore sanity checks and prompts.
1128 dry_run: Print deployment plan but do not deploy anything.
1129
1130 Raises:
1131 ValueError: Invalid parameter or parameter combination.
1132 DeployError: Unrecoverable failure during deploy.
1133 """
1134 if deep_rev:
1135 deep = True
1136 if deep:
1137 update = True
1138
Gilad Arnolda0a98062015-07-07 08:34:27 -07001139 if not packages:
1140 raise DeployError('No packages provided, nothing to deploy.')
1141
David Pursell9476bf42015-03-30 13:34:27 -07001142 if update and not emerge:
1143 raise ValueError('Cannot update and unmerge.')
1144
David Pursell2e773382015-04-03 14:30:47 -07001145 if device:
1146 hostname, username, port = device.hostname, device.username, device.port
1147 else:
1148 hostname, username, port = None, None, None
1149
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001150 lsb_release = None
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001151 sysroot = None
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001152 try:
Mike Frysinger17844a02019-08-24 18:21:02 -04001153 # Somewhat confusing to clobber, but here we are.
1154 # pylint: disable=redefined-argument-from-local
Gilad Arnold5dc243a2015-07-07 08:22:43 -07001155 with remote_access.ChromiumOSDeviceHandler(
1156 hostname, port=port, username=username, private_key=ssh_private_key,
1157 base_dir=_DEVICE_BASE_DIR, ping=ping) as device:
Mike Frysinger539db512015-05-21 18:14:01 -04001158 lsb_release = device.lsb_release
David Pursell9476bf42015-03-30 13:34:27 -07001159
Gilad Arnolda0a98062015-07-07 08:34:27 -07001160 board = cros_build_lib.GetBoard(device_board=device.board,
1161 override_board=board)
1162 if not force and board != device.board:
1163 raise DeployError('Device (%s) is incompatible with board %s. Use '
Brian Norrisbee77382016-06-02 14:50:29 -07001164 '--force to deploy anyway.' % (device.board, board))
Bertrand SIMONNET60c94492015-04-30 17:46:28 -07001165
Gilad Arnolda0a98062015-07-07 08:34:27 -07001166 sysroot = cros_build_lib.GetSysroot(board=board)
David Pursell9476bf42015-03-30 13:34:27 -07001167
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001168 # Don't bother trying to clean for unmerges. We won't use the local db,
1169 # and it just slows things down for the user.
1170 if emerge and clean_binpkg:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001171 logging.notice('Cleaning outdated binary packages from %s', sysroot)
Bertrand SIMONNET0f6029f2015-04-30 17:44:13 -07001172 portage_util.CleanOutdatedBinaryPackages(sysroot)
David Pursell9476bf42015-03-30 13:34:27 -07001173
Achuith Bhandarkar0487c312019-04-22 12:19:25 -07001174 # Remount rootfs as writable if necessary.
1175 if not device.MountRootfsReadWrite():
1176 raise DeployError('Cannot remount rootfs as read-write. Exiting.')
David Pursell9476bf42015-03-30 13:34:27 -07001177
1178 # Obtain list of packages to upgrade/remove.
1179 pkg_scanner = _InstallPackageScanner(sysroot)
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001180 pkgs, listed, num_updates, pkgs_attrs = pkg_scanner.Run(
Mike Frysinger539db512015-05-21 18:14:01 -04001181 device, root, packages, update, deep, deep_rev)
David Pursell9476bf42015-03-30 13:34:27 -07001182 if emerge:
1183 action_str = 'emerge'
1184 else:
1185 pkgs.reverse()
1186 action_str = 'unmerge'
1187
1188 if not pkgs:
Ralph Nathane01ccf12015-04-16 10:40:32 -07001189 logging.notice('No packages to %s', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001190 return
1191
Mike Frysinger5c7b9512020-12-04 02:30:56 -05001192 # Warn when the user installs & didn't `cros workon start`.
1193 if emerge:
1194 worked_on_cps = workon_helper.WorkonHelper(sysroot).ListAtoms()
1195 for package in listed:
1196 cp = package_info.SplitCPV(package).cp
1197 if cp not in worked_on_cps:
1198 logging.warning(
1199 'Are you intentionally deploying unmodified packages, or did '
1200 'you forget to run `cros workon --board=$BOARD start %s`?', cp)
Kimiyuki Onakaa4ec7f62020-08-25 13:58:48 +09001201
Ralph Nathane01ccf12015-04-16 10:40:32 -07001202 logging.notice('These are the packages to %s:', action_str)
David Pursell9476bf42015-03-30 13:34:27 -07001203 for i, pkg in enumerate(pkgs):
Ralph Nathane01ccf12015-04-16 10:40:32 -07001204 logging.notice('%s %d) %s', '*' if pkg in listed else ' ', i + 1, pkg)
David Pursell9476bf42015-03-30 13:34:27 -07001205
1206 if dry_run or not _ConfirmDeploy(num_updates):
1207 return
1208
Ralph Nathane01ccf12015-04-16 10:40:32 -07001209 # Select function (emerge or unmerge) and bind args.
1210 if emerge:
Mike Frysinger539db512015-05-21 18:14:01 -04001211 func = functools.partial(_EmergePackages, pkgs, device, strip,
Andrew06a5f812020-01-23 08:08:32 -08001212 sysroot, root, board, emerge_args)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001213 else:
Xiaochu Liu2726e7c2019-07-18 10:28:10 -07001214 func = functools.partial(_UnmergePackages, pkgs, device, root,
1215 pkgs_attrs)
Ralph Nathane01ccf12015-04-16 10:40:32 -07001216
1217 # Call the function with the progress bar or with normal output.
1218 if command.UseProgressBar():
1219 op = BrilloDeployOperation(len(pkgs), emerge)
1220 op.Run(func, log_level=logging.DEBUG)
1221 else:
1222 func()
David Pursell9476bf42015-03-30 13:34:27 -07001223
Ben Pastene5f03b052019-08-12 18:03:24 -07001224 if device.IsSELinuxAvailable():
Qijiang Fan8a945032019-04-25 20:53:29 +09001225 if sum(x.count('selinux-policy') for x in pkgs):
1226 logging.warning(
1227 'Deploying SELinux policy will not take effect until reboot. '
Ian Barkley-Yeung6b2d8672020-08-13 18:58:10 -07001228 'SELinux policy is loaded by init. Also, changing the security '
1229 'contexts (labels) of a file will require building a new image '
1230 'and flashing the image onto the device.')
Qijiang Fan352d0eb2019-02-25 13:10:08 +09001231
David Pursell9476bf42015-03-30 13:34:27 -07001232 logging.warning('Please restart any updated services on the device, '
1233 'or just reboot it.')
Gilad Arnold4d3ade72015-04-28 15:13:35 -07001234 except Exception:
1235 if lsb_release:
1236 lsb_entries = sorted(lsb_release.items())
1237 logging.info('Following are the LSB version details of the device:\n%s',
1238 '\n'.join('%s=%s' % (k, v) for k, v in lsb_entries))
1239 raise