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