Upstream version 8.36.161.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / buildbot / portage_utilities.py
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.
4
5 """Routines and classes for working with Portage overlays and ebuilds."""
6
7 import collections
8 import filecmp
9 import fileinput
10 import glob
11 import logging
12 import multiprocessing
13 import os
14 import re
15 import shutil
16 import sys
17
18 from chromite.buildbot 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
23
24 _PRIVATE_PREFIX = '%(buildroot)s/src/private-overlays'
25 _GLOBAL_OVERLAYS = [
26   '%s/chromeos-overlay' % _PRIVATE_PREFIX,
27   '%s/chromeos-partner-overlay' % _PRIVATE_PREFIX,
28   '%(buildroot)s/src/third_party/chromiumos-overlay',
29   '%(buildroot)s/src/third_party/portage-stable',
30 ]
31
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)
36
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)
45
46 # This regex matches blank lines, commented lines, and the EAPI line.
47 _blank_or_eapi_re = re.compile(r'^\s*(?:#|EAPI=|$)')
48
49
50 def _ListOverlays(board=None, buildroot=constants.SOURCE_ROOT):
51   """Return the list of overlays to use for a given buildbot.
52
53   Always returns all overlays, and does not perform any filtering.
54
55   Args:
56     board: Board to look at.
57     buildroot: Source root to find overlays.
58   """
59   overlays, patterns = [], []
60   if board is None:
61     patterns += ['overlay*']
62   else:
63     board_no_variant, _, variant = board.partition('_')
64     patterns += ['overlay-%s' % board_no_variant]
65     if variant:
66       patterns += ['overlay-variant-%s' % board.replace('_', '-')]
67
68   for d in _GLOBAL_OVERLAYS:
69     d %= dict(buildroot=buildroot)
70     if os.path.isdir(d):
71       overlays.append(d)
72
73   for p in patterns:
74     overlays += glob.glob('%s/src/overlays/%s' % (buildroot, p))
75     overlays += glob.glob('%s/src/private-overlays/%s-private' % (buildroot, p))
76
77   return overlays
78
79
80 def FindOverlays(overlay_type, board=None, buildroot=constants.SOURCE_ROOT):
81   """Return the list of overlays to use for a given buildbot.
82
83   Args:
84     overlay_type: A string describing which overlays you want.
85       'private': Just the private overlays.
86       'public': Just the public overlays.
87       'both': Both the public and private overlays.
88     board: Board to look at.
89     buildroot: Source root to find overlays.
90   """
91   overlays = _ListOverlays(board=board, buildroot=buildroot)
92   private_prefix = _PRIVATE_PREFIX % dict(buildroot=buildroot)
93   if overlay_type == constants.PRIVATE_OVERLAYS:
94     return [x for x in overlays if x.startswith(private_prefix)]
95   elif overlay_type == constants.PUBLIC_OVERLAYS:
96     return [x for x in overlays if not x.startswith(private_prefix)]
97   elif overlay_type == constants.BOTH_OVERLAYS:
98     return overlays
99   else:
100     assert overlay_type is None
101     return []
102
103
104 def ReadOverlayFile(filename, overlay_type='both', board=None,
105                     buildroot=constants.SOURCE_ROOT):
106   """Attempt to open a file in the overlay directories.
107
108   Args:
109     filename: Path to open inside the overlay.
110     overlay_type: A string describing which overlays you want.
111       'private': Just the private overlays.
112       'public': Just the public overlays.
113       'both': Both the public and private overlays.
114     board: Board to look at.
115     buildroot: Source root to find overlays.
116
117   Returns:
118     The contents of the file, or None if no files could be opened.
119   """
120   for overlay in FindOverlays(overlay_type, board, buildroot):
121     try:
122       return osutils.ReadFile(os.path.join(overlay, filename))
123     except IOError as e:
124       if e.errno != os.errno.ENOENT:
125         raise
126
127
128 class MissingOverlayException(Exception):
129   """This exception indicates that a needed overlay is missing."""
130
131
132 def FindPrimaryOverlay(overlay_type, board, buildroot=constants.SOURCE_ROOT):
133   """Return the primary overlay to use for a given buildbot.
134
135   An overlay is only considered a primary overlay if it has a make.conf and a
136   toolchain.conf. If multiple primary overlays are found, the first primary
137   overlay is returned.
138
139   Args:
140     overlay_type: A string describing which overlays you want.
141       'private': Just the private overlays.
142       'public': Just the public overlays.
143       'both': Both the public and private overlays.
144     board: Board to look at.
145     buildroot: Path to root of build directory.
146
147   Raises:
148     MissingOverlayException: No primary overlay found.
149   """
150   for overlay in FindOverlays(overlay_type, board, buildroot):
151     if (os.path.exists(os.path.join(overlay, 'make.conf')) and
152         os.path.exists(os.path.join(overlay, 'toolchain.conf'))):
153       return overlay
154   raise MissingOverlayException('No primary overlay found for board=%r' % board)
155
156
157 def GetOverlayName(overlay):
158   try:
159     return open('%s/profiles/repo_name' % overlay).readline().rstrip()
160   except IOError:
161     # Not all overlays have a repo_name, so don't make a fuss.
162     return None
163
164
165 class EBuildVersionFormatException(Exception):
166   """Exception for bad ebuild version string format."""
167   def __init__(self, filename):
168     self.filename = filename
169     message = ('Ebuild file name %s '
170                'does not match expected format.' % filename)
171     super(EBuildVersionFormatException, self).__init__(message)
172
173
174 class EbuildFormatIncorrectException(Exception):
175   """Exception for bad ebuild format."""
176   def __init__(self, filename, message):
177     message = 'Ebuild %s has invalid format: %s ' % (filename, message)
178     super(EbuildFormatIncorrectException, self).__init__(message)
179
180
181 class EBuild(object):
182   """Wrapper class for information about an ebuild."""
183
184   VERBOSE = False
185   _PACKAGE_VERSION_PATTERN = re.compile(
186     r'.*-(([0-9][0-9a-z_.]*)(-r[0-9]+)?)[.]ebuild')
187   _WORKON_COMMIT_PATTERN = re.compile(r'^CROS_WORKON_COMMIT="(.*)"$')
188
189   # A structure to hold computed values of CROS_WORKON_*.
190   CrosWorkonVars = collections.namedtuple(
191       'CrosWorkonVars', ('localname', 'project', 'subdir'))
192
193   @classmethod
194   def _Print(cls, message):
195     """Verbose print function."""
196     if cls.VERBOSE:
197       cros_build_lib.Info(message)
198
199   @classmethod
200   def _RunCommand(cls, command, **kwargs):
201     kwargs.setdefault('capture_output', True)
202     return cros_build_lib.RunCommand(
203         command, print_cmd=cls.VERBOSE, **kwargs).output
204
205   @classmethod
206   def _RunGit(cls, cwd, command, **kwargs):
207     result = git.RunGit(cwd, command, print_cmd=cls.VERBOSE, **kwargs)
208     return None if result is None else result.output
209
210   def IsSticky(self):
211     """Returns True if the ebuild is sticky."""
212     return self.is_stable and self.current_revision == 0
213
214   @classmethod
215   def UpdateEBuild(cls, ebuild_path, variables, redirect_file=None,
216                    make_stable=True):
217     """Static function that updates WORKON information in the ebuild.
218
219     This function takes an ebuild_path and updates WORKON information.
220
221     Args:
222       ebuild_path: The path of the ebuild.
223       variables: Dictionary of variables to update in ebuild.
224       redirect_file: Optionally redirect output of new ebuild somewhere else.
225       make_stable: Actually make the ebuild stable.
226     """
227     written = False
228     for line in fileinput.input(ebuild_path, inplace=1):
229       # Has to be done here to get changes to sys.stdout from fileinput.input.
230       if not redirect_file:
231         redirect_file = sys.stdout
232
233       # Always add variables at the top of the ebuild, before the first
234       # nonblank line other than the EAPI line.
235       if not written and not _blank_or_eapi_re.match(line):
236         for key, value in sorted(variables.items()):
237           assert key is not None and value is not None
238           redirect_file.write('%s=%s\n' % (key, value))
239         written = True
240
241       # Mark KEYWORDS as stable by removing ~'s.
242       if line.startswith('KEYWORDS=') and make_stable:
243         line = line.replace('~', '')
244
245       varname, eq, _ = line.partition('=')
246       if not (eq == '=' and varname.strip() in variables):
247         # Don't write out the old value of the variable.
248         redirect_file.write(line)
249
250     fileinput.close()
251
252   @classmethod
253   def MarkAsStable(cls, unstable_ebuild_path, new_stable_ebuild_path,
254                    variables, redirect_file=None, make_stable=True):
255     """Static function that creates a revved stable ebuild.
256
257     This function assumes you have already figured out the name of the new
258     stable ebuild path and then creates that file from the given unstable
259     ebuild and marks it as stable.  If the commit_value is set, it also
260     set the commit_keyword=commit_value pair in the ebuild.
261
262     Args:
263       unstable_ebuild_path: The path to the unstable ebuild.
264       new_stable_ebuild_path: The path you want to use for the new stable
265         ebuild.
266       variables: Dictionary of variables to update in ebuild.
267       redirect_file: Optionally redirect output of new ebuild somewhere else.
268       make_stable: Actually make the ebuild stable.
269     """
270     shutil.copyfile(unstable_ebuild_path, new_stable_ebuild_path)
271     EBuild.UpdateEBuild(new_stable_ebuild_path, variables, redirect_file,
272                         make_stable)
273
274   @classmethod
275   def CommitChange(cls, message, overlay):
276     """Commits current changes in git locally with given commit message.
277
278     Args:
279       message: the commit string to write when committing to git.
280       overlay: directory in which to commit the changes.
281
282     Raises:
283       RunCommandError: Error occurred while committing.
284     """
285     logging.info('Committing changes with commit message: %s', message)
286     git_commit_cmd = ['commit', '-a', '-m', message]
287     cls._RunGit(overlay, git_commit_cmd)
288
289   def __init__(self, path):
290     """Sets up data about an ebuild from its path.
291
292     Args:
293       path: Path to the ebuild.
294     """
295     self._overlay, self._category, self._pkgname, filename = path.rsplit('/', 3)
296     m = self._PACKAGE_VERSION_PATTERN.match(filename)
297     if not m:
298       raise EBuildVersionFormatException(filename)
299     self.version, self.version_no_rev, revision = m.groups()
300     if revision is not None:
301       self.current_revision = int(revision.replace('-r', ''))
302     else:
303       self.current_revision = 0
304     self.package = '%s/%s' % (self._category, self._pkgname)
305
306     self._ebuild_path_no_version = os.path.join(
307         os.path.dirname(path), self._pkgname)
308     self.ebuild_path_no_revision = '%s-%s' % (
309         self._ebuild_path_no_version, self.version_no_rev)
310     self._unstable_ebuild_path = '%s-9999.ebuild' % (
311         self._ebuild_path_no_version)
312     self.ebuild_path = path
313
314     self.is_workon = False
315     self.is_stable = False
316     self.is_blacklisted = False
317     self._ReadEBuild(path)
318
319   def _ReadEBuild(self, path):
320     """Determine the settings of `is_workon` and `is_stable`.
321
322     `is_workon` is determined by whether the ebuild inherits from
323     the 'cros-workon' eclass.  `is_stable` is determined by whether
324     there's a '~' in the KEYWORDS setting in the ebuild.
325
326     This function is separate from __init__() to allow unit tests to
327     stub it out.
328     """
329     for line in fileinput.input(path):
330       if line.startswith('inherit ') and 'cros-workon' in line:
331         self.is_workon = True
332       elif line.startswith('KEYWORDS='):
333         for keyword in line.split('=', 1)[1].strip("\"'").split():
334           if not keyword.startswith('~') and keyword != '-*':
335             self.is_stable = True
336       elif line.startswith('CROS_WORKON_BLACKLIST='):
337         self.is_blacklisted = True
338     fileinput.close()
339
340   @staticmethod
341   def GetCrosWorkonVars(ebuild_path, pkg_name):
342     """Return computed (as sourced ebuild script) values of:
343
344       * CROS_WORKON_LOCALNAME
345       * CROS_WORKON_PROJECT
346       * CROS_WORKON_SUBDIR
347
348     Args:
349       ebuild_path: Path to the ebuild file (e.g: platform2-9999.ebuild).
350       pkg_name: The package name (e.g.: platform2).
351
352     Returns:
353       A CrosWorkonVars tuple.
354     """
355     workon_vars = (
356         'CROS_WORKON_LOCALNAME',
357         'CROS_WORKON_PROJECT',
358         'CROS_WORKON_SUBDIR',
359     )
360     env = {
361         'CROS_WORKON_LOCALNAME': pkg_name,
362         'CROS_WORKON_PROJECT': pkg_name,
363         'CROS_WORKON_SUBDIR': '',
364     }
365     settings = osutils.SourceEnvironment(ebuild_path, workon_vars, env=env)
366     localnames = settings['CROS_WORKON_LOCALNAME'].split(',')
367     projects = settings['CROS_WORKON_PROJECT'].split(',')
368     subdirs = settings['CROS_WORKON_SUBDIR'].split(',')
369
370     return EBuild.CrosWorkonVars(localnames, projects, subdirs)
371
372   def GetSourcePath(self, srcroot, manifest):
373     """Get the project and path for this ebuild.
374
375     The path is guaranteed to exist, be a directory, and be absolute.
376     """
377
378     localnames, projects, subdirs = EBuild.GetCrosWorkonVars(
379         self._unstable_ebuild_path, self._pkgname)
380     # Sanity checks and completion.
381     # Each project specification has to have the same amount of items.
382     if len(projects) != len(localnames):
383       raise EbuildFormatIncorrectException(self._unstable_ebuild_path,
384           'Number of _PROJECT and _LOCALNAME items don\'t match.')
385     # Subdir must be either 0,1 or len(project)
386     if len(projects) != len(subdirs) and len(subdirs) > 1:
387       raise EbuildFormatIncorrectException(self._unstable_ebuild_path,
388           'Incorrect number of _SUBDIR items.')
389     # If there's one, apply it to all.
390     if len(subdirs) == 1:
391       subdirs = subdirs * len(projects)
392     # If there is none, make an empty list to avoid exceptions later.
393     if len(subdirs) == 0:
394       subdirs = [''] * len(projects)
395
396     # Calculate srcdir.
397     if self._category == 'chromeos-base':
398       dir_ = '' # 'platform2'
399     else:
400       dir_ = 'third_party'
401
402     # Once all targets are moved from platform to platform2, uncomment
403     # the following lines as well as dir_ = 'platform2' above,
404     # and delete the loop that builds |subdir_paths| below.
405
406     # subdir_paths = [os.path.realpath(os.path.join(srcroot, dir_, l, s))
407     #                for l, s in zip(localnames, subdirs)]
408
409     subdir_paths = []
410     for local, sub in zip(localnames, subdirs):
411       subdir_path = os.path.realpath(os.path.join(srcroot, dir_, local, sub))
412       if dir_ == '' and not os.path.isdir(subdir_path):
413         subdir_path = os.path.realpath(os.path.join(srcroot, 'platform',
414                                                     local, sub))
415       subdir_paths.append(subdir_path)
416
417     for subdir_path, project in zip(subdir_paths, projects):
418       if not os.path.isdir(subdir_path):
419         cros_build_lib.Die('Source repository %s '
420                            'for project %s does not exist.' % (subdir_path,
421                                                                self._pkgname))
422       # Verify that we're grabbing the commit id from the right project name.
423       real_project = manifest.FindCheckoutFromPath(subdir_path)['name']
424       if project != real_project:
425         cros_build_lib.Die('Project name mismatch for %s '
426                            '(found %s, expected %s)' % (subdir_path,
427                                                         real_project,
428                                                         project))
429     return projects, subdir_paths
430
431   def GetCommitId(self, srcdir):
432     """Get the commit id for this ebuild."""
433     output = self._RunGit(srcdir, ['rev-parse', 'HEAD'])
434     if not output:
435       cros_build_lib.Die('Cannot determine HEAD commit for %s' % srcdir)
436     return output.rstrip()
437
438   def GetTreeId(self, srcdir):
439     """Get the SHA1 of the source tree for this ebuild.
440
441     Unlike the commit hash, the SHA1 of the source tree is unaffected by the
442     history of the repository, or by commit messages.
443     """
444     output = self._RunGit(srcdir, ['log', '-1', '--format=%T'])
445     if not output:
446       cros_build_lib.Die('Cannot determine HEAD tree hash for %s' % srcdir)
447     return output.rstrip()
448
449   def GetVersion(self, srcroot, manifest, default):
450     """Get the base version number for this ebuild.
451
452     The version is provided by the ebuild through a specific script in
453     the $FILESDIR (chromeos-version.sh).
454     """
455     vers_script = os.path.join(os.path.dirname(self._ebuild_path_no_version),
456                                'files', 'chromeos-version.sh')
457
458     if not os.path.exists(vers_script):
459       return default
460
461     srcdirs = self.GetSourcePath(srcroot, manifest)[1]
462
463     # The chromeos-version script will output a usable raw version number,
464     # or nothing in case of error or no available version
465     try:
466       output = self._RunCommand([vers_script] + srcdirs).strip()
467     except cros_build_lib.RunCommandError as e:
468       cros_build_lib.Die('Package %s chromeos-version.sh failed: %s' %
469                          (self._pkgname, e))
470
471     if not output:
472       cros_build_lib.Die('Package %s has a chromeos-version.sh script but '
473                          'it returned no valid version for "%s"' %
474                          (self._pkgname, ' '.join(srcdirs)))
475
476     return output
477
478   @staticmethod
479   def FormatBashArray(unformatted_list):
480     """Returns a python list in a bash array format.
481
482     If the list only has one item, format as simple quoted value.
483     That is both backwards-compatible and more readable.
484
485     Args:
486       unformatted_list: an iterable to format as a bash array. This variable
487         has to be sanitized first, as we don't do any safeties.
488
489     Returns:
490       A text string that can be used by bash as array declaration.
491     """
492     if len(unformatted_list) > 1:
493       return '("%s")' % '" "'.join(unformatted_list)
494     else:
495       return '"%s"' % unformatted_list[0]
496
497   def RevWorkOnEBuild(self, srcroot, manifest, redirect_file=None):
498     """Revs a workon ebuild given the git commit hash.
499
500     By default this class overwrites a new ebuild given the normal
501     ebuild rev'ing logic.  However, a user can specify a redirect_file
502     to redirect the new stable ebuild to another file.
503
504     Args:
505       srcroot: full path to the 'src' subdirectory in the source
506         repository.
507       manifest: git.ManifestCheckout object.
508       redirect_file: Optional file to write the new ebuild.  By default
509         it is written using the standard rev'ing logic.  This file must be
510         opened and closed by the caller.
511
512     Returns:
513       If the revved package is different than the old ebuild, return the full
514       revved package name, including the version number. Otherwise, return None.
515
516     Raises:
517       OSError: Error occurred while creating a new ebuild.
518       IOError: Error occurred while writing to the new revved ebuild file.
519     """
520
521     if self.is_stable:
522       stable_version_no_rev = self.GetVersion(srcroot, manifest,
523                                               self.version_no_rev)
524     else:
525       # If given unstable ebuild, use preferred version rather than 9999.
526       stable_version_no_rev = self.GetVersion(srcroot, manifest, '0.0.1')
527
528     new_version = '%s-r%d' % (
529         stable_version_no_rev, self.current_revision + 1)
530     new_stable_ebuild_path = '%s-%s.ebuild' % (
531         self._ebuild_path_no_version, new_version)
532
533     self._Print('Creating new stable ebuild %s' % new_stable_ebuild_path)
534     if not os.path.exists(self._unstable_ebuild_path):
535       cros_build_lib.Die('Missing unstable ebuild: %s' %
536                          self._unstable_ebuild_path)
537
538     srcdirs = self.GetSourcePath(srcroot, manifest)[1]
539     commit_ids = map(self.GetCommitId, srcdirs)
540     tree_ids = map(self.GetTreeId, srcdirs)
541     variables = dict(CROS_WORKON_COMMIT=self.FormatBashArray(commit_ids),
542                      CROS_WORKON_TREE=self.FormatBashArray(tree_ids))
543     self.MarkAsStable(self._unstable_ebuild_path, new_stable_ebuild_path,
544                       variables, redirect_file)
545
546     old_ebuild_path = self.ebuild_path
547     if filecmp.cmp(old_ebuild_path, new_stable_ebuild_path, shallow=False):
548       os.unlink(new_stable_ebuild_path)
549       return None
550     else:
551       self._Print('Adding new stable ebuild to git')
552       self._RunGit(self._overlay, ['add', new_stable_ebuild_path])
553
554       if self.is_stable:
555         self._Print('Removing old ebuild from git')
556         self._RunGit(self._overlay, ['rm', old_ebuild_path])
557
558       return '%s-%s' % (self.package, new_version)
559
560   @classmethod
561   def GitRepoHasChanges(cls, directory):
562     """Returns True if there are changes in the given directory."""
563     # Refresh the index first. This squashes just metadata changes.
564     cls._RunGit(directory, ['update-index', '-q', '--refresh'])
565     output = cls._RunGit(directory, ['diff-index', '--name-only', 'HEAD'])
566     return output not in [None, '']
567
568   @staticmethod
569   def _GetSHA1ForPath(manifest, path):
570     """Get the latest SHA1 for a given project from Gerrit.
571
572     This function looks up the remote and branch for a given project in the
573     manifest, and uses this to lookup the SHA1 from Gerrit. This only makes
574     sense for unpinned manifests.
575
576     Args:
577       manifest: git.ManifestCheckout object.
578       path: Path of project.
579
580     Raises:
581       Exception if the manifest is pinned.
582     """
583     checkout = manifest.FindCheckoutFromPath(path)
584     project = checkout['name']
585     helper = gerrit.GetGerritHelper(checkout['remote'])
586     manifest_branch = checkout['revision']
587     branch = git.StripRefsHeads(manifest_branch)
588     return helper.GetLatestSHA1ForBranch(project, branch)
589
590   @staticmethod
591   def _GetEBuildPaths(buildroot, manifest, overlay_list, changes):
592     """Calculate ebuild->path map for changed ebuilds.
593
594     Args:
595       buildroot: Path to root of build directory.
596       manifest: git.ManifestCheckout object.
597       overlay_list: List of all overlays.
598       changes: Changes from Gerrit that are being pushed.
599
600     Returns:
601       A dictionary mapping changed ebuilds to lists of associated paths.
602     """
603     directory_src = os.path.join(buildroot, 'src')
604     overlay_dict = dict((o, []) for o in overlay_list)
605     BuildEBuildDictionary(overlay_dict, True, None)
606     changed_paths = set(c.GetCheckout(manifest).GetPath(absolute=True)
607                         for c in changes)
608     ebuild_projects = {}
609     for ebuilds in overlay_dict.itervalues():
610       for ebuild in ebuilds:
611         _projects, paths = ebuild.GetSourcePath(directory_src, manifest)
612         if changed_paths.intersection(paths):
613           ebuild_projects[ebuild] = paths
614
615     return ebuild_projects
616
617   @classmethod
618   def UpdateCommitHashesForChanges(cls, changes, buildroot, manifest):
619     """Updates the commit hashes for the EBuilds uprevved in changes.
620
621     Args:
622       changes: Changes from Gerrit that are being pushed.
623       buildroot: Path to root of build directory.
624       manifest: git.ManifestCheckout object.
625     """
626     path_sha1s = {}
627     overlay_list = FindOverlays(constants.BOTH_OVERLAYS, buildroot=buildroot)
628     ebuild_paths = cls._GetEBuildPaths(buildroot, manifest, overlay_list,
629                                        changes)
630     for ebuild, paths in ebuild_paths.iteritems():
631       # Calculate any SHA1s that are not already in path_sha1s.
632       for path in set(paths).difference(path_sha1s):
633         path_sha1s[path] = cls._GetSHA1ForPath(manifest, path)
634
635       sha1s = [path_sha1s[path] for path in paths]
636       logging.info('Updating ebuild for package %s with commit hashes %r',
637                    ebuild.package, sha1s)
638       updates = dict(CROS_WORKON_COMMIT=cls.FormatBashArray(sha1s))
639       EBuild.UpdateEBuild(ebuild.ebuild_path, updates)
640
641     # Commit any changes to all overlays.
642     for overlay in overlay_list:
643       if EBuild.GitRepoHasChanges(overlay):
644         EBuild.CommitChange('Updating commit hashes in ebuilds '
645                             'to match remote repository.', overlay=overlay)
646
647
648 def BestEBuild(ebuilds):
649   """Returns the newest EBuild from a list of EBuild objects."""
650   # pylint: disable=F0401
651   from portage.versions import vercmp
652   winner = ebuilds[0]
653   for ebuild in ebuilds[1:]:
654     if vercmp(winner.version, ebuild.version) < 0:
655       winner = ebuild
656   return winner
657
658
659 def _FindUprevCandidates(files):
660   """Return the uprev candidate ebuild from a specified list of files.
661
662   Usually an uprev candidate is a the stable ebuild in a cros_workon
663   directory.  However, if no such stable ebuild exists (someone just
664   checked in the 9999 ebuild), this is the unstable ebuild.
665
666   If the package isn't a cros_workon package, return None.
667
668   Args:
669     files: List of files in a package directory.
670   """
671   stable_ebuilds = []
672   unstable_ebuilds = []
673   for path in files:
674     if not path.endswith('.ebuild') or os.path.islink(path):
675       continue
676     ebuild = EBuild(path)
677     if not ebuild.is_workon or ebuild.is_blacklisted:
678       continue
679     if ebuild.is_stable:
680       if ebuild.version == '9999':
681         cros_build_lib.Die('KEYWORDS in 9999 ebuild should not be stable %s'
682                            % path)
683       stable_ebuilds.append(ebuild)
684     else:
685       unstable_ebuilds.append(ebuild)
686
687   # If both ebuild lists are empty, the passed in file list was for
688   # a non-workon package.
689   if not unstable_ebuilds:
690     if stable_ebuilds:
691       path = os.path.dirname(stable_ebuilds[0].ebuild_path)
692       cros_build_lib.Die('Missing 9999 ebuild in %s' % path)
693     return None
694
695   path = os.path.dirname(unstable_ebuilds[0].ebuild_path)
696   if len(unstable_ebuilds) > 1:
697     cros_build_lib.Die('Found multiple unstable ebuilds in %s' % path)
698
699   if not stable_ebuilds:
700     cros_build_lib.Warning('Missing stable ebuild in %s' % path)
701     return unstable_ebuilds[0]
702
703   if len(stable_ebuilds) == 1:
704     return stable_ebuilds[0]
705
706   stable_versions = set(ebuild.version_no_rev for ebuild in stable_ebuilds)
707   if len(stable_versions) > 1:
708     package = stable_ebuilds[0].package
709     message = 'Found multiple stable ebuild versions in %s:' % path
710     for version in stable_versions:
711       message += '\n    %s-%s' % (package, version)
712     cros_build_lib.Die(message)
713
714   uprev_ebuild = max(stable_ebuilds, key=lambda eb: eb.current_revision)
715   for ebuild in stable_ebuilds:
716     if ebuild != uprev_ebuild:
717       cros_build_lib.Warning('Ignoring stable ebuild revision %s in %s' %
718                              (ebuild.version, path))
719   return uprev_ebuild
720
721
722 def BuildEBuildDictionary(overlays, use_all, packages):
723   """Build a dictionary of the ebuilds in the specified overlays.
724
725   overlays: A map which maps overlay directories to arrays of stable EBuilds
726     inside said directories.
727   use_all: Whether to include all ebuilds in the specified directories.
728     If true, then we gather all packages in the directories regardless
729     of whether they are in our set of packages.
730   packages: A set of the packages we want to gather.  If use_all is
731     True, this argument is ignored, and should be None.
732   """
733   for overlay in overlays:
734     for package_dir, _dirs, files in os.walk(overlay):
735       # Add stable ebuilds to overlays[overlay].
736       paths = [os.path.join(package_dir, path) for path in files]
737       ebuild = _FindUprevCandidates(paths)
738
739       # If the --all option isn't used, we only want to update packages that
740       # are in packages.
741       if ebuild and (use_all or ebuild.package in packages):
742         overlays[overlay].append(ebuild)
743
744
745 def RegenCache(overlay):
746   """Regenerate the cache of the specified overlay.
747
748   overlay: The tree to regenerate the cache for.
749   """
750   repo_name = GetOverlayName(overlay)
751   if not repo_name:
752     return
753
754   layout = cros_build_lib.LoadKeyValueFile('%s/metadata/layout.conf' % overlay,
755                                            ignore_missing=True)
756   if layout.get('cache-format') != 'md5-dict':
757     return
758
759   # Regen for the whole repo.
760   cros_build_lib.RunCommand(['egencache', '--update', '--repo', repo_name,
761                              '--jobs', str(multiprocessing.cpu_count())])
762   # If there was nothing new generated, then let's just bail.
763   result = git.RunGit(overlay, ['status', '-s', 'metadata/'])
764   if not result.output:
765     return
766   # Explicitly add any new files to the index.
767   git.RunGit(overlay, ['add', 'metadata/'])
768   # Explicitly tell git to also include rm-ed files.
769   git.RunGit(overlay, ['commit', '-m', 'regen cache', 'metadata/'])
770
771
772 def ParseBashArray(value):
773   """Parse a valid bash array into python list."""
774   # The syntax for bash arrays is nontrivial, so let's use bash to do the
775   # heavy lifting for us.
776   sep = ','
777   # Because %s may contain bash comments (#), put a clever newline in the way.
778   cmd = 'ARR=%s\nIFS=%s; echo -n "${ARR[*]}"' % (value, sep)
779   return cros_build_lib.RunCommand(
780       cmd, print_cmd=False, shell=True, capture_output=True).output.split(sep)
781
782
783 def GetWorkonProjectMap(overlay, subdirectories):
784   """Get the project -> ebuild mapping for cros_workon ebuilds.
785
786   Args:
787     overlay: Overlay to look at.
788     subdirectories: List of subdirectories to look in on the overlay.
789
790   Returns:
791     A list of (filename, projects) tuples for cros-workon ebuilds in the
792     given overlay under the given subdirectories.
793   """
794   # Search ebuilds for project names, ignoring non-existent directories.
795   for subdir in subdirectories:
796     for root, _dirs, files in os.walk(os.path.join(overlay, subdir)):
797       for filename in files:
798         if filename.endswith('-9999.ebuild'):
799           full_path = os.path.join(root, filename)
800           pkg_name = os.path.basename(root)
801           _, projects, _ = EBuild.GetCrosWorkonVars(full_path, pkg_name)
802           relpath = os.path.relpath(full_path, start=overlay)
803           yield relpath, projects
804
805
806 def SplitEbuildPath(path):
807   """Split an ebuild path into its components.
808
809   Given a specified ebuild filename, returns $CATEGORY, $PN, $P. It does not
810   perform any check on ebuild name elements or their validity, merely splits
811   a filename, absolute or relative, and returns the last 3 components.
812
813   Example: For /any/path/chromeos-base/power_manager/power_manager-9999.ebuild,
814   returns ('chromeos-base', 'power_manager', 'power_manager-9999').
815
816   Returns:
817     $CATEGORY, $PN, $P
818   """
819   return os.path.splitext(path)[0].rsplit('/', 3)[-3:]
820
821
822 def SplitPV(pv, strict=True):
823   """Takes a PV value and splits it into individual components.
824
825   Args:
826     pv: Package name and version.
827     strict: If True, returns None if version or package name is missing.
828       Otherwise, only package name is mandatory.
829
830   Returns:
831     A collection with named members:
832       pv, package, version, version_no_rev, rev
833   """
834   m = _pvr_re.match(pv)
835
836   if m is None and strict:
837     return None
838
839   if m is None:
840     return PV(**{'pv': None, 'package': pv, 'version': None,
841                  'version_no_rev': None, 'rev': None})
842
843   return PV(**m.groupdict())
844
845
846 def SplitCPV(cpv, strict=True):
847   """Splits a CPV value into components.
848
849   Args:
850     cpv: Category, package name, and version of a package.
851     strict: If True, returns None if any of the components is missing.
852       Otherwise, only package name is mandatory.
853
854   Returns:
855     A collection with named members:
856       category, pv, package, version, version_no_rev, rev
857   """
858   chunks = cpv.split('/')
859   if len(chunks) > 2:
860     raise ValueError('Unexpected package format %s' % cpv)
861   if len(chunks) == 1:
862     category = None
863   else:
864     category = chunks[0]
865
866   m = SplitPV(chunks[-1], strict=strict)
867   if strict and (category is None or m is None):
868     return None
869   # pylint: disable=W0212
870   return CPV(category=category, **m._asdict())
871
872
873 def FindWorkonProjects(packages):
874   """Find the projects associated with the specified cros_workon packages.
875
876   Args:
877     packages: List of cros_workon packages.
878
879   Returns:
880     The set of projects associated with the specified cros_workon packages.
881   """
882   all_projects = set()
883   buildroot, both = constants.SOURCE_ROOT, constants.BOTH_OVERLAYS
884   for overlay in FindOverlays(both, buildroot=buildroot):
885     for _, projects in GetWorkonProjectMap(overlay, packages):
886       all_projects.update(projects)
887   return all_projects
888
889
890 def ListInstalledPackages(sysroot):
891   """Lists all portage packages in a given portage-managed root.
892
893   Assumes the existence of a /var/db/pkg package database.
894
895   Args:
896     sysroot: The root being inspected.
897
898   Returns:
899     A list of (cp,v) tuples in the given sysroot.
900   """
901   vdb_path = os.path.join(sysroot, 'var/db/pkg')
902   ebuild_pattern = os.path.join(vdb_path, '*/*/*.ebuild')
903   packages = []
904   for path in glob.glob(ebuild_pattern):
905     category, package, packagecheck = SplitEbuildPath(path)
906     pv = SplitPV(package)
907     if package == packagecheck and pv is not None:
908       packages.append(('%s/%s' % (category, pv.package), pv.version))
909   return packages
910
911
912 def BestVisible(atom, board=None, pkg_type='ebuild',
913                 buildroot=constants.SOURCE_ROOT):
914   """Get the best visible ebuild CPV for the given atom.
915
916   Args:
917     atom: Portage atom.
918     board: Board to look at. By default, look in chroot.
919     pkg_type: Package type (ebuild, binary, or installed).
920     buildroot: Directory
921
922   Returns:
923     A CPV object.
924   """
925   portageq = 'portageq' if board is None else 'portageq-%s' % board
926   root = cros_build_lib.GetSysroot(board=board)
927   cmd = [portageq, 'best_visible', root, pkg_type, atom]
928   result = cros_build_lib.RunCommand(
929       cmd, cwd=buildroot, enter_chroot=True, debug_level=logging.DEBUG,
930       capture_output=True)
931   return SplitCPV(result.output.strip())
932
933
934 def IsPackageInstalled(package, sysroot='/'):
935   """Return whether a portage package is in a given portage-managed root.
936
937   Args:
938     package: The CP to look for.
939     sysroot: The root being inspected.
940   """
941   for key, _version in ListInstalledPackages(sysroot):
942     if key == package:
943       return True
944
945   return False
946
947
948 def FindPackageNameMatches(pkg_str, board=None):
949   """Finds a list of installed packages matching |pkg_str|.
950
951   Args:
952     pkg_str: The package name with optional category, version, and slot.
953     board: The board to insepct.
954
955   Returns:
956     A list of matched CPV objects.
957   """
958   cmd = ['equery']
959   if board:
960     cmd = ['equery-%s' % board]
961
962   cmd += ['list', pkg_str]
963   result = cros_build_lib.RunCommand(
964       cmd, capture_output=True, error_code_ok=True)
965
966   matches = []
967   if result.returncode == 0:
968     matches = [SplitCPV(x) for x in result.output.splitlines()]
969
970   return matches
971
972
973 def GetBinaryPackageDir(sysroot='/', packages_dir=None):
974   """Returns the binary package directory of |sysroot|."""
975   dir_name = packages_dir if packages_dir else 'packages'
976   return os.path.join(sysroot, dir_name)
977
978
979 def GetBinaryPackagePath(c, p, v, sysroot='/', packages_dir=None):
980   """Returns the path to the binary package.
981
982   Args:
983     c: category.
984     p: package.
985     v: version.
986     sysroot: The root being inspected.
987     packages_dir: Name of the packages directory in |sysroot|.
988
989   Returns:
990     The path to the binary package.
991   """
992   pkgdir = GetBinaryPackageDir(sysroot=sysroot, packages_dir=packages_dir)
993   path = os.path.join(pkgdir, c, '%s-%s.tbz2' % (p, v))
994   if not os.path.exists(path):
995     raise ValueError('Cannot find the binary package %s!' % path)
996
997   return path
998
999
1000 def CleanOutdatedBinaryPackages(board):
1001   """Cleans outdated binary packages for |board|."""
1002   return cros_build_lib.RunCommand(['eclean-%s' % board, '-d', 'packages'])