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