Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / lib / portage_util.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.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
23
24 _PRIVATE_PREFIX = '%(buildroot)s/src/private-overlays'
25 _GLOBAL_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',
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 in parent -> child order, and does not
54   perform any filtering.
55
56   Args:
57     board: Board to look at.
58     buildroot: Source root to find overlays.
59   """
60   overlays, patterns = [], []
61   if board is None:
62     patterns += ['overlay*']
63   else:
64     board_no_variant, _, variant = board.partition('_')
65     patterns += ['overlay-%s' % board_no_variant]
66     if variant:
67       patterns += ['overlay-variant-%s' % board.replace('_', '-')]
68
69   for d in _GLOBAL_OVERLAYS:
70     overlays += glob.glob(d % dict(buildroot=buildroot))
71
72   for p in patterns:
73     overlays += glob.glob('%s/src/overlays/%s' % (buildroot, p))
74     overlays += glob.glob('%s/src/private-overlays/%s-private' % (buildroot, p))
75
76   return overlays
77
78
79 def FindOverlays(overlay_type, board=None, buildroot=constants.SOURCE_ROOT):
80   """Return the list of overlays to use for a given buildbot.
81
82   The returned list of overlays will be in parent -> child order.
83
84   Args:
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.
91   """
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:
99     return overlays
100   else:
101     assert overlay_type is None
102     return []
103
104
105 def ReadOverlayFile(filename, overlay_type='both', board=None,
106                     buildroot=constants.SOURCE_ROOT):
107   """Attempt to open a file in the overlay directories.
108
109   Searches through this board's overlays for the specified file. The
110   overlays are searched in child -> parent order.
111
112   Args:
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.
120
121   Returns:
122     The contents of the file, or None if no files could be opened.
123   """
124   for overlay in reversed(FindOverlays(overlay_type, board, buildroot)):
125     try:
126       return osutils.ReadFile(os.path.join(overlay, filename))
127     except IOError as e:
128       if e.errno != os.errno.ENOENT:
129         raise
130
131
132 class MissingOverlayException(Exception):
133   """This exception indicates that a needed overlay is missing."""
134
135
136 def FindPrimaryOverlay(overlay_type, board, buildroot=constants.SOURCE_ROOT):
137   """Return the primary overlay to use for a given buildbot.
138
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
141   overlay is returned.
142
143   Args:
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.
150
151   Raises:
152     MissingOverlayException: No primary overlay found.
153   """
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'))):
157       return overlay
158   raise MissingOverlayException('No primary overlay found for board=%r' % board)
159
160
161 def GetOverlayName(overlay):
162   """Get the self-declared repo name for the |overlay| path."""
163   try:
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.
168     try:
169       with open(os.path.join(overlay, 'profiles', 'repo_name')) as f:
170         return f.readline().rstrip()
171     except IOError:
172       # Not all overlays have a repo_name, so don't make a fuss.
173       return None
174
175
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)
183
184
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)
190
191
192 class EBuild(object):
193   """Wrapper class for information about an ebuild."""
194
195   VERBOSE = False
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="(.*)"$')
199
200   # A structure to hold computed values of CROS_WORKON_*.
201   CrosWorkonVars = collections.namedtuple(
202       'CrosWorkonVars', ('localname', 'project', 'subdir'))
203
204   @classmethod
205   def _Print(cls, message):
206     """Verbose print function."""
207     if cls.VERBOSE:
208       cros_build_lib.Info(message)
209
210   @classmethod
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
215
216   @classmethod
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
220
221   def IsSticky(self):
222     """Returns True if the ebuild is sticky."""
223     return self.is_stable and self.current_revision == 0
224
225   @classmethod
226   def UpdateEBuild(cls, ebuild_path, variables, redirect_file=None,
227                    make_stable=True):
228     """Static function that updates WORKON information in the ebuild.
229
230     This function takes an ebuild_path and updates WORKON information.
231
232     Args:
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.
237     """
238     written = False
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
243
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))
250         written = True
251
252       # Mark KEYWORDS as stable by removing ~'s.
253       if line.startswith('KEYWORDS=') and make_stable:
254         line = line.replace('~', '')
255
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)
260
261     fileinput.close()
262
263   @classmethod
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.
267
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.
272
273     Args:
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
276         ebuild.
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.
280     """
281     shutil.copyfile(unstable_ebuild_path, new_stable_ebuild_path)
282     EBuild.UpdateEBuild(new_stable_ebuild_path, variables, redirect_file,
283                         make_stable)
284
285   @classmethod
286   def CommitChange(cls, message, overlay):
287     """Commits current changes in git locally with given commit message.
288
289     Args:
290       message: the commit string to write when committing to git.
291       overlay: directory in which to commit the changes.
292
293     Raises:
294       RunCommandError: Error occurred while committing.
295     """
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)
299
300   def __init__(self, path):
301     """Sets up data about an ebuild from its path.
302
303     Args:
304       path: Path to the ebuild.
305     """
306     self._overlay, self._category, self._pkgname, filename = path.rsplit('/', 3)
307     m = self._PACKAGE_VERSION_PATTERN.match(filename)
308     if not m:
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', ''))
313     else:
314       self.current_revision = 0
315     self.package = '%s/%s' % (self._category, self._pkgname)
316
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
324
325     self.is_workon = False
326     self.is_stable = False
327     self.is_blacklisted = False
328     self._ReadEBuild(path)
329
330   @staticmethod
331   def Classify(ebuild_path):
332     """Return whether this ebuild is workon, stable, and/or blacklisted
333
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='
338     """
339     is_workon = False
340     is_stable = False
341     is_blacklisted = False
342     for line in fileinput.input(ebuild_path):
343       if line.startswith('inherit ') and 'cros-workon' in line:
344         is_workon = True
345       elif line.startswith('KEYWORDS='):
346         for keyword in line.split('=', 1)[1].strip("\"'").split():
347           if not keyword.startswith('~') and keyword != '-*':
348             is_stable = True
349       elif line.startswith('CROS_WORKON_BLACKLIST='):
350         is_blacklisted = True
351     fileinput.close()
352     return is_workon, is_stable, is_blacklisted
353
354   def _ReadEBuild(self, path):
355     """Determine the settings of `is_workon`, `is_stable` and is_blacklisted
356
357     These are determined using the static Classify function.
358     """
359     self.is_workon, self.is_stable, self.is_blacklisted = EBuild.Classify(path)
360
361   @staticmethod
362   def GetCrosWorkonVars(ebuild_path, pkg_name):
363     """Return computed (as sourced ebuild script) values of:
364
365       * CROS_WORKON_LOCALNAME
366       * CROS_WORKON_PROJECT
367       * CROS_WORKON_SUBDIR
368
369     Args:
370       ebuild_path: Path to the ebuild file (e.g: platform2-9999.ebuild).
371       pkg_name: The package name (e.g.: platform2).
372
373     Returns:
374       A CrosWorkonVars tuple.
375     """
376     workon_vars = (
377         'CROS_WORKON_LOCALNAME',
378         'CROS_WORKON_PROJECT',
379         'CROS_WORKON_SUBDIR',
380     )
381     env = {
382         'CROS_WORKON_LOCALNAME': pkg_name,
383         'CROS_WORKON_SUBDIR': '',
384     }
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(',')
399
400     return EBuild.CrosWorkonVars(localnames, projects, subdirs)
401
402   def GetSourcePath(self, srcroot, manifest):
403     """Get the project and path for this ebuild.
404
405     The path is guaranteed to exist, be a directory, and be absolute.
406     """
407
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)
425
426     # Calculate srcdir.
427     if self._category == 'chromeos-base':
428       dir_ = '' # 'platform2'
429     else:
430       dir_ = 'third_party'
431
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.
435
436     # subdir_paths = [os.path.realpath(os.path.join(srcroot, dir_, l, s))
437     #                for l, s in zip(localnames, subdirs)]
438
439     subdir_paths = []
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',
444                                                     local, sub))
445       subdir_paths.append(subdir_path)
446
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,
451                                                                self._pkgname))
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,
457                                                         real_project,
458                                                         project))
459     return projects, subdir_paths
460
461   def GetCommitId(self, srcdir):
462     """Get the commit id for this ebuild."""
463     output = self._RunGit(srcdir, ['rev-parse', 'HEAD'])
464     if not output:
465       cros_build_lib.Die('Cannot determine HEAD commit for %s' % srcdir)
466     return output.rstrip()
467
468   def GetTreeId(self, srcdir):
469     """Get the SHA1 of the source tree for this ebuild.
470
471     Unlike the commit hash, the SHA1 of the source tree is unaffected by the
472     history of the repository, or by commit messages.
473     """
474     output = self._RunGit(srcdir, ['log', '-1', '--format=%T'])
475     if not output:
476       cros_build_lib.Die('Cannot determine HEAD tree hash for %s' % srcdir)
477     return output.rstrip()
478
479   def GetVersion(self, srcroot, manifest, default):
480     """Get the base version number for this ebuild.
481
482     The version is provided by the ebuild through a specific script in
483     the $FILESDIR (chromeos-version.sh).
484     """
485     vers_script = os.path.join(os.path.dirname(self._ebuild_path_no_version),
486                                'files', 'chromeos-version.sh')
487
488     if not os.path.exists(vers_script):
489       return default
490
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.")
494
495     srcdirs = self.GetSourcePath(srcroot, manifest)[1]
496
497     # The chromeos-version script will output a usable raw version number,
498     # or nothing in case of error or no available version
499     try:
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' %
503                          (self._pkgname, e))
504
505     if not output:
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)))
509
510     return output
511
512   @staticmethod
513   def FormatBashArray(unformatted_list):
514     """Returns a python list in a bash array format.
515
516     If the list only has one item, format as simple quoted value.
517     That is both backwards-compatible and more readable.
518
519     Args:
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.
522
523     Returns:
524       A text string that can be used by bash as array declaration.
525     """
526     if len(unformatted_list) > 1:
527       return '("%s")' % '" "'.join(unformatted_list)
528     else:
529       return '"%s"' % unformatted_list[0]
530
531   def RevWorkOnEBuild(self, srcroot, manifest, redirect_file=None):
532     """Revs a workon ebuild given the git commit hash.
533
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.
537
538     Args:
539       srcroot: full path to the 'src' subdirectory in the source
540         repository.
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.
545
546     Returns:
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.
549
550     Raises:
551       OSError: Error occurred while creating a new ebuild.
552       IOError: Error occurred while writing to the new revved ebuild file.
553     """
554
555     if self.is_stable:
556       stable_version_no_rev = self.GetVersion(srcroot, manifest,
557                                               self.version_no_rev)
558     else:
559       # If given unstable ebuild, use preferred version rather than 9999.
560       stable_version_no_rev = self.GetVersion(srcroot, manifest, '0.0.1')
561
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)
566
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)
571
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)
579
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)
583       return None
584     else:
585       self._Print('Adding new stable ebuild to git')
586       self._RunGit(self._overlay, ['add', new_stable_ebuild_path])
587
588       if self.is_stable:
589         self._Print('Removing old ebuild from git')
590         self._RunGit(self._overlay, ['rm', old_ebuild_path])
591
592       return '%s-%s' % (self.package, new_version)
593
594   @classmethod
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, '']
601
602   @staticmethod
603   def _GetSHA1ForPath(manifest, path):
604     """Get the latest SHA1 for a given project from Gerrit.
605
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.
609
610     Args:
611       manifest: git.ManifestCheckout object.
612       path: Path of project.
613
614     Raises:
615       Exception if the manifest is pinned.
616     """
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)
623
624   @staticmethod
625   def _GetEBuildPaths(buildroot, manifest, overlay_list, changes):
626     """Calculate ebuild->path map for changed ebuilds.
627
628     Args:
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.
633
634     Returns:
635       A dictionary mapping changed ebuilds to lists of associated paths.
636     """
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)
641                         for c in changes)
642     ebuild_projects = {}
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
648
649     return ebuild_projects
650
651   @classmethod
652   def UpdateCommitHashesForChanges(cls, changes, buildroot, manifest):
653     """Updates the commit hashes for the EBuilds uprevved in changes.
654
655     Args:
656       changes: Changes from Gerrit that are being pushed.
657       buildroot: Path to root of build directory.
658       manifest: git.ManifestCheckout object.
659     """
660     path_sha1s = {}
661     overlay_list = FindOverlays(constants.BOTH_OVERLAYS, buildroot=buildroot)
662     ebuild_paths = cls._GetEBuildPaths(buildroot, manifest, overlay_list,
663                                        changes)
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)
668
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)
674
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)
680
681
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
686   winner = ebuilds[0]
687   for ebuild in ebuilds[1:]:
688     if vercmp(winner.version, ebuild.version) < 0:
689       winner = ebuild
690   return winner
691
692
693 def _FindUprevCandidates(files):
694   """Return the uprev candidate ebuild from a specified list of files.
695
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.
699
700   If the package isn't a cros_workon package, return None.
701
702   Args:
703     files: List of files in a package directory.
704   """
705   stable_ebuilds = []
706   unstable_ebuilds = []
707   for path in files:
708     if not path.endswith('.ebuild') or os.path.islink(path):
709       continue
710     ebuild = EBuild(path)
711     if not ebuild.is_workon or ebuild.is_blacklisted:
712       continue
713     if ebuild.is_stable:
714       if ebuild.version == '9999':
715         cros_build_lib.Die('KEYWORDS in 9999 ebuild should not be stable %s'
716                            % path)
717       stable_ebuilds.append(ebuild)
718     else:
719       unstable_ebuilds.append(ebuild)
720
721   # If both ebuild lists are empty, the passed in file list was for
722   # a non-workon package.
723   if not unstable_ebuilds:
724     if stable_ebuilds:
725       path = os.path.dirname(stable_ebuilds[0].ebuild_path)
726       cros_build_lib.Die('Missing 9999 ebuild in %s' % path)
727     return None
728
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)
732
733   if not stable_ebuilds:
734     cros_build_lib.Warning('Missing stable ebuild in %s' % path)
735     return unstable_ebuilds[0]
736
737   if len(stable_ebuilds) == 1:
738     return stable_ebuilds[0]
739
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)
747
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))
753   return uprev_ebuild
754
755
756 def BuildEBuildDictionary(overlays, use_all, packages):
757   """Build a dictionary of the ebuilds in the specified overlays.
758
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.
766   """
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)
772
773       # If the --all option isn't used, we only want to update packages that
774       # are in packages.
775       if ebuild and (use_all or ebuild.package in packages):
776         overlays[overlay].append(ebuild)
777
778
779 def RegenCache(overlay):
780   """Regenerate the cache of the specified overlay.
781
782   overlay: The tree to regenerate the cache for.
783   """
784   repo_name = GetOverlayName(overlay)
785   if not repo_name:
786     return
787
788   layout = cros_build_lib.LoadKeyValueFile('%s/metadata/layout.conf' % overlay,
789                                            ignore_missing=True)
790   if layout.get('cache-format') != 'md5-dict':
791     return
792
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:
799     return
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/'])
804
805
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.
810   sep = ','
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)
815
816
817 def GetWorkonProjectMap(overlay, subdirectories):
818   """Get the project -> ebuild mapping for cros_workon ebuilds.
819
820   Args:
821     overlay: Overlay to look at.
822     subdirectories: List of subdirectories to look in on the overlay.
823
824   Returns:
825     A list of (filename, projects) tuples for cros-workon ebuilds in the
826     given overlay under the given subdirectories.
827   """
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]
836           if not is_workon:
837             continue
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
842
843
844 def SplitEbuildPath(path):
845   """Split an ebuild path into its components.
846
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.
850
851   Example: For /any/path/chromeos-base/power_manager/power_manager-9999.ebuild,
852   returns ('chromeos-base', 'power_manager', 'power_manager-9999').
853
854   Returns:
855     $CATEGORY, $PN, $P
856   """
857   return os.path.splitext(path)[0].rsplit('/', 3)[-3:]
858
859
860 def SplitPV(pv, strict=True):
861   """Takes a PV value and splits it into individual components.
862
863   Args:
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.
867
868   Returns:
869     A collection with named members:
870       pv, package, version, version_no_rev, rev
871   """
872   m = _pvr_re.match(pv)
873
874   if m is None and strict:
875     return None
876
877   if m is None:
878     return PV(**{'pv': None, 'package': pv, 'version': None,
879                  'version_no_rev': None, 'rev': None})
880
881   return PV(**m.groupdict())
882
883
884 def SplitCPV(cpv, strict=True):
885   """Splits a CPV value into components.
886
887   Args:
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.
891
892   Returns:
893     A collection with named members:
894       category, pv, package, version, version_no_rev, rev
895   """
896   chunks = cpv.split('/')
897   if len(chunks) > 2:
898     raise ValueError('Unexpected package format %s' % cpv)
899   if len(chunks) == 1:
900     category = None
901   else:
902     category = chunks[0]
903
904   m = SplitPV(chunks[-1], strict=strict)
905   if strict and (category is None or m is None):
906     return None
907   # pylint: disable=W0212
908   return CPV(category=category, **m._asdict())
909
910
911 def FindWorkonProjects(packages):
912   """Find the projects associated with the specified cros_workon packages.
913
914   Args:
915     packages: List of cros_workon packages.
916
917   Returns:
918     The set of projects associated with the specified cros_workon packages.
919   """
920   all_projects = set()
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)
925   return all_projects
926
927
928 def ListInstalledPackages(sysroot):
929   """Lists all portage packages in a given portage-managed root.
930
931   Assumes the existence of a /var/db/pkg package database.
932
933   Args:
934     sysroot: The root being inspected.
935
936   Returns:
937     A list of (cp,v) tuples in the given sysroot.
938   """
939   vdb_path = os.path.join(sysroot, 'var/db/pkg')
940   ebuild_pattern = os.path.join(vdb_path, '*/*/*.ebuild')
941   packages = []
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))
947   return packages
948
949
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.
953
954   Args:
955     atom: Portage atom.
956     board: Board to look at. By default, look in chroot.
957     pkg_type: Package type (ebuild, binary, or installed).
958     buildroot: Directory
959
960   Returns:
961     A CPV object.
962   """
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,
968       capture_output=True)
969   return SplitCPV(result.output.strip())
970
971
972 def IsPackageInstalled(package, sysroot='/'):
973   """Return whether a portage package is in a given portage-managed root.
974
975   Args:
976     package: The CP to look for.
977     sysroot: The root being inspected.
978   """
979   for key, _version in ListInstalledPackages(sysroot):
980     if key == package:
981       return True
982
983   return False
984
985
986 def FindPackageNameMatches(pkg_str, board=None):
987   """Finds a list of installed packages matching |pkg_str|.
988
989   Args:
990     pkg_str: The package name with optional category, version, and slot.
991     board: The board to insepct.
992
993   Returns:
994     A list of matched CPV objects.
995   """
996   cmd = ['equery']
997   if board:
998     cmd = ['equery-%s' % board]
999
1000   cmd += ['list', pkg_str]
1001   result = cros_build_lib.RunCommand(
1002       cmd, capture_output=True, error_code_ok=True)
1003
1004   matches = []
1005   if result.returncode == 0:
1006     matches = [SplitCPV(x) for x in result.output.splitlines()]
1007
1008   return matches
1009
1010
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)
1015
1016
1017 def GetBinaryPackagePath(c, p, v, sysroot='/', packages_dir=None):
1018   """Returns the path to the binary package.
1019
1020   Args:
1021     c: category.
1022     p: package.
1023     v: version.
1024     sysroot: The root being inspected.
1025     packages_dir: Name of the packages directory in |sysroot|.
1026
1027   Returns:
1028     The path to the binary package.
1029   """
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)
1034
1035   return path
1036
1037
1038 def CleanOutdatedBinaryPackages(board):
1039   """Cleans outdated binary packages for |board|."""
1040   return cros_build_lib.RunCommand(['eclean-%s' % board, '-d', 'packages'])