Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / cbuildbot / manifest_version.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 """A library to generate and store the manifests for cros builders to use.
6 """
7
8 from __future__ import print_function
9
10 import cPickle
11 import fnmatch
12 import glob
13 import logging
14 import os
15 import re
16 import shutil
17 import tempfile
18
19 from chromite.cbuildbot import constants
20 from chromite.cbuildbot import repository
21 from chromite.lib import cidb
22 from chromite.lib import cros_build_lib
23 from chromite.lib import git
24 from chromite.lib import gs
25 from chromite.lib import osutils
26 from chromite.lib import timeout_util
27
28
29 BUILD_STATUS_URL = '%s/builder-status' % constants.MANIFEST_VERSIONS_GS_URL
30 PUSH_BRANCH = 'temp_auto_checkin_branch'
31 NUM_RETRIES = 20
32
33
34 class VersionUpdateException(Exception):
35   """Exception gets thrown for failing to update the version file"""
36
37
38 class StatusUpdateException(Exception):
39   """Exception gets thrown for failure to update the status"""
40
41
42 class GenerateBuildSpecException(Exception):
43   """Exception gets thrown for failure to Generate a buildspec for the build"""
44
45
46 class BuildSpecsValueError(Exception):
47   """Exception gets thrown when a encountering invalid values."""
48
49
50 def RefreshManifestCheckout(manifest_dir, manifest_repo):
51   """Checks out manifest-versions into the manifest directory.
52
53   If a repository is already present, it will be cleansed of any local
54   changes and restored to its pristine state, checking out the origin.
55   """
56   reinitialize = True
57   if os.path.exists(manifest_dir):
58     result = git.RunGit(manifest_dir, ['config', 'remote.origin.url'],
59                         error_code_ok=True)
60     if (result.returncode == 0 and
61         result.output.rstrip() == manifest_repo):
62       logging.info('Updating manifest-versions checkout.')
63       try:
64         git.RunGit(manifest_dir, ['gc', '--auto'])
65         git.CleanAndCheckoutUpstream(manifest_dir)
66       except cros_build_lib.RunCommandError:
67         logging.warning('Could not update manifest-versions checkout.')
68       else:
69         reinitialize = False
70   else:
71     logging.info('No manifest-versions checkout exists at %s', manifest_dir)
72
73   if reinitialize:
74     logging.info('Cloning fresh manifest-versions checkout.')
75     osutils.RmDir(manifest_dir, ignore_missing=True)
76     repository.CloneGitRepo(manifest_dir, manifest_repo)
77
78
79 def _PushGitChanges(git_repo, message, dry_run=True, push_to=None):
80   """Push the final commit into the git repo.
81
82   Args:
83     git_repo: git repo to push
84     message: Commit message
85     dry_run: If true, don't actually push changes to the server
86     push_to: A git.RemoteRef object specifying the remote branch to push to.
87       Defaults to the tracking branch of the current branch.
88   """
89   push_branch = None
90   if push_to is None:
91     remote, push_branch = git.GetTrackingBranch(
92         git_repo, for_checkout=False, for_push=True)
93     push_to = git.RemoteRef(remote, push_branch)
94
95   git.RunGit(git_repo, ['add', '-A'])
96
97   # It's possible that while we are running on dry_run, someone has already
98   # committed our change.
99   try:
100     git.RunGit(git_repo, ['commit', '-m', message])
101   except cros_build_lib.RunCommandError:
102     if dry_run:
103       return
104     raise
105
106   git.GitPush(git_repo, PUSH_BRANCH, push_to, dryrun=dry_run, force=dry_run)
107
108
109 def CreateSymlink(src_file, dest_file):
110   """Creates a relative symlink from src to dest with optional removal of file.
111
112   More robust symlink creation that creates a relative symlink from src_file to
113   dest_file.
114
115   This is useful for multiple calls of CreateSymlink where you are using
116   the dest_file location to store information about the status of the src_file.
117
118   Args:
119     src_file: source for the symlink
120     dest_file: destination for the symlink
121   """
122   dest_dir = os.path.dirname(dest_file)
123   osutils.SafeUnlink(dest_file)
124   osutils.SafeMakedirs(dest_dir)
125
126   rel_src_file = os.path.relpath(src_file, dest_dir)
127   logging.debug('Linking %s to %s', rel_src_file, dest_file)
128   os.symlink(rel_src_file, dest_file)
129
130
131 class VersionInfo(object):
132   """Class to encapsulate the Chrome OS version info scheme.
133
134   You can instantiate this class in three ways.
135   1) using a version file, specifically chromeos_version.sh,
136      which contains the version information.
137   2) passing in a string with the 3 version components.
138   3) using a source repo and calling from_repo().
139
140   Args:
141     version_string: Optional 3 component version string to parse.  Contains:
142         build_number: release build number.
143         branch_build_number: current build number on a branch.
144         patch_number: patch number.
145     chrome_branch: If version_string specified, specify chrome_branch i.e. 13.
146     incr_type: How we should increment this version -
147         chrome_branch|build|branch|patch
148     version_file: version file location.
149   """
150   # Pattern for matching build name format.  Includes chrome branch hack.
151   VER_PATTERN = r'(\d+).(\d+).(\d+)(?:-R(\d+))*'
152   KEY_VALUE_PATTERN = r'%s=(\d+)\s*$'
153   VALID_INCR_TYPES = ('chrome_branch', 'build', 'branch', 'patch')
154
155   def __init__(self, version_string=None, chrome_branch=None,
156                incr_type='build', version_file=None):
157     if version_file:
158       self.version_file = version_file
159       logging.debug('Using VERSION _FILE = %s', version_file)
160       self._LoadFromFile()
161     else:
162       match = re.search(self.VER_PATTERN, version_string)
163       self.build_number = match.group(1)
164       self.branch_build_number = match.group(2)
165       self.patch_number = match.group(3)
166       self.chrome_branch = chrome_branch
167       self.version_file = None
168
169     self.incr_type = incr_type
170
171   @classmethod
172   def from_repo(cls, source_repo, **kwargs):
173     kwargs['version_file'] = os.path.join(source_repo, constants.VERSION_FILE)
174     return cls(**kwargs)
175
176   def _LoadFromFile(self):
177     """Read the version file and set the version components"""
178     with open(self.version_file, 'r') as version_fh:
179       for line in version_fh:
180         if not line.strip():
181           continue
182
183         match = self.FindValue('CHROME_BRANCH', line)
184         if match:
185           self.chrome_branch = match
186           logging.debug('Set the Chrome branch number to:%s',
187                         self.chrome_branch)
188           continue
189
190         match = self.FindValue('CHROMEOS_BUILD', line)
191         if match:
192           self.build_number = match
193           logging.debug('Set the build version to:%s', self.build_number)
194           continue
195
196         match = self.FindValue('CHROMEOS_BRANCH', line)
197         if match:
198           self.branch_build_number = match
199           logging.debug('Set the branch version to:%s',
200                         self.branch_build_number)
201           continue
202
203         match = self.FindValue('CHROMEOS_PATCH', line)
204         if match:
205           self.patch_number = match
206           logging.debug('Set the patch version to:%s', self.patch_number)
207           continue
208
209     logging.debug(self.VersionString())
210
211   def FindValue(self, key, line):
212     """Given the key find the value from the line, if it finds key = value
213
214     Args:
215       key: key to look for
216       line: string to search
217
218     Returns:
219       None: on a non match
220       value: for a matching key
221     """
222     match = re.search(self.KEY_VALUE_PATTERN % (key,), line)
223     return match.group(1) if match else None
224
225   def IncrementVersion(self):
226     """Updates the version file by incrementing the patch component.
227
228     Args:
229       message: Commit message to use when incrementing the version.
230       dry_run: Git dry_run.
231     """
232     if not self.incr_type or self.incr_type not in self.VALID_INCR_TYPES:
233       raise VersionUpdateException('Need to specify the part of the version to'
234                                    ' increment')
235
236     if self.incr_type == 'chrome_branch':
237       self.chrome_branch = str(int(self.chrome_branch) + 1)
238
239     # Increment build_number for 'chrome_branch' incr_type to avoid
240     # crbug.com/213075.
241     if self.incr_type in ('build', 'chrome_branch'):
242       self.build_number = str(int(self.build_number) + 1)
243       self.branch_build_number = '0'
244       self.patch_number = '0'
245     elif self.incr_type == 'branch' and self.patch_number == '0':
246       self.branch_build_number = str(int(self.branch_build_number) + 1)
247     else:
248       self.patch_number = str(int(self.patch_number) + 1)
249
250     return self.VersionString()
251
252   def UpdateVersionFile(self, message, dry_run, push_to=None):
253     """Update the version file with our current version."""
254
255     if not self.version_file:
256       raise VersionUpdateException('Cannot call UpdateVersionFile without '
257                                    'an associated version_file')
258
259     components = (('CHROMEOS_BUILD', self.build_number),
260                   ('CHROMEOS_BRANCH', self.branch_build_number),
261                   ('CHROMEOS_PATCH', self.patch_number),
262                   ('CHROME_BRANCH', self.chrome_branch))
263
264     with tempfile.NamedTemporaryFile(prefix='mvp') as temp_fh:
265       with open(self.version_file, 'r') as source_version_fh:
266         for line in source_version_fh:
267           for key, value in components:
268             line = re.sub(self.KEY_VALUE_PATTERN % (key,),
269                           '%s=%s\n' % (key, value), line)
270           temp_fh.write(line)
271
272       temp_fh.flush()
273
274       repo_dir = os.path.dirname(self.version_file)
275
276       try:
277         git.CreateBranch(repo_dir, PUSH_BRANCH)
278         shutil.copyfile(temp_fh.name, self.version_file)
279         _PushGitChanges(repo_dir, message, dry_run=dry_run, push_to=push_to)
280       finally:
281         # Update to the remote version that contains our changes. This is needed
282         # to ensure that we don't build a release using a local commit.
283         git.CleanAndCheckoutUpstream(repo_dir)
284
285   def VersionString(self):
286     """returns the version string"""
287     return '%s.%s.%s' % (self.build_number, self.branch_build_number,
288                          self.patch_number)
289
290   @classmethod
291   def VersionCompare(cls, version_string):
292     """Useful method to return a comparable version of a LKGM string."""
293     info = cls(version_string)
294     return map(int, [info.build_number, info.branch_build_number,
295                      info.patch_number])
296
297   def BuildPrefix(self):
298     """Returns the build prefix to match the buildspecs in  manifest-versions"""
299     if self.incr_type == 'branch':
300       if self.patch_number == '0':
301         return '%s.' % self.build_number
302       else:
303         return '%s.%s.' % (self.build_number, self.branch_build_number)
304     # Default to build incr_type.
305     return ''
306
307
308 class BuilderStatus(object):
309   """Object representing the status of a build."""
310   # Various statuses builds can be in.  These status values are retrieved from
311   # Google Storage, which each builder writes to.  The MISSING status is used
312   # for the status of any builder which has no value in Google Storage.
313   STATUS_FAILED = 'fail'
314   STATUS_PASSED = 'pass'
315   STATUS_INFLIGHT = 'inflight'
316   STATUS_MISSING = 'missing' # i.e. never started.
317   STATUS_ABORTED = 'aborted'
318   COMPLETED_STATUSES = (STATUS_PASSED, STATUS_FAILED, STATUS_ABORTED)
319   ALL_STATUSES = (STATUS_FAILED, STATUS_PASSED, STATUS_INFLIGHT,
320                   STATUS_MISSING, STATUS_ABORTED)
321
322   MISSING_MESSAGE = ('Unknown run, it probably never started:'
323                      ' %(builder)s, version %(version)s')
324
325   def __init__(self, status, message, dashboard_url=None):
326     """Constructor for BuilderStatus.
327
328     Args:
329       status: Status string (should be one of STATUS_FAILED, STATUS_PASSED,
330               STATUS_INFLIGHT, or STATUS_MISSING).
331       message: A failures_lib.BuildFailureMessage object with details
332                of builder failure. Or, None.
333       dashboard_url: Optional url linking to builder dashboard for this build.
334     """
335     self.status = status
336     self.message = message
337     self.dashboard_url = dashboard_url
338
339   # Helper methods to make checking the status object easy.
340
341   def Failed(self):
342     """Returns True if the Builder failed."""
343     return self.status == BuilderStatus.STATUS_FAILED
344
345   def Passed(self):
346     """Returns True if the Builder passed."""
347     return self.status == BuilderStatus.STATUS_PASSED
348
349   def Inflight(self):
350     """Returns True if the Builder is still inflight."""
351     return self.status == BuilderStatus.STATUS_INFLIGHT
352
353   def Missing(self):
354     """Returns True if the Builder is missing any status."""
355     return self.status == BuilderStatus.STATUS_MISSING
356
357   def Completed(self):
358     """Returns True if the Builder has completed."""
359     return self.status in BuilderStatus.COMPLETED_STATUSES
360
361   @classmethod
362   def GetCompletedStatus(cls, success):
363     """Return the appropriate status constant for a completed build.
364
365     Args:
366       success: Whether the build was successful or not.
367     """
368     if success:
369       return cls.STATUS_PASSED
370     else:
371       return cls.STATUS_FAILED
372
373   def AsFlatDict(self):
374     """Returns a flat json-able representation of this builder status.
375
376     Returns:
377       A dictionary of the form {'status' : status, 'message' : message,
378       'dashboard_url' : dashboard_url} where all values are guaranteed
379       to be strings. If dashboard_url is None, the key will be excluded.
380     """
381     flat_dict = {'status' : str(self.status),
382                  'message' : str(self.message),
383                  'reason' : str(None if self.message is None
384                                 else self.message.reason)}
385     if self.dashboard_url is not None:
386       flat_dict['dashboard_url'] = str(self.dashboard_url)
387     return flat_dict
388
389   def AsPickledDict(self):
390     """Returns a pickled dictionary representation of this builder status."""
391     return cPickle.dumps(dict(status=self.status, message=self.message,
392                               dashboard_url=self.dashboard_url))
393
394
395 class BuildSpecsManager(object):
396   """A Class to manage buildspecs and their states."""
397
398   SLEEP_TIMEOUT = 2 * 60
399
400   def __init__(self, source_repo, manifest_repo, build_names, incr_type, force,
401                branch, manifest=constants.DEFAULT_MANIFEST, dry_run=True,
402                master=False):
403     """Initializes a build specs manager.
404
405     Args:
406       source_repo: Repository object for the source code.
407       manifest_repo: Manifest repository for manifest versions / buildspecs.
408       build_names: Identifiers for the build. Must match cbuildbot_config
409           entries. If multiple identifiers are provided, the first item in the
410           list must be an identifier for the group.
411       incr_type: How we should increment this version - build|branch|patch
412       force: Create a new manifest even if there are no changes.
413       branch: Branch this builder is running on.
414       manifest: Manifest to use for checkout. E.g. 'full' or 'buildtools'.
415       dry_run: Whether we actually commit changes we make or not.
416       master: Whether we are the master builder.
417     """
418     self.cros_source = source_repo
419     buildroot = source_repo.directory
420     if manifest_repo.startswith(constants.INTERNAL_GOB_URL):
421       self.manifest_dir = os.path.join(buildroot, 'manifest-versions-internal')
422     else:
423       self.manifest_dir = os.path.join(buildroot, 'manifest-versions')
424
425     self.manifest_repo = manifest_repo
426     self.build_names = build_names
427     self.incr_type = incr_type
428     self.force = force
429     self.branch = branch
430     self.manifest = manifest
431     self.dry_run = dry_run
432     self.master = master
433
434     # Directories and specifications are set once we load the specs.
435     self.all_specs_dir = None
436     self.pass_dirs = None
437     self.fail_dirs = None
438
439     # Specs.
440     self.latest = None
441     self._latest_status = None
442     self.latest_unprocessed = None
443     self.compare_versions_fn = VersionInfo.VersionCompare
444
445     self.current_version = None
446     self.rel_working_dir = ''
447
448   def _LatestSpecFromList(self, specs):
449     """Find the latest spec in a list of specs.
450
451     Args:
452       specs: List of specs.
453
454     Returns:
455       The latest spec if specs is non-empty.
456       None otherwise.
457     """
458     if specs:
459       return max(specs, key=self.compare_versions_fn)
460
461   def _LatestSpecFromDir(self, version_info, directory):
462     """Returns the latest buildspec that match '*.xml' in a directory.
463
464     Args:
465       version_info: A VersionInfo object which will provide a build prefix
466                     to match for.
467       directory: Directory of the buildspecs.
468     """
469     if os.path.exists(directory):
470       match_string = version_info.BuildPrefix() + '*.xml'
471       specs = fnmatch.filter(os.listdir(directory), match_string)
472       return self._LatestSpecFromList([os.path.splitext(m)[0] for m in specs])
473
474   def RefreshManifestCheckout(self):
475     """Checks out manifest versions into the manifest directory."""
476     RefreshManifestCheckout(self.manifest_dir, self.manifest_repo)
477
478   def InitializeManifestVariables(self, version_info=None, version=None):
479     """Initializes manifest-related instance variables.
480
481     Args:
482       version_info: Info class for version information of cros. If None,
483                     version must be specified instead.
484       version: Requested version. If None, build the latest version.
485
486     Returns:
487       Whether the requested version was found.
488     """
489     assert version_info or version, 'version or version_info must be specified'
490     working_dir = os.path.join(self.manifest_dir, self.rel_working_dir)
491     specs_for_builder = os.path.join(working_dir, 'build-name', '%(builder)s')
492     buildspecs = os.path.join(working_dir, 'buildspecs')
493
494     # If version is specified, find out what Chrome branch it is on.
495     if version is not None:
496       dirs = glob.glob(os.path.join(buildspecs, '*', version + '.xml'))
497       if len(dirs) == 0:
498         return False
499       assert len(dirs) <= 1, 'More than one spec found for %s' % version
500       dir_pfx = os.path.basename(os.path.dirname(dirs[0]))
501       version_info = VersionInfo(chrome_branch=dir_pfx, version_string=version)
502     else:
503       dir_pfx = version_info.chrome_branch
504
505     self.all_specs_dir = os.path.join(buildspecs, dir_pfx)
506     self.pass_dirs, self.fail_dirs = [], []
507     for build_name in self.build_names:
508       specs_for_build = specs_for_builder % {'builder': build_name}
509       self.pass_dirs.append(os.path.join(specs_for_build,
510                                          BuilderStatus.STATUS_PASSED, dir_pfx))
511       self.fail_dirs.append(os.path.join(specs_for_build,
512                                          BuilderStatus.STATUS_FAILED, dir_pfx))
513
514     # Calculate the status of the latest build, and whether the build was
515     # processed.
516     if version is None:
517       self.latest = self._LatestSpecFromDir(version_info, self.all_specs_dir)
518       if self.latest is not None:
519         self._latest_status = self.GetBuildStatus(self.build_names[0],
520                                                   self.latest)
521         if self._latest_status.Missing():
522           self.latest_unprocessed = self.latest
523
524     return True
525
526   def GetCurrentVersionInfo(self):
527     """Returns the current version info from the version file."""
528     version_file_path = self.cros_source.GetRelativePath(constants.VERSION_FILE)
529     return VersionInfo(version_file=version_file_path, incr_type=self.incr_type)
530
531   def HasCheckoutBeenBuilt(self):
532     """Checks to see if we've previously built this checkout.
533     """
534     if self._latest_status and self._latest_status.Passed():
535       latest_spec_file = '%s.xml' % os.path.join(
536           self.all_specs_dir, self.latest)
537       # We've built this checkout before if the manifest isn't different than
538       # the last one we've built.
539       return not self.cros_source.IsManifestDifferent(latest_spec_file)
540     else:
541       # We've never built this manifest before so this checkout is always new.
542       return False
543
544   def CreateManifest(self):
545     """Returns the path to a new manifest based on the current source checkout.
546     """
547     new_manifest = tempfile.mkstemp('manifest_versions.manifest')[1]
548     osutils.WriteFile(new_manifest,
549                       self.cros_source.ExportManifest(mark_revision=True))
550     return new_manifest
551
552   def GetNextVersion(self, version_info):
553     """Returns the next version string that should be built."""
554     version = version_info.VersionString()
555     if self.latest == version:
556       message = ('Automatic: %s - Updating to a new version number from %s' % (
557                  self.build_names[0], version))
558       version = version_info.IncrementVersion()
559       version_info.UpdateVersionFile(message, dry_run=self.dry_run)
560       assert version != self.latest
561       cros_build_lib.Info('Incremented version number to  %s', version)
562
563     return version
564
565   def PublishManifest(self, manifest, version, build_id=None):
566     """Publishes the manifest as the manifest for the version to others.
567
568     Args:
569       manifest: Path to manifest file to publish.
570       version: Manifest version string, e.g. 6102.0.0-rc4
571       build_id: Optional integer giving build_id of the build that is
572                 publishing this manifest. If specified and non-negative,
573                 build_id will be included in the commit message.
574     """
575     # Note: This commit message is used by master.cfg for figuring out when to
576     #       trigger slave builders.
577     commit_message = 'Automatic: Start %s %s %s' % (self.build_names[0],
578                                                     self.branch, version)
579     if build_id is not None and build_id >= 0:
580       commit_message += '\nCrOS-Build-Id: %s' % build_id
581
582     logging.info('Publishing build spec for: %s', version)
583     logging.info('Publishing with commit message: %s', commit_message)
584     logging.debug('Manifest contents below.\n%s', osutils.ReadFile(manifest))
585
586     # Copy the manifest into the manifest repository.
587     spec_file = '%s.xml' % os.path.join(self.all_specs_dir, version)
588     osutils.SafeMakedirs(os.path.dirname(spec_file))
589
590     shutil.copyfile(manifest, spec_file)
591
592     # Actually push the manifest.
593     self.PushSpecChanges(commit_message)
594
595   def DidLastBuildFail(self):
596     """Returns True if the last build failed."""
597     return self._latest_status and self._latest_status.Failed()
598
599   @staticmethod
600   def GetBuildStatus(builder, version, retries=NUM_RETRIES):
601     """Returns a BuilderStatus instance for the given the builder.
602
603     Args:
604       builder: Builder to look at.
605       version: Version string.
606       retries: Number of retries for getting the status.
607
608     Returns:
609       A BuilderStatus instance containing the builder status and any optional
610       message associated with the status passed by the builder.  If no status
611       is found for this builder then the returned BuilderStatus object will
612       have status STATUS_MISSING.
613     """
614     url = BuildSpecsManager._GetStatusUrl(builder, version)
615     ctx = gs.GSContext(retries=retries)
616     try:
617       output = ctx.Cat(url)
618     except gs.GSNoSuchKey:
619       return BuilderStatus(BuilderStatus.STATUS_MISSING, None)
620
621     return BuildSpecsManager._UnpickleBuildStatus(output)
622
623   @staticmethod
624   def GetSlaveStatusesFromCIDB(master_build_id):
625     """Get statuses of slaves associated with |master_build_id|.
626
627     Args:
628       master_build_id: Master build id to check.
629
630     Returns:
631       A dictionary mapping the slave name to a status in
632       BuildStatus.ALL_STATUSES.
633     """
634     status_dict = dict()
635     db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
636     assert db, 'No database connection to use.'
637     status_list = db.GetSlaveStatuses(master_build_id)
638     for d in status_list:
639       status_dict[d['build_config']] = d['status']
640     return status_dict
641
642   def GetBuildersStatus(self, master_build_id, builders_array, timeout=3 * 60):
643     """Get the statuses of the slave builders of the master.
644
645     This function checks the status of slaves in |builders_array|. It
646     queries CIDB for all builds associated with the |master_build_id|,
647     then filters out builds that are not in |builders_array| (e.g.,
648     slaves that are not important).
649
650     Args:
651       master_build_id: Master build id to check.
652       builders_array: A list of the names of the builders to check.
653       timeout: Number of seconds to wait for the results.
654
655     Returns:
656       A build-names->status dictionary of build statuses.
657
658     """
659     builders_completed = set()
660
661     def _GetStatusesFromDB():
662       """Helper function that iterates through current statuses."""
663       status_dict = self.GetSlaveStatusesFromCIDB(master_build_id)
664       for builder in set(builders_array) - set(status_dict.keys()):
665         logging.warn('No status found for builder %s.', builder)
666
667       latest_completed = set(
668           [b for b, s in status_dict.iteritems() if s in
669            BuilderStatus.COMPLETED_STATUSES and b in builders_array])
670       for builder in sorted(latest_completed - builders_completed):
671         logging.info('Builder %s completed with status "%s".',
672                      builder, status_dict[builder])
673       builders_completed.update(latest_completed)
674
675       if len(builders_completed) < len(builders_array):
676         logging.info('Still waiting for the following builds to complete: %r',
677                      sorted(set(builders_array).difference(builders_completed)))
678         return None
679       else:
680         return 'Builds completed.'
681
682     def _PrintRemainingTime(minutes_left):
683       logging.info('%d more minutes until timeout...', minutes_left)
684
685     # Check for build completion until all builders report in.
686     try:
687       builds_succeeded = timeout_util.WaitForSuccess(
688           lambda x: x is None,
689           _GetStatusesFromDB,
690           timeout,
691           period=self.SLEEP_TIMEOUT,
692           side_effect_func=_PrintRemainingTime)
693     except timeout_util.TimeoutError:
694       builds_succeeded = None
695
696     # Actually fetch the BuildStatus pickles from Google Storage.
697     builder_statuses = {}
698     for builder in builders_array:
699       logging.debug("Checking for builder %s's status", builder)
700       builder_status = self.GetBuildStatus(builder, self.current_version)
701       builder_statuses[builder] = builder_status
702
703     if not builds_succeeded:
704       logging.error('Not all builds finished before timeout (%d minutes)'
705                     ' reached.', int((timeout / 60) + 0.5))
706
707     return builder_statuses
708
709   @staticmethod
710   def _UnpickleBuildStatus(pickle_string):
711     """Returns a BuilderStatus instance from a pickled string."""
712     try:
713       status_dict = cPickle.loads(pickle_string)
714     except (cPickle.UnpicklingError, AttributeError, EOFError,
715             ImportError, IndexError, TypeError) as e:
716       # The above exceptions are listed as possible unpickling exceptions
717       # by http://docs.python.org/2/library/pickle
718       # In addition to the exceptions listed in the doc, we've also observed
719       # TypeError in the wild.
720       logging.warning('Failed with %r to unpickle status file.', e)
721       return BuilderStatus(BuilderStatus.STATUS_FAILED, message=None)
722
723     return BuilderStatus(**status_dict)
724
725   def GetLatestPassingSpec(self):
726     """Get the last spec file that passed in the current branch."""
727     version_info = self.GetCurrentVersionInfo()
728     return self._LatestSpecFromDir(version_info, self.pass_dirs[0])
729
730   def GetLocalManifest(self, version=None):
731     """Return path to local copy of manifest given by version.
732
733     Returns:
734       Path of |version|.  By default if version is not set, returns the path
735       of the current version.
736     """
737     if not self.all_specs_dir:
738       raise BuildSpecsValueError('GetLocalManifest failed, BuildSpecsManager '
739                                  'instance not yet initialized by call to '
740                                  'InitializeManifestVariables.')
741     if version:
742       return os.path.join(self.all_specs_dir, version + '.xml')
743     elif self.current_version:
744       return os.path.join(self.all_specs_dir, self.current_version + '.xml')
745
746     return None
747
748   def BootstrapFromVersion(self, version):
749     """Initializes spec data from release version and returns path to manifest.
750     """
751     # Only refresh the manifest checkout if needed.
752     if not self.InitializeManifestVariables(version=version):
753       self.RefreshManifestCheckout()
754       if not self.InitializeManifestVariables(version=version):
755         raise BuildSpecsValueError('Failure in BootstrapFromVersion. '
756                                    'InitializeManifestVariables failed after '
757                                    'RefreshManifestCheckout for version '
758                                    '%s.' % version)
759
760     # Return the current manifest.
761     self.current_version = version
762     return self.GetLocalManifest(self.current_version)
763
764   def CheckoutSourceCode(self):
765     """Syncs the cros source to the latest git hashes for the branch."""
766     self.cros_source.Sync(self.manifest)
767
768   def GetNextBuildSpec(self, retries=NUM_RETRIES, dashboard_url=None,
769                        build_id=None):
770     """Returns a path to the next manifest to build.
771
772     Args:
773       retries: Number of retries for updating the status.
774       dashboard_url: Optional url linking to builder dashboard for this build.
775       build_id: Optional integer cidb id of this build, which will be used to
776                 annotate the manifest-version commit if one is created.
777
778     Raises:
779       GenerateBuildSpecException in case of failure to generate a buildspec
780     """
781     last_error = None
782     for index in range(0, retries + 1):
783       try:
784         self.CheckoutSourceCode()
785
786         version_info = self.GetCurrentVersionInfo()
787         self.RefreshManifestCheckout()
788         self.InitializeManifestVariables(version_info)
789
790         if not self.force and self.HasCheckoutBeenBuilt():
791           return None
792
793         # If we're the master, always create a new build spec. Otherwise,
794         # only create a new build spec if we've already built the existing
795         # spec.
796         if self.master or not self.latest_unprocessed:
797           git.CreatePushBranch(PUSH_BRANCH, self.manifest_dir, sync=False)
798           version = self.GetNextVersion(version_info)
799           new_manifest = self.CreateManifest()
800           self.PublishManifest(new_manifest, version, build_id=build_id)
801         else:
802           version = self.latest_unprocessed
803
804         self.SetInFlight(version, dashboard_url=dashboard_url)
805         self.current_version = version
806         return self.GetLocalManifest(version)
807       except cros_build_lib.RunCommandError as e:
808         last_error = 'Failed to generate buildspec. error: %s' % e
809         logging.error(last_error)
810         logging.error('Retrying to generate buildspec:  Retry %d/%d', index + 1,
811                       retries)
812     else:
813       # Cleanse any failed local changes and throw an exception.
814       self.RefreshManifestCheckout()
815       raise GenerateBuildSpecException(last_error)
816
817   @staticmethod
818   def _GetStatusUrl(builder, version):
819     """Get the status URL in Google Storage for a given builder / version."""
820     return os.path.join(BUILD_STATUS_URL, version, builder)
821
822   def _UploadStatus(self, version, status, message=None, fail_if_exists=False,
823                     dashboard_url=None):
824     """Upload build status to Google Storage.
825
826     Args:
827       version: Version number to use. Must be a string.
828       status: Status string.
829       message: A failures_lib.BuildFailureMessage object with details
830                of builder failure, or None (default).
831       fail_if_exists: If set, fail if the status already exists.
832       dashboard_url: Optional url linking to builder dashboard for this build.
833     """
834     data = BuilderStatus(status, message, dashboard_url).AsPickledDict()
835
836     # This HTTP header tells Google Storage to return the PreconditionFailed
837     # error message if the file already exists.
838     gs_version = 0 if fail_if_exists else None
839
840     logging.info('Recording status %s for %s', status, self.build_names)
841     for build_name in self.build_names:
842       url = BuildSpecsManager._GetStatusUrl(build_name, version)
843
844       # Do the actual upload.
845       ctx = gs.GSContext(dry_run=self.dry_run)
846       ctx.Copy('-', url, input=data, version=gs_version)
847
848   def UploadStatus(self, success, message=None, dashboard_url=None):
849     """Uploads the status of the build for the current build spec.
850
851     Args:
852       success: True for success, False for failure
853       message: A failures_lib.BuildFailureMessage object with details
854                of builder failure, or None (default).
855       dashboard_url: Optional url linking to builder dashboard for this build.
856     """
857     status = BuilderStatus.GetCompletedStatus(success)
858     self._UploadStatus(self.current_version, status, message=message,
859                        dashboard_url=dashboard_url)
860
861   def SetInFlight(self, version, dashboard_url=None):
862     """Marks the buildspec as inflight in Google Storage."""
863     try:
864       self._UploadStatus(version, BuilderStatus.STATUS_INFLIGHT,
865                          fail_if_exists=True,
866                          dashboard_url=dashboard_url)
867     except gs.GSContextPreconditionFailed:
868       raise GenerateBuildSpecException('Builder already inflight')
869     except gs.GSContextException as e:
870       raise GenerateBuildSpecException(e)
871
872   def _SetPassSymlinks(self, success_map):
873     """Marks the buildspec as passed by creating a symlink in passed dir.
874
875     Args:
876       success_map: Map of config names to whether they succeeded.
877     """
878     src_file = '%s.xml' % os.path.join(self.all_specs_dir, self.current_version)
879     for i, build_name in enumerate(self.build_names):
880       if success_map[build_name]:
881         sym_dir = self.pass_dirs[i]
882       else:
883         sym_dir = self.fail_dirs[i]
884       dest_file = '%s.xml' % os.path.join(sym_dir, self.current_version)
885       status = BuilderStatus.GetCompletedStatus(success_map[build_name])
886       logging.debug('Build %s: %s -> %s', status, src_file, dest_file)
887       CreateSymlink(src_file, dest_file)
888
889   def PushSpecChanges(self, commit_message):
890     """Pushes any changes you have in the manifest directory."""
891     _PushGitChanges(self.manifest_dir, commit_message, dry_run=self.dry_run)
892
893   def UpdateStatus(self, success_map, message=None, retries=NUM_RETRIES,
894                    dashboard_url=None):
895     """Updates the status of the build for the current build spec.
896
897     Args:
898       success_map: Map of config names to whether they succeeded.
899       message: Message accompanied with change in status.
900       retries: Number of retries for updating the status
901       dashboard_url: Optional url linking to builder dashboard for this build.
902     """
903     last_error = None
904     if message:
905       logging.info('Updating status with message %s', message)
906     for index in range(0, retries + 1):
907       try:
908         self.RefreshManifestCheckout()
909         git.CreatePushBranch(PUSH_BRANCH, self.manifest_dir, sync=False)
910         success = all(success_map.values())
911         commit_message = ('Automatic checkin: status=%s build_version %s for '
912                           '%s' % (BuilderStatus.GetCompletedStatus(success),
913                                   self.current_version,
914                                   self.build_names[0]))
915
916         self._SetPassSymlinks(success_map)
917
918         self.PushSpecChanges(commit_message)
919       except cros_build_lib.RunCommandError as e:
920         last_error = ('Failed to update the status for %s with the '
921                       'following error %s' % (self.build_names[0],
922                                               e.message))
923         logging.error(last_error)
924         logging.error('Retrying to generate buildspec:  Retry %d/%d', index + 1,
925                       retries)
926       else:
927         # Upload status to Google Storage as well.
928         self.UploadStatus(success, message=message, dashboard_url=dashboard_url)
929         return
930     else:
931       # Cleanse any failed local changes and throw an exception.
932       self.RefreshManifestCheckout()
933       raise StatusUpdateException(last_error)