1 # Copyright (c) 2013 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 """Module containing the generic stages."""
7 from __future__ import print_function
18 # We import mox so that we can identify mox exceptions and pass them through
19 # in our exception handling code.
21 # pylint: disable=F0401
26 from chromite.cbuildbot import cbuildbot_config
27 from chromite.cbuildbot import commands
28 from chromite.cbuildbot import failures_lib
29 from chromite.cbuildbot import results_lib
30 from chromite.cbuildbot import constants
31 from chromite.cbuildbot import repository
32 from chromite.lib import cidb
33 from chromite.lib import cros_build_lib
34 from chromite.lib import gs
35 from chromite.lib import osutils
36 from chromite.lib import parallel
37 from chromite.lib import portage_util
38 from chromite.lib import retry_util
39 from chromite.lib import timeout_util
42 class BuilderStage(object):
43 """Parent class for stages to be performed by a builder."""
44 # Used to remove 'Stage' suffix of stage class when generating stage name.
45 name_stage_re = re.compile(r'(\w+)Stage')
47 # TODO(sosa): Remove these once we have a SEND/RECIEVE IPC mechanism
52 # Class should set this if they have a corresponding no<stage> option that
54 # TODO(mtennant): Rename this something like skip_option_name.
57 # Class should set this if they have a corresponding setting in
58 # the build_config that skips their stage.
59 # TODO(mtennant): Rename this something like skip_config_name.
63 def StageNamePrefix(cls):
64 """Return cls.__name__ with any 'Stage' suffix removed."""
65 match = cls.name_stage_re.match(cls.__name__)
66 assert match, 'Class name %s does not end with Stage' % cls.__name__
69 def __init__(self, builder_run, suffix=None, attempt=None, max_retry=None):
70 """Create a builder stage.
73 builder_run: The BuilderRun object for the run this stage is part of.
74 suffix: The suffix to append to the buildbot name. Defaults to None.
75 attempt: If this build is to be retried, the current attempt number
76 (starting from 1). Defaults to None. Is only valid if |max_retry| is
78 max_retry: The maximum number of retries. Defaults to None. Is only valid
79 if |attempt| is also specified.
81 self._run = builder_run
83 self._attempt = attempt
84 self._max_retry = max_retry
86 # Construct self.name, the name string for this stage instance.
87 self.name = self._prefix = self.StageNamePrefix()
91 # TODO(mtennant): Phase this out and use self._run.bot_id directly.
92 self._bot_id = self._run.bot_id
94 # self._boards holds list of boards involved in this run.
95 # TODO(mtennant): Replace self._boards with a self._run.boards?
96 self._boards = self._run.config.boards
98 # TODO(mtennant): Try to rely on just self._run.buildroot directly, if
99 # the os.path.abspath can be applied there instead.
100 self._build_root = os.path.abspath(self._run.buildroot)
101 self._prebuilt_type = None
102 if self._run.ShouldUploadPrebuilts():
103 self._prebuilt_type = self._run.config.build_type
105 # Determine correct chrome_rev.
106 self._chrome_rev = self._run.config.chrome_rev
107 if self._run.options.chrome_rev:
108 self._chrome_rev = self._run.options.chrome_rev
110 # USE and enviroment variable settings.
111 self._portage_extra_env = {}
112 useflags = self._run.config.useflags[:]
114 if self._run.options.clobber:
115 self._portage_extra_env['IGNORE_PREFLIGHT_BINHOST'] = '1'
117 if self._run.options.chrome_root:
118 self._portage_extra_env['CHROME_ORIGIN'] = 'LOCAL_SOURCE'
120 self._latest_toolchain = (self._run.config.latest_toolchain or
121 self._run.options.latest_toolchain)
122 if self._latest_toolchain and self._run.config.gcc_githash:
123 useflags.append('git_gcc')
124 self._portage_extra_env['GCC_GITHASH'] = self._run.config.gcc_githash
127 self._portage_extra_env['USE'] = ' '.join(useflags)
129 def GetStageNames(self):
130 """Get a list of the places where this stage has recorded results."""
133 # TODO(akeshet): Eliminate this method and update the callers to use
134 # builder run directly.
135 def ConstructDashboardURL(self, stage=None):
136 """Return the dashboard URL
138 This is the direct link to buildbot logs as seen in build.chromium.org
141 stage: Link to a specific |stage|, otherwise the general buildbot log
146 return self._run.ConstructDashboardURL(stage=stage)
148 def _ExtractOverlays(self):
149 """Extracts list of overlays into class."""
150 overlays = portage_util.FindOverlays(
151 self._run.config.overlays, buildroot=self._build_root)
152 push_overlays = portage_util.FindOverlays(
153 self._run.config.push_overlays, buildroot=self._build_root)
156 # We cannot push to overlays that we don't rev.
157 assert set(push_overlays).issubset(set(overlays))
158 # Either has to be a master or not have any push overlays.
159 assert self._run.config.master or not push_overlays
161 return overlays, push_overlays
163 def GetRepoRepository(self, **kwargs):
164 """Create a new repo repository object."""
165 manifest_url = self._run.options.manifest_repo_url
166 if manifest_url is None:
167 manifest_url = self._run.config.manifest_repo_url
169 kwargs.setdefault('referenced_repo', self._run.options.reference_repo)
170 kwargs.setdefault('branch', self._run.manifest_branch)
171 kwargs.setdefault('manifest', self._run.config.manifest)
173 return repository.RepoRepository(manifest_url, self._build_root, **kwargs)
175 def _Print(self, msg):
176 """Prints a msg to stderr."""
178 print(msg, file=sys.stderr)
181 def _PrintLoudly(self, msg):
182 """Prints a msg with loudly."""
184 border_line = '*' * 60
188 print(border_line, file=sys.stderr)
190 msg_lines = msg.split('\n')
192 # If the last line is whitespace only drop it.
193 if not msg_lines[-1].rstrip():
196 for msg_line in msg_lines:
197 print('%s %s' % (edge, msg_line), file=sys.stderr)
199 print(border_line, file=sys.stderr)
202 def _GetPortageEnvVar(self, envvar, board):
203 """Get a portage environment variable for the configuration's board.
206 envvar: The environment variable to get. E.g. 'PORTAGE_BINHOST'.
207 board: The board to apply, if any. Specify None to use host.
210 The value of the environment variable, as a string. If no such variable
211 can be found, return the empty string.
213 cwd = os.path.join(self._build_root, 'src', 'scripts')
215 portageq = 'portageq-%s' % board
217 portageq = 'portageq'
218 binhost = cros_build_lib.RunCommand(
219 [portageq, 'envvar', envvar], cwd=cwd, redirect_stdout=True,
220 enter_chroot=True, error_code_ok=True)
221 return binhost.output.rstrip('\n')
223 def _GetSlaveConfigs(self):
224 """Get the slave configs for the current build config.
226 This assumes self._run.config is a master config.
229 A list of build configs corresponding to the slaves for the master
230 build config at self._run.config.
233 See cbuildbot_config.GetSlavesForMaster for details.
235 return cbuildbot_config.GetSlavesForMaster(self._run.config)
238 """Can be overridden. Called before a stage is performed."""
240 # Tell the buildbot we are starting a new step for the waterfall
241 cros_build_lib.PrintBuildbotStepName(self.name)
243 self._PrintLoudly('Start Stage %s - %s\n\n%s' % (
244 self.name, cros_build_lib.UserDateTimeFormat(), self.__doc__))
247 """Can be overridden. Called after a stage has been performed."""
248 self._PrintLoudly('Finished Stage %s - %s' %
249 (self.name, cros_build_lib.UserDateTimeFormat()))
251 def PerformStage(self):
252 """Subclassed stages must override this function to perform what they want
256 def _HandleExceptionAsSuccess(self, _exc_info):
257 """Use instead of HandleStageException to ignore an exception."""
258 return results_lib.Results.SUCCESS, None
261 def _StringifyException(exc_info):
262 """Convert an exception into a string.
265 exc_info: A (type, value, traceback) tuple as returned by sys.exc_info().
268 A string description of the exception.
270 exc_type, exc_value = exc_info[:2]
271 if issubclass(exc_type, failures_lib.StepFailure):
272 return str(exc_value)
274 return ''.join(traceback.format_exception(*exc_info))
277 def _HandleExceptionAsWarning(cls, exc_info, retrying=False):
278 """Use instead of HandleStageException to treat an exception as a warning.
280 This is used by the ForgivingBuilderStage's to treat any exceptions as
281 warnings instead of stage failures.
283 description = cls._StringifyException(exc_info)
284 cros_build_lib.PrintBuildbotStepWarnings()
285 cros_build_lib.Warning(description)
286 return (results_lib.Results.FORGIVEN, description, retrying)
289 def _HandleExceptionAsError(cls, exc_info):
290 """Handle an exception as an error, but ignore stage retry settings.
292 Meant as a helper for _HandleStageException code only.
295 exc_info: A (type, value, traceback) tuple as returned by sys.exc_info().
298 Result tuple of (exception, description, retrying).
300 # Tell the user about the exception, and record it.
302 description = cls._StringifyException(exc_info)
303 cros_build_lib.PrintBuildbotStepFailure()
304 cros_build_lib.Error(description)
305 return (exc_info[1], description, retrying)
307 def _HandleStageException(self, exc_info):
308 """Called when PerformStage throws an exception. Can be overriden.
311 exc_info: A (type, value, traceback) tuple as returned by sys.exc_info().
314 Result tuple of (exception, description, retrying). If it isn't an
315 exception, then description will be None.
317 if self._attempt and self._max_retry and self._attempt <= self._max_retry:
318 return self._HandleExceptionAsWarning(exc_info, retrying=True)
320 return self._HandleExceptionAsError(exc_info)
322 def _TopHandleStageException(self):
323 """Called when PerformStage throws an unhandled exception.
325 Should only be called by the Run function. Provides a wrapper around
326 _HandleStageException to handle buggy handlers. We must go deeper...
328 exc_info = sys.exc_info()
330 return self._HandleStageException(exc_info)
332 cros_build_lib.Error(
333 'An exception was thrown while running _HandleStageException')
334 cros_build_lib.Error('The original exception was:', exc_info=exc_info)
335 cros_build_lib.Error('The new exception is:', exc_info=True)
336 return self._HandleExceptionAsError(exc_info)
338 def HandleSkip(self):
339 """Run if the stage is skipped."""
342 def _RecordResult(self, *args, **kwargs):
343 """Record a successful or failed result."""
344 results_lib.Results.Record(*args, **kwargs)
347 """Have the builder execute the stage."""
348 # See if this stage should be skipped.
349 if (self.option_name and not getattr(self._run.options, self.option_name) or
350 self.config_name and not getattr(self._run.config, self.config_name)):
351 self._PrintLoudly('Not running Stage %s' % self.name)
353 self._RecordResult(self.name, results_lib.Results.SKIPPED,
357 record = results_lib.Results.PreviouslyCompletedRecord(self.name)
359 # Success is stored in the results log for a stage that completed
360 # successfully in a previous run.
361 self._PrintLoudly('Stage %s processed previously' % self.name)
363 self._RecordResult(self.name, results_lib.Results.SUCCESS,
364 prefix=self._prefix, board=record.board,
365 time=float(record.time))
368 start_time = time.time()
371 result = results_lib.Results.SUCCESS
378 # TODO(davidjames): Verify that PerformStage always returns None. See
381 except SystemExit as e:
383 result, description, retrying = self._TopHandleStageException()
386 except Exception as e:
387 if mox is not None and isinstance(e, mox.Error):
390 # Tell the build bot this step failed for the waterfall.
391 result, description, retrying = self._TopHandleStageException()
392 if result not in (results_lib.Results.FORGIVEN,
393 results_lib.Results.SUCCESS):
394 raise failures_lib.StepFailure()
396 raise failures_lib.RetriableStepFailure()
397 except BaseException:
398 result, description, retrying = self._TopHandleStageException()
401 elapsed_time = time.time() - start_time
402 self._RecordResult(self.name, result, description, prefix=self._prefix,
409 class NonHaltingBuilderStage(BuilderStage):
410 """Build stage that fails a build but finishes the other steps."""
414 super(NonHaltingBuilderStage, self).Run()
415 except failures_lib.StepFailure:
416 name = self.__class__.__name__
417 cros_build_lib.Error('Ignoring StepFailure in %s', name)
420 class ForgivingBuilderStage(BuilderStage):
421 """Build stage that turns a build step red but not a build."""
423 def _HandleStageException(self, exc_info):
424 """Override and don't set status to FAIL but FORGIVEN instead."""
425 return self._HandleExceptionAsWarning(exc_info)
428 class RetryStage(object):
429 """Retry a given stage multiple times to see if it passes."""
431 def __init__(self, builder_run, max_retry, stage, *args, **kwargs):
432 """Create a RetryStage object.
435 builder_run: See arguments to BuilderStage.__init__()
436 max_retry: The number of times to try the given stage.
437 stage: The stage class to create.
438 *args: A list of arguments to pass to the stage constructor.
439 **kwargs: A list of keyword arguments to pass to the stage constructor.
441 self._run = builder_run
442 self.max_retry = max_retry
444 self.args = (builder_run,) + args
449 def GetStageNames(self):
450 """Get a list of the places where this stage has recorded results."""
453 def _PerformStage(self):
454 """Run the stage once, incrementing the attempt number as needed."""
455 suffix = ' (attempt %d)' % (self.attempt,)
456 stage_obj = self.stage(
457 *self.args, attempt=self.attempt, max_retry=self.max_retry,
458 suffix=suffix, **self.kwargs)
459 self.names.extend(stage_obj.GetStageNames())
464 """Retry the given stage multiple times to see if it passes."""
466 retry_util.RetryException(
467 failures_lib.RetriableStepFailure, self.max_retry, self._PerformStage)
470 class RepeatStage(object):
471 """Run a given stage multiple times to see if it fails."""
473 def __init__(self, builder_run, count, stage, *args, **kwargs):
474 """Create a RepeatStage object.
477 builder_run: See arguments to BuilderStage.__init__()
478 count: The number of times to try the given stage.
479 stage: The stage class to create.
480 *args: A list of arguments to pass to the stage constructor.
481 **kwargs: A list of keyword arguments to pass to the stage constructor.
483 self._run = builder_run
486 self.args = (builder_run,) + args
491 def GetStageNames(self):
492 """Get a list of the places where this stage has recorded results."""
495 def _PerformStage(self):
496 """Run the stage once."""
497 suffix = ' (attempt %d)' % (self.attempt,)
498 stage_obj = self.stage(
499 *self.args, attempt=self.attempt, suffix=suffix, **self.kwargs)
500 self.names.extend(stage_obj.GetStageNames())
504 """Retry the given stage multiple times to see if it passes."""
505 for i in range(self.count):
510 class BoardSpecificBuilderStage(BuilderStage):
511 """Builder stage that is specific to a board.
513 The following attributes are provided on self:
514 _current_board: The active board for this stage.
515 board_runattrs: BoardRunAttributes object for this stage.
518 def __init__(self, builder_run, board, **kwargs):
519 super(BoardSpecificBuilderStage, self).__init__(builder_run, **kwargs)
520 self._current_board = board
522 self.board_runattrs = builder_run.GetBoardRunAttrs(board)
524 if not isinstance(board, basestring):
525 raise TypeError('Expected string, got %r' % (board,))
527 # Add a board name suffix to differentiate between various boards (in case
528 # more than one board is built on a single builder.)
529 if len(self._boards) > 1 or self._run.config.grouped:
530 self.name = '%s [%s]' % (self.name, board)
532 def _RecordResult(self, *args, **kwargs):
533 """Record a successful or failed result."""
534 kwargs.setdefault('board', self._current_board)
535 super(BoardSpecificBuilderStage, self)._RecordResult(*args, **kwargs)
537 def GetParallel(self, board_attr, timeout=None, pretty_name=None):
538 """Wait for given |board_attr| to show up.
541 board_attr: A valid board runattribute name.
542 timeout: Timeout in seconds. None value means wait forever.
543 pretty_name: Optional name to use instead of raw board_attr in
547 Value of board_attr found.
550 AttrTimeoutError if timeout occurs.
552 timeout_str = 'forever'
553 if timeout is not None:
554 timeout_str = '%d minutes' % int((timeout / 60) + 0.5)
556 if pretty_name is None:
557 pretty_name = board_attr
559 cros_build_lib.Info('Waiting up to %s for %s ...', timeout_str, pretty_name)
560 return self.board_runattrs.GetParallel(board_attr, timeout=timeout)
562 def GetImageDirSymlink(self, pointer='latest-cbuildbot'):
563 """Get the location of the current image."""
564 return os.path.join(self._run.buildroot, 'src', 'build', 'images',
565 self._current_board, pointer)
568 class ArchivingStageMixin(object):
569 """Stage with utilities for uploading artifacts.
571 This provides functionality for doing archiving. All it needs is access
572 to the BuilderRun object at self._run. No __init__ needed.
575 acl: GS ACL to use for uploads.
576 archive: Archive object.
577 archive_path: Local path where archives are kept for this run. Also copy
578 of self.archive.archive_path.
579 download_url: The URL where artifacts for this run can be downloaded.
580 Also copy of self.archive.download_url.
581 upload_url: The Google Storage location where artifacts for this run should
582 be uploaded. Also copy of self.archive.upload_url.
583 version: Copy of self.archive.version.
590 """Retrieve the Archive object to use."""
591 # pylint: disable=W0201
592 if not hasattr(self, '_archive'):
593 self._archive = self._run.GetArchive()
599 """Retrieve GS ACL to use for uploads."""
600 return self.archive.upload_acl
602 # TODO(mtennant): Get rid of this property.
605 """Retrieve the ChromeOS version for the archiving."""
606 return self.archive.version
609 def archive_path(self):
610 """Local path where archives are kept for this run."""
611 return self.archive.archive_path
613 # TODO(mtennant): Rename base_archive_path.
615 def bot_archive_root(self):
616 """Path of directory one level up from self.archive_path."""
617 return os.path.dirname(self.archive_path)
620 def upload_url(self):
621 """The GS location where artifacts should be uploaded for this run."""
622 return self.archive.upload_url
625 def base_upload_url(self):
626 """The GS path one level up from self.upload_url."""
627 return os.path.dirname(self.upload_url)
630 def download_url(self):
631 """The URL where artifacts for this run can be downloaded."""
632 return self.archive.download_url
634 @contextlib.contextmanager
635 def ArtifactUploader(self, queue=None, archive=True, strict=True):
636 """Upload each queued input in the background.
638 This context manager starts a set of workers in the background, who each
639 wait for input on the specified queue. These workers run
640 self.UploadArtifact(*args, archive=archive) for each input in the queue.
643 queue: Queue to use. Add artifacts to this queue, and they will be
644 uploaded in the background. If None, one will be created on the fly.
645 archive: Whether to automatically copy files to the archive dir.
646 strict: Whether to treat upload errors as fatal.
649 The queue to use. This is only useful if you did not supply a queue.
651 upload = lambda path: self.UploadArtifact(path, archive, strict)
652 with parallel.BackgroundTaskRunner(upload, queue=queue,
653 processes=self.PROCESSES) as bg_queue:
656 def PrintDownloadLink(self, filename, prefix='', text_to_display=None):
657 """Print a link to an artifact in Google Storage.
660 filename: The filename of the uploaded file.
661 prefix: The prefix to put in front of the filename.
662 text_to_display: Text to display. If None, use |prefix| + |filename|.
664 url = '%s/%s' % (self.download_url.rstrip('/'), filename)
665 if not text_to_display:
666 text_to_display = '%s%s' % (prefix, filename)
667 cros_build_lib.PrintBuildbotLink(text_to_display, url)
669 def _IsInUploadBlacklist(self, filename):
670 """Check if this file is blacklisted to go into a board's extra buckets.
673 filename: The filename of the file we want to check is in the blacklist.
676 True if the file is blacklisted, False otherwise.
678 for blacklisted_file in constants.EXTRA_BUCKETS_FILES_BLACKLIST:
679 if fnmatch.fnmatch(filename, blacklisted_file):
683 def _GetUploadUrls(self, filename, builder_run=None):
684 """Returns a list of all urls for which to upload filename to.
687 filename: The filename of the file we want to upload.
688 builder_run: builder_run object from which to get the board, base upload
689 url, and bot_id. If none, this stage's values.
692 urls = [self.upload_url]
693 bot_id = self._bot_id
695 urls = [builder_run.GetArchive().upload_url]
696 bot_id = builder_run.GetArchive().bot_id
697 if (builder_run.config['boards'] and
698 len(builder_run.config['boards']) == 1):
699 board = builder_run.config['boards'][0]
700 if (not self._IsInUploadBlacklist(filename) and
701 (hasattr(self, '_current_board') or board)):
702 board = board or self._current_board
703 custom_artifacts_file = portage_util.ReadOverlayFile(
704 'scripts/artifacts.json', board=board)
705 if custom_artifacts_file is not None:
706 json_file = json.loads(custom_artifacts_file)
707 for url in json_file.get('extra_upload_urls', []):
708 urls.append('/'.join([url, bot_id, self.version]))
711 @failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
712 def UploadArtifact(self, path, archive=True, strict=True):
713 """Upload generated artifact to Google Storage.
716 path: Path of local file to upload to Google Storage
717 if |archive| is True. Otherwise, this is the name of the file
718 in self.archive_path.
719 archive: Whether to automatically copy files to the archive dir.
720 strict: Whether to treat upload errors as fatal.
724 filename = commands.ArchiveFile(path, self.archive_path)
725 upload_urls = self._GetUploadUrls(filename)
727 commands.UploadArchivedFile(
728 self.archive_path, upload_urls, filename, self._run.debug,
729 update_list=True, acl=self.acl)
730 except failures_lib.GSUploadFailure as e:
731 cros_build_lib.PrintBuildbotStepText('Upload failed')
732 if e.HasFatalFailure(
733 whitelist=[gs.GSContextException, timeout_util.TimeoutError]):
738 # Treat gsutil flake as a warning if it's the only problem.
739 self._HandleExceptionAsWarning(sys.exc_info())
741 @failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
742 def UploadMetadata(self, upload_queue=None, filename=None):
743 """Create and upload JSON file of the builder run's metadata, and to cidb.
745 This uses the existing metadata stored in the builder run. The default
746 metadata.json file should only be uploaded once, at the end of the run,
747 and considered immutable. During the build, intermediate metadata snapshots
748 can be uploaded to other files, such as partial-metadata.json.
750 This method also updates the metadata in the cidb database, if there is a
751 valid cidb connection set up.
754 upload_queue: If specified then put the artifact file to upload on
755 this queue. If None then upload it directly now.
756 filename: Name of file to dump metadata to.
757 Defaults to constants.METADATA_JSON
759 filename = filename or constants.METADATA_JSON
761 metadata_json = os.path.join(self.archive_path, filename)
763 # Stages may run in parallel, so we have to do atomic updates on this.
764 cros_build_lib.Info('Writing metadata to %s.', metadata_json)
765 osutils.WriteFile(metadata_json, self._run.attrs.metadata.GetJSON(),
766 atomic=True, makedirs=True)
768 if upload_queue is not None:
769 cros_build_lib.Info('Adding metadata file %s to upload queue.',
771 upload_queue.put([filename])
773 cros_build_lib.Info('Uploading metadata file %s now.', metadata_json)
774 self.UploadArtifact(filename, archive=False)
776 if cidb.CIDBConnectionFactory.IsCIDBSetup():
777 db = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
779 build_id = self._run.attrs.metadata.GetValue('build_id')
780 cros_build_lib.Info('Writing updated metadata to database for build_id '
782 db.UpdateMetadata(build_id, self._run.attrs.metadata)
784 cros_build_lib.Info('Skipping database update.')