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.
8 from __future__ import print_function
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
29 BUILD_STATUS_URL = '%s/builder-status' % constants.MANIFEST_VERSIONS_GS_URL
30 PUSH_BRANCH = 'temp_auto_checkin_branch'
34 class VersionUpdateException(Exception):
35 """Exception gets thrown for failing to update the version file"""
38 class StatusUpdateException(Exception):
39 """Exception gets thrown for failure to update the status"""
42 class GenerateBuildSpecException(Exception):
43 """Exception gets thrown for failure to Generate a buildspec for the build"""
46 class BuildSpecsValueError(Exception):
47 """Exception gets thrown when a encountering invalid values."""
50 def RefreshManifestCheckout(manifest_dir, manifest_repo):
51 """Checks out manifest-versions into the manifest directory.
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.
57 if os.path.exists(manifest_dir):
58 result = git.RunGit(manifest_dir, ['config', 'remote.origin.url'],
60 if (result.returncode == 0 and
61 result.output.rstrip() == manifest_repo):
62 logging.info('Updating manifest-versions checkout.')
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.')
71 logging.info('No manifest-versions checkout exists at %s', manifest_dir)
74 logging.info('Cloning fresh manifest-versions checkout.')
75 osutils.RmDir(manifest_dir, ignore_missing=True)
76 repository.CloneGitRepo(manifest_dir, manifest_repo)
79 def _PushGitChanges(git_repo, message, dry_run=True, push_to=None):
80 """Push the final commit into the git repo.
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.
91 remote, push_branch = git.GetTrackingBranch(
92 git_repo, for_checkout=False, for_push=True)
93 push_to = git.RemoteRef(remote, push_branch)
95 git.RunGit(git_repo, ['add', '-A'])
97 # It's possible that while we are running on dry_run, someone has already
98 # committed our change.
100 git.RunGit(git_repo, ['commit', '-m', message])
101 except cros_build_lib.RunCommandError:
106 git.GitPush(git_repo, PUSH_BRANCH, push_to, dryrun=dry_run, force=dry_run)
109 def CreateSymlink(src_file, dest_file):
110 """Creates a relative symlink from src to dest with optional removal of file.
112 More robust symlink creation that creates a relative symlink from src_file to
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.
119 src_file: source for the symlink
120 dest_file: destination for the symlink
122 dest_dir = os.path.dirname(dest_file)
123 osutils.SafeUnlink(dest_file)
124 osutils.SafeMakedirs(dest_dir)
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)
131 class VersionInfo(object):
132 """Class to encapsulate the Chrome OS version info scheme.
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().
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.
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')
155 def __init__(self, version_string=None, chrome_branch=None,
156 incr_type='build', version_file=None):
158 self.version_file = version_file
159 logging.debug('Using VERSION _FILE = %s', version_file)
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
169 self.incr_type = incr_type
172 def from_repo(cls, source_repo, **kwargs):
173 kwargs['version_file'] = os.path.join(source_repo, constants.VERSION_FILE)
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:
183 match = self.FindValue('CHROME_BRANCH', line)
185 self.chrome_branch = match
186 logging.debug('Set the Chrome branch number to:%s',
190 match = self.FindValue('CHROMEOS_BUILD', line)
192 self.build_number = match
193 logging.debug('Set the build version to:%s', self.build_number)
196 match = self.FindValue('CHROMEOS_BRANCH', line)
198 self.branch_build_number = match
199 logging.debug('Set the branch version to:%s',
200 self.branch_build_number)
203 match = self.FindValue('CHROMEOS_PATCH', line)
205 self.patch_number = match
206 logging.debug('Set the patch version to:%s', self.patch_number)
209 logging.debug(self.VersionString())
211 def FindValue(self, key, line):
212 """Given the key find the value from the line, if it finds key = value
216 line: string to search
220 value: for a matching key
222 match = re.search(self.KEY_VALUE_PATTERN % (key,), line)
223 return match.group(1) if match else None
225 def IncrementVersion(self):
226 """Updates the version file by incrementing the patch component.
229 message: Commit message to use when incrementing the version.
230 dry_run: Git dry_run.
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'
236 if self.incr_type == 'chrome_branch':
237 self.chrome_branch = str(int(self.chrome_branch) + 1)
239 # Increment build_number for 'chrome_branch' incr_type to avoid
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)
248 self.patch_number = str(int(self.patch_number) + 1)
250 return self.VersionString()
252 def UpdateVersionFile(self, message, dry_run, push_to=None):
253 """Update the version file with our current version."""
255 if not self.version_file:
256 raise VersionUpdateException('Cannot call UpdateVersionFile without '
257 'an associated version_file')
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))
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)
274 repo_dir = os.path.dirname(self.version_file)
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)
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)
285 def VersionString(self):
286 """returns the version string"""
287 return '%s.%s.%s' % (self.build_number, self.branch_build_number,
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,
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
303 return '%s.%s.' % (self.build_number, self.branch_build_number)
304 # Default to build incr_type.
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)
322 MISSING_MESSAGE = ('Unknown run, it probably never started:'
323 ' %(builder)s, version %(version)s')
325 def __init__(self, status, message, dashboard_url=None):
326 """Constructor for BuilderStatus.
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.
336 self.message = message
337 self.dashboard_url = dashboard_url
339 # Helper methods to make checking the status object easy.
342 """Returns True if the Builder failed."""
343 return self.status == BuilderStatus.STATUS_FAILED
346 """Returns True if the Builder passed."""
347 return self.status == BuilderStatus.STATUS_PASSED
350 """Returns True if the Builder is still inflight."""
351 return self.status == BuilderStatus.STATUS_INFLIGHT
354 """Returns True if the Builder is missing any status."""
355 return self.status == BuilderStatus.STATUS_MISSING
358 """Returns True if the Builder has completed."""
359 return self.status in BuilderStatus.COMPLETED_STATUSES
362 def GetCompletedStatus(cls, success):
363 """Return the appropriate status constant for a completed build.
366 success: Whether the build was successful or not.
369 return cls.STATUS_PASSED
371 return cls.STATUS_FAILED
373 def AsFlatDict(self):
374 """Returns a flat json-able representation of this builder status.
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.
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)
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))
395 class BuildSpecsManager(object):
396 """A Class to manage buildspecs and their states."""
398 SLEEP_TIMEOUT = 2 * 60
400 def __init__(self, source_repo, manifest_repo, build_names, incr_type, force,
401 branch, manifest=constants.DEFAULT_MANIFEST, dry_run=True,
403 """Initializes a build specs manager.
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.
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')
423 self.manifest_dir = os.path.join(buildroot, 'manifest-versions')
425 self.manifest_repo = manifest_repo
426 self.build_names = build_names
427 self.incr_type = incr_type
430 self.manifest = manifest
431 self.dry_run = dry_run
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
441 self._latest_status = None
442 self.latest_unprocessed = None
443 self.compare_versions_fn = VersionInfo.VersionCompare
445 self.current_version = None
446 self.rel_working_dir = ''
448 def _LatestSpecFromList(self, specs):
449 """Find the latest spec in a list of specs.
452 specs: List of specs.
455 The latest spec if specs is non-empty.
459 return max(specs, key=self.compare_versions_fn)
461 def _LatestSpecFromDir(self, version_info, directory):
462 """Returns the latest buildspec that match '*.xml' in a directory.
465 version_info: A VersionInfo object which will provide a build prefix
467 directory: Directory of the buildspecs.
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])
474 def RefreshManifestCheckout(self):
475 """Checks out manifest versions into the manifest directory."""
476 RefreshManifestCheckout(self.manifest_dir, self.manifest_repo)
478 def InitializeManifestVariables(self, version_info=None, version=None):
479 """Initializes manifest-related instance variables.
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.
487 Whether the requested version was found.
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')
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'))
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)
503 dir_pfx = version_info.chrome_branch
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))
514 # Calculate the status of the latest build, and whether the build was
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],
521 if self._latest_status.Missing():
522 self.latest_unprocessed = self.latest
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)
531 def HasCheckoutBeenBuilt(self):
532 """Checks to see if we've previously built this checkout.
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)
541 # We've never built this manifest before so this checkout is always new.
544 def CreateManifest(self):
545 """Returns the path to a new manifest based on the current source checkout.
547 new_manifest = tempfile.mkstemp('manifest_versions.manifest')[1]
548 osutils.WriteFile(new_manifest,
549 self.cros_source.ExportManifest(mark_revision=True))
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)
565 def PublishManifest(self, manifest, version, build_id=None):
566 """Publishes the manifest as the manifest for the version to others.
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.
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
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))
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))
590 shutil.copyfile(manifest, spec_file)
592 # Actually push the manifest.
593 self.PushSpecChanges(commit_message)
595 def DidLastBuildFail(self):
596 """Returns True if the last build failed."""
597 return self._latest_status and self._latest_status.Failed()
600 def GetBuildStatus(builder, version, retries=NUM_RETRIES):
601 """Returns a BuilderStatus instance for the given the builder.
604 builder: Builder to look at.
605 version: Version string.
606 retries: Number of retries for getting the status.
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.
614 url = BuildSpecsManager._GetStatusUrl(builder, version)
615 ctx = gs.GSContext(retries=retries)
617 output = ctx.Cat(url)
618 except gs.GSNoSuchKey:
619 return BuilderStatus(BuilderStatus.STATUS_MISSING, None)
621 return BuildSpecsManager._UnpickleBuildStatus(output)
624 def GetSlaveStatusesFromCIDB(master_build_id):
625 """Get statuses of slaves associated with |master_build_id|.
628 master_build_id: Master build id to check.
631 A dictionary mapping the slave name to a status in
632 BuildStatus.ALL_STATUSES.
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']
642 def GetBuildersStatus(self, master_build_id, builders_array, timeout=3 * 60):
643 """Get the statuses of the slave builders of the master.
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).
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.
656 A build-names->status dictionary of build statuses.
659 builders_completed = set()
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)
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)
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)))
680 return 'Builds completed.'
682 def _PrintRemainingTime(minutes_left):
683 logging.info('%d more minutes until timeout...', minutes_left)
685 # Check for build completion until all builders report in.
687 builds_succeeded = timeout_util.WaitForSuccess(
691 period=self.SLEEP_TIMEOUT,
692 side_effect_func=_PrintRemainingTime)
693 except timeout_util.TimeoutError:
694 builds_succeeded = None
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
703 if not builds_succeeded:
704 logging.error('Not all builds finished before timeout (%d minutes)'
705 ' reached.', int((timeout / 60) + 0.5))
707 return builder_statuses
710 def _UnpickleBuildStatus(pickle_string):
711 """Returns a BuilderStatus instance from a pickled string."""
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)
723 return BuilderStatus(**status_dict)
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])
730 def GetLocalManifest(self, version=None):
731 """Return path to local copy of manifest given by version.
734 Path of |version|. By default if version is not set, returns the path
735 of the current version.
737 if not self.all_specs_dir:
738 raise BuildSpecsValueError('GetLocalManifest failed, BuildSpecsManager '
739 'instance not yet initialized by call to '
740 'InitializeManifestVariables.')
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')
748 def BootstrapFromVersion(self, version):
749 """Initializes spec data from release version and returns path to manifest.
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 '
760 # Return the current manifest.
761 self.current_version = version
762 return self.GetLocalManifest(self.current_version)
764 def CheckoutSourceCode(self):
765 """Syncs the cros source to the latest git hashes for the branch."""
766 self.cros_source.Sync(self.manifest)
768 def GetNextBuildSpec(self, retries=NUM_RETRIES, dashboard_url=None,
770 """Returns a path to the next manifest to build.
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.
779 GenerateBuildSpecException in case of failure to generate a buildspec
782 for index in range(0, retries + 1):
784 self.CheckoutSourceCode()
786 version_info = self.GetCurrentVersionInfo()
787 self.RefreshManifestCheckout()
788 self.InitializeManifestVariables(version_info)
790 if not self.force and self.HasCheckoutBeenBuilt():
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
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)
802 version = self.latest_unprocessed
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,
813 # Cleanse any failed local changes and throw an exception.
814 self.RefreshManifestCheckout()
815 raise GenerateBuildSpecException(last_error)
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)
822 def _UploadStatus(self, version, status, message=None, fail_if_exists=False,
824 """Upload build status to Google Storage.
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.
834 data = BuilderStatus(status, message, dashboard_url).AsPickledDict()
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
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)
844 # Do the actual upload.
845 ctx = gs.GSContext(dry_run=self.dry_run)
846 ctx.Copy('-', url, input=data, version=gs_version)
848 def UploadStatus(self, success, message=None, dashboard_url=None):
849 """Uploads the status of the build for the current build spec.
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.
857 status = BuilderStatus.GetCompletedStatus(success)
858 self._UploadStatus(self.current_version, status, message=message,
859 dashboard_url=dashboard_url)
861 def SetInFlight(self, version, dashboard_url=None):
862 """Marks the buildspec as inflight in Google Storage."""
864 self._UploadStatus(version, BuilderStatus.STATUS_INFLIGHT,
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)
872 def _SetPassSymlinks(self, success_map):
873 """Marks the buildspec as passed by creating a symlink in passed dir.
876 success_map: Map of config names to whether they succeeded.
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]
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)
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)
893 def UpdateStatus(self, success_map, message=None, retries=NUM_RETRIES,
895 """Updates the status of the build for the current build spec.
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.
905 logging.info('Updating status with message %s', message)
906 for index in range(0, retries + 1):
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]))
916 self._SetPassSymlinks(success_map)
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],
923 logging.error(last_error)
924 logging.error('Retrying to generate buildspec: Retry %d/%d', index + 1,
927 # Upload status to Google Storage as well.
928 self.UploadStatus(success, message=message, dashboard_url=dashboard_url)
931 # Cleanse any failed local changes and throw an exception.
932 self.RefreshManifestCheckout()
933 raise StatusUpdateException(last_error)