blob: b473f253828906093615665c5626f1db365d35ec [file] [log] [blame]
#!/usr/bin/python2
# Copyright (c) 2013 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.
"""Wrapper for building the Chromium OS platform.
Takes care of running gyp/ninja/etc... with all the right values.
"""
from __future__ import print_function
import argparse
import glob
import os
import shutil
import subprocess
import sys
from chromite.lib import osutils
from chromite.lib.cros_build_lib import ShellUnquote
class Platform2(object):
"""Main builder logic for platform2"""
def __init__(self, use_flags, board=None, host=False, libdir=None,
incremental=True, verbose=False, enable_tests=False,
cache_dir=None, jobs=None):
self.board = board
self.host = host
self.incremental = incremental
self.jobs = jobs
self.verbose = verbose
if use_flags:
self.use_flags = use_flags
else:
self.use_flags = self.get_platform2_use_flags()
if enable_tests:
self.use_flags.add('test')
if self.host:
self.sysroot = '/'
self.pkgconfig = 'pkg-config'
else:
board_vars = self.get_portageq_envvars(['SYSROOT', 'PKG_CONFIG'],
board=board)
self.sysroot = board_vars['SYSROOT']
self.pkgconfig = board_vars['PKG_CONFIG']
if libdir:
self.libdir = libdir
else:
self.libdir = '/usr/lib'
if cache_dir:
self.cache_dir = cache_dir
else:
self.cache_dir = os.path.join(self.sysroot,
'var/cache/portage/chromeos-base/platform2')
def get_src_dir(self):
"""Return the path to build tools and common GYP files"""
return os.path.realpath(os.path.dirname(__file__))
def get_platform2_root(self):
"""Return the path to src/platform2"""
return os.path.dirname(self.get_src_dir())
def get_buildroot(self):
"""Return the path to the folder where build artifacts are located."""
if not self.incremental:
workdir = os.environ.get('WORKDIR')
if workdir:
# Matches $(cros-workon_get_build_dir) behavior.
return os.path.join(workdir, 'build')
else:
return os.getcwd()
else:
return self.cache_dir
def get_products_path(self):
"""Return the path to the folder where build product are located."""
return os.path.join(self.get_buildroot(), 'out/Default')
def get_portageq_envvars(self, varnames, board=None):
"""Returns the values of a given set of variables using portageq."""
if isinstance(varnames, basestring):
varnames = [varnames]
# See if the env already has these settings. If so, grab them directly.
# This avoids the need to specify --board at all most of the time.
try:
board_vars = {}
for varname in varnames:
board_vars[varname] = os.environ[varname]
return board_vars
except KeyError:
pass
if board is None and not self.host:
board = self.board
# Portage will set this to an incomplete list which breaks portageq
# walking all of the repos. Clear it and let the value be repopulated.
os.environ.pop('PORTDIR_OVERLAY', None)
portageq_bin = 'portageq' if not board else 'portageq-%s' % board
cmd = [portageq_bin, 'envvar', '-v']
cmd += varnames
try:
output = subprocess.check_output(cmd)
except (UnboundLocalError, OSError):
raise AssertionError('Error running %r' % cmd)
output_lines = [x for x in output.splitlines() if x]
output_items = [x.split('=', 1) for x in output_lines]
board_vars = dict(dict([(k, ShellUnquote(v)) for k, v in output_items]))
return board_vars if len(board_vars) > 1 else board_vars.values()[0]
def get_platform2_use_flags(self):
"""Returns the set of USE flags set for the Platform2 package."""
equery_bin = 'equery' if not self.board else 'equery-%s' % self.board
cmd = [equery_bin, 'u', 'platform2']
try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, errors = p.communicate()
except UnboundLocalError:
raise AssertionError('Error running %s' % equery_bin)
except OSError:
raise AssertionError('Error running equery: %s' % errors)
return set([x for x in output.splitlines() if x])
def get_build_environment(self):
"""Returns a dict containing environment variables we will use to run GYP.
We do this to set the various CC/CXX/AR names for the target board.
"""
varnames = ['CHOST', 'AR', 'CC', 'CXX']
if not self.host and not self.board:
for v in varnames:
os.environ.setdefault(v, '')
board_env = self.get_portageq_envvars(varnames)
tool_names = {
'AR': 'ar',
'CC': 'gcc',
'CXX': 'g++',
}
env = os.environ.copy()
for var, tool in tool_names.items():
env['%s_target' % var] = (board_env[var] if board_env[var] else \
'%s-%s' % (board_env['CHOST'], tool))
return env
def get_components_glob(self):
"""Return a glob of marker files for components/projects that were built.
Each project spits out a file whilst building: we return a glob of them
so we can install/test those projects or reset between compiles to ensure
components that are no longer part of the build don't get installed.
"""
return glob.glob(os.path.join(self.get_products_path(),
'gen/components_*'))
def use(self, flag):
"""Returns a boolean describing whether or not a given USE flag is set."""
return flag in self.use_flags
def configure(self, args):
"""Runs the configure step of the Platform2 build.
Creates the build root if it doesn't already exists. Generates flags to
run GYP with, and then runs GYP.
"""
if not os.path.isdir(self.get_buildroot()):
os.makedirs(self.get_buildroot())
if not self.incremental and os.path.isdir(self.get_products_path()):
shutil.rmtree(self.get_products_path())
targets = [os.path.join(self.get_src_dir(), 'platform.gyp')]
if args:
targets = args
common_gyp = os.path.join(self.get_src_dir(), 'common.gypi')
libbase_ver = os.environ.get('BASE_VER', '')
if not libbase_ver:
# If BASE_VER variable not set, read the content of common_mk/BASE_VER
# file which contains the default libchrome revision number.
base_ver_file = os.path.join(self.get_src_dir(), 'BASE_VER')
libbase_ver = osutils.ReadFile(base_ver_file).strip()
assert libbase_ver
# The common root folder of platform2/.
# Used as (DEPTH) variable in specific project .gyp files.
src_root = os.path.normpath(os.path.join(self.get_src_dir(), '..'))
# Do NOT pass the board name into GYP. If you think you need to so, you're
# probably doing it wrong.
gyp_args = ['gyp'] + targets + [
'--format=ninja',
'--include=%s' % common_gyp,
'--depth=%s' % src_root,
'--toplevel-dir=%s' % self.get_platform2_root(),
'--generator-output=%s' % self.get_buildroot(),
'-DOS=linux',
'-Dpkg-config=%s' % self.pkgconfig,
'-Dsysroot=%s' % self.sysroot,
'-Dlibdir=%s' % self.libdir,
'-Dbuild_root=%s' % self.get_buildroot(),
'-Dplatform2_root=%s' % self.get_platform2_root(),
'-Dlibbase_ver=%s' % libbase_ver,
'-Dclang_syntax=%s' % os.environ.get('CROS_WORKON_CLANG', ''),
'-Denable_exceptions=%s' % os.environ.get('CXXEXCEPTIONS', '0'),
'-Dexternal_cflags=%s' % os.environ.get('CFLAGS', ''),
'-Dexternal_cxxflags=%s' % os.environ.get('CXXFLAGS', ''),
'-Dexternal_cppflags=%s' % os.environ.get('CPPFLAGS', ''),
'-Dexternal_ldflags=%s' % os.environ.get('LDFLAGS', ''),
]
gyp_args += ['-DUSE_%s=1' % (use_flag,) for use_flag in self.use_flags]
try:
subprocess.check_call(gyp_args, env=self.get_build_environment(),
cwd=self.get_platform2_root())
except subprocess.CalledProcessError:
raise AssertionError('Error running: %s'
% ' '.join(map(repr, gyp_args)))
except OSError:
raise AssertionError('Error running %s' % (gyp_args[0]))
def compile(self, args):
"""Runs the compile step of the Platform2 build.
Removes any existing component markers that may exist (so we don't run
tests/install for projects that have been disabled since the last
build). Builds arguments for running Ninja and then runs Ninja.
"""
for component in self.get_components_glob():
os.remove(component)
if not args:
args = ['all']
ninja_args = ['ninja', '-C', self.get_products_path()]
if self.jobs:
ninja_args += ['-j', str(self.jobs)]
ninja_args += args
if self.verbose:
ninja_args.append('-v')
if os.environ.get('NINJA_ARGS'):
ninja_args.extend(os.environ['NINJA_ARGS'].split())
try:
subprocess.check_call(ninja_args)
except subprocess.CalledProcessError:
raise AssertionError('Error running: %s'
% ' '.join(map(repr, ninja_args)))
except OSError:
raise AssertionError('Error running %s' % (ninja_args[0]))
def deviterate(self, args):
"""Runs the configure and compile steps of the Platform2 build.
This is the default action, to allow easy iterative testing of changes
as a developer.
"""
self.configure([])
self.compile(args)
class _ParseStringSetAction(argparse.Action):
"""Helper for turning a string argument into a list"""
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, set(values.split()))
def main(argv):
actions = ['configure', 'compile', 'deviterate']
parser = argparse.ArgumentParser()
parser.add_argument('--action', default='deviterate',
choices=actions, help='action to run')
parser.add_argument('--board',
help='board to build for')
parser.add_argument('--cache_dir',
help='directory to use as cache for incremental build')
parser.add_argument('--disable_incremental', action='store_false',
dest='incremental', help='disable incremental build')
parser.add_argument('--enable_tests', action='store_true',
help='build and run tests')
parser.add_argument('--host', action='store_true',
help='specify that we\'re building for the host')
parser.add_argument('--libdir',
help='the libdir for the specific board, eg /usr/lib64')
parser.add_argument('--use_flags',
action=_ParseStringSetAction, help='USE flags to enable')
parser.add_argument('-j', '--jobs', type=int, default=None,
help='number of jobs to run in parallel')
parser.add_argument('--verbose', action='store_true', default=None,
help='enable verbose log output')
parser.add_argument('args', nargs='*')
options = parser.parse_args(argv)
if options.host and options.board:
raise AssertionError('You must provide only one of --board or --host')
if options.verbose is None:
# Should convert to cros_build_lib.BooleanShellValue.
options.verbose = (os.environ.get('VERBOSE', '0') == '1')
p2 = Platform2(options.use_flags, options.board, options.host,
options.libdir, options.incremental, options.verbose,
options.enable_tests, options.cache_dir, jobs=options.jobs)
getattr(p2, options.action)(options.args)
if __name__ == '__main__':
main(sys.argv[1:])