1 # Copyright (c) 2012 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.
5 """Routines and classes for working with Portage overlays and ebuilds."""
7 from __future__ import print_function
14 import multiprocessing
20 from chromite.cbuildbot import constants
21 from chromite.lib import cros_build_lib
22 from chromite.lib import gerrit
23 from chromite.lib import git
24 from chromite.lib import osutils
26 _PRIVATE_PREFIX = '%(buildroot)s/src/private-overlays'
28 # Define datastructures for holding PV and CPV objects.
29 _PV_FIELDS = ['pv', 'package', 'version', 'version_no_rev', 'rev']
30 PV = collections.namedtuple('PV', _PV_FIELDS)
31 CPV = collections.namedtuple('CPV', ['category'] + _PV_FIELDS)
33 # Package matching regexp, as dictated by package manager specification:
34 # http://www.gentoo.org/proj/en/qa/pms.xml
35 _pkg = r'(?P<package>' + r'[\w+][\w+-]*)'
36 _ver = r'(?P<version>' + \
37 r'(?P<version_no_rev>(\d+)((\.\d+)*)([a-z]?)' + \
38 r'((_(pre|p|beta|alpha|rc)\d*)*))' + \
39 r'(-(?P<rev>r(\d+)))?)'
40 _pvr_re = re.compile(r'^(?P<pv>%s-%s)$' % (_pkg, _ver), re.VERBOSE)
42 # This regex matches blank lines, commented lines, and the EAPI line.
43 _blank_or_eapi_re = re.compile(r'^\s*(?:#|EAPI=|$)')
46 class MissingOverlayException(Exception):
47 """This exception indicates that a needed overlay is missing."""
50 def _ListOverlays(board=None, buildroot=constants.SOURCE_ROOT):
51 """Return the list of overlays to use for a given buildbot.
53 Always returns all overlays in parent -> child order, and does not
54 perform any filtering.
57 board: Board to look at.
58 buildroot: Source root to find overlays.
60 # Load all the known overlays so we can extract the details below.
63 'src/private-overlays',
68 path = os.path.join(buildroot, path, '*')
69 for overlay in glob.glob(path):
70 name = GetOverlayName(overlay)
74 # Sanity check the sets of repos.
76 raise RuntimeError('multiple repos with same name "%s": %s and %s' % (
77 name, overlays[name]['path'], overlay))
80 masters = cros_build_lib.LoadKeyValueFile(
81 '%s/metadata/layout.conf' % overlay)['masters'].split()
82 except (KeyError, IOError):
89 # Easy enough -- dump them all.
91 return [x['path'] for x in overlays.values()]
93 # Build up the list of repos we need.
96 def _AddRepo(repo, optional=False):
97 """Recursively add |repo|'s masters from |overlays| to |ret|.
100 repo: The repo name to look up.
101 optional: If |repo| does not exist, return False, else
102 throw a KeyError exception.
105 True if |repo| was found.
107 if optional and repo not in overlays:
109 for master in overlays[repo]['masters'] + [repo]:
110 if master not in seen:
113 ret.append(overlays[master]['path'])
116 # Legacy: load the global configs. In the future, this should be found
117 # via the overlay's masters.
118 _AddRepo('chromeos', optional=True)
119 path = os.path.join(buildroot, 'src', 'private-overlays',
120 'chromeos-*-overlay')
121 ret += glob.glob(path)
122 _AddRepo('chromiumos')
123 _AddRepo('portage-stable')
125 # Locate the board repo by name.
126 board_no_variant = board.split('_', 1)[0]
127 search_boards = [board_no_variant]
128 if board != board_no_variant:
129 search_boards.append(board)
130 for b in search_boards:
131 # Load the public & private versions if available.
132 found_pub = _AddRepo(b, optional=True)
133 found_priv = _AddRepo('%s-private' % b, optional=True)
135 # If neither public nor private board was found, die.
136 if not found_pub and not found_priv:
137 raise MissingOverlayException('board overlay not found: %s' % b)
142 def FindOverlays(overlay_type, board=None, buildroot=constants.SOURCE_ROOT):
143 """Return the list of overlays to use for a given buildbot.
145 The returned list of overlays will be in parent -> child order.
148 overlay_type: A string describing which overlays you want.
149 'private': Just the private overlays.
150 'public': Just the public overlays.
151 'both': Both the public and private overlays.
152 board: Board to look at.
153 buildroot: Source root to find overlays.
155 overlays = _ListOverlays(board=board, buildroot=buildroot)
156 private_prefix = _PRIVATE_PREFIX % dict(buildroot=buildroot)
157 if overlay_type == constants.PRIVATE_OVERLAYS:
158 return [x for x in overlays if x.startswith(private_prefix)]
159 elif overlay_type == constants.PUBLIC_OVERLAYS:
160 return [x for x in overlays if not x.startswith(private_prefix)]
161 elif overlay_type == constants.BOTH_OVERLAYS:
164 assert overlay_type is None
168 def ReadOverlayFile(filename, overlay_type='both', board=None,
169 buildroot=constants.SOURCE_ROOT):
170 """Attempt to open a file in the overlay directories.
172 Searches through this board's overlays for the specified file. The
173 overlays are searched in child -> parent order.
176 filename: Path to open inside the overlay.
177 overlay_type: A string describing which overlays you want.
178 'private': Just the private overlays.
179 'public': Just the public overlays.
180 'both': Both the public and private overlays.
181 board: Board to look at.
182 buildroot: Source root to find overlays.
185 The contents of the file, or None if no files could be opened.
187 for overlay in reversed(FindOverlays(overlay_type, board, buildroot)):
189 return osutils.ReadFile(os.path.join(overlay, filename))
191 if e.errno != os.errno.ENOENT:
195 def FindPrimaryOverlay(overlay_type, board, buildroot=constants.SOURCE_ROOT):
196 """Return the primary overlay to use for a given buildbot.
198 An overlay is only considered a primary overlay if it has a make.conf and a
199 toolchain.conf. If multiple primary overlays are found, the first primary
203 overlay_type: A string describing which overlays you want.
204 'private': Just the private overlays.
205 'public': Just the public overlays.
206 'both': Both the public and private overlays.
207 board: Board to look at.
208 buildroot: Path to root of build directory.
211 MissingOverlayException: No primary overlay found.
213 for overlay in FindOverlays(overlay_type, board, buildroot):
214 if (os.path.exists(os.path.join(overlay, 'make.conf')) and
215 os.path.exists(os.path.join(overlay, 'toolchain.conf'))):
217 raise MissingOverlayException('No primary overlay found for board=%r' % board)
220 def GetOverlayName(overlay):
221 """Get the self-declared repo name for the |overlay| path."""
223 return cros_build_lib.LoadKeyValueFile(
224 '%s/metadata/layout.conf' % overlay)['repo-name']
225 except (KeyError, IOError):
226 # Not all layout.conf files have a repo-name, so don't make a fuss.
228 with open(os.path.join(overlay, 'profiles', 'repo_name')) as f:
229 return f.readline().rstrip()
231 # Not all overlays have a repo_name, so don't make a fuss.
235 class EBuildVersionFormatException(Exception):
236 """Exception for bad ebuild version string format."""
237 def __init__(self, filename):
238 self.filename = filename
239 message = ('Ebuild file name %s '
240 'does not match expected format.' % filename)
241 super(EBuildVersionFormatException, self).__init__(message)
244 class EbuildFormatIncorrectException(Exception):
245 """Exception for bad ebuild format."""
246 def __init__(self, filename, message):
247 message = 'Ebuild %s has invalid format: %s ' % (filename, message)
248 super(EbuildFormatIncorrectException, self).__init__(message)
251 class EBuild(object):
252 """Wrapper class for information about an ebuild."""
255 _PACKAGE_VERSION_PATTERN = re.compile(
256 r'.*-(([0-9][0-9a-z_.]*)(-r[0-9]+)?)[.]ebuild')
257 _WORKON_COMMIT_PATTERN = re.compile(r'^CROS_WORKON_COMMIT="(.*)"$')
259 # A structure to hold computed values of CROS_WORKON_*.
260 CrosWorkonVars = collections.namedtuple(
261 'CrosWorkonVars', ('localname', 'project', 'subdir'))
264 def _Print(cls, message):
265 """Verbose print function."""
267 cros_build_lib.Info(message)
270 def _RunCommand(cls, command, **kwargs):
271 kwargs.setdefault('capture_output', True)
272 return cros_build_lib.RunCommand(
273 command, print_cmd=cls.VERBOSE, **kwargs).output
276 def _RunGit(cls, cwd, command, **kwargs):
277 result = git.RunGit(cwd, command, print_cmd=cls.VERBOSE, **kwargs)
278 return None if result is None else result.output
281 """Returns True if the ebuild is sticky."""
282 return self.is_stable and self.current_revision == 0
285 def UpdateEBuild(cls, ebuild_path, variables, redirect_file=None,
287 """Static function that updates WORKON information in the ebuild.
289 This function takes an ebuild_path and updates WORKON information.
292 ebuild_path: The path of the ebuild.
293 variables: Dictionary of variables to update in ebuild.
294 redirect_file: Optionally redirect output of new ebuild somewhere else.
295 make_stable: Actually make the ebuild stable.
298 for line in fileinput.input(ebuild_path, inplace=1):
299 # Has to be done here to get changes to sys.stdout from fileinput.input.
300 if not redirect_file:
301 redirect_file = sys.stdout
303 # Always add variables at the top of the ebuild, before the first
304 # nonblank line other than the EAPI line.
305 if not written and not _blank_or_eapi_re.match(line):
306 for key, value in sorted(variables.items()):
307 assert key is not None and value is not None
308 redirect_file.write('%s=%s\n' % (key, value))
311 # Mark KEYWORDS as stable by removing ~'s.
312 if line.startswith('KEYWORDS=') and make_stable:
313 line = line.replace('~', '')
315 varname, eq, _ = line.partition('=')
316 if not (eq == '=' and varname.strip() in variables):
317 # Don't write out the old value of the variable.
318 redirect_file.write(line)
323 def MarkAsStable(cls, unstable_ebuild_path, new_stable_ebuild_path,
324 variables, redirect_file=None, make_stable=True):
325 """Static function that creates a revved stable ebuild.
327 This function assumes you have already figured out the name of the new
328 stable ebuild path and then creates that file from the given unstable
329 ebuild and marks it as stable. If the commit_value is set, it also
330 set the commit_keyword=commit_value pair in the ebuild.
333 unstable_ebuild_path: The path to the unstable ebuild.
334 new_stable_ebuild_path: The path you want to use for the new stable
336 variables: Dictionary of variables to update in ebuild.
337 redirect_file: Optionally redirect output of new ebuild somewhere else.
338 make_stable: Actually make the ebuild stable.
340 shutil.copyfile(unstable_ebuild_path, new_stable_ebuild_path)
341 EBuild.UpdateEBuild(new_stable_ebuild_path, variables, redirect_file,
345 def CommitChange(cls, message, overlay):
346 """Commits current changes in git locally with given commit message.
349 message: the commit string to write when committing to git.
350 overlay: directory in which to commit the changes.
353 RunCommandError: Error occurred while committing.
355 logging.info('Committing changes with commit message: %s', message)
356 git_commit_cmd = ['commit', '-a', '-m', message]
357 cls._RunGit(overlay, git_commit_cmd)
359 def __init__(self, path):
360 """Sets up data about an ebuild from its path.
363 path: Path to the ebuild.
365 self._overlay, self._category, self._pkgname, filename = path.rsplit('/', 3)
366 m = self._PACKAGE_VERSION_PATTERN.match(filename)
368 raise EBuildVersionFormatException(filename)
369 self.version, self.version_no_rev, revision = m.groups()
370 if revision is not None:
371 self.current_revision = int(revision.replace('-r', ''))
373 self.current_revision = 0
374 self.package = '%s/%s' % (self._category, self._pkgname)
376 self._ebuild_path_no_version = os.path.join(
377 os.path.dirname(path), self._pkgname)
378 self.ebuild_path_no_revision = '%s-%s' % (
379 self._ebuild_path_no_version, self.version_no_rev)
380 self._unstable_ebuild_path = '%s-9999.ebuild' % (
381 self._ebuild_path_no_version)
382 self.ebuild_path = path
384 self.is_workon = False
385 self.is_stable = False
386 self.is_blacklisted = False
387 self._ReadEBuild(path)
390 def Classify(ebuild_path):
391 """Return whether this ebuild is workon, stable, and/or blacklisted
393 workon is determined by whether the ebuild inherits from the
394 'cros-workon' eclass. stable is determined by whether there's a '~'
395 in the KEYWORDS setting in the ebuild. An ebuild is considered blacklisted
396 if a line in it starts with 'CROS_WORKON_BLACKLIST='
400 is_blacklisted = False
401 for line in fileinput.input(ebuild_path):
402 if line.startswith('inherit ') and 'cros-workon' in line:
404 elif line.startswith('KEYWORDS='):
405 for keyword in line.split('=', 1)[1].strip("\"'").split():
406 if not keyword.startswith('~') and keyword != '-*':
408 elif line.startswith('CROS_WORKON_BLACKLIST='):
409 is_blacklisted = True
411 return is_workon, is_stable, is_blacklisted
413 def _ReadEBuild(self, path):
414 """Determine the settings of `is_workon`, `is_stable` and is_blacklisted
416 These are determined using the static Classify function.
418 self.is_workon, self.is_stable, self.is_blacklisted = EBuild.Classify(path)
421 def GetCrosWorkonVars(ebuild_path, pkg_name):
422 """Return computed (as sourced ebuild script) values of:
424 * CROS_WORKON_LOCALNAME
425 * CROS_WORKON_PROJECT
429 ebuild_path: Path to the ebuild file (e.g: platform2-9999.ebuild).
430 pkg_name: The package name (e.g.: platform2).
433 A CrosWorkonVars tuple.
436 'CROS_WORKON_LOCALNAME',
437 'CROS_WORKON_PROJECT',
438 'CROS_WORKON_SUBDIR',
441 'CROS_WORKON_LOCALNAME': pkg_name,
442 'CROS_WORKON_SUBDIR': '',
444 settings = osutils.SourceEnvironment(ebuild_path, workon_vars, env=env)
445 # Try to detect problems extracting the variables by checking whether
446 # CROS_WORKON_PROJECT is set. If it isn't, something went wrong, possibly
447 # because we're simplistically sourcing the ebuild without most of portage
448 # being available. That still breaks this script and needs to be flagged
449 # as an error. We won't catch problems setting CROS_WORKON_LOCALNAME or
450 # CROS_WORKON_SUBDIR or if CROS_WORKON_PROJECT is set to the wrong thing,
451 # but at least this covers some types of failures.
452 if 'CROS_WORKON_PROJECT' not in settings:
453 raise EbuildFormatIncorrectException(ebuild_path,
454 'Unable to determine CROS_WORKON_PROJECT value.')
455 localnames = settings['CROS_WORKON_LOCALNAME'].split(',')
456 projects = settings['CROS_WORKON_PROJECT'].split(',')
457 subdirs = settings['CROS_WORKON_SUBDIR'].split(',')
459 return EBuild.CrosWorkonVars(localnames, projects, subdirs)
461 def GetSourcePath(self, srcroot, manifest):
462 """Get the project and path for this ebuild.
464 The path is guaranteed to exist, be a directory, and be absolute.
467 localnames, projects, subdirs = EBuild.GetCrosWorkonVars(
468 self._unstable_ebuild_path, self._pkgname)
469 # Sanity checks and completion.
470 # Each project specification has to have the same amount of items.
471 if len(projects) != len(localnames):
472 raise EbuildFormatIncorrectException(self._unstable_ebuild_path,
473 'Number of _PROJECT and _LOCALNAME items don\'t match.')
474 # Subdir must be either 0,1 or len(project)
475 if len(projects) != len(subdirs) and len(subdirs) > 1:
476 raise EbuildFormatIncorrectException(self._unstable_ebuild_path,
477 'Incorrect number of _SUBDIR items.')
478 # If there's one, apply it to all.
479 if len(subdirs) == 1:
480 subdirs = subdirs * len(projects)
481 # If there is none, make an empty list to avoid exceptions later.
482 if len(subdirs) == 0:
483 subdirs = [''] * len(projects)
486 if self._category == 'chromeos-base':
487 dir_ = '' # 'platform2'
491 # Once all targets are moved from platform to platform2, uncomment
492 # the following lines as well as dir_ = 'platform2' above,
493 # and delete the loop that builds |subdir_paths| below.
495 # subdir_paths = [os.path.realpath(os.path.join(srcroot, dir_, l, s))
496 # for l, s in zip(localnames, subdirs)]
499 for local, sub in zip(localnames, subdirs):
500 subdir_path = os.path.realpath(os.path.join(srcroot, dir_, local, sub))
501 if dir_ == '' and not os.path.isdir(subdir_path):
502 subdir_path = os.path.realpath(os.path.join(srcroot, 'platform',
504 subdir_paths.append(subdir_path)
506 for subdir_path, project in zip(subdir_paths, projects):
507 if not os.path.isdir(subdir_path):
508 cros_build_lib.Die('Source repository %s '
509 'for project %s does not exist.' % (subdir_path,
511 # Verify that we're grabbing the commit id from the right project name.
512 real_project = manifest.FindCheckoutFromPath(subdir_path)['name']
513 if project != real_project:
514 cros_build_lib.Die('Project name mismatch for %s '
515 '(found %s, expected %s)' % (subdir_path,
518 return projects, subdir_paths
520 def GetCommitId(self, srcdir):
521 """Get the commit id for this ebuild."""
522 output = self._RunGit(srcdir, ['rev-parse', 'HEAD'])
524 cros_build_lib.Die('Cannot determine HEAD commit for %s' % srcdir)
525 return output.rstrip()
527 def GetTreeId(self, srcdir):
528 """Get the SHA1 of the source tree for this ebuild.
530 Unlike the commit hash, the SHA1 of the source tree is unaffected by the
531 history of the repository, or by commit messages.
533 output = self._RunGit(srcdir, ['log', '-1', '--format=%T'])
535 cros_build_lib.Die('Cannot determine HEAD tree hash for %s' % srcdir)
536 return output.rstrip()
538 def GetVersion(self, srcroot, manifest, default):
539 """Get the base version number for this ebuild.
541 The version is provided by the ebuild through a specific script in
542 the $FILESDIR (chromeos-version.sh).
544 vers_script = os.path.join(os.path.dirname(self._ebuild_path_no_version),
545 'files', 'chromeos-version.sh')
547 if not os.path.exists(vers_script):
550 if not self.is_workon:
551 raise EbuildFormatIncorrectException(self._ebuild_path_no_version,
552 "Package has a chromeos-version.sh script but is not workon-able.")
554 srcdirs = self.GetSourcePath(srcroot, manifest)[1]
556 # The chromeos-version script will output a usable raw version number,
557 # or nothing in case of error or no available version
559 output = self._RunCommand([vers_script] + srcdirs).strip()
560 except cros_build_lib.RunCommandError as e:
561 cros_build_lib.Die('Package %s chromeos-version.sh failed: %s' %
565 cros_build_lib.Die('Package %s has a chromeos-version.sh script but '
566 'it returned no valid version for "%s"' %
567 (self._pkgname, ' '.join(srcdirs)))
572 def FormatBashArray(unformatted_list):
573 """Returns a python list in a bash array format.
575 If the list only has one item, format as simple quoted value.
576 That is both backwards-compatible and more readable.
579 unformatted_list: an iterable to format as a bash array. This variable
580 has to be sanitized first, as we don't do any safeties.
583 A text string that can be used by bash as array declaration.
585 if len(unformatted_list) > 1:
586 return '("%s")' % '" "'.join(unformatted_list)
588 return '"%s"' % unformatted_list[0]
590 def RevWorkOnEBuild(self, srcroot, manifest, redirect_file=None):
591 """Revs a workon ebuild given the git commit hash.
593 By default this class overwrites a new ebuild given the normal
594 ebuild rev'ing logic. However, a user can specify a redirect_file
595 to redirect the new stable ebuild to another file.
598 srcroot: full path to the 'src' subdirectory in the source
600 manifest: git.ManifestCheckout object.
601 redirect_file: Optional file to write the new ebuild. By default
602 it is written using the standard rev'ing logic. This file must be
603 opened and closed by the caller.
606 If the revved package is different than the old ebuild, return the full
607 revved package name, including the version number. Otherwise, return None.
610 OSError: Error occurred while creating a new ebuild.
611 IOError: Error occurred while writing to the new revved ebuild file.
615 stable_version_no_rev = self.GetVersion(srcroot, manifest,
618 # If given unstable ebuild, use preferred version rather than 9999.
619 stable_version_no_rev = self.GetVersion(srcroot, manifest, '0.0.1')
621 new_version = '%s-r%d' % (
622 stable_version_no_rev, self.current_revision + 1)
623 new_stable_ebuild_path = '%s-%s.ebuild' % (
624 self._ebuild_path_no_version, new_version)
626 self._Print('Creating new stable ebuild %s' % new_stable_ebuild_path)
627 if not os.path.exists(self._unstable_ebuild_path):
628 cros_build_lib.Die('Missing unstable ebuild: %s' %
629 self._unstable_ebuild_path)
631 srcdirs = self.GetSourcePath(srcroot, manifest)[1]
632 commit_ids = map(self.GetCommitId, srcdirs)
633 tree_ids = map(self.GetTreeId, srcdirs)
634 variables = dict(CROS_WORKON_COMMIT=self.FormatBashArray(commit_ids),
635 CROS_WORKON_TREE=self.FormatBashArray(tree_ids))
636 self.MarkAsStable(self._unstable_ebuild_path, new_stable_ebuild_path,
637 variables, redirect_file)
639 old_ebuild_path = self.ebuild_path
640 if filecmp.cmp(old_ebuild_path, new_stable_ebuild_path, shallow=False):
641 os.unlink(new_stable_ebuild_path)
644 self._Print('Adding new stable ebuild to git')
645 self._RunGit(self._overlay, ['add', new_stable_ebuild_path])
648 self._Print('Removing old ebuild from git')
649 self._RunGit(self._overlay, ['rm', old_ebuild_path])
651 return '%s-%s' % (self.package, new_version)
654 def GitRepoHasChanges(cls, directory):
655 """Returns True if there are changes in the given directory."""
656 # Refresh the index first. This squashes just metadata changes.
657 cls._RunGit(directory, ['update-index', '-q', '--refresh'])
658 output = cls._RunGit(directory, ['diff-index', '--name-only', 'HEAD'])
659 return output not in [None, '']
662 def _GetSHA1ForPath(manifest, path):
663 """Get the latest SHA1 for a given project from Gerrit.
665 This function looks up the remote and branch for a given project in the
666 manifest, and uses this to lookup the SHA1 from Gerrit. This only makes
667 sense for unpinned manifests.
670 manifest: git.ManifestCheckout object.
671 path: Path of project.
674 Exception if the manifest is pinned.
676 checkout = manifest.FindCheckoutFromPath(path)
677 project = checkout['name']
678 helper = gerrit.GetGerritHelper(checkout['remote'])
679 manifest_branch = checkout['revision']
680 branch = git.StripRefsHeads(manifest_branch)
681 return helper.GetLatestSHA1ForBranch(project, branch)
684 def _GetEBuildPaths(buildroot, manifest, overlay_list, changes):
685 """Calculate ebuild->path map for changed ebuilds.
688 buildroot: Path to root of build directory.
689 manifest: git.ManifestCheckout object.
690 overlay_list: List of all overlays.
691 changes: Changes from Gerrit that are being pushed.
694 A dictionary mapping changed ebuilds to lists of associated paths.
696 directory_src = os.path.join(buildroot, 'src')
697 overlay_dict = dict((o, []) for o in overlay_list)
698 BuildEBuildDictionary(overlay_dict, True, None)
699 changed_paths = set(c.GetCheckout(manifest).GetPath(absolute=True)
702 for ebuilds in overlay_dict.itervalues():
703 for ebuild in ebuilds:
704 _projects, paths = ebuild.GetSourcePath(directory_src, manifest)
705 if changed_paths.intersection(paths):
706 ebuild_projects[ebuild] = paths
708 return ebuild_projects
711 def UpdateCommitHashesForChanges(cls, changes, buildroot, manifest):
712 """Updates the commit hashes for the EBuilds uprevved in changes.
715 changes: Changes from Gerrit that are being pushed.
716 buildroot: Path to root of build directory.
717 manifest: git.ManifestCheckout object.
720 overlay_list = FindOverlays(constants.BOTH_OVERLAYS, buildroot=buildroot)
721 ebuild_paths = cls._GetEBuildPaths(buildroot, manifest, overlay_list,
723 for ebuild, paths in ebuild_paths.iteritems():
724 # Calculate any SHA1s that are not already in path_sha1s.
725 for path in set(paths).difference(path_sha1s):
726 path_sha1s[path] = cls._GetSHA1ForPath(manifest, path)
728 sha1s = [path_sha1s[path] for path in paths]
729 logging.info('Updating ebuild for package %s with commit hashes %r',
730 ebuild.package, sha1s)
731 updates = dict(CROS_WORKON_COMMIT=cls.FormatBashArray(sha1s))
732 EBuild.UpdateEBuild(ebuild.ebuild_path, updates)
734 # Commit any changes to all overlays.
735 for overlay in overlay_list:
736 if EBuild.GitRepoHasChanges(overlay):
737 EBuild.CommitChange('Updating commit hashes in ebuilds '
738 'to match remote repository.', overlay=overlay)
741 def BestEBuild(ebuilds):
742 """Returns the newest EBuild from a list of EBuild objects."""
743 # pylint: disable=F0401
744 from portage.versions import vercmp
746 for ebuild in ebuilds[1:]:
747 if vercmp(winner.version, ebuild.version) < 0:
752 def _FindUprevCandidates(files):
753 """Return the uprev candidate ebuild from a specified list of files.
755 Usually an uprev candidate is a the stable ebuild in a cros_workon
756 directory. However, if no such stable ebuild exists (someone just
757 checked in the 9999 ebuild), this is the unstable ebuild.
759 If the package isn't a cros_workon package, return None.
762 files: List of files in a package directory.
765 unstable_ebuilds = []
767 if not path.endswith('.ebuild') or os.path.islink(path):
769 ebuild = EBuild(path)
770 if not ebuild.is_workon or ebuild.is_blacklisted:
773 if ebuild.version == '9999':
774 cros_build_lib.Die('KEYWORDS in 9999 ebuild should not be stable %s'
776 stable_ebuilds.append(ebuild)
778 unstable_ebuilds.append(ebuild)
780 # If both ebuild lists are empty, the passed in file list was for
781 # a non-workon package.
782 if not unstable_ebuilds:
784 path = os.path.dirname(stable_ebuilds[0].ebuild_path)
785 cros_build_lib.Die('Missing 9999 ebuild in %s' % path)
788 path = os.path.dirname(unstable_ebuilds[0].ebuild_path)
789 if len(unstable_ebuilds) > 1:
790 cros_build_lib.Die('Found multiple unstable ebuilds in %s' % path)
792 if not stable_ebuilds:
793 cros_build_lib.Warning('Missing stable ebuild in %s' % path)
794 return unstable_ebuilds[0]
796 if len(stable_ebuilds) == 1:
797 return stable_ebuilds[0]
799 stable_versions = set(ebuild.version_no_rev for ebuild in stable_ebuilds)
800 if len(stable_versions) > 1:
801 package = stable_ebuilds[0].package
802 message = 'Found multiple stable ebuild versions in %s:' % path
803 for version in stable_versions:
804 message += '\n %s-%s' % (package, version)
805 cros_build_lib.Die(message)
807 uprev_ebuild = max(stable_ebuilds, key=lambda eb: eb.current_revision)
808 for ebuild in stable_ebuilds:
809 if ebuild != uprev_ebuild:
810 cros_build_lib.Warning('Ignoring stable ebuild revision %s in %s' %
811 (ebuild.version, path))
815 def BuildEBuildDictionary(overlays, use_all, packages):
816 """Build a dictionary of the ebuilds in the specified overlays.
818 overlays: A map which maps overlay directories to arrays of stable EBuilds
819 inside said directories.
820 use_all: Whether to include all ebuilds in the specified directories.
821 If true, then we gather all packages in the directories regardless
822 of whether they are in our set of packages.
823 packages: A set of the packages we want to gather. If use_all is
824 True, this argument is ignored, and should be None.
826 for overlay in overlays:
827 for package_dir, _dirs, files in os.walk(overlay):
828 # Add stable ebuilds to overlays[overlay].
829 paths = [os.path.join(package_dir, path) for path in files]
830 ebuild = _FindUprevCandidates(paths)
832 # If the --all option isn't used, we only want to update packages that
834 if ebuild and (use_all or ebuild.package in packages):
835 overlays[overlay].append(ebuild)
838 def RegenCache(overlay):
839 """Regenerate the cache of the specified overlay.
841 overlay: The tree to regenerate the cache for.
843 repo_name = GetOverlayName(overlay)
847 layout = cros_build_lib.LoadKeyValueFile('%s/metadata/layout.conf' % overlay,
849 if layout.get('cache-format') != 'md5-dict':
852 # Regen for the whole repo.
853 cros_build_lib.RunCommand(['egencache', '--update', '--repo', repo_name,
854 '--jobs', str(multiprocessing.cpu_count())])
855 # If there was nothing new generated, then let's just bail.
856 result = git.RunGit(overlay, ['status', '-s', 'metadata/'])
857 if not result.output:
859 # Explicitly add any new files to the index.
860 git.RunGit(overlay, ['add', 'metadata/'])
861 # Explicitly tell git to also include rm-ed files.
862 git.RunGit(overlay, ['commit', '-m', 'regen cache', 'metadata/'])
865 def ParseBashArray(value):
866 """Parse a valid bash array into python list."""
867 # The syntax for bash arrays is nontrivial, so let's use bash to do the
868 # heavy lifting for us.
870 # Because %s may contain bash comments (#), put a clever newline in the way.
871 cmd = 'ARR=%s\nIFS=%s; echo -n "${ARR[*]}"' % (value, sep)
872 return cros_build_lib.RunCommand(
873 cmd, print_cmd=False, shell=True, capture_output=True).output.split(sep)
876 def GetWorkonProjectMap(overlay, subdirectories):
877 """Get the project -> ebuild mapping for cros_workon ebuilds.
880 overlay: Overlay to look at.
881 subdirectories: List of subdirectories to look in on the overlay.
884 A list of (filename, projects) tuples for cros-workon ebuilds in the
885 given overlay under the given subdirectories.
887 # Search ebuilds for project names, ignoring non-existent directories.
888 # Also filter out ebuilds which are not cros_workon.
889 for subdir in subdirectories:
890 for root, _dirs, files in os.walk(os.path.join(overlay, subdir)):
891 for filename in files:
892 if filename.endswith('-9999.ebuild'):
893 full_path = os.path.join(root, filename)
894 is_workon = EBuild.Classify(full_path)[0]
897 pkg_name = os.path.basename(root)
898 _, projects, _ = EBuild.GetCrosWorkonVars(full_path, pkg_name)
899 relpath = os.path.relpath(full_path, start=overlay)
900 yield relpath, projects
903 def SplitEbuildPath(path):
904 """Split an ebuild path into its components.
906 Given a specified ebuild filename, returns $CATEGORY, $PN, $P. It does not
907 perform any check on ebuild name elements or their validity, merely splits
908 a filename, absolute or relative, and returns the last 3 components.
910 Example: For /any/path/chromeos-base/power_manager/power_manager-9999.ebuild,
911 returns ('chromeos-base', 'power_manager', 'power_manager-9999').
916 return os.path.splitext(path)[0].rsplit('/', 3)[-3:]
919 def SplitPV(pv, strict=True):
920 """Takes a PV value and splits it into individual components.
923 pv: Package name and version.
924 strict: If True, returns None if version or package name is missing.
925 Otherwise, only package name is mandatory.
928 A collection with named members:
929 pv, package, version, version_no_rev, rev
931 m = _pvr_re.match(pv)
933 if m is None and strict:
937 return PV(**{'pv': None, 'package': pv, 'version': None,
938 'version_no_rev': None, 'rev': None})
940 return PV(**m.groupdict())
943 def SplitCPV(cpv, strict=True):
944 """Splits a CPV value into components.
947 cpv: Category, package name, and version of a package.
948 strict: If True, returns None if any of the components is missing.
949 Otherwise, only package name is mandatory.
952 A collection with named members:
953 category, pv, package, version, version_no_rev, rev
955 chunks = cpv.split('/')
957 raise ValueError('Unexpected package format %s' % cpv)
963 m = SplitPV(chunks[-1], strict=strict)
964 if strict and (category is None or m is None):
966 # pylint: disable=W0212
967 return CPV(category=category, **m._asdict())
970 def FindWorkonProjects(packages):
971 """Find the projects associated with the specified cros_workon packages.
974 packages: List of cros_workon packages.
977 The set of projects associated with the specified cros_workon packages.
980 buildroot, both = constants.SOURCE_ROOT, constants.BOTH_OVERLAYS
981 for overlay in FindOverlays(both, buildroot=buildroot):
982 for _, projects in GetWorkonProjectMap(overlay, packages):
983 all_projects.update(projects)
987 def ListInstalledPackages(sysroot):
988 """Lists all portage packages in a given portage-managed root.
990 Assumes the existence of a /var/db/pkg package database.
993 sysroot: The root being inspected.
996 A list of (cp,v) tuples in the given sysroot.
998 vdb_path = os.path.join(sysroot, 'var/db/pkg')
999 ebuild_pattern = os.path.join(vdb_path, '*/*/*.ebuild')
1001 for path in glob.glob(ebuild_pattern):
1002 category, package, packagecheck = SplitEbuildPath(path)
1003 pv = SplitPV(package)
1004 if package == packagecheck and pv is not None:
1005 packages.append(('%s/%s' % (category, pv.package), pv.version))
1009 def BestVisible(atom, board=None, pkg_type='ebuild',
1010 buildroot=constants.SOURCE_ROOT):
1011 """Get the best visible ebuild CPV for the given atom.
1015 board: Board to look at. By default, look in chroot.
1016 pkg_type: Package type (ebuild, binary, or installed).
1017 buildroot: Directory
1022 portageq = 'portageq' if board is None else 'portageq-%s' % board
1023 root = cros_build_lib.GetSysroot(board=board)
1024 cmd = [portageq, 'best_visible', root, pkg_type, atom]
1025 result = cros_build_lib.RunCommand(
1026 cmd, cwd=buildroot, enter_chroot=True, debug_level=logging.DEBUG,
1027 capture_output=True)
1028 return SplitCPV(result.output.strip())
1031 def IsPackageInstalled(package, sysroot='/'):
1032 """Return whether a portage package is in a given portage-managed root.
1035 package: The CP to look for.
1036 sysroot: The root being inspected.
1038 for key, _version in ListInstalledPackages(sysroot):
1045 def FindPackageNameMatches(pkg_str, board=None):
1046 """Finds a list of installed packages matching |pkg_str|.
1049 pkg_str: The package name with optional category, version, and slot.
1050 board: The board to insepct.
1053 A list of matched CPV objects.
1057 cmd = ['equery-%s' % board]
1059 cmd += ['list', pkg_str]
1060 result = cros_build_lib.RunCommand(
1061 cmd, capture_output=True, error_code_ok=True)
1064 if result.returncode == 0:
1065 matches = [SplitCPV(x) for x in result.output.splitlines()]
1070 def GetBinaryPackageDir(sysroot='/', packages_dir=None):
1071 """Returns the binary package directory of |sysroot|."""
1072 dir_name = packages_dir if packages_dir else 'packages'
1073 return os.path.join(sysroot, dir_name)
1076 def GetBinaryPackagePath(c, p, v, sysroot='/', packages_dir=None):
1077 """Returns the path to the binary package.
1083 sysroot: The root being inspected.
1084 packages_dir: Name of the packages directory in |sysroot|.
1087 The path to the binary package.
1089 pkgdir = GetBinaryPackageDir(sysroot=sysroot, packages_dir=packages_dir)
1090 path = os.path.join(pkgdir, c, '%s-%s.tbz2' % (p, v))
1091 if not os.path.exists(path):
1092 raise ValueError('Cannot find the binary package %s!' % path)
1097 def CleanOutdatedBinaryPackages(board):
1098 """Cleans outdated binary packages for |board|."""
1099 return cros_build_lib.RunCommand(['eclean-%s' % board, '-d', 'packages'])