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.
5 """A library to generate and store the manifests for cros builders to use.
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
26 BUILD_STATUS_URL = '%s/builder-status' % constants.MANIFEST_VERSIONS_GS_URL
27 PUSH_BRANCH = 'temp_auto_checkin_branch'
31 class VersionUpdateException(Exception):
32 """Exception gets thrown for failing to update the version file"""
35 class StatusUpdateException(Exception):
36 """Exception gets thrown for failure to update the status"""
39 class GenerateBuildSpecException(Exception):
40 """Exception gets thrown for failure to Generate a buildspec for the build"""
43 class BuildSpecsValueError(Exception):
44 """Exception gets thrown when a encountering invalid values."""
47 def RefreshManifestCheckout(manifest_dir, manifest_repo):
48 """Checks out manifest-versions into the manifest directory.
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.
54 if os.path.exists(manifest_dir):
55 result = git.RunGit(manifest_dir, ['config', 'remote.origin.url'],
57 if (result.returncode == 0 and
58 result.output.rstrip() == manifest_repo):
59 logging.info('Updating manifest-versions checkout.')
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.')
68 logging.info('No manifest-versions checkout exists at %s', manifest_dir)
71 logging.info('Cloning fresh manifest-versions checkout.')
72 osutils.RmDir(manifest_dir, ignore_missing=True)
73 repository.CloneGitRepo(manifest_dir, manifest_repo)
76 def _PushGitChanges(git_repo, message, dry_run=True, push_to=None):
77 """Push the final commit into the git repo.
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.
88 remote, push_branch = git.GetTrackingBranch(
89 git_repo, for_checkout=False, for_push=True)
90 push_to = git.RemoteRef(remote, push_branch)
92 git.RunGit(git_repo, ['add', '-A'])
94 # It's possible that while we are running on dry_run, someone has already
95 # committed our change.
97 git.RunGit(git_repo, ['commit', '-m', message])
98 except cros_build_lib.RunCommandError:
103 git.GitPush(git_repo, PUSH_BRANCH, push_to, dryrun=dry_run, force=dry_run)
106 def CreateSymlink(src_file, dest_file):
107 """Creates a relative symlink from src to dest with optional removal of file.
109 More robust symlink creation that creates a relative symlink from src_file to
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.
116 src_file: source for the symlink
117 dest_file: destination for the symlink
119 dest_dir = os.path.dirname(dest_file)
120 osutils.SafeUnlink(dest_file)
121 osutils.SafeMakedirs(dest_dir)
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)
128 class VersionInfo(object):
129 """Class to encapsulate the Chrome OS version info scheme.
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().
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.
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')
152 def __init__(self, version_string=None, chrome_branch=None,
153 incr_type='build', version_file=None):
155 self.version_file = version_file
156 logging.debug('Using VERSION _FILE = %s', version_file)
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
166 self.incr_type = incr_type
169 def from_repo(cls, source_repo, **kwargs):
170 kwargs['version_file'] = os.path.join(source_repo, constants.VERSION_FILE)
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:
180 match = self.FindValue('CHROME_BRANCH', line)
182 self.chrome_branch = match
183 logging.debug('Set the Chrome branch number to:%s',
187 match = self.FindValue('CHROMEOS_BUILD', line)
189 self.build_number = match
190 logging.debug('Set the build version to:%s', self.build_number)
193 match = self.FindValue('CHROMEOS_BRANCH', line)
195 self.branch_build_number = match
196 logging.debug('Set the branch version to:%s',
197 self.branch_build_number)
200 match = self.FindValue('CHROMEOS_PATCH', line)
202 self.patch_number = match
203 logging.debug('Set the patch version to:%s', self.patch_number)
206 logging.debug(self.VersionString())
208 def FindValue(self, key, line):
209 """Given the key find the value from the line, if it finds key = value
213 line: string to search
217 value: for a matching key
219 match = re.search(self.KEY_VALUE_PATTERN % (key,), line)
220 return match.group(1) if match else None
222 def IncrementVersion(self):
223 """Updates the version file by incrementing the patch component.
226 message: Commit message to use when incrementing the version.
227 dry_run: Git dry_run.
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'
233 if self.incr_type == 'chrome_branch':
234 self.chrome_branch = str(int(self.chrome_branch) + 1)
236 # Increment build_number for 'chrome_branch' incr_type to avoid
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)
245 self.patch_number = str(int(self.patch_number) + 1)
247 return self.VersionString()
249 def UpdateVersionFile(self, message, dry_run, push_to=None):
250 """Update the version file with our current version."""
252 if not self.version_file:
253 raise VersionUpdateException('Cannot call UpdateVersionFile without '
254 'an associated version_file')
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))
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)
271 repo_dir = os.path.dirname(self.version_file)
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)
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)
282 def VersionString(self):
283 """returns the version string"""
284 return '%s.%s.%s' % (self.build_number, self.branch_build_number,
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,
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
300 return '%s.%s.' % (self.build_number, self.branch_build_number)
301 # Default to build incr_type.
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)
319 MISSING_MESSAGE = ('Unknown run, it probably never started:'
320 ' %(builder)s, version %(version)s')
322 def __init__(self, status, message, dashboard_url=None):
323 """Constructor for BuilderStatus.
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.
333 self.message = message
334 self.dashboard_url = dashboard_url
336 # Helper methods to make checking the status object easy.
339 """Returns True if the Builder failed."""
340 return self.status == BuilderStatus.STATUS_FAILED
343 """Returns True if the Builder passed."""
344 return self.status == BuilderStatus.STATUS_PASSED
347 """Returns True if the Builder is still inflight."""
348 return self.status == BuilderStatus.STATUS_INFLIGHT
351 """Returns True if the Builder is missing any status."""
352 return self.status == BuilderStatus.STATUS_MISSING
355 """Returns True if the Builder has completed."""
356 return self.status in BuilderStatus.COMPLETED_STATUSES
359 def GetCompletedStatus(cls, success):
360 """Return the appropriate status constant for a completed build.
363 success: Whether the build was successful or not.
366 return cls.STATUS_PASSED
368 return cls.STATUS_FAILED
370 def AsFlatDict(self):
371 """Returns a flat json-able representation of this builder status.
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.
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)
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))
392 class BuildSpecsManager(object):
393 """A Class to manage buildspecs and their states."""
397 def __init__(self, source_repo, manifest_repo, build_names, incr_type, force,
398 branch, manifest=constants.DEFAULT_MANIFEST, dry_run=True,
400 """Initializes a build specs manager.
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.
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')
420 self.manifest_dir = os.path.join(buildroot, 'manifest-versions')
422 self.manifest_repo = manifest_repo
423 self.build_names = build_names
424 self.incr_type = incr_type
427 self.manifest = manifest
428 self.dry_run = dry_run
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
438 self._latest_status = None
439 self.latest_unprocessed = None
440 self.compare_versions_fn = VersionInfo.VersionCompare
442 self.current_version = None
443 self.rel_working_dir = ''
445 def _LatestSpecFromList(self, specs):
446 """Find the latest spec in a list of specs.
449 specs: List of specs.
452 The latest spec if specs is non-empty.
456 return max(specs, key=self.compare_versions_fn)
458 def _LatestSpecFromDir(self, version_info, directory):
459 """Returns the latest buildspec that match '*.xml' in a directory.
462 version_info: A VersionInfo object which will provide a build prefix
464 directory: Directory of the buildspecs.
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])
471 def RefreshManifestCheckout(self):
472 """Checks out manifest versions into the manifest directory."""
473 RefreshManifestCheckout(self.manifest_dir, self.manifest_repo)
475 def InitializeManifestVariables(self, version_info=None, version=None):
476 """Initializes manifest-related instance variables.
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.
484 Whether the requested version was found.
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')
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'))
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)
500 dir_pfx = version_info.chrome_branch
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))
511 # Calculate the status of the latest build, and whether the build was
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],
518 if self._latest_status.Missing():
519 self.latest_unprocessed = self.latest
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)
528 def HasCheckoutBeenBuilt(self):
529 """Checks to see if we've previously built this checkout.
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)
538 # We've never built this manifest before so this checkout is always new.
541 def CreateManifest(self):
542 """Returns the path to a new manifest based on the current source checkout.
544 new_manifest = tempfile.mkstemp('manifest_versions.manifest')[1]
545 osutils.WriteFile(new_manifest,
546 self.cros_source.ExportManifest(mark_revision=True))
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)
562 def PublishManifest(self, manifest, version, build_id=None):
563 """Publishes the manifest as the manifest for the version to others.
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.
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
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))
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))
587 shutil.copyfile(manifest, spec_file)
589 # Actually push the manifest.
590 self.PushSpecChanges(commit_message)
592 def DidLastBuildFail(self):
593 """Returns True if the last build failed."""
594 return self._latest_status and self._latest_status.Failed()
597 def GetBuildStatus(builder, version, retries=NUM_RETRIES):
598 """Returns a BuilderStatus instance for the given the builder.
601 builder: Builder to look at.
602 version: Version string.
603 retries: Number of retries for getting the status.
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.
611 url = BuildSpecsManager._GetStatusUrl(builder, version)
612 ctx = gs.GSContext(retries=retries)
614 output = ctx.Cat(url).output
615 except gs.GSNoSuchKey:
616 return BuilderStatus(BuilderStatus.STATUS_MISSING, None)
618 return BuildSpecsManager._UnpickleBuildStatus(output)
620 def GetBuildersStatus(self, builders_array, timeout=3 * 60):
621 """Get the statuses of the builders.
624 builders_array: A list of the names of the builders to check.
625 timeout: Number of seconds to wait for the results.
628 A build-names->status dictionary of build statuses.
630 builders_completed = set()
631 builder_statuses = {}
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)
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)))
654 return 'Builds completed.'
656 def _PrintRemainingTime(minutes_left):
657 logging.info('%d more minutes until timeout...', minutes_left)
659 # Check for build completion until all builders report in.
661 builds_succeeded = timeout_util.WaitForSuccess(
663 _CheckStatusOfBuildersArray,
665 period=self.SLEEP_TIMEOUT,
666 side_effect_func=_PrintRemainingTime)
667 except timeout_util.TimeoutError:
668 builds_succeeded = None
670 if not builds_succeeded:
671 logging.error('Not all builds finished before timeout (%d minutes)'
672 ' reached.', int((timeout / 60) + 0.5))
674 return builder_statuses
677 def _UnpickleBuildStatus(pickle_string):
678 """Returns a BuilderStatus instance from a pickled string."""
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)
688 return BuilderStatus(**status_dict)
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])
695 def GetLocalManifest(self, version=None):
696 """Return path to local copy of manifest given by version.
699 Path of |version|. By default if version is not set, returns the path
700 of the current version.
702 if not self.all_specs_dir:
703 raise BuildSpecsValueError('GetLocalManifest failed, BuildSpecsManager '
704 'instance not yet initialized by call to '
705 'InitializeManifestVariables.')
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')
713 def BootstrapFromVersion(self, version):
714 """Initializes spec data from release version and returns path to manifest.
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 '
725 # Return the current manifest.
726 self.current_version = version
727 return self.GetLocalManifest(self.current_version)
729 def CheckoutSourceCode(self):
730 """Syncs the cros source to the latest git hashes for the branch."""
731 self.cros_source.Sync(self.manifest)
733 def GetNextBuildSpec(self, retries=NUM_RETRIES, dashboard_url=None,
735 """Returns a path to the next manifest to build.
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.
744 GenerateBuildSpecException in case of failure to generate a buildspec
747 for index in range(0, retries + 1):
749 self.CheckoutSourceCode()
751 version_info = self.GetCurrentVersionInfo()
752 self.RefreshManifestCheckout()
753 self.InitializeManifestVariables(version_info)
755 if not self.force and self.HasCheckoutBeenBuilt():
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
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)
767 version = self.latest_unprocessed
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,
778 # Cleanse any failed local changes and throw an exception.
779 self.RefreshManifestCheckout()
780 raise GenerateBuildSpecException(last_error)
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)
787 def _UploadStatus(self, version, status, message=None, fail_if_exists=False,
789 """Upload build status to Google Storage.
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.
799 data = BuilderStatus(status, message, dashboard_url).AsPickledDict()
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
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)
809 # Do the actual upload.
810 ctx = gs.GSContext(dry_run=self.dry_run)
811 ctx.Copy('-', url, input=data, version=gs_version)
813 def UploadStatus(self, success, message=None, dashboard_url=None):
814 """Uploads the status of the build for the current build spec.
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.
822 status = BuilderStatus.GetCompletedStatus(success)
823 self._UploadStatus(self.current_version, status, message=message,
824 dashboard_url=dashboard_url)
826 def SetInFlight(self, version, dashboard_url=None):
827 """Marks the buildspec as inflight in Google Storage."""
829 self._UploadStatus(version, BuilderStatus.STATUS_INFLIGHT,
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)
837 def _SetPassSymlinks(self, success_map):
838 """Marks the buildspec as passed by creating a symlink in passed dir.
841 success_map: Map of config names to whether they succeeded.
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]
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)
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)
858 def UpdateStatus(self, success_map, message=None, retries=NUM_RETRIES,
860 """Updates the status of the build for the current build spec.
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.
870 logging.info('Updating status with message %s', message)
871 for index in range(0, retries + 1):
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]))
881 self._SetPassSymlinks(success_map)
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],
888 logging.error(last_error)
889 logging.error('Retrying to generate buildspec: Retry %d/%d', index + 1,
892 # Upload status to Google Storage as well.
893 self.UploadStatus(success, message=message, dashboard_url=dashboard_url)
896 # Cleanse any failed local changes and throw an exception.
897 self.RefreshManifestCheckout()
898 raise StatusUpdateException(last_error)