blob: fe33670e8bfad1da84322f1e18930b172384a1e9 [file] [log] [blame]
# Copyright (c) 2012 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.
import collections
import contextlib
import json
import logging
import os
from chromite import cros
from chromite.lib import cache
from chromite.lib import chrome_util
from chromite.lib import cros_build_lib
from chromite.lib import gclient
from chromite.lib import gs
from chromite.lib import osutils
from chromite.buildbot import cbuildbot_config
from chromite.buildbot import constants
COMMAND_NAME = 'chrome-sdk'
SDK_VERSION_ENV = '%CHROME_SDK_VERSION'
class ChromeSDK(object):
"""Functionality for fetching a Chrome SDK environment.
For the version of ChromeOS specified, the class downloads and caches:
1. A dev image of the specified ChromeOS version.
2. Generated scripts that are used to operate on the image.
3. A sysroot containing build dependencies of Chrome.
4. The required cross-compiler toolchain.
5. The Chrome ebuild environment.
"""
SDKContext = collections.namedtuple('SDKContext', ['version', 'key_map'])
TARBALL_CACHE = 'tarballs'
MISC_CACHE = 'misc'
TARGET_TOOLCHAIN_KEY = 'target_toolchain'
CONTENTS = (constants.BASE_IMAGE_TAR, constants.CHROME_SYSROOT_TAR,
constants.IMAGE_SCRIPTS_TAR, constants.CHROME_ENV_TAR)
def __init__(self, cache_dir, board):
"""Initialize the class.
Arguments:
cache_dir: The toplevel cache dir to use.
board: The board to manage the SDK for.
"""
self.gs_ctx = gs.GSContext.Cached(cache_dir, init_boto=True)
self.cache_base = os.path.join(cache_dir, COMMAND_NAME)
self.tarball_cache = cache.TarballCache(
os.path.join(self.cache_base, self.TARBALL_CACHE))
self.misc_cache = cache.DiskCache(
os.path.join(self.cache_base, self.MISC_CACHE))
self.board = board
self.gs_base = self._GetGSBaseForBoard(board)
@staticmethod
def _GetGSBaseForBoard(board):
config = cbuildbot_config.FindCanonicalConfigForBoard(board)
return '%s/%s' % (constants.DEFAULT_ARCHIVE_BUCKET, config['name'])
def _UpdateTarball(self, url, ref):
"""Worker function to fetch tarballs"""
with osutils.TempDirContextManager(
base_dir=self.tarball_cache.staging_dir) as tempdir:
local_path = os.path.join(tempdir, os.path.basename(url))
self.gs_ctx.Copy(url, tempdir)
ref.SetDefault(local_path, lock=True)
def _GetMetadata(self, version):
"""Return metadata (in the form of a dict) for a given version."""
raw_json = None
version_base = os.path.join(self.gs_base, version)
with self.misc_cache.Lookup(
(self.board, version, constants.METADATA_JSON)) as ref:
if ref.Exists(lock=True):
raw_json = osutils.ReadFile(ref.path)
else:
raw_json = self.gs_ctx.Cat(
os.path.join(version_base, constants.METADATA_JSON)).output
ref.AssignText(raw_json)
return json.loads(raw_json)
def _GetChromeLKGM(self):
"""Get ChromeOS LKGM checked into the Chrome tree."""
gclient_root = gclient.FindGclientCheckoutRoot(os.getcwd())
if gclient_root is not None:
version = osutils.ReadFile(os.path.join(
gclient_root, constants.PATH_TO_CHROME_LKGM))
real_path = self.gs_ctx.LS(os.path.join(
self.gs_base, 'R??-%s' % version)).output.splitlines()[0]
if not real_path.startswith(self.gs_base):
raise AssertionError('%s does not start with %s'
% (real_path, self.gs_base))
real_path = real_path[len(self.gs_base) + 1:]
full_version = real_path.partition('/')[0]
if not full_version.endswith(version):
raise AssertionError('%s does not end with %s'
% (full_version, version))
return full_version
return None
def _GetNewestManifestVersion(self):
version_file = '%s/LATEST-master' % self.gs_base
return self.gs_ctx.Cat(version_file).output
def GetDefaultVersion(self):
"""Get the default SDK version to use.
If we are in an existing SDK shell, use the SDK version of the shell.
Otherwise, use what we defaulted to last time. If there was no last time,
use the result of UpdateDefaultVersion().
"""
sdk_version = os.environ.get(SDK_VERSION_ENV)
if sdk_version is not None:
return sdk_version
with self.misc_cache.Lookup((self.board, 'latest')) as ref:
if ref.Exists(lock=True):
return osutils.ReadFile(ref.path).strip()
else:
return self.UpdateDefaultVersion()
def _SetDefaultVersion(self, version):
"""Set the new default version."""
with self.misc_cache.Lookup((self.board, 'latest')) as ref:
ref.AssignText(version)
def UpdateDefaultVersion(self):
"""Update the version that we default to using."""
target = self._GetChromeLKGM()
if target is None:
target = self._GetNewestManifestVersion()
self._SetDefaultVersion(target)
return target
@contextlib.contextmanager
def Prepare(self, version=None, components=None):
"""Ensures the components of an SDK exist and are read-locked.
For a given SDK version, pulls down missing components, and provides a
context where the components are read-locked, which prevents the cache from
deleting them during its purge operations.
Arguments:
gs_ctx: GSContext object.
version: The version to prepare. If not set, uses the version returned by
GetDefaultVersion().
components: A list of specific components(tarballs) to prepare, if
specified. Otherwise, every tarball in the SDK will be prepared.
Yields: An ChromeSDK.SDKContext namedtuple object. The attributes of the
object are:
version: The version that was prepared.
key_map: Dictionary that contains CacheReference objects for the SDK
artifacts, indexed by cache key.
"""
if version is None:
version = self.GetDefaultVersion()
key_map = {}
version_base = os.path.join(self.gs_base, version)
metadata = self._GetMetadata(version)
fetch_urls = dict([(t, os.path.join(version_base, t))
for t in self.CONTENTS])
key_tuple_map = {
self.TARGET_TOOLCHAIN_KEY: metadata['toolchain-tuple'][0],
}
for key, tc_tuple in key_tuple_map.iteritems():
fetch_urls[key] = os.path.join(
'gs://', constants.SDK_GS_BUCKET,
metadata['toolchain-url'] % {'target': tc_tuple})
if components is not None:
fetch_urls = dict([(k, v) for k, v in fetch_urls.iteritems()
if k in components])
try:
for key, url in fetch_urls.iteritems():
cache_key = (self.board, version, key)
ref = self.tarball_cache.Lookup(cache_key)
key_map[key] = ref
ref.Acquire()
if not ref.Exists(lock=True):
# TODO(rcui): Parallelize this. Requires acquiring locks *before*
# generating worker processes; therefore the functionality needs to
# be moved into the DiskCache class itself -
# i.e.,DiskCache.ParallelSetDefault().
self._UpdateTarball(url, ref)
yield self.SDKContext(version, key_map)
finally:
# TODO(rcui): Move to using cros_build_lib.ContextManagerStack()
cros_build_lib.SafeRun([ref.Release for ref in key_map.itervalues()])
@cros.CommandDecorator(COMMAND_NAME)
class ChromeSDKCommand(cros.CrosCommand):
"""
Pulls down SDK components for building and testing Chrome for ChromeOS,
sets up the environment for building Chrome, and runs a command in the
environment, starting a bash session if no command is specified.
The bash session environment is set up by a user-configurable rc file located
at ~/chromite/chrome_sdk.bashrc.
"""
EBUILD_ENV = (
'CXX',
'CC',
'AR',
'AS',
'LD',
'RANLIB',
'GOLD_SET',
'GYP_DEFINES',
)
@classmethod
def AddParser(cls, parser):
super(ChromeSDKCommand, cls).AddParser(parser)
parser.add_argument(
'--board', required=True, help='The board SDK to use.')
parser.add_argument(
'--bashrc', default=constants.CHROME_SDK_BASHRC,
help='A bashrc file used to set up the SDK shell environment. '
'Defaults to %s.' % constants.CHROME_SDK_BASHRC)
parser.add_argument(
'--update', action='store_true', default=False,
help='Force download of latest SDK version for the board.')
parser.add_argument(
'--version', default=None,
help='Specify version of SDK to use. Defaults to determining version '
'based on the type of checkout (Chrome or ChromeOS) you are '
'executing from.')
parser.add_argument(
'cmd', nargs='*', default=None,
help='The command to execute in the SDK environment. Defaults to '
'starting a bash shell.')
def __init__(self, options):
cros.CrosCommand.__init__(self, options)
self.board = options.board
# Lazy initialized.
self.sdk = None
@staticmethod
def _CreatePS1(board, version):
"""Returns PS1 string that sets commandline and xterm window caption."""
sdk_version = '(sdk %s %s)' % (board, version)
label = '\\u@\\h: \\w'
window_caption = "\\[\\e]0;%(sdk_version)s %(label)s \\a\\]"
command_line = "%(sdk_version)s \\[\\e[1;33m\\]%(label)s \\$ \\[\\e[m\\]"
ps1 = window_caption + command_line
return (ps1 % {'sdk_version': sdk_version,
'label': label})
def _FixGoldPath(self, var_contents):
"""Point to the right gold linker.
Strips a command of form '<cmd> -B<gold_path>', and appends
'-fuse-ld=ld.gold' instead.
Arguments:
var_contents: The contents of the environment variable.
"""
cmd = var_contents.partition(' -B')[0]
return '%s -fuse-ld=ld.gold' % cmd
def _SetupEnvironment(self, version, key_map):
"""Sets environment variables to export to the SDK shell."""
sysroot = key_map[constants.CHROME_SYSROOT_TAR].path
environment = os.path.join(key_map[constants.CHROME_ENV_TAR].path,
'environment')
target_tc = key_map[self.sdk.TARGET_TOOLCHAIN_KEY].path
env = osutils.SourceEnvironment(environment, self.EBUILD_ENV)
os.environ[SDK_VERSION_ENV] = version
# SYSROOT is necessary for Goma and the sysroot wrapper.
env['SYSROOT'] = sysroot
gyp_dict = chrome_util.ProcessGypDefines(env['GYP_DEFINES'])
gyp_dict['sysroot'] = sysroot
gyp_dict.pop('order_text_section', None)
env['GYP_DEFINES'] = chrome_util.DictToGypDefines(gyp_dict)
for tc_path in (target_tc,):
env['PATH'] = '%s:%s' % (os.path.join(tc_path, 'bin'), os.environ['PATH'])
for var in ('CXX', 'CC', 'LD'):
env[var] = self._FixGoldPath(env[var])
env['CXX_host'] = 'g++'
env['CC_host'] = 'gcc'
# PS1 sets the command line prompt and xterm window caption.
env['PS1'] = self._CreatePS1(self.board, version)
out_dir = 'out_%s' % self.board
env['builddir_name'] = out_dir
env['GYP_GENERATORS'] = 'ninja'
env['GYP_GENERATOR_FLAGS'] = 'output_dir=%s' % out_dir
return env
@contextlib.contextmanager
def _GetRCFile(self, env, user_rc):
"""Returns path to dynamically created bashrc file.
The bashrc file sets the environment variables contained in |env|, as well
as sources the user-editable chrome_sdk.bashrc file in the user's home
directory. That rc file is created if it doesn't already exist.
Arguments:
env: A dictionary of environment variables that will be set by the rc
file.
"""
if not os.path.exists(user_rc):
osutils.Touch(user_rc, makedirs=True)
# We need a temporary rc file to 'wrap' the user configuration file,
# because running with '--rcfile' causes bash to ignore bash special
# variables passed through subprocess.Popen, such as PS1. So we set them
# here.
# Having a wrapper rc file will also allow us to inject bash functions into
# the environment, not just variables.
with osutils.TempDirContextManager() as tempdir:
contents = []
for key, value in env.iteritems():
contents.append("export %s='%s'\n" % (key, value))
contents.append('. "%s"\n' % user_rc)
rc_file = os.path.join(tempdir, 'rcfile')
osutils.WriteFile(rc_file, contents)
yield rc_file
def Run(self):
"""Perform the command."""
if os.environ.get(SDK_VERSION_ENV) is not None:
cros_build_lib.Die('Already in an SDK shell.')
# Lazy initialize because ChromeSDK creates a GSContext() object in its
# constructor, which may block on user input.
self.sdk = ChromeSDK(self.options.cache_dir, self.options.board)
prepare_version = self.options.version
if self.options.update:
prepare_version = self.sdk.UpdateDefaultVersion()
with self.sdk.Prepare(version=prepare_version) as ctx:
env = self._SetupEnvironment(ctx.version, ctx.key_map)
with self._GetRCFile(env, self.options.bashrc) as rcfile:
bash_header = ['/bin/bash', '--noprofile', '--rcfile', rcfile]
if not self.options.cmd:
cmd = ['-i']
else:
# The '"$@"' expands out to the properly quoted positional args
# coming after the '--'.
cmd = ['-c', '"$@"', '--'] + self.options.cmd
cros_build_lib.RunCommand(
bash_header + cmd, print_cmd=False, debug_level=logging.CRITICAL,
error_code_ok=True)