blob: 904fd524682174779eaaa3d738ed8c72f1a1d384 [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 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 GN/ninja/etc... with all the right values.
"""
from __future__ import print_function
import collections
import glob
import json
import os
import six
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib import portage_util
import common_utils
import ebuild_function
# USE flags used in BUILD.gn should be listed in _IUSE or _IUSE_TRUE.
# USE flags whose default values are false.
_IUSE = [
'amd64',
'arc_adb_sideloading',
'arcvm',
'arm',
'asan',
'attestation',
'beanstalk',
'bluetooth_suspend_management',
'buffet',
'cdm_factory_daemon',
'cellular',
'cert_provision',
'cfm',
'cfm_enabled_device',
'cheets',
'chromeless_tty',
'containers',
'coverage',
'cros_arm64',
'cros_host',
'cros_i686',
'crosvm_wl_dmabuf',
'crypto',
'cryptohome_userdataauth_interface',
'dbus',
'device_mapper',
'dhcpv6',
'direncryption',
'dlc',
'double_extend_pcr_issue',
'enable_slow_boot_notify',
'fake_drivefs_launcher',
'fast_transition',
'feedback',
'fp_on_power_button',
'ftdi_tpm',
'fuzzer',
'hammerd_api',
'houdini',
'houdini64',
'hwid_override',
'iioservice',
'ipu6',
'ipu6se',
'iwlwifi_dump',
'jetstream_routing',
'kvm_guest',
'kvm_host',
'metrics_uploader',
'mojo',
'mount_oop',
'msan',
'mtd',
'ndk_translation',
'ondevice_handwriting',
'ondevice_handwriting_dlc',
'ondevice_speech',
'opengles',
'passive_metrics',
'pinweaver',
'power_management',
'pppoe',
'profiling',
'selinux',
'systemd',
'tcmalloc',
'test',
'timers',
'tpm',
'tpm2',
'tpm2_simulator',
'trogdor_sar_hack',
'ubsan',
'udev',
'usb_camera_monocle',
'user_session_isolation',
'vm_borealis',
'vpn',
'wake_on_wifi',
'wifi',
'wired_8021x',
]
# USE flags whose default values are true.
_IUSE_TRUE = [
'chrome_kiosk_app',
'chrome_network_proxy',
]
class Platform2(object):
"""Main builder logic for platform2"""
def __init__(self, use_flags=None, board=None, host=False, libdir=None,
incremental=True, verbose=False, enable_tests=False,
cache_dir=None, jobs=None, platform_subdir=None):
self.board = board
self.host = host
self.incremental = incremental
self.jobs = jobs
self.verbose = verbose
self.platform_subdir = platform_subdir
if use_flags is not None:
self.use_flags = use_flags
else:
self.use_flags = portage_util.GetBoardUseFlags(self.board)
if enable_tests:
self.use_flags.add('test')
if self.host:
self.sysroot = '/'
else:
board_vars = self.get_portageq_envvars(['SYSROOT'], board=board)
self.sysroot = board_vars['SYSROOT']
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')
self.libbase_ver = os.environ.get('BASE_VER', '')
if not self.libbase_ver:
# If BASE_VER variable not set, read the content of
# $SYSROOT/usr/share/libchrome/BASE_VER
# file which contains the default libchrome revision number.
base_ver_file = os.path.join(self.sysroot,
'usr/share/libchrome/BASE_VER')
try:
self.libbase_ver = osutils.ReadFile(base_ver_file).strip()
except FileNotFoundError:
# Software not depending on libchrome still uses platform2.py, Instead
# of asserting here. Provide a human readable bad value that is not
# supposed to be used.
self.libbase_ver = 'NOT-INSTALLED'
def get_src_dir(self):
"""Return the path to build tools and common GN 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."""
# 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)
return portage_util.PortageqEnvvars(varnames, board=board,
allow_undefined=True)
def get_build_environment(self):
"""Returns a dict containing environment variables we will use to run GN.
We do this to set the various toolchain names for the target board.
"""
varnames = ['CHOST', 'AR', 'CC', 'CXX', 'PKG_CONFIG']
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++',
'PKG_CONFIG': 'pkg-config',
}
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 can_use_gn(self):
"""Returns true if GN can be used on configure.
All packages in platform2/ should be configured by GN.
"""
build_gn = os.path.join(self.get_platform2_root(), self.platform_subdir,
'BUILD.gn')
return os.path.isfile(build_gn)
def configure(self, args):
"""Runs the configure step of the Platform2 build.
Creates the build root if it doesn't already exists. Then runs the
appropriate configure tool. Currenty only GN is supported.
"""
assert self.can_use_gn()
# The args was used only for gyp.
# TODO(crbug.com/767517): remove code for handling args.
# There is a logic to in the platform eclass file, which detects a .gyp
# file under project root and passes it to here an arg.
if args:
print('Warning: Args for GYP was given. We will no longer use GYP. '
'Ignoring it and continuing configuration with GN.')
if not os.path.isdir(self.get_buildroot()):
os.makedirs(self.get_buildroot())
if not self.incremental:
osutils.RmDir(self.get_products_path(), ignore_missing=True)
self.configure_gn()
def gen_common_args(self, should_parse_shell_string):
"""Generates common arguments for the tools to configure as a dict.
Returned value types are str, bool or list of strs.
Lists are returned only when should_parse_shell_string is set to True.
"""
def flags(s):
if should_parse_shell_string:
return common_utils.parse_shell_args(s)
return s
args = {
'OS': 'linux',
'sysroot': self.sysroot,
'libdir': self.libdir,
'build_root': self.get_buildroot(),
'platform2_root': self.get_platform2_root(),
'libbase_ver': self.libbase_ver,
'enable_exceptions': os.environ.get('CXXEXCEPTIONS', 0) == '1',
'external_cflags': flags(os.environ.get('CFLAGS', '')),
'external_cxxflags': flags(os.environ.get('CXXFLAGS', '')),
'external_cppflags': flags(os.environ.get('CPPFLAGS', '')),
'external_ldflags': flags(os.environ.get('LDFLAGS', '')),
}
return args
def configure_gn_args(self):
"""Configure with GN.
Generates flags to run GN with, and then runs GN.
"""
def to_gn_string(s):
return '"%s"' % s.replace('"', '\\"')
def to_gn_list(strs):
return '[%s]' % ','.join([to_gn_string(s) for s in strs])
def to_gn_args_args(gn_args):
for k, v in gn_args.items():
if isinstance(v, bool):
v = str(v).lower()
elif isinstance(v, list):
v = to_gn_list(v)
elif isinstance(v, six.string_types):
v = to_gn_string(v)
else:
raise AssertionError('Unexpected %s, %r=%r' % (type(v), k, v))
yield '%s=%s' % (k.replace('-', '_'), v)
buildenv = self.get_build_environment()
gn_args = {
'platform_subdir': self.platform_subdir,
'cc': buildenv.get('CC_target', buildenv.get('CC', '')),
'cxx': buildenv.get('CXX_target', buildenv.get('CXX', '')),
'ar': buildenv.get('AR_target', buildenv.get('AR', '')),
'pkg-config': buildenv.get('PKG_CONFIG_target',
buildenv.get('PKG_CONFIG', '')),
}
gn_args['clang_cc'] = 'clang' in gn_args['cc']
gn_args['clang_cxx'] = 'clang' in gn_args['cxx']
gn_args.update(self.gen_common_args(True))
gn_args_args = list(to_gn_args_args(gn_args))
# Set use flags as a scope.
uses = {}
for flag in _IUSE:
uses[flag] = False
for flag in _IUSE_TRUE:
uses[flag] = True
for x in self.use_flags:
uses[x.replace('-', '_')] = True
use_args = ['%s=%s' % (x, str(uses[x]).lower()) for x in uses]
gn_args_args += ['use={%s}' % (' '.join(use_args))]
return gn_args_args
def configure_gn(self):
"""Configure with GN.
Runs gn gen with generated flags.
"""
gn_args_args = self.configure_gn_args()
gn_args = ['gn', 'gen']
if self.verbose:
gn_args += ['-v']
gn_args += [
'--root=%s' % self.get_platform2_root(),
'--args=%s' % ' '.join(gn_args_args),
self.get_products_path(),
]
cros_build_lib.run(gn_args, env=self.get_build_environment(),
cwd=self.get_platform2_root())
def gn_desc(self, *args):
"""Describe BUILD.gn.
Runs gn desc with generated flags.
"""
gn_args_args = self.configure_gn_args()
cmd = [
'gn', 'desc',
self.get_products_path(),
'//%s/*' % self.platform_subdir,
'--root=%s' % self.get_platform2_root(),
'--args=%s' % ' '.join(gn_args_args),
'--format=json',
]
cmd += args
result = cros_build_lib.run(cmd, env=self.get_build_environment(),
cwd=self.get_platform2_root(),
stdout=True, encoding='utf-8')
return json.loads(result.output)
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)
args = ['%s:%s' % (self.platform_subdir, x) for x in args]
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())
cros_build_lib.run(ninja_args)
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)
def configure_test(self):
"""Generates test options from GN."""
def to_options(options):
"""Convert dict to shell string."""
result = []
for key, value in options.items():
if isinstance(value, bool):
if value:
result.append('--%s' % key)
continue
if key == 'raw':
result.append(value)
continue
result.append('--%s=%s' % (key, value))
return result
conf = self.gn_desc('--all', '--type=executable')
group_all = conf.get('//%s:all' % self.platform_subdir, {})
group_all_deps = group_all.get('deps', [])
options_list = []
for target_name in group_all_deps:
test_target = conf.get(target_name)
outputs = test_target.get('outputs', [])
if len(outputs) != 1:
continue
output = outputs[0]
metadata = test_target.get('metadata', {})
run_test = unwrap_value(metadata, '_run_test', False)
if not run_test:
continue
test_config = unwrap_value(metadata, '_test_config', {})
p2_test_py = os.path.join(self.get_src_dir(), 'platform2_test.py')
options = [
p2_test_py,
'--action=run',
'--sysroot=%s' % self.sysroot,
]
if self.host:
options += ['--host']
p2_test_filter = os.environ.get('P2_TEST_FILTER')
if p2_test_filter:
options += ['--user_gtest_filter=%s' % p2_test_filter]
options += to_options(test_config)
options += ['--', output]
options_list.append(options)
return options_list
def test_all(self, _args):
"""Runs all tests described from GN."""
test_options_list = self.configure_test()
for test_options in test_options_list:
cros_build_lib.run(test_options, encoding='utf-8')
def configure_install(self):
"""Generates installation commands of ebuild."""
conf = self.gn_desc('--all')
group_all = conf.get('//%s:all' % self.platform_subdir, {})
group_all_deps = group_all.get('deps', [])
config_group = collections.defaultdict(list)
for target_name in group_all_deps:
target_conf = conf.get(target_name, {})
metadata = target_conf.get('metadata', {})
install_config = unwrap_value(metadata, '_install_config')
if not install_config:
continue
sources = install_config.get('sources')
if not sources:
continue
install_path = install_config.get('install_path')
outputs = install_config.get('outputs')
symlinks = install_config.get('symlinks')
recursive = install_config.get('recursive')
options = install_config.get('options')
command_type = install_config.get('type')
config_key = (install_path, recursive, options, command_type)
config_group[config_key].append((sources, outputs, symlinks))
cmd_list = []
for install_config, install_args in config_group.items():
args = []
# Commands to install sources without explicit outputs nor symlinks can be
# merged into one. Concat all such sources.
sources = sum([sources for sources, outputs, symlinks in install_args
if not outputs and not symlinks], [])
if sources:
args.append((sources, None, None))
# Append all remaining sources/outputs/symlinks.
args += [(sources, outputs, symlinks) for
sources, outputs, symlinks in install_args
if outputs or symlinks]
# Generate the command line.
install_path, recursive, options, command_type = install_config
for sources, outputs, symlinks in args:
cmd_list += ebuild_function.generate(sources=sources,
install_path=install_path,
outputs=outputs,
symlinks=symlinks,
recursive=recursive,
options=options,
command_type=command_type)
return cmd_list
def install(self, _args):
"""Outputs the installation commands of ebuild as a standard output."""
install_cmd_list = self.configure_install()
for install_cmd in install_cmd_list:
# An error occurs at six.moves.shlex_quote when running pylint.
# https://github.com/PyCQA/pylint/issues/1965
# pylint: disable=too-many-function-args
print(' '.join(six.moves.shlex_quote(arg) for arg in install_cmd))
def unwrap_value(metadata, attr, default=None):
"""Gets a value like dict.get() with unwrapping it."""
data = metadata.get(attr)
if data is None:
return default
return data[0]
def GetParser():
"""Return a command line parser."""
actions = ['configure', 'compile', 'deviterate', 'test_all', 'install']
parser = commandline.ArgumentParser(description=__doc__)
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='split_extend', 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('--platform_subdir', required=True,
help='subdir in platform2 where the package is located')
parser.add_argument('args', nargs='*')
return parser
def main(argv):
parser = GetParser()
# Temporary measure. Moving verbose argument, but can't do it all in one
# sweep due to CROS_WORKON_BLACKLISTed packages. Use parse_known_args and
# manually handle verbose parsing to maintain compatibility.
options, unknown = parser.parse_known_args(argv)
if not hasattr(options, 'verbose'):
options.verbose = '--verbose' in unknown
if '--verbose' in unknown:
unknown.remove('--verbose')
if unknown:
parser.error('Unrecognized arguments: %s' % unknown)
if options.host and options.board:
raise AssertionError('You must provide only one of --board or --host')
if not options.verbose:
# 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,
platform_subdir=options.platform_subdir)
getattr(p2, options.action)(options.args)
if __name__ == '__main__':
commandline.ScriptWrapperMain(lambda _: main)