blob: 17d291e5acdabb4e0a36c27db416dc924f51b714 [file] [log] [blame]
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module containing the various stages that a builder runs."""
import datetime
import math
import os
import re
import sys
import tempfile
import time
import traceback
import chromite.buildbot.cbuildbot_commands as commands
import chromite.buildbot.manifest_version as manifest_version
import chromite.lib.cros_build_lib as cros_lib
_FULL_BINHOST = 'FULL_BINHOST'
_PORTAGE_BINHOST = 'PORTAGE_BINHOST'
PUBLIC_OVERLAY = '%(buildroot)s/src/third_party/chromiumos-overlay'
_CROS_ARCHIVE_URL = 'CROS_ARCHIVE_URL'
OVERLAY_LIST_CMD = '%(buildroot)s/src/platform/dev/host/cros_overlay_list'
VERSION_FILE = os.path.join('src/third_party/chromiumos-overlay',
'chromeos/config/chromeos_version.sh')
class BuildException(Exception):
pass
class Results:
"""Static class that collects the results of our BuildStages as they run."""
# List of results for all stages that's built up as we run. Members are of
# the form ('name', SUCCESS | SKIPPED | Exception, None | description)
_results_log = []
# Stages run in a previous run and restored. Stored as a list of
# stage names.
_previous = []
# Stored in the results log for a stage skipped because it was previously
# completed successfully.
SUCCESS = "Stage was successful"
SKIPPED = "Stage skipped because successful on previous run"
@classmethod
def Clear(cls):
"""Clear existing stage results."""
cls._results_log = []
cls._previous = []
@classmethod
def PreviouslyCompleted(cls, name):
"""Check to see if this stage was previously completed.
Returns:
A boolean showing the stage was successful in the previous run.
"""
return name in cls._previous
@classmethod
def Success(cls):
"""Return true if all stages so far have passed."""
for entry in cls._results_log:
_, result, _, _ = entry
if not result in (cls.SUCCESS, cls.SKIPPED):
return False
return True
@classmethod
def Record(cls, name, result, description=None, time=0):
"""Store off an additional stage result.
Args:
name: The name of the stage
result:
Result should be one of:
Results.SUCCESS if the stage was successful.
Results.SKIPPED if the stage was completed in a previous run.
The exception the stage errored with.
description:
The textual backtrace of the exception, or None
"""
cls._results_log.append((name, result, description, time))
@classmethod
def Get(cls):
"""Fetch stage results.
Returns:
A list with one entry per stage run with a result.
"""
return cls._results_log
@classmethod
def GetPrevious(cls):
"""Fetch stage results.
Returns:
A list of stages names that were completed in a previous run.
"""
return cls._previous
@classmethod
def SaveCompletedStages(cls, file):
"""Save out the successfully completed stages to the provided file."""
for name, result, _, _ in cls._results_log:
if result != cls.SUCCESS and result != cls.SKIPPED: break
file.write(name)
file.write('\n')
@classmethod
def RestoreCompletedStages(cls, file):
"""Load the successfully completed stages from the provided file."""
# Read the file, and strip off the newlines
cls._previous = [line.strip() for line in file.readlines()]
@classmethod
def Report(cls, file):
"""Generate a user friendly text display of the results data."""
results = cls._results_log
line = '*' * 60 + '\n'
edge = '*' * 2
if ManifestVersionedSyncStage.manifest_manager:
file.write(line)
file.write(edge +
' RELEASE VERSION: ' +
ManifestVersionedSyncStage.manifest_manager.current_version +
'\n')
file.write(line)
file.write(edge + ' Stage Results\n')
first_exception = None
for name, result, description, run_time in results:
timestr = datetime.timedelta(seconds=math.ceil(run_time))
file.write(line)
if result == cls.SUCCESS:
# These was no error
file.write('%s PASS %s (%s)\n' % (edge, name, timestr))
elif result == cls.SKIPPED:
# The stage was executed previously, and skipped this time
file.write('%s %s previously completed\n' %
(edge, name))
else:
if type(result) in (cros_lib.RunCommandException,
cros_lib.RunCommandError):
# If there was a RunCommand error, give just the command that
# failed, not it's full argument list, since those are usually
# too long.
file.write('%s FAIL %s (%s) in %s\n' %
(edge, name, timestr, result.cmd[0]))
else:
# There was a normal error, give the type of exception
file.write('%s FAIL %s (%s) with %s\n' %
(edge, name, timestr, type(result).__name__))
if not first_exception:
first_exception = description
file.write(line)
if BuilderStage.archive_url:
file.write('%s BUILD ARTIFACTS FOR THIS BUILD CAN BE FOUND AT:\n' % edge)
file.write('%s %s\n' % (edge, BuilderStage.archive_url))
file.write(line)
if first_exception:
file.write('\n')
file.write('Build failed with:\n')
file.write('\n')
file.write(first_exception)
class BuilderStage():
"""Parent class for stages to be performed by a builder."""
name_stage_re = re.compile('(\w+)Stage')
# TODO(sosa): Remove these once we have a SEND/RECIEVE IPC mechanism
# implemented.
new_binhost = None
old_binhost = None
test_tarball = None
rev_overlays = None
push_overlays = None
archive_url = None
# Class variable that stores the branch to build and test
_tracking_branch = None
@staticmethod
def SetTrackingBranch(tracking_branch):
BuilderStage._tracking_branch = tracking_branch
def __init__(self, bot_id, options, build_config):
self._bot_id = bot_id
self._options = options
self._build_config = build_config
self._name = self.name_stage_re.match(self.__class__.__name__).group(1)
self._prebuilt_type = None
self._ExtractVariables()
repo_dir = os.path.join(self._build_root, '.repo')
if not self._options.clobber and os.path.isdir(repo_dir):
self._ExtractOverlays()
def _ExtractVariables(self):
"""Extracts common variables from build config and options into class."""
self._build_root = os.path.abspath(self._options.buildroot)
if (self._options.prebuilts and self._build_config['prebuilts'] and not
self._options.debug):
self._prebuilt_type = self._build_config['build_type']
def _ExtractOverlays(self):
"""Extracts list of overlays into class."""
if not BuilderStage.rev_overlays or not BuilderStage.push_overlays:
rev_overlays = self._ResolveOverlays(self._build_config['rev_overlays'])
push_overlays = self._ResolveOverlays(self._build_config['push_overlays'])
# Sanity checks.
# We cannot push to overlays that we don't rev.
assert set(push_overlays).issubset(set(rev_overlays))
# Either has to be a master or not have any push overlays.
assert self._build_config['master'] or not push_overlays
BuilderStage.rev_overlays = rev_overlays
BuilderStage.push_overlays = push_overlays
def _ResolveOverlays(self, overlays):
"""Return the list of overlays to use for a given buildbot.
Args:
overlays: A string describing which overlays you want.
'private': Just the private overlay.
'public': Just the public overlay.
'both': Both the public and private overlays.
"""
cmd = OVERLAY_LIST_CMD % {'buildroot': self._build_root}
# Check in case we haven't checked out the source yet.
if not os.path.exists(cmd):
return []
public_overlays = cros_lib.RunCommand([cmd, '--all_boards', '--noprivate'],
redirect_stdout=True,
print_cmd=False).output.split()
private_overlays = cros_lib.RunCommand([cmd, '--all_boards', '--nopublic'],
redirect_stdout=True,
print_cmd=False).output.split()
# TODO(davidjames): cros_overlay_list should include chromiumos-overlay in
# its list of public overlays. But it doesn't yet...
public_overlays.append(PUBLIC_OVERLAY % {'buildroot': self._build_root})
if overlays == 'private':
paths = private_overlays
elif overlays == 'public':
paths = public_overlays
elif overlays == 'both':
paths = public_overlays + private_overlays
else:
cros_lib.Info('No overlays found.')
paths = []
return paths
def _PrintLoudly(self, msg):
"""Prints a msg with loudly."""
border_line = '*' * 60
edge = '*' * 2
print border_line
msg_lines = msg.split('\n')
# If the last line is whitespace only drop it.
if not msg_lines[-1].rstrip():
del msg_lines[-1]
for msg_line in msg_lines:
print '%s %s'% (edge, msg_line)
print border_line
def _GetPortageEnvVar(self, envvar, board):
"""Get a portage environment variable for the configuration's board.
envvar: The environment variable to get. E.g. 'PORTAGE_BINHOST'.
Returns:
The value of the environment variable, as a string. If no such variable
can be found, return the empty string.
"""
cwd = os.path.join(self._build_root, 'src', 'scripts')
if board:
portageq = 'portageq-%s' % board
else:
portageq = 'portageq'
binhost = cros_lib.OldRunCommand(
[portageq, 'envvar', envvar], cwd=cwd, redirect_stdout=True,
enter_chroot=True, error_ok=True)
return binhost.rstrip('\n')
def _Begin(self):
"""Can be overridden. Called before a stage is performed."""
self._PrintLoudly('Start Stage %s - %s\n\n%s' % (
self._name, time.strftime('%H:%M:%S'), self.__doc__))
def _Finish(self):
"""Can be overridden. Called after a stage has been performed."""
self._PrintLoudly('Finished Stage %s - %s' %
(self._name, time.strftime('%H:%M:%S')))
def _PerformStage(self):
"""Subclassed stages must override this function to perform what they want
to be done.
"""
pass
def Run(self):
"""Have the builder execute the stage."""
if Results.PreviouslyCompleted(self._name):
self._PrintLoudly('Skipping Stage %s' % self._name)
Results.Record(self._name, Results.SKIPPED)
return
start_time = time.time()
self._Begin()
try:
self._PerformStage()
except Exception as e:
# Tell the user about the exception, and record it
description = traceback.format_exc()
print >> sys.stderr, description
elapsed_time = time.time() - start_time
Results.Record(self._name, e, description, time=elapsed_time)
raise BuildException()
else:
elapsed_time = time.time() - start_time
Results.Record(self._name, Results.SUCCESS, time=elapsed_time)
finally:
self._Finish()
class SyncStage(BuilderStage):
"""Stage that performs syncing for the builder."""
def _PerformStage(self):
if self._options.clobber or not os.path.isdir(os.path.join(self._build_root,
'.repo')):
commands.FullCheckout(self._build_root,
self._tracking_branch,
url=self._build_config['git_url'])
else:
board = self._build_config['board']
commands.PreFlightRinse(self._build_root, board,
BuilderStage.rev_overlays)
BuilderStage.old_binhost = self._GetPortageEnvVar(_FULL_BINHOST, board)
commands.IncrementalCheckout(self._build_root)
# Check that all overlays can be found.
self._ExtractOverlays() # Our list of overlays are from pre-sync, refresh
for path in BuilderStage.rev_overlays:
assert os.path.isdir(path), 'Missing overlay: %s' % path
class ManifestVersionedSyncStage(BuilderStage):
"""Stage that generates a unique manifest file, and sync's to it."""
manifest_manager = None
def _PerformStage(self):
increment = 'branch' if self._tracking_branch == 'master' else 'patch'
manifest_manager = manifest_version.BuildSpecsManager(
source_dir=self._build_root,
checkout_repo=self._build_config['git_url'],
manifest_repo=self._build_config['manifest_version'],
branch=self._tracking_branch,
build_name=self._build_config['board'],
incr_type=increment,
clobber=self._options.clobber,
dry_run=self._options.debug)
if os.path.isdir(os.path.join(self._build_root, '.repo')):
commands.PreFlightRinse(self._build_root, self._build_config['board'],
BuilderStage.rev_overlays)
next_manifest = manifest_manager.GetNextBuildSpec(VERSION_FILE, latest=True)
if not next_manifest:
print 'AUTOREV: Nothing to build!'
sys.exit(0);
# Log this early on for the release team to grep out before we finish.
print
print "RELEASETAG: %s" % manifest_manager.current_version
print
# Store off this value where the Completion stage can find it...
ManifestVersionedSyncStage.manifest_manager = manifest_manager
commands.ManifestCheckout(self._build_root,
self._tracking_branch,
next_manifest,
url=self._build_config['git_url'])
# Check that all overlays can be found.
self._ExtractOverlays() # Our list of overlays are from pre-sync, refresh
for path in BuilderStage.rev_overlays:
assert os.path.isdir(path), 'Missing overlay: %s' % path
class ManifestVersionedSyncCompletionStage(BuilderStage):
"""Stage that records board specific results for a unique manifest file."""
def __init__(self, bot_id, options, build_config, success):
BuilderStage.__init__(self, bot_id, options, build_config)
self.success = success
def _PerformStage(self):
if ManifestVersionedSyncStage.manifest_manager:
ManifestVersionedSyncStage.manifest_manager.UpdateStatus(
success=self.success)
class BuildBoardStage(BuilderStage):
"""Stage that is responsible for building host pkgs and setting up a board."""
def _PerformStage(self):
chroot_path = os.path.join(self._build_root, 'chroot')
if not os.path.isdir(chroot_path) or self._build_config['chroot_replace']:
commands.MakeChroot(
buildroot=self._build_root,
replace=self._build_config['chroot_replace'],
fast=self._build_config['fast'],
usepkg=self._build_config['usepkg_chroot'])
# If board is a string, convert to array.
if isinstance(self._build_config['board'], str):
board = [self._build_config['board']]
else:
assert self._build_config == 'chroot', 'Board array requires chroot type.'
board = self._build_config['board']
assert isinstance(board, list), 'Board was neither an array or a string.'
# Iterate through boards to setup.
for board_to_build in board:
# Only build the board if the directory does not exist.
board_path = os.path.join(chroot_path, 'build', board_to_build)
if os.path.exists(board_path):
continue
commands.SetupBoard(self._build_root,
board=board_to_build,
fast=self._build_config['fast'],
usepkg=self._build_config['usepkg_setup_board'])
class UprevStage(BuilderStage):
"""Stage that uprevs Chromium OS packages that the builder intends to
validate.
"""
def _PerformStage(self):
# Perform chrome uprev.
chrome_atom_to_build = None
if self._options.chrome_rev:
chrome_atom_to_build = commands.MarkChromeAsStable(
self._build_root, self._tracking_branch,
self._options.chrome_rev, self._build_config['board'])
# Perform other uprevs.
if self._build_config['uprev']:
commands.UprevPackages(self._build_root,
self._build_config['board'],
BuilderStage.rev_overlays)
elif self._options.chrome_rev and not chrome_atom_to_build:
# TODO(sosa): Do this in a better way.
sys.exit(0)
class BuildTargetStage(BuilderStage):
"""This stage builds Chromium OS for a target.
Specifically, we build Chromium OS packages and perform imaging to get
the images we want per the build spec."""
def _PerformStage(self):
board = self._build_config['board']
BuilderStage.new_binhost = self._GetPortageEnvVar(_FULL_BINHOST, board)
emptytree = (BuilderStage.old_binhost and
BuilderStage.old_binhost != BuilderStage.new_binhost)
build_autotest = (self._build_config['build_tests'] and
self._options.tests)
env = {}
if self._build_config.get('useflags'):
env['USE'] = ' '.join(self._build_config['useflags'])
commands.Build(self._build_root,
emptytree=emptytree,
build_autotest=build_autotest,
fast=self._build_config['fast'],
usepkg=self._build_config['usepkg_build_packages'],
nowithdebug=self._build_config['nowithdebug'],
extra_env=env)
if self._prebuilt_type == 'full':
commands.UploadPrebuilts(
self._build_root, self._build_config['board'],
self._build_config['rev_overlays'], [], self._prebuilt_type,
None, self._options.buildnumber)
commands.BuildImage(self._build_root, extra_env=env)
if self._build_config['vm_tests']:
commands.BuildVMImageForTesting(self._build_root, extra_env=env)
class TestStage(BuilderStage):
"""Stage that performs testing steps."""
def _CreateTestRoot(self):
"""Returns a temporary directory for test results in chroot.
Returns relative path from chroot rather than whole path.
"""
# Create test directory within tmp in chroot.
chroot = os.path.join(self._build_root, 'chroot')
chroot_tmp = os.path.join(chroot, 'tmp')
test_root = tempfile.mkdtemp(prefix='cbuildbot', dir=chroot_tmp)
# Relative directory.
(_, _, relative_path) = test_root.partition(chroot)
return relative_path
def _PerformStage(self):
if self._build_config['unittests']:
commands.RunUnitTests(self._build_root,
full=(not self._build_config['quick_unit']),
nowithdebug=self._build_config['nowithdebug'])
if self._build_config['vm_tests']:
test_results_dir = self._CreateTestRoot()
try:
commands.RunTestSuite(self._build_root,
self._build_config['board'],
os.path.join(test_results_dir,
'test_harness'),
full=(not self._build_config['quick_vm']))
if self._build_config['chrome_tests']:
commands.RunChromeSuite(self._build_root,
os.path.join(test_results_dir,
'chrome_results'))
finally:
BuilderStage.test_tarball = commands.ArchiveTestResults(
self._build_root, test_results_dir)
class RemoteTestStatusStage(BuilderStage):
"""Stage that performs testing steps."""
def _PerformStage(self):
test_status_cmd = ['./crostools/get_test_status.py',
'--board=%s' % self._build_config['board'],
'--build=%s' % self._options.buildnumber]
for job in self._options.remote_test_status.split(','):
result = cros_lib.RunCommand(
test_status_cmd + ['--category=%s' % job],
redirect_stdout=True, print_cmd=False)
# Emit annotations for buildbot status updates.
print result.output
class ArchiveStage(BuilderStage):
"""Archives build and test artifacts for developer consumption."""
def _PerformStage(self):
BuilderStage.archive_url, archive_dir = commands.LegacyArchiveBuild(
self._build_root, self._bot_id, self._build_config,
self._options.buildnumber, BuilderStage.test_tarball,
self._options.debug)
if not self._options.debug and self._build_config['upload_symbols']:
commands.UploadSymbols(self._build_root,
board=self._build_config['board'],
official=self._build_config['chromeos_official'])
# TODO: When we support branches fully, the friendly name of the branch
# needs to be used with PushImages
if not self._options.debug and self._build_config['push_image']:
commands.PushImages(self._build_root,
board=self._build_config['board'],
branch_name='master',
archive_dir=archive_dir)
class PushChangesStage(BuilderStage):
"""Pushes pfq and prebuilt url changes to git."""
def _PerformStage(self):
if self._prebuilt_type in ('binary', 'chrome'):
board = self._build_config['board']
binhosts = []
binhosts.extend(self._GetPortageEnvVar(_PORTAGE_BINHOST, board).split())
binhosts.extend(self._GetPortageEnvVar(_PORTAGE_BINHOST, None).split())
commands.UploadPrebuilts(
self._build_root, board, self._build_config['rev_overlays'],
binhosts, self._prebuilt_type, self._options.chrome_rev,
self._options.buildnumber)
commands.UprevPush(self._build_root,
self._build_config['board'],
BuilderStage.push_overlays,
self._options.debug)