2 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """A library to generate and store the manifests for cros builders to use."""
12 from xml.dom import minidom
14 from chromite.cbuildbot import cbuildbot_config
15 from chromite.cbuildbot import constants
16 from chromite.cbuildbot import manifest_version
17 from chromite.lib import cros_build_lib
18 from chromite.lib import git
19 from chromite.lib import timeout_util
22 # Paladin constants for manifest names.
23 PALADIN_COMMIT_ELEMENT = 'pending_commit'
24 PALADIN_REMOTE_ATTR = 'remote'
25 PALADIN_GERRIT_NUMBER_ATTR = 'gerrit_number'
26 PALADIN_PROJECT_ATTR = 'project'
27 PALADIN_BRANCH_ATTR = 'branch'
28 PALADIN_PROJECT_URL_ATTR = 'project_url'
29 PALADIN_REF_ATTR = 'ref'
30 PALADIN_CHANGE_ID_ATTR = 'change_id'
31 PALADIN_COMMIT_ATTR = 'commit'
32 PALADIN_PATCH_NUMBER_ATTR = 'patch_number'
33 PALADIN_OWNER_EMAIL_ATTR = 'owner_email'
34 PALADIN_FAIL_COUNT_ATTR = 'fail_count'
35 PALADIN_PASS_COUNT_ATTR = 'pass_count'
36 PALADIN_TOTAL_FAIL_COUNT_ATTR = 'total_fail_count'
38 CHROME_ELEMENT = 'chrome'
39 CHROME_VERSION_ATTR = 'version'
41 MANIFEST_ELEMENT = 'manifest'
42 DEFAULT_ELEMENT = 'default'
43 PROJECT_ELEMENT = 'project'
44 PROJECT_NAME_ATTR = 'name'
45 PROJECT_REMOTE_ATTR = 'remote'
48 class PromoteCandidateException(Exception):
49 """Exception thrown for failure to promote manifest candidate."""
52 class FilterManifestException(Exception):
53 """Exception thrown when failing to filter the internal manifest."""
56 class _LKGMCandidateInfo(manifest_version.VersionInfo):
57 """Class to encapsualte the chrome os lkgm candidate info
59 You can instantiate this class in two ways.
60 1)using a version file, specifically chromeos_version.sh,
61 which contains the version information.
62 2) just passing in the 4 version components (major, minor, sp, patch and
65 You can instantiate this class in two ways.
66 1)using a version file, specifically chromeos_version.sh,
67 which contains the version information.
68 2) passing in a string with the 3 version components + revision e.g. 41.0.0-r1
70 version_string: Optional 3 component version string to parse. Contains:
71 build_number: release build number.
72 branch_build_number: current build number on a branch.
73 patch_number: patch number.
74 revision_number: version revision
75 chrome_branch: If version_string specified, specify chrome_branch i.e. 13.
76 version_file: version file location.
78 LKGM_RE = r'(\d+\.\d+\.\d+)(?:-rc(\d+))?'
80 def __init__(self, version_string=None, chrome_branch=None, incr_type=None,
82 self.revision_number = 1
84 match = re.search(self.LKGM_RE, version_string)
85 assert match, 'LKGM did not re %s' % self.LKGM_RE
86 super(_LKGMCandidateInfo, self).__init__(match.group(1), chrome_branch,
89 self.revision_number = int(match.group(2))
92 super(_LKGMCandidateInfo, self).__init__(version_file=version_file,
95 def VersionString(self):
96 """returns the full version string of the lkgm candidate"""
97 return '%s.%s.%s-rc%s' % (self.build_number, self.branch_build_number,
98 self.patch_number, self.revision_number)
101 def VersionCompare(cls, version_string):
102 """Useful method to return a comparable version of a LKGM string."""
103 lkgm = cls(version_string)
104 return map(int, [lkgm.build_number, lkgm.branch_build_number,
105 lkgm.patch_number, lkgm.revision_number])
107 def IncrementVersion(self):
108 """Increments the version by incrementing the revision #."""
109 self.revision_number += 1
110 return self.VersionString()
112 def UpdateVersionFile(self, *args, **kwargs):
113 """Update the version file on disk.
115 For LKGMCandidateInfo there is no version file so this function is a no-op.
119 class LKGMManager(manifest_version.BuildSpecsManager):
120 """A Class to manage lkgm candidates and their states.
123 lkgm_subdir: Subdirectory within manifest repo to store candidates.
125 # Sub-directories for LKGM and Chrome LKGM's.
126 LKGM_SUBDIR = 'LKGM-candidates'
127 CHROME_PFQ_SUBDIR = 'chrome-LKGM-candidates'
128 COMMIT_QUEUE_SUBDIR = 'paladin'
130 # Set path in repository to keep latest approved LKGM manifest.
131 LKGM_PATH = 'LKGM/lkgm.xml'
133 def __init__(self, source_repo, manifest_repo, build_names, build_type,
134 incr_type, force, branch, manifest=constants.DEFAULT_MANIFEST,
135 dry_run=True, master=False):
136 """Initialize an LKGM Manager.
139 source_repo: Repository object for the source code.
140 manifest_repo: Manifest repository for manifest versions/buildspecs.
141 build_names: Identifiers for the build. Must match cbuildbot_config
142 entries. If multiple identifiers are provided, the first item in the
143 list must be an identifier for the group.
144 build_type: Type of build. Must be a pfq type.
145 incr_type: How we should increment this version - build|branch|patch
146 force: Create a new manifest even if there are no changes.
147 branch: Branch this builder is running on.
148 manifest: Manifest to use for checkout. E.g. 'full' or 'buildtools'.
149 dry_run: Whether we actually commit changes we make or not.
150 master: Whether we are the master builder.
152 super(LKGMManager, self).__init__(
153 source_repo=source_repo, manifest_repo=manifest_repo,
154 manifest=manifest, build_names=build_names, incr_type=incr_type,
155 force=force, branch=branch, dry_run=dry_run, master=master)
157 self.lkgm_path = os.path.join(self.manifest_dir, self.LKGM_PATH)
158 self.compare_versions_fn = _LKGMCandidateInfo.VersionCompare
159 self.build_type = build_type
160 # Chrome PFQ and PFQ's exist at the same time and version separately so they
161 # must have separate subdirs in the manifest-versions repository.
162 if self.build_type == constants.CHROME_PFQ_TYPE:
163 self.rel_working_dir = self.CHROME_PFQ_SUBDIR
164 elif cbuildbot_config.IsCQType(self.build_type):
165 self.rel_working_dir = self.COMMIT_QUEUE_SUBDIR
167 assert cbuildbot_config.IsPFQType(self.build_type)
168 self.rel_working_dir = self.LKGM_SUBDIR
170 def GetCurrentVersionInfo(self):
171 """Returns the lkgm version info from the version file."""
172 version_info = super(LKGMManager, self).GetCurrentVersionInfo()
173 return _LKGMCandidateInfo(version_info.VersionString(),
174 chrome_branch=version_info.chrome_branch,
175 incr_type=self.incr_type)
177 def _AddChromeVersionToManifest(self, manifest, chrome_version):
178 """Adds the chrome element with version |chrome_version| to |manifest|.
180 The manifest file should contain the Chrome version to build for
184 manifest: Path to the manifest
185 chrome_version: A string representing the version of Chrome
188 manifest_dom = minidom.parse(manifest)
189 chrome = manifest_dom.createElement(CHROME_ELEMENT)
190 chrome.setAttribute(CHROME_VERSION_ATTR, chrome_version)
191 manifest_dom.documentElement.appendChild(chrome)
192 with open(manifest, 'w+') as manifest_file:
193 manifest_dom.writexml(manifest_file)
195 def _AddPatchesToManifest(self, manifest, patches):
196 """Adds list of |patches| to given |manifest|.
198 The manifest should have sufficient information for the slave
199 builders to fetch the patches from Gerrit and to print the CL link
200 (see cros_patch.GerritFetchOnlyPatch).
203 manifest: Path to the manifest.
204 patches: A list of cros_patch.GerritPatch objects.
206 manifest_dom = minidom.parse(manifest)
207 for patch in patches:
208 pending_commit = manifest_dom.createElement(PALADIN_COMMIT_ELEMENT)
209 pending_commit.setAttribute(PALADIN_REMOTE_ATTR, patch.remote)
210 pending_commit.setAttribute(
211 PALADIN_GERRIT_NUMBER_ATTR, patch.gerrit_number)
212 pending_commit.setAttribute(PALADIN_PROJECT_ATTR, patch.project)
213 pending_commit.setAttribute(PALADIN_PROJECT_URL_ATTR, patch.project_url)
214 pending_commit.setAttribute(PALADIN_REF_ATTR, patch.ref)
215 pending_commit.setAttribute(PALADIN_BRANCH_ATTR, patch.tracking_branch)
216 pending_commit.setAttribute(PALADIN_CHANGE_ID_ATTR, patch.change_id)
217 pending_commit.setAttribute(PALADIN_COMMIT_ATTR, patch.commit)
218 pending_commit.setAttribute(PALADIN_PATCH_NUMBER_ATTR, patch.patch_number)
219 pending_commit.setAttribute(PALADIN_OWNER_EMAIL_ATTR, patch.owner_email)
220 pending_commit.setAttribute(PALADIN_FAIL_COUNT_ATTR,
221 str(patch.fail_count))
222 pending_commit.setAttribute(PALADIN_PASS_COUNT_ATTR,
223 str(patch.pass_count))
224 pending_commit.setAttribute(PALADIN_TOTAL_FAIL_COUNT_ATTR,
225 str(patch.total_fail_count))
226 manifest_dom.documentElement.appendChild(pending_commit)
228 with open(manifest, 'w+') as manifest_file:
229 manifest_dom.writexml(manifest_file)
232 def _GetDefaultRemote(manifest_dom):
233 """Returns the default remote in a manifest (if any).
236 manifest_dom: DOM Document object representing the manifest.
239 Default remote if one exists, None otherwise.
241 default_nodes = manifest_dom.getElementsByTagName(DEFAULT_ELEMENT)
243 if len(default_nodes) > 1:
244 raise FilterManifestException(
245 'More than one <default> element found in manifest')
246 return default_nodes[0].getAttribute(PROJECT_REMOTE_ATTR)
250 def _FilterCrosInternalProjectsFromManifest(
251 manifest, whitelisted_remotes=constants.EXTERNAL_REMOTES):
252 """Returns a path to a new manifest with internal repositories stripped.
255 manifest: Path to an existing manifest that may have internal
257 whitelisted_remotes: Tuple of remotes to allow in the external manifest.
258 Only projects with those remotes will be included in the external
262 Path to a new manifest that is a copy of the original without internal
263 repositories or pending commits.
265 temp_fd, new_path = tempfile.mkstemp('external_manifest')
266 manifest_dom = minidom.parse(manifest)
267 manifest_node = manifest_dom.getElementsByTagName(MANIFEST_ELEMENT)[0]
268 projects = manifest_dom.getElementsByTagName(PROJECT_ELEMENT)
269 pending_commits = manifest_dom.getElementsByTagName(PALADIN_COMMIT_ELEMENT)
271 default_remote = LKGMManager._GetDefaultRemote(manifest_dom)
272 internal_projects = set()
273 for project_element in projects:
274 project_remote = project_element.getAttribute(PROJECT_REMOTE_ATTR)
275 project = project_element.getAttribute(PROJECT_NAME_ATTR)
276 if not project_remote:
277 if not default_remote:
278 # This should not happen for a valid manifest. Either each
279 # project must have a remote specified or there should
280 # be manifest default we could use.
281 raise FilterManifestException(
282 'Project %s has unspecified remote with no default' % project)
283 project_remote = default_remote
284 if project_remote not in whitelisted_remotes:
285 internal_projects.add(project)
286 manifest_node.removeChild(project_element)
288 for commit_element in pending_commits:
289 if commit_element.getAttribute(
290 PALADIN_PROJECT_ATTR) in internal_projects:
291 manifest_node.removeChild(commit_element)
293 with os.fdopen(temp_fd, 'w') as manifest_file:
294 # Filter out empty lines.
295 filtered_manifest_noempty = filter(
296 str.strip, manifest_dom.toxml('utf-8').splitlines())
297 manifest_file.write(os.linesep.join(filtered_manifest_noempty))
301 def CreateNewCandidate(self, validation_pool=None,
303 retries=manifest_version.NUM_RETRIES,
305 """Creates, syncs to, and returns the next candidate manifest.
308 validation_pool: Validation pool to apply to the manifest before
310 chrome_version: The Chrome version to write in the manifest. Defaults
311 to None, in which case no version is written.
312 retries: Number of retries for updating the status. Defaults to
313 manifest_version.NUM_RETRIES.
314 build_id: Optional integer cidb id of the build that is creating
318 GenerateBuildSpecException in case of failure to generate a buildspec
320 self.CheckoutSourceCode()
322 # Refresh manifest logic from manifest_versions repository to grab the
323 # LKGM to generate the blamelist.
324 version_info = self.GetCurrentVersionInfo()
325 self.RefreshManifestCheckout()
326 self.InitializeManifestVariables(version_info)
328 self._GenerateBlameListSinceLKGM()
329 new_manifest = self.CreateManifest()
331 # For Chrome PFQ, add the version of Chrome to use.
333 self._AddChromeVersionToManifest(new_manifest, chrome_version)
335 # For the Commit Queue, apply the validation pool as part of checkout.
337 # If we have nothing that could apply from the validation pool and
338 # we're not also a pfq type, we got nothing to do.
339 assert self.cros_source.directory == validation_pool.build_root
340 if (not validation_pool.ApplyPoolIntoRepo() and
341 not cbuildbot_config.IsPFQType(self.build_type)):
344 self._AddPatchesToManifest(new_manifest, validation_pool.changes)
347 for attempt in range(0, retries + 1):
349 # Refresh manifest logic from manifest_versions repository.
350 # Note we don't need to do this on our first attempt as we needed to
351 # have done it to get the LKGM.
353 self.RefreshManifestCheckout()
354 self.InitializeManifestVariables(version_info)
356 # If we don't have any valid changes to test, make sure the checkout
357 # is at least different.
358 if ((not validation_pool or not validation_pool.changes) and
359 not self.force and self.HasCheckoutBeenBuilt()):
362 # Check whether the latest spec available in manifest-versions is
363 # newer than our current version number. If so, use it as the base
364 # version number. Otherwise, we default to 'rc1'.
366 latest = max(self.latest, version_info.VersionString(),
367 key=self.compare_versions_fn)
368 version_info = _LKGMCandidateInfo(
369 latest, chrome_branch=version_info.chrome_branch,
370 incr_type=self.incr_type)
372 git.CreatePushBranch(manifest_version.PUSH_BRANCH, self.manifest_dir,
374 version = self.GetNextVersion(version_info)
375 self.PublishManifest(new_manifest, version, build_id=build_id)
376 self.current_version = version
377 return self.GetLocalManifest(version)
378 except cros_build_lib.RunCommandError as e:
379 err_msg = 'Failed to generate LKGM Candidate. error: %s' % e
380 logging.error(err_msg)
383 raise manifest_version.GenerateBuildSpecException(last_error)
385 def CreateFromManifest(self, manifest, retries=manifest_version.NUM_RETRIES,
386 dashboard_url=None, build_id=None):
387 """Sets up an lkgm_manager from the given manifest.
389 This method sets up an LKGM manager and publishes a new manifest to the
390 manifest versions repo based on the passed in manifest but filtering
391 internal repositories and changes out of it.
394 manifest: A manifest that possibly contains private changes/projects. It
395 is named with the given version we want to create a new manifest from
396 i.e R20-1920.0.1-rc7.xml where R20-1920.0.1-rc7 is the version.
397 retries: Number of retries for updating the status.
398 dashboard_url: Optional url linking to builder dashboard for this build.
399 build_id: Optional integer cidb build id of the build publishing the
403 GenerateBuildSpecException in case of failure to check-in the new
404 manifest because of a git error or the manifest is already checked-in.
407 new_manifest = self._FilterCrosInternalProjectsFromManifest(manifest)
408 version_info = self.GetCurrentVersionInfo()
409 for _attempt in range(0, retries + 1):
411 self.RefreshManifestCheckout()
412 self.InitializeManifestVariables(version_info)
414 git.CreatePushBranch(manifest_version.PUSH_BRANCH, self.manifest_dir,
416 version = os.path.splitext(os.path.basename(manifest))[0]
417 logging.info('Publishing filtered build spec')
418 self.PublishManifest(new_manifest, version, build_id=build_id)
419 self.SetInFlight(version, dashboard_url=dashboard_url)
420 self.current_version = version
421 return self.GetLocalManifest(version)
422 except cros_build_lib.RunCommandError as e:
423 err_msg = 'Failed to generate LKGM Candidate. error: %s' % e
424 logging.error(err_msg)
427 raise manifest_version.GenerateBuildSpecException(last_error)
429 def GetLatestCandidate(self, dashboard_url=None, timeout=3 * 60):
430 """Gets and syncs to the next candiate manifest.
433 retries: Number of retries for updating the status
434 dashboard_url: Optional url linking to builder dashboard for this build.
435 timeout: The timeout in seconds.
438 Local path to manifest to build or None in case of no need to build.
441 GenerateBuildSpecException in case of failure to generate a buildspec
443 def _AttemptToGetLatestCandidate():
444 """Attempts to acquire latest candidate using manifest repo."""
445 self.RefreshManifestCheckout()
446 self.InitializeManifestVariables(self.GetCurrentVersionInfo())
447 if self.latest_unprocessed:
448 return self.latest_unprocessed
449 elif self.dry_run and self.latest:
452 def _PrintRemainingTime(minutes_left):
453 logging.info('Found nothing new to build, will keep trying for %d more'
454 ' minutes.', minutes_left)
455 logging.info('If this is a PFQ, then you should have forced the master'
456 ', which runs cbuildbot_master')
458 # TODO(sosa): We only really need the overlay for the version info but we
459 # do a full checkout here because we have no way of refining it currently.
460 self.CheckoutSourceCode()
462 version_to_build = timeout_util.WaitForSuccess(
464 _AttemptToGetLatestCandidate,
466 period=self.SLEEP_TIMEOUT,
467 side_effect_func=_PrintRemainingTime)
468 except timeout_util.TimeoutError:
469 version_to_build = None
472 logging.info('Starting build spec: %s', version_to_build)
473 self.SetInFlight(version_to_build, dashboard_url=dashboard_url)
474 self.current_version = version_to_build
476 # Actually perform the sync.
477 manifest = self.GetLocalManifest(version_to_build)
478 self.cros_source.Sync(manifest)
479 self._GenerateBlameListSinceLKGM()
484 def PromoteCandidate(self, retries=manifest_version.NUM_RETRIES):
485 """Promotes the current LKGM candidate to be a real versioned LKGM."""
486 assert self.current_version, 'No current manifest exists.'
489 path_to_candidate = self.GetLocalManifest(self.current_version)
490 assert os.path.exists(path_to_candidate), 'Candidate not found locally.'
492 # This may potentially fail for not being at TOT while pushing.
493 for attempt in range(0, retries + 1):
496 self.RefreshManifestCheckout()
497 git.CreatePushBranch(manifest_version.PUSH_BRANCH,
498 self.manifest_dir, sync=False)
499 manifest_version.CreateSymlink(path_to_candidate, self.lkgm_path)
500 git.RunGit(self.manifest_dir, ['add', self.LKGM_PATH])
501 self.PushSpecChanges(
502 'Automatic: %s promoting %s to LKGM' % (self.build_names[0],
503 self.current_version))
505 except cros_build_lib.RunCommandError as e:
506 last_error = 'Failed to promote manifest. error: %s' % e
507 logging.error(last_error)
508 logging.error('Retrying to promote manifest: Retry %d/%d', attempt + 1,
512 raise PromoteCandidateException(last_error)
514 def _ShouldGenerateBlameListSinceLKGM(self):
515 """Returns True if we should generate the blamelist."""
516 # We want to generate the blamelist only for valid pfq types and if we are
517 # building on the master branch i.e. revving the build number.
518 return (self.incr_type == 'build' and
519 cbuildbot_config.IsPFQType(self.build_type) and
520 self.build_type != constants.CHROME_PFQ_TYPE)
522 def _GenerateBlameListSinceLKGM(self):
523 """Prints out links to all CL's that have been committed since LKGM.
525 Add buildbot trappings to print <a href='url'>text</a> in the waterfall for
526 each CL committed since we last had a passing build.
528 if not self._ShouldGenerateBlameListSinceLKGM():
529 logging.info('Not generating blamelist for lkgm as it is not appropriate '
530 'for this build type.')
532 # Suppress re-printing changes we tried ourselves on paladin
533 # builders since they are redundant.
534 only_print_chumps = self.build_type == constants.PALADIN_TYPE
535 GenerateBlameList(self.cros_source, self.lkgm_path,
536 only_print_chumps=only_print_chumps)
538 def GetLatestPassingSpec(self):
539 """Get the last spec file that passed in the current branch."""
540 raise NotImplementedError()
543 def GenerateBlameList(source_repo, lkgm_path, only_print_chumps=False):
544 """Generate the blamelist since the specified manifest.
547 source_repo: Repository object for the source code.
548 lkgm_path: Path to LKGM manifest.
549 only_print_chumps: If True, only print changes that were chumped.
551 handler = git.Manifest(lkgm_path)
552 reviewed_on_re = re.compile(r'\s*Reviewed-on:\s*(\S+)')
553 author_re = re.compile(r'\s*Author:.*<(\S+)@\S+>\s*')
554 committer_re = re.compile(r'\s*Commit:.*<(\S+)@\S+>\s*')
555 for rel_src_path, checkout in handler.checkouts_by_path.iteritems():
556 project = checkout['name']
558 # Additional case in case the repo has been removed from the manifest.
559 src_path = source_repo.GetRelativePath(rel_src_path)
560 if not os.path.exists(src_path):
561 cros_build_lib.Info('Detected repo removed from manifest %s' % project)
564 revision = checkout['revision']
565 cmd = ['log', '--pretty=full', '%s..HEAD' % revision]
567 result = git.RunGit(src_path, cmd)
568 except cros_build_lib.RunCommandError as ex:
569 # Git returns 128 when the revision does not exist.
570 if ex.result.returncode != 128:
572 cros_build_lib.Warning('Detected branch removed from local checkout.')
573 cros_build_lib.PrintBuildbotStepWarnings()
575 current_author = None
576 current_committer = None
577 for line in unicode(result.output, 'ascii', 'ignore').splitlines():
578 author_match = author_re.match(line)
580 current_author = author_match.group(1)
582 committer_match = committer_re.match(line)
584 current_committer = committer_match.group(1)
586 review_match = reviewed_on_re.match(line)
588 review = review_match.group(1)
589 _, _, change_number = review.rpartition('/')
591 os.path.basename(project),
595 if current_committer not in ('chrome-bot', 'chrome-internal-fetch',
596 'chromeos-commit-bot'):
597 items.insert(0, 'CHUMP')
598 elif only_print_chumps:
600 cros_build_lib.PrintBuildbotLink(' | '.join(items), review)