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."""
12 import multiprocessing
18 from chromite.cbuildbot import constants
19 from chromite.lib import cros_build_lib
20 from chromite.lib import gerrit
21 from chromite.lib import git
22 from chromite.lib import osutils
24 _PRIVATE_PREFIX = '%(buildroot)s/src/private-overlays'
26 '%s/chromeos-overlay' % _PRIVATE_PREFIX,
27 '%s/chromeos-*-overlay' % _PRIVATE_PREFIX,
28 '%(buildroot)s/src/third_party/chromiumos-overlay',
29 '%(buildroot)s/src/third_party/portage-stable',
32 # Define datastructures for holding PV and CPV objects.
33 _PV_FIELDS = ['pv', 'package', 'version', 'version_no_rev', 'rev']
34 PV = collections.namedtuple('PV', _PV_FIELDS)
35 CPV = collections.namedtuple('CPV', ['category'] + _PV_FIELDS)
37 # Package matching regexp, as dictated by package manager specification:
38 # http://www.gentoo.org/proj/en/qa/pms.xml
39 _pkg = r'(?P<package>' + r'[\w+][\w+-]*)'
40 _ver = r'(?P<version>' + \
41 r'(?P<version_no_rev>(\d+)((\.\d+)*)([a-z]?)' + \
42 r'((_(pre|p|beta|alpha|rc)\d*)*))' + \
43 r'(-(?P<rev>r(\d+)))?)'
44 _pvr_re = re.compile(r'^(?P<pv>%s-%s)$' % (_pkg, _ver), re.VERBOSE)
46 # This regex matches blank lines, commented lines, and the EAPI line.
47 _blank_or_eapi_re = re.compile(r'^\s*(?:#|EAPI=|$)')
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 overlays, patterns = [], []
62 patterns += ['overlay*']
64 board_no_variant, _, variant = board.partition('_')
65 patterns += ['overlay-%s' % board_no_variant]
67 patterns += ['overlay-variant-%s' % board.replace('_', '-')]
69 for d in _GLOBAL_OVERLAYS:
70 overlays += glob.glob(d % dict(buildroot=buildroot))
73 overlays += glob.glob('%s/src/overlays/%s' % (buildroot, p))
74 overlays += glob.glob('%s/src/private-overlays/%s-private' % (buildroot, p))
79 def FindOverlays(overlay_type, board=None, buildroot=constants.SOURCE_ROOT):
80 """Return the list of overlays to use for a given buildbot.
82 The returned list of overlays will be in parent -> child order.
85 overlay_type: A string describing which overlays you want.
86 'private': Just the private overlays.
87 'public': Just the public overlays.
88 'both': Both the public and private overlays.
89 board: Board to look at.
90 buildroot: Source root to find overlays.
92 overlays = _ListOverlays(board=board, buildroot=buildroot)
93 private_prefix = _PRIVATE_PREFIX % dict(buildroot=buildroot)
94 if overlay_type == constants.PRIVATE_OVERLAYS:
95 return [x for x in overlays if x.startswith(private_prefix)]
96 elif overlay_type == constants.PUBLIC_OVERLAYS:
97 return [x for x in overlays if not x.startswith(private_prefix)]
98 elif overlay_type == constants.BOTH_OVERLAYS:
101 assert overlay_type is None
105 def ReadOverlayFile(filename, overlay_type='both', board=None,
106 buildroot=constants.SOURCE_ROOT):
107 """Attempt to open a file in the overlay directories.
109 Searches through this board's overlays for the specified file. The
110 overlays are searched in child -> parent order.
113 filename: Path to open inside the overlay.
114 overlay_type: A string describing which overlays you want.
115 'private': Just the private overlays.
116 'public': Just the public overlays.
117 'both': Both the public and private overlays.
118 board: Board to look at.
119 buildroot: Source root to find overlays.
122 The contents of the file, or None if no files could be opened.
124 for overlay in reversed(FindOverlays(overlay_type, board, buildroot)):
126 return osutils.ReadFile(os.path.join(overlay, filename))
128 if e.errno != os.errno.ENOENT:
132 class MissingOverlayException(Exception):
133 """This exception indicates that a needed overlay is missing."""
136 def FindPrimaryOverlay(overlay_type, board, buildroot=constants.SOURCE_ROOT):
137 """Return the primary overlay to use for a given buildbot.
139 An overlay is only considered a primary overlay if it has a make.conf and a
140 toolchain.conf. If multiple primary overlays are found, the first primary
144 overlay_type: A string describing which overlays you want.
145 'private': Just the private overlays.
146 'public': Just the public overlays.
147 'both': Both the public and private overlays.
148 board: Board to look at.
149 buildroot: Path to root of build directory.
152 MissingOverlayException: No primary overlay found.
154 for overlay in FindOverlays(overlay_type, board, buildroot):
155 if (os.path.exists(os.path.join(overlay, 'make.conf')) and
156 os.path.exists(os.path.join(overlay, 'toolchain.conf'))):
158 raise MissingOverlayException('No primary overlay found for board=%r' % board)
161 def GetOverlayName(overlay):
162 """Get the self-declared repo name for the |overlay| path."""
164 return cros_build_lib.LoadKeyValueFile(
165 '%s/metadata/layout.conf' % overlay)['repo-name']
166 except (KeyError, IOError):
167 # Not all layout.conf files have a repo-name, so don't make a fuss.
169 with open(os.path.join(overlay, 'profiles', 'repo_name')) as f:
170 return f.readline().rstrip()
172 # Not all overlays have a repo_name, so don't make a fuss.
176 class EBuildVersionFormatException(Exception):
177 """Exception for bad ebuild version string format."""
178 def __init__(self, filename):
179 self.filename = filename
180 message = ('Ebuild file name %s '
181 'does not match expected format.' % filename)
182 super(EBuildVersionFormatException, self).__init__(message)
185 class EbuildFormatIncorrectException(Exception):
186 """Exception for bad ebuild format."""
187 def __init__(self, filename, message):
188 message = 'Ebuild %s has invalid format: %s ' % (filename, message)
189 super(EbuildFormatIncorrectException, self).__init__(message)
192 class EBuild(object):
193 """Wrapper class for information about an ebuild."""
196 _PACKAGE_VERSION_PATTERN = re.compile(
197 r'.*-(([0-9][0-9a-z_.]*)(-r[0-9]+)?)[.]ebuild')
198 _WORKON_COMMIT_PATTERN = re.compile(r'^CROS_WORKON_COMMIT="(.*)"$')
200 # A structure to hold computed values of CROS_WORKON_*.
201 CrosWorkonVars = collections.namedtuple(
202 'CrosWorkonVars', ('localname', 'project', 'subdir'))
205 def _Print(cls, message):
206 """Verbose print function."""
208 cros_build_lib.Info(message)
211 def _RunCommand(cls, command, **kwargs):
212 kwargs.setdefault('capture_output', True)
213 return cros_build_lib.RunCommand(
214 command, print_cmd=cls.VERBOSE, **kwargs).output
217 def _RunGit(cls, cwd, command, **kwargs):
218 result = git.RunGit(cwd, command, print_cmd=cls.VERBOSE, **kwargs)
219 return None if result is None else result.output
222 """Returns True if the ebuild is sticky."""
223 return self.is_stable and self.current_revision == 0
226 def UpdateEBuild(cls, ebuild_path, variables, redirect_file=None,
228 """Static function that updates WORKON information in the ebuild.
230 This function takes an ebuild_path and updates WORKON information.
233 ebuild_path: The path of the ebuild.
234 variables: Dictionary of variables to update in ebuild.
235 redirect_file: Optionally redirect output of new ebuild somewhere else.
236 make_stable: Actually make the ebuild stable.
239 for line in fileinput.input(ebuild_path, inplace=1):
240 # Has to be done here to get changes to sys.stdout from fileinput.input.
241 if not redirect_file:
242 redirect_file = sys.stdout
244 # Always add variables at the top of the ebuild, before the first
245 # nonblank line other than the EAPI line.
246 if not written and not _blank_or_eapi_re.match(line):
247 for key, value in sorted(variables.items()):
248 assert key is not None and value is not None
249 redirect_file.write('%s=%s\n' % (key, value))
252 # Mark KEYWORDS as stable by removing ~'s.
253 if line.startswith('KEYWORDS=') and make_stable:
254 line = line.replace('~', '')
256 varname, eq, _ = line.partition('=')
257 if not (eq == '=' and varname.strip() in variables):
258 # Don't write out the old value of the variable.
259 redirect_file.write(line)
264 def MarkAsStable(cls, unstable_ebuild_path, new_stable_ebuild_path,
265 variables, redirect_file=None, make_stable=True):
266 """Static function that creates a revved stable ebuild.
268 This function assumes you have already figured out the name of the new
269 stable ebuild path and then creates that file from the given unstable
270 ebuild and marks it as stable. If the commit_value is set, it also
271 set the commit_keyword=commit_value pair in the ebuild.
274 unstable_ebuild_path: The path to the unstable ebuild.
275 new_stable_ebuild_path: The path you want to use for the new stable
277 variables: Dictionary of variables to update in ebuild.
278 redirect_file: Optionally redirect output of new ebuild somewhere else.
279 make_stable: Actually make the ebuild stable.
281 shutil.copyfile(unstable_ebuild_path, new_stable_ebuild_path)
282 EBuild.UpdateEBuild(new_stable_ebuild_path, variables, redirect_file,
286 def CommitChange(cls, message, overlay):
287 """Commits current changes in git locally with given commit message.
290 message: the commit string to write when committing to git.
291 overlay: directory in which to commit the changes.
294 RunCommandError: Error occurred while committing.
296 logging.info('Committing changes with commit message: %s', message)
297 git_commit_cmd = ['commit', '-a', '-m', message]
298 cls._RunGit(overlay, git_commit_cmd)
300 def __init__(self, path):
301 """Sets up data about an ebuild from its path.
304 path: Path to the ebuild.
306 self._overlay, self._category, self._pkgname, filename = path.rsplit('/', 3)
307 m = self._PACKAGE_VERSION_PATTERN.match(filename)
309 raise EBuildVersionFormatException(filename)
310 self.version, self.version_no_rev, revision = m.groups()
311 if revision is not None:
312 self.current_revision = int(revision.replace('-r', ''))
314 self.current_revision = 0
315 self.package = '%s/%s' % (self._category, self._pkgname)
317 self._ebuild_path_no_version = os.path.join(
318 os.path.dirname(path), self._pkgname)
319 self.ebuild_path_no_revision = '%s-%s' % (
320 self._ebuild_path_no_version, self.version_no_rev)
321 self._unstable_ebuild_path = '%s-9999.ebuild' % (
322 self._ebuild_path_no_version)
323 self.ebuild_path = path
325 self.is_workon = False
326 self.is_stable = False
327 self.is_blacklisted = False
328 self._ReadEBuild(path)
331 def Classify(ebuild_path):
332 """Return whether this ebuild is workon, stable, and/or blacklisted
334 workon is determined by whether the ebuild inherits from the
335 'cros-workon' eclass. stable is determined by whether there's a '~'
336 in the KEYWORDS setting in the ebuild. An ebuild is considered blacklisted
337 if a line in it starts with 'CROS_WORKON_BLACKLIST='
341 is_blacklisted = False
342 for line in fileinput.input(ebuild_path):
343 if line.startswith('inherit ') and 'cros-workon' in line:
345 elif line.startswith('KEYWORDS='):
346 for keyword in line.split('=', 1)[1].strip("\"'").split():
347 if not keyword.startswith('~') and keyword != '-*':
349 elif line.startswith('CROS_WORKON_BLACKLIST='):
350 is_blacklisted = True
352 return is_workon, is_stable, is_blacklisted
354 def _ReadEBuild(self, path):
355 """Determine the settings of `is_workon`, `is_stable` and is_blacklisted
357 These are determined using the static Classify function.
359 self.is_workon, self.is_stable, self.is_blacklisted = EBuild.Classify(path)
362 def GetCrosWorkonVars(ebuild_path, pkg_name):
363 """Return computed (as sourced ebuild script) values of:
365 * CROS_WORKON_LOCALNAME
366 * CROS_WORKON_PROJECT
370 ebuild_path: Path to the ebuild file (e.g: platform2-9999.ebuild).
371 pkg_name: The package name (e.g.: platform2).
374 A CrosWorkonVars tuple.
377 'CROS_WORKON_LOCALNAME',
378 'CROS_WORKON_PROJECT',
379 'CROS_WORKON_SUBDIR',
382 'CROS_WORKON_LOCALNAME': pkg_name,
383 'CROS_WORKON_SUBDIR': '',
385 settings = osutils.SourceEnvironment(ebuild_path, workon_vars, env=env)
386 # Try to detect problems extracting the variables by checking whether
387 # CROS_WORKON_PROJECT is set. If it isn't, something went wrong, possibly
388 # because we're simplistically sourcing the ebuild without most of portage
389 # being available. That still breaks this script and needs to be flagged
390 # as an error. We won't catch problems setting CROS_WORKON_LOCALNAME or
391 # CROS_WORKON_SUBDIR or if CROS_WORKON_PROJECT is set to the wrong thing,
392 # but at least this covers some types of failures.
393 if 'CROS_WORKON_PROJECT' not in settings:
394 raise EbuildFormatIncorrectException(ebuild_path,
395 'Unable to determine CROS_WORKON_PROJECT value.')
396 localnames = settings['CROS_WORKON_LOCALNAME'].split(',')
397 projects = settings['CROS_WORKON_PROJECT'].split(',')
398 subdirs = settings['CROS_WORKON_SUBDIR'].split(',')
400 return EBuild.CrosWorkonVars(localnames, projects, subdirs)
402 def GetSourcePath(self, srcroot, manifest):
403 """Get the project and path for this ebuild.
405 The path is guaranteed to exist, be a directory, and be absolute.
408 localnames, projects, subdirs = EBuild.GetCrosWorkonVars(
409 self._unstable_ebuild_path, self._pkgname)
410 # Sanity checks and completion.
411 # Each project specification has to have the same amount of items.
412 if len(projects) != len(localnames):
413 raise EbuildFormatIncorrectException(self._unstable_ebuild_path,
414 'Number of _PROJECT and _LOCALNAME items don\'t match.')
415 # Subdir must be either 0,1 or len(project)
416 if len(projects) != len(subdirs) and len(subdirs) > 1:
417 raise EbuildFormatIncorrectException(self._unstable_ebuild_path,
418 'Incorrect number of _SUBDIR items.')
419 # If there's one, apply it to all.
420 if len(subdirs) == 1:
421 subdirs = subdirs * len(projects)
422 # If there is none, make an empty list to avoid exceptions later.
423 if len(subdirs) == 0:
424 subdirs = [''] * len(projects)
427 if self._category == 'chromeos-base':
428 dir_ = '' # 'platform2'
432 # Once all targets are moved from platform to platform2, uncomment
433 # the following lines as well as dir_ = 'platform2' above,
434 # and delete the loop that builds |subdir_paths| below.
436 # subdir_paths = [os.path.realpath(os.path.join(srcroot, dir_, l, s))
437 # for l, s in zip(localnames, subdirs)]
440 for local, sub in zip(localnames, subdirs):
441 subdir_path = os.path.realpath(os.path.join(srcroot, dir_, local, sub))
442 if dir_ == '' and not os.path.isdir(subdir_path):
443 subdir_path = os.path.realpath(os.path.join(srcroot, 'platform',
445 subdir_paths.append(subdir_path)
447 for subdir_path, project in zip(subdir_paths, projects):
448 if not os.path.isdir(subdir_path):
449 cros_build_lib.Die('Source repository %s '
450 'for project %s does not exist.' % (subdir_path,
452 # Verify that we're grabbing the commit id from the right project name.
453 real_project = manifest.FindCheckoutFromPath(subdir_path)['name']
454 if project != real_project:
455 cros_build_lib.Die('Project name mismatch for %s '
456 '(found %s, expected %s)' % (subdir_path,
459 return projects, subdir_paths
461 def GetCommitId(self, srcdir):
462 """Get the commit id for this ebuild."""
463 output = self._RunGit(srcdir, ['rev-parse', 'HEAD'])
465 cros_build_lib.Die('Cannot determine HEAD commit for %s' % srcdir)
466 return output.rstrip()
468 def GetTreeId(self, srcdir):
469 """Get the SHA1 of the source tree for this ebuild.
471 Unlike the commit hash, the SHA1 of the source tree is unaffected by the
472 history of the repository, or by commit messages.
474 output = self._RunGit(srcdir, ['log', '-1', '--format=%T'])
476 cros_build_lib.Die('Cannot determine HEAD tree hash for %s' % srcdir)
477 return output.rstrip()
479 def GetVersion(self, srcroot, manifest, default):
480 """Get the base version number for this ebuild.
482 The version is provided by the ebuild through a specific script in
483 the $FILESDIR (chromeos-version.sh).
485 vers_script = os.path.join(os.path.dirname(self._ebuild_path_no_version),
486 'files', 'chromeos-version.sh')
488 if not os.path.exists(vers_script):
491 if not self.is_workon:
492 raise EbuildFormatIncorrectException(self._ebuild_path_no_version,
493 "Package has a chromeos-version.sh script but is not workon-able.")
495 srcdirs = self.GetSourcePath(srcroot, manifest)[1]
497 # The chromeos-version script will output a usable raw version number,
498 # or nothing in case of error or no available version
500 output = self._RunCommand([vers_script] + srcdirs).strip()
501 except cros_build_lib.RunCommandError as e:
502 cros_build_lib.Die('Package %s chromeos-version.sh failed: %s' %
506 cros_build_lib.Die('Package %s has a chromeos-version.sh script but '
507 'it returned no valid version for "%s"' %
508 (self._pkgname, ' '.join(srcdirs)))
513 def FormatBashArray(unformatted_list):
514 """Returns a python list in a bash array format.
516 If the list only has one item, format as simple quoted value.
517 That is both backwards-compatible and more readable.
520 unformatted_list: an iterable to format as a bash array. This variable
521 has to be sanitized first, as we don't do any safeties.
524 A text string that can be used by bash as array declaration.
526 if len(unformatted_list) > 1:
527 return '("%s")' % '" "'.join(unformatted_list)
529 return '"%s"' % unformatted_list[0]
531 def RevWorkOnEBuild(self, srcroot, manifest, redirect_file=None):
532 """Revs a workon ebuild given the git commit hash.
534 By default this class overwrites a new ebuild given the normal
535 ebuild rev'ing logic. However, a user can specify a redirect_file
536 to redirect the new stable ebuild to another file.
539 srcroot: full path to the 'src' subdirectory in the source
541 manifest: git.ManifestCheckout object.
542 redirect_file: Optional file to write the new ebuild. By default
543 it is written using the standard rev'ing logic. This file must be
544 opened and closed by the caller.
547 If the revved package is different than the old ebuild, return the full
548 revved package name, including the version number. Otherwise, return None.
551 OSError: Error occurred while creating a new ebuild.
552 IOError: Error occurred while writing to the new revved ebuild file.
556 stable_version_no_rev = self.GetVersion(srcroot, manifest,
559 # If given unstable ebuild, use preferred version rather than 9999.
560 stable_version_no_rev = self.GetVersion(srcroot, manifest, '0.0.1')
562 new_version = '%s-r%d' % (
563 stable_version_no_rev, self.current_revision + 1)
564 new_stable_ebuild_path = '%s-%s.ebuild' % (
565 self._ebuild_path_no_version, new_version)
567 self._Print('Creating new stable ebuild %s' % new_stable_ebuild_path)
568 if not os.path.exists(self._unstable_ebuild_path):
569 cros_build_lib.Die('Missing unstable ebuild: %s' %
570 self._unstable_ebuild_path)
572 srcdirs = self.GetSourcePath(srcroot, manifest)[1]
573 commit_ids = map(self.GetCommitId, srcdirs)
574 tree_ids = map(self.GetTreeId, srcdirs)
575 variables = dict(CROS_WORKON_COMMIT=self.FormatBashArray(commit_ids),
576 CROS_WORKON_TREE=self.FormatBashArray(tree_ids))
577 self.MarkAsStable(self._unstable_ebuild_path, new_stable_ebuild_path,
578 variables, redirect_file)
580 old_ebuild_path = self.ebuild_path
581 if filecmp.cmp(old_ebuild_path, new_stable_ebuild_path, shallow=False):
582 os.unlink(new_stable_ebuild_path)
585 self._Print('Adding new stable ebuild to git')
586 self._RunGit(self._overlay, ['add', new_stable_ebuild_path])
589 self._Print('Removing old ebuild from git')
590 self._RunGit(self._overlay, ['rm', old_ebuild_path])
592 return '%s-%s' % (self.package, new_version)
595 def GitRepoHasChanges(cls, directory):
596 """Returns True if there are changes in the given directory."""
597 # Refresh the index first. This squashes just metadata changes.
598 cls._RunGit(directory, ['update-index', '-q', '--refresh'])
599 output = cls._RunGit(directory, ['diff-index', '--name-only', 'HEAD'])
600 return output not in [None, '']
603 def _GetSHA1ForPath(manifest, path):
604 """Get the latest SHA1 for a given project from Gerrit.
606 This function looks up the remote and branch for a given project in the
607 manifest, and uses this to lookup the SHA1 from Gerrit. This only makes
608 sense for unpinned manifests.
611 manifest: git.ManifestCheckout object.
612 path: Path of project.
615 Exception if the manifest is pinned.
617 checkout = manifest.FindCheckoutFromPath(path)
618 project = checkout['name']
619 helper = gerrit.GetGerritHelper(checkout['remote'])
620 manifest_branch = checkout['revision']
621 branch = git.StripRefsHeads(manifest_branch)
622 return helper.GetLatestSHA1ForBranch(project, branch)
625 def _GetEBuildPaths(buildroot, manifest, overlay_list, changes):
626 """Calculate ebuild->path map for changed ebuilds.
629 buildroot: Path to root of build directory.
630 manifest: git.ManifestCheckout object.
631 overlay_list: List of all overlays.
632 changes: Changes from Gerrit that are being pushed.
635 A dictionary mapping changed ebuilds to lists of associated paths.
637 directory_src = os.path.join(buildroot, 'src')
638 overlay_dict = dict((o, []) for o in overlay_list)
639 BuildEBuildDictionary(overlay_dict, True, None)
640 changed_paths = set(c.GetCheckout(manifest).GetPath(absolute=True)
643 for ebuilds in overlay_dict.itervalues():
644 for ebuild in ebuilds:
645 _projects, paths = ebuild.GetSourcePath(directory_src, manifest)
646 if changed_paths.intersection(paths):
647 ebuild_projects[ebuild] = paths
649 return ebuild_projects
652 def UpdateCommitHashesForChanges(cls, changes, buildroot, manifest):
653 """Updates the commit hashes for the EBuilds uprevved in changes.
656 changes: Changes from Gerrit that are being pushed.
657 buildroot: Path to root of build directory.
658 manifest: git.ManifestCheckout object.
661 overlay_list = FindOverlays(constants.BOTH_OVERLAYS, buildroot=buildroot)
662 ebuild_paths = cls._GetEBuildPaths(buildroot, manifest, overlay_list,
664 for ebuild, paths in ebuild_paths.iteritems():
665 # Calculate any SHA1s that are not already in path_sha1s.
666 for path in set(paths).difference(path_sha1s):
667 path_sha1s[path] = cls._GetSHA1ForPath(manifest, path)
669 sha1s = [path_sha1s[path] for path in paths]
670 logging.info('Updating ebuild for package %s with commit hashes %r',
671 ebuild.package, sha1s)
672 updates = dict(CROS_WORKON_COMMIT=cls.FormatBashArray(sha1s))
673 EBuild.UpdateEBuild(ebuild.ebuild_path, updates)
675 # Commit any changes to all overlays.
676 for overlay in overlay_list:
677 if EBuild.GitRepoHasChanges(overlay):
678 EBuild.CommitChange('Updating commit hashes in ebuilds '
679 'to match remote repository.', overlay=overlay)
682 def BestEBuild(ebuilds):
683 """Returns the newest EBuild from a list of EBuild objects."""
684 # pylint: disable=F0401
685 from portage.versions import vercmp
687 for ebuild in ebuilds[1:]:
688 if vercmp(winner.version, ebuild.version) < 0:
693 def _FindUprevCandidates(files):
694 """Return the uprev candidate ebuild from a specified list of files.
696 Usually an uprev candidate is a the stable ebuild in a cros_workon
697 directory. However, if no such stable ebuild exists (someone just
698 checked in the 9999 ebuild), this is the unstable ebuild.
700 If the package isn't a cros_workon package, return None.
703 files: List of files in a package directory.
706 unstable_ebuilds = []
708 if not path.endswith('.ebuild') or os.path.islink(path):
710 ebuild = EBuild(path)
711 if not ebuild.is_workon or ebuild.is_blacklisted:
714 if ebuild.version == '9999':
715 cros_build_lib.Die('KEYWORDS in 9999 ebuild should not be stable %s'
717 stable_ebuilds.append(ebuild)
719 unstable_ebuilds.append(ebuild)
721 # If both ebuild lists are empty, the passed in file list was for
722 # a non-workon package.
723 if not unstable_ebuilds:
725 path = os.path.dirname(stable_ebuilds[0].ebuild_path)
726 cros_build_lib.Die('Missing 9999 ebuild in %s' % path)
729 path = os.path.dirname(unstable_ebuilds[0].ebuild_path)
730 if len(unstable_ebuilds) > 1:
731 cros_build_lib.Die('Found multiple unstable ebuilds in %s' % path)
733 if not stable_ebuilds:
734 cros_build_lib.Warning('Missing stable ebuild in %s' % path)
735 return unstable_ebuilds[0]
737 if len(stable_ebuilds) == 1:
738 return stable_ebuilds[0]
740 stable_versions = set(ebuild.version_no_rev for ebuild in stable_ebuilds)
741 if len(stable_versions) > 1:
742 package = stable_ebuilds[0].package
743 message = 'Found multiple stable ebuild versions in %s:' % path
744 for version in stable_versions:
745 message += '\n %s-%s' % (package, version)
746 cros_build_lib.Die(message)
748 uprev_ebuild = max(stable_ebuilds, key=lambda eb: eb.current_revision)
749 for ebuild in stable_ebuilds:
750 if ebuild != uprev_ebuild:
751 cros_build_lib.Warning('Ignoring stable ebuild revision %s in %s' %
752 (ebuild.version, path))
756 def BuildEBuildDictionary(overlays, use_all, packages):
757 """Build a dictionary of the ebuilds in the specified overlays.
759 overlays: A map which maps overlay directories to arrays of stable EBuilds
760 inside said directories.
761 use_all: Whether to include all ebuilds in the specified directories.
762 If true, then we gather all packages in the directories regardless
763 of whether they are in our set of packages.
764 packages: A set of the packages we want to gather. If use_all is
765 True, this argument is ignored, and should be None.
767 for overlay in overlays:
768 for package_dir, _dirs, files in os.walk(overlay):
769 # Add stable ebuilds to overlays[overlay].
770 paths = [os.path.join(package_dir, path) for path in files]
771 ebuild = _FindUprevCandidates(paths)
773 # If the --all option isn't used, we only want to update packages that
775 if ebuild and (use_all or ebuild.package in packages):
776 overlays[overlay].append(ebuild)
779 def RegenCache(overlay):
780 """Regenerate the cache of the specified overlay.
782 overlay: The tree to regenerate the cache for.
784 repo_name = GetOverlayName(overlay)
788 layout = cros_build_lib.LoadKeyValueFile('%s/metadata/layout.conf' % overlay,
790 if layout.get('cache-format') != 'md5-dict':
793 # Regen for the whole repo.
794 cros_build_lib.RunCommand(['egencache', '--update', '--repo', repo_name,
795 '--jobs', str(multiprocessing.cpu_count())])
796 # If there was nothing new generated, then let's just bail.
797 result = git.RunGit(overlay, ['status', '-s', 'metadata/'])
798 if not result.output:
800 # Explicitly add any new files to the index.
801 git.RunGit(overlay, ['add', 'metadata/'])
802 # Explicitly tell git to also include rm-ed files.
803 git.RunGit(overlay, ['commit', '-m', 'regen cache', 'metadata/'])
806 def ParseBashArray(value):
807 """Parse a valid bash array into python list."""
808 # The syntax for bash arrays is nontrivial, so let's use bash to do the
809 # heavy lifting for us.
811 # Because %s may contain bash comments (#), put a clever newline in the way.
812 cmd = 'ARR=%s\nIFS=%s; echo -n "${ARR[*]}"' % (value, sep)
813 return cros_build_lib.RunCommand(
814 cmd, print_cmd=False, shell=True, capture_output=True).output.split(sep)
817 def GetWorkonProjectMap(overlay, subdirectories):
818 """Get the project -> ebuild mapping for cros_workon ebuilds.
821 overlay: Overlay to look at.
822 subdirectories: List of subdirectories to look in on the overlay.
825 A list of (filename, projects) tuples for cros-workon ebuilds in the
826 given overlay under the given subdirectories.
828 # Search ebuilds for project names, ignoring non-existent directories.
829 # Also filter out ebuilds which are not cros_workon.
830 for subdir in subdirectories:
831 for root, _dirs, files in os.walk(os.path.join(overlay, subdir)):
832 for filename in files:
833 if filename.endswith('-9999.ebuild'):
834 full_path = os.path.join(root, filename)
835 is_workon = EBuild.Classify(full_path)[0]
838 pkg_name = os.path.basename(root)
839 _, projects, _ = EBuild.GetCrosWorkonVars(full_path, pkg_name)
840 relpath = os.path.relpath(full_path, start=overlay)
841 yield relpath, projects
844 def SplitEbuildPath(path):
845 """Split an ebuild path into its components.
847 Given a specified ebuild filename, returns $CATEGORY, $PN, $P. It does not
848 perform any check on ebuild name elements or their validity, merely splits
849 a filename, absolute or relative, and returns the last 3 components.
851 Example: For /any/path/chromeos-base/power_manager/power_manager-9999.ebuild,
852 returns ('chromeos-base', 'power_manager', 'power_manager-9999').
857 return os.path.splitext(path)[0].rsplit('/', 3)[-3:]
860 def SplitPV(pv, strict=True):
861 """Takes a PV value and splits it into individual components.
864 pv: Package name and version.
865 strict: If True, returns None if version or package name is missing.
866 Otherwise, only package name is mandatory.
869 A collection with named members:
870 pv, package, version, version_no_rev, rev
872 m = _pvr_re.match(pv)
874 if m is None and strict:
878 return PV(**{'pv': None, 'package': pv, 'version': None,
879 'version_no_rev': None, 'rev': None})
881 return PV(**m.groupdict())
884 def SplitCPV(cpv, strict=True):
885 """Splits a CPV value into components.
888 cpv: Category, package name, and version of a package.
889 strict: If True, returns None if any of the components is missing.
890 Otherwise, only package name is mandatory.
893 A collection with named members:
894 category, pv, package, version, version_no_rev, rev
896 chunks = cpv.split('/')
898 raise ValueError('Unexpected package format %s' % cpv)
904 m = SplitPV(chunks[-1], strict=strict)
905 if strict and (category is None or m is None):
907 # pylint: disable=W0212
908 return CPV(category=category, **m._asdict())
911 def FindWorkonProjects(packages):
912 """Find the projects associated with the specified cros_workon packages.
915 packages: List of cros_workon packages.
918 The set of projects associated with the specified cros_workon packages.
921 buildroot, both = constants.SOURCE_ROOT, constants.BOTH_OVERLAYS
922 for overlay in FindOverlays(both, buildroot=buildroot):
923 for _, projects in GetWorkonProjectMap(overlay, packages):
924 all_projects.update(projects)
928 def ListInstalledPackages(sysroot):
929 """Lists all portage packages in a given portage-managed root.
931 Assumes the existence of a /var/db/pkg package database.
934 sysroot: The root being inspected.
937 A list of (cp,v) tuples in the given sysroot.
939 vdb_path = os.path.join(sysroot, 'var/db/pkg')
940 ebuild_pattern = os.path.join(vdb_path, '*/*/*.ebuild')
942 for path in glob.glob(ebuild_pattern):
943 category, package, packagecheck = SplitEbuildPath(path)
944 pv = SplitPV(package)
945 if package == packagecheck and pv is not None:
946 packages.append(('%s/%s' % (category, pv.package), pv.version))
950 def BestVisible(atom, board=None, pkg_type='ebuild',
951 buildroot=constants.SOURCE_ROOT):
952 """Get the best visible ebuild CPV for the given atom.
956 board: Board to look at. By default, look in chroot.
957 pkg_type: Package type (ebuild, binary, or installed).
963 portageq = 'portageq' if board is None else 'portageq-%s' % board
964 root = cros_build_lib.GetSysroot(board=board)
965 cmd = [portageq, 'best_visible', root, pkg_type, atom]
966 result = cros_build_lib.RunCommand(
967 cmd, cwd=buildroot, enter_chroot=True, debug_level=logging.DEBUG,
969 return SplitCPV(result.output.strip())
972 def IsPackageInstalled(package, sysroot='/'):
973 """Return whether a portage package is in a given portage-managed root.
976 package: The CP to look for.
977 sysroot: The root being inspected.
979 for key, _version in ListInstalledPackages(sysroot):
986 def FindPackageNameMatches(pkg_str, board=None):
987 """Finds a list of installed packages matching |pkg_str|.
990 pkg_str: The package name with optional category, version, and slot.
991 board: The board to insepct.
994 A list of matched CPV objects.
998 cmd = ['equery-%s' % board]
1000 cmd += ['list', pkg_str]
1001 result = cros_build_lib.RunCommand(
1002 cmd, capture_output=True, error_code_ok=True)
1005 if result.returncode == 0:
1006 matches = [SplitCPV(x) for x in result.output.splitlines()]
1011 def GetBinaryPackageDir(sysroot='/', packages_dir=None):
1012 """Returns the binary package directory of |sysroot|."""
1013 dir_name = packages_dir if packages_dir else 'packages'
1014 return os.path.join(sysroot, dir_name)
1017 def GetBinaryPackagePath(c, p, v, sysroot='/', packages_dir=None):
1018 """Returns the path to the binary package.
1024 sysroot: The root being inspected.
1025 packages_dir: Name of the packages directory in |sysroot|.
1028 The path to the binary package.
1030 pkgdir = GetBinaryPackageDir(sysroot=sysroot, packages_dir=packages_dir)
1031 path = os.path.join(pkgdir, c, '%s-%s.tbz2' % (p, v))
1032 if not os.path.exists(path):
1033 raise ValueError('Cannot find the binary package %s!' % path)
1038 def CleanOutdatedBinaryPackages(board):
1039 """Cleans outdated binary packages for |board|."""
1040 return cros_build_lib.RunCommand(['eclean-%s' % board, '-d', 'packages'])