461 lines
19 KiB
Python
461 lines
19 KiB
Python
# Copyright (c) 2018 Foundries.io
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import argparse
|
|
import os
|
|
import pathlib
|
|
import shlex
|
|
import sys
|
|
|
|
from west import log
|
|
from west.configuration import config
|
|
from zcmake import DEFAULT_CMAKE_GENERATOR, run_cmake, run_build, CMakeCache
|
|
from build_helpers import is_zephyr_build, find_build_dir, \
|
|
FIND_BUILD_DIR_DESCRIPTION
|
|
|
|
from zephyr_ext_common import Forceable
|
|
|
|
_ARG_SEPARATOR = '--'
|
|
|
|
BUILD_USAGE = '''\
|
|
west build [-h] [-b BOARD] [-d BUILD_DIR]
|
|
[-t TARGET] [-p {auto, always, never}] [-c] [--cmake-only]
|
|
[-n] [-o BUILD_OPT] [-f]
|
|
[source_dir] -- [cmake_opt [cmake_opt ...]]
|
|
'''
|
|
|
|
BUILD_DESCRIPTION = f'''\
|
|
Convenience wrapper for building Zephyr applications.
|
|
|
|
{FIND_BUILD_DIR_DESCRIPTION}
|
|
|
|
positional arguments:
|
|
source_dir application source directory
|
|
cmake_opt extra options to pass to cmake; implies -c
|
|
(these must come after "--" as shown above)
|
|
'''
|
|
|
|
PRISTINE_DESCRIPTION = """\
|
|
A "pristine" build directory is empty. The -p option controls
|
|
whether the build directory is made pristine before the build
|
|
is done. A bare '--pristine' with no value is the same as
|
|
--pristine=always. Setting --pristine=auto uses heuristics to
|
|
guess if a pristine build may be necessary."""
|
|
|
|
def _banner(msg):
|
|
log.inf('-- west build: ' + msg, colorize=True)
|
|
|
|
def config_get(option, fallback):
|
|
return config.get('build', option, fallback=fallback)
|
|
|
|
def config_getboolean(option, fallback):
|
|
return config.getboolean('build', option, fallback=fallback)
|
|
|
|
class AlwaysIfMissing(argparse.Action):
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
setattr(namespace, self.dest, values or 'always')
|
|
|
|
class Build(Forceable):
|
|
|
|
def __init__(self):
|
|
super(Build, self).__init__(
|
|
'build',
|
|
# Keep this in sync with the string in west-commands.yml.
|
|
'compile a Zephyr application',
|
|
BUILD_DESCRIPTION,
|
|
accepts_unknown_args=True)
|
|
|
|
self.source_dir = None
|
|
'''Source directory for the build, or None on error.'''
|
|
|
|
self.build_dir = None
|
|
'''Final build directory used to run the build, or None on error.'''
|
|
|
|
self.created_build_dir = False
|
|
'''True if the build directory was created; False otherwise.'''
|
|
|
|
self.run_cmake = False
|
|
'''True if CMake was run; False otherwise.
|
|
|
|
Note: this only describes CMake runs done by this command. The
|
|
build system generated by CMake may also update itself due to
|
|
internal logic.'''
|
|
|
|
self.cmake_cache = None
|
|
'''Final parsed CMake cache for the build, or None on error.'''
|
|
|
|
def do_add_parser(self, parser_adder):
|
|
parser = parser_adder.add_parser(
|
|
self.name,
|
|
help=self.help,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description=self.description,
|
|
usage=BUILD_USAGE)
|
|
|
|
# Remember to update scripts/west-completion.bash if you add or remove
|
|
# flags
|
|
|
|
parser.add_argument('-b', '--board', help='board to build for')
|
|
# Hidden option for backwards compatibility
|
|
parser.add_argument('-s', '--source-dir', help=argparse.SUPPRESS)
|
|
parser.add_argument('-d', '--build-dir',
|
|
help='build directory to create or use')
|
|
self.add_force_arg(parser)
|
|
|
|
group = parser.add_argument_group('cmake and build tool')
|
|
group.add_argument('-c', '--cmake', action='store_true',
|
|
help='force a cmake run')
|
|
group.add_argument('--cmake-only', action='store_true',
|
|
help="just run cmake; don't build (implies -c)")
|
|
group.add_argument('-t', '--target',
|
|
help='''run this build system target (try "-t usage"
|
|
or "-t help")''')
|
|
group.add_argument('-o', '--build-opt', default=[], action='append',
|
|
help='''options to pass to the build tool
|
|
(make or ninja); may be given more than once''')
|
|
group.add_argument('-n', '--just-print', '--dry-run', '--recon',
|
|
dest='dry_run', action='store_true',
|
|
help="just print build commands; don't run them")
|
|
|
|
group = parser.add_argument_group('pristine builds',
|
|
PRISTINE_DESCRIPTION)
|
|
group.add_argument('-p', '--pristine', choices=['auto', 'always',
|
|
'never'], action=AlwaysIfMissing, nargs='?',
|
|
help='pristine build folder setting')
|
|
|
|
return parser
|
|
|
|
def do_run(self, args, remainder):
|
|
self.args = args # Avoid having to pass them around
|
|
self.config_board = config_get('board', None)
|
|
log.dbg('args: {} remainder: {}'.format(args, remainder),
|
|
level=log.VERBOSE_EXTREME)
|
|
# Store legacy -s option locally
|
|
source_dir = self.args.source_dir
|
|
self._parse_remainder(remainder)
|
|
if source_dir:
|
|
if self.args.source_dir:
|
|
log.die("source directory specified twice:({} and {})".format(
|
|
source_dir, self.args.source_dir))
|
|
self.args.source_dir = source_dir
|
|
log.dbg('source_dir: {} cmake_opts: {}'.format(self.args.source_dir,
|
|
self.args.cmake_opts),
|
|
level=log.VERBOSE_EXTREME)
|
|
self._sanity_precheck()
|
|
self._setup_build_dir()
|
|
|
|
if args.pristine is not None:
|
|
pristine = args.pristine
|
|
else:
|
|
# Load the pristine={auto, always, never} configuration value
|
|
pristine = config_get('pristine', 'auto')
|
|
if pristine not in ['auto', 'always', 'never']:
|
|
log.wrn(
|
|
'treating unknown build.pristine value "{}" as "never"'.
|
|
format(pristine))
|
|
pristine = 'never'
|
|
self.auto_pristine = (pristine == 'auto')
|
|
|
|
log.dbg('pristine: {} auto_pristine: {}'.format(pristine,
|
|
self.auto_pristine),
|
|
level=log.VERBOSE_VERY)
|
|
if is_zephyr_build(self.build_dir):
|
|
if pristine == 'always':
|
|
self._run_pristine()
|
|
self.run_cmake = True
|
|
else:
|
|
self._update_cache()
|
|
if (self.args.cmake or self.args.cmake_opts or
|
|
self.args.cmake_only):
|
|
self.run_cmake = True
|
|
else:
|
|
self.run_cmake = True
|
|
self.source_dir = self._find_source_dir()
|
|
self._sanity_check()
|
|
|
|
board, origin = self._find_board()
|
|
self._run_cmake(board, origin, self.args.cmake_opts)
|
|
if args.cmake_only:
|
|
return
|
|
|
|
self._sanity_check()
|
|
self._update_cache()
|
|
|
|
self._run_build(args.target)
|
|
|
|
def _find_board(self):
|
|
board, origin = None, None
|
|
if self.cmake_cache:
|
|
board, origin = (self.cmake_cache.get('CACHED_BOARD'),
|
|
'CMakeCache.txt')
|
|
elif self.args.board:
|
|
board, origin = self.args.board, 'command line'
|
|
elif 'BOARD' in os.environ:
|
|
board, origin = os.environ['BOARD'], 'env'
|
|
elif self.config_board is not None:
|
|
board, origin = self.config_board, 'configfile'
|
|
return board, origin
|
|
|
|
def _parse_remainder(self, remainder):
|
|
self.args.source_dir = None
|
|
self.args.cmake_opts = None
|
|
try:
|
|
# Only one source_dir is allowed, as the first positional arg
|
|
if remainder[0] != _ARG_SEPARATOR:
|
|
self.args.source_dir = remainder[0]
|
|
remainder = remainder[1:]
|
|
# Only the first argument separator is consumed, the rest are
|
|
# passed on to CMake
|
|
if remainder[0] == _ARG_SEPARATOR:
|
|
remainder = remainder[1:]
|
|
if remainder:
|
|
self.args.cmake_opts = remainder
|
|
except IndexError:
|
|
return
|
|
|
|
def _sanity_precheck(self):
|
|
app = self.args.source_dir
|
|
if app:
|
|
self.check_force(
|
|
os.path.isdir(app),
|
|
'source directory {} does not exist'.format(app))
|
|
self.check_force(
|
|
'CMakeLists.txt' in os.listdir(app),
|
|
"{} doesn't contain a CMakeLists.txt".format(app))
|
|
|
|
def _update_cache(self):
|
|
try:
|
|
self.cmake_cache = CMakeCache.from_build_dir(self.build_dir)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
def _setup_build_dir(self):
|
|
# Initialize build_dir and created_build_dir attributes.
|
|
# If we created the build directory, we must run CMake.
|
|
log.dbg('setting up build directory', level=log.VERBOSE_EXTREME)
|
|
# The CMake Cache has not been loaded yet, so this is safe
|
|
board, _ = self._find_board()
|
|
source_dir = self._find_source_dir()
|
|
app = os.path.split(source_dir)[1]
|
|
build_dir = find_build_dir(self.args.build_dir, board=board,
|
|
source_dir=source_dir, app=app)
|
|
if not build_dir:
|
|
log.die('Unable to determine a default build folder. Check '
|
|
'your build.dir-fmt configuration option')
|
|
|
|
if os.path.exists(build_dir):
|
|
if not os.path.isdir(build_dir):
|
|
log.die('build directory {} exists and is not a directory'.
|
|
format(build_dir))
|
|
else:
|
|
os.makedirs(build_dir, exist_ok=False)
|
|
self.created_build_dir = True
|
|
self.run_cmake = True
|
|
|
|
self.build_dir = build_dir
|
|
|
|
def _find_source_dir(self):
|
|
# Initialize source_dir attribute, either from command line argument,
|
|
# implicitly from the build directory's CMake cache, or using the
|
|
# default (current working directory).
|
|
log.dbg('setting up source directory', level=log.VERBOSE_EXTREME)
|
|
if self.args.source_dir:
|
|
source_dir = self.args.source_dir
|
|
elif self.cmake_cache:
|
|
source_dir = self.cmake_cache.get('CMAKE_HOME_DIRECTORY')
|
|
if not source_dir:
|
|
# This really ought to be there. The build directory
|
|
# must be corrupted somehow. Let's see what we can do.
|
|
log.die('build directory', self.build_dir,
|
|
'CMake cache has no CMAKE_HOME_DIRECTORY;',
|
|
'please give a source_dir')
|
|
else:
|
|
source_dir = os.getcwd()
|
|
return os.path.abspath(source_dir)
|
|
|
|
def _sanity_check_source_dir(self):
|
|
if self.source_dir == self.build_dir:
|
|
# There's no forcing this.
|
|
log.die('source and build directory {} cannot be the same; '
|
|
'use --build-dir {} to specify a build directory'.
|
|
format(self.source_dir, self.build_dir))
|
|
|
|
srcrel = os.path.relpath(self.source_dir)
|
|
self.check_force(
|
|
not is_zephyr_build(self.source_dir),
|
|
'it looks like {srcrel} is a build directory: '
|
|
'did you mean --build-dir {srcrel} instead?'.
|
|
format(srcrel=srcrel))
|
|
self.check_force(
|
|
'CMakeLists.txt' in os.listdir(self.source_dir),
|
|
'source directory "{srcrel}" does not contain '
|
|
'a CMakeLists.txt; is this really what you '
|
|
'want to build? (Use -s SOURCE_DIR to specify '
|
|
'the application source directory)'.
|
|
format(srcrel=srcrel))
|
|
|
|
def _sanity_check(self):
|
|
# Sanity check the build configuration.
|
|
# Side effect: may update cmake_cache attribute.
|
|
log.dbg('sanity checking the build', level=log.VERBOSE_EXTREME)
|
|
self._sanity_check_source_dir()
|
|
|
|
if not self.cmake_cache:
|
|
return # That's all we can check without a cache.
|
|
|
|
if "CMAKE_PROJECT_NAME" not in self.cmake_cache:
|
|
# This happens sometimes when a build system is not
|
|
# completely generated due to an error during the
|
|
# CMake configuration phase.
|
|
self.run_cmake = True
|
|
|
|
cached_app = self.cmake_cache.get('APPLICATION_SOURCE_DIR')
|
|
log.dbg('APPLICATION_SOURCE_DIR:', cached_app,
|
|
level=log.VERBOSE_EXTREME)
|
|
source_abs = (os.path.abspath(self.args.source_dir)
|
|
if self.args.source_dir else None)
|
|
cached_abs = os.path.abspath(cached_app) if cached_app else None
|
|
|
|
log.dbg('pristine:', self.auto_pristine, level=log.VERBOSE_EXTREME)
|
|
|
|
# If the build directory specifies a source app, make sure it's
|
|
# consistent with --source-dir.
|
|
apps_mismatched = (source_abs and cached_abs and
|
|
pathlib.PurePath(source_abs) != pathlib.PurePath(cached_abs))
|
|
|
|
self.check_force(
|
|
not apps_mismatched or self.auto_pristine,
|
|
'Build directory "{}" is for application "{}", but source '
|
|
'directory "{}" was specified; please clean it, use --pristine, '
|
|
'or use --build-dir to set another build directory'.
|
|
format(self.build_dir, cached_abs, source_abs))
|
|
|
|
if apps_mismatched:
|
|
self.run_cmake = True # If they insist, we need to re-run cmake.
|
|
|
|
# If CACHED_BOARD is not defined, we need some other way to
|
|
# find the board.
|
|
cached_board = self.cmake_cache.get('CACHED_BOARD')
|
|
log.dbg('CACHED_BOARD:', cached_board, level=log.VERBOSE_EXTREME)
|
|
# If apps_mismatched and self.auto_pristine are true, we will
|
|
# run pristine on the build, invalidating the cached
|
|
# board. In that case, we need some way of getting the board.
|
|
self.check_force((cached_board and
|
|
not (apps_mismatched and self.auto_pristine))
|
|
or self.args.board or self.config_board or
|
|
os.environ.get('BOARD'),
|
|
'Cached board not defined, please provide it '
|
|
'(provide --board, set default with '
|
|
'"west config build.board <BOARD>", or set '
|
|
'BOARD in the environment)')
|
|
|
|
# Check consistency between cached board and --board.
|
|
boards_mismatched = (self.args.board and cached_board and
|
|
self.args.board != cached_board)
|
|
self.check_force(
|
|
not boards_mismatched or self.auto_pristine,
|
|
'Build directory {} targets board {}, but board {} was specified. '
|
|
'(Clean the directory, use --pristine, or use --build-dir to '
|
|
'specify a different one.)'.
|
|
format(self.build_dir, cached_board, self.args.board))
|
|
|
|
if self.auto_pristine and (apps_mismatched or boards_mismatched):
|
|
self._run_pristine()
|
|
self.cmake_cache = None
|
|
log.dbg('run_cmake:', True, level=log.VERBOSE_EXTREME)
|
|
self.run_cmake = True
|
|
|
|
# Tricky corner-case: The user has not specified a build folder but
|
|
# there was one in the CMake cache. Since this is going to be
|
|
# invalidated, reset to CWD and re-run the basic tests.
|
|
if ((boards_mismatched and not apps_mismatched) and
|
|
(not source_abs and cached_abs)):
|
|
self.source_dir = self._find_source_dir()
|
|
self._sanity_check_source_dir()
|
|
|
|
def _run_cmake(self, board, origin, cmake_opts):
|
|
if board is None and config_getboolean('board_warn', True):
|
|
log.wrn('This looks like a fresh build and BOARD is unknown;',
|
|
"so it probably won't work. To fix, use",
|
|
'--board=<your-board>.')
|
|
log.inf('Note: to silence the above message, run',
|
|
"'west config build.board_warn false'")
|
|
|
|
if not self.run_cmake:
|
|
return
|
|
|
|
_banner('generating a build system')
|
|
|
|
if board is not None and origin != 'CMakeCache.txt':
|
|
cmake_opts = ['-DBOARD={}'.format(board)]
|
|
else:
|
|
cmake_opts = []
|
|
if self.args.cmake_opts:
|
|
cmake_opts.extend(self.args.cmake_opts)
|
|
|
|
user_args = config_get('cmake-args', None)
|
|
if user_args:
|
|
cmake_opts.extend(shlex.split(user_args))
|
|
|
|
# Invoke CMake from the current working directory using the
|
|
# -S and -B options (officially introduced in CMake 3.13.0).
|
|
# This is important because users expect invocations like this
|
|
# to Just Work:
|
|
#
|
|
# west build -- -DOVERLAY_CONFIG=relative-path.conf
|
|
final_cmake_args = ['-DWEST_PYTHON={}'.format(sys.executable),
|
|
'-B{}'.format(self.build_dir),
|
|
'-S{}'.format(self.source_dir),
|
|
'-G{}'.format(config_get('generator',
|
|
DEFAULT_CMAKE_GENERATOR))]
|
|
if cmake_opts:
|
|
final_cmake_args.extend(cmake_opts)
|
|
run_cmake(final_cmake_args, dry_run=self.args.dry_run)
|
|
|
|
def _run_pristine(self):
|
|
_banner('making build dir {} pristine'.format(self.build_dir))
|
|
if not is_zephyr_build(self.build_dir):
|
|
log.die('Refusing to run pristine on a folder that is not a '
|
|
'Zephyr build system')
|
|
|
|
cache = CMakeCache.from_build_dir(self.build_dir)
|
|
cmake_args = ['-P', cache['ZEPHYR_BASE'] + '/cmake/pristine.cmake']
|
|
run_cmake(cmake_args, cwd=self.build_dir, dry_run=self.args.dry_run)
|
|
|
|
def _run_build(self, target):
|
|
if target:
|
|
_banner('running target {}'.format(target))
|
|
elif self.run_cmake:
|
|
_banner('building application')
|
|
extra_args = ['--target', target] if target else []
|
|
if self.args.build_opt:
|
|
extra_args.append('--')
|
|
extra_args.extend(self.args.build_opt)
|
|
if self.args.verbose:
|
|
self._append_verbose_args(extra_args,
|
|
not bool(self.args.build_opt))
|
|
run_build(self.build_dir, extra_args=extra_args,
|
|
dry_run=self.args.dry_run)
|
|
|
|
def _append_verbose_args(self, extra_args, add_dashes):
|
|
# These hacks are only needed for CMake versions earlier than
|
|
# 3.14. When Zephyr's minimum version is at least that, we can
|
|
# drop this nonsense and just run "cmake --build BUILD -v".
|
|
self._update_cache()
|
|
if not self.cmake_cache:
|
|
return
|
|
generator = self.cmake_cache.get('CMAKE_GENERATOR')
|
|
if not generator:
|
|
return
|
|
# Substring matching is for things like "Eclipse CDT4 - Ninja".
|
|
if 'Ninja' in generator:
|
|
if add_dashes:
|
|
extra_args.append('--')
|
|
extra_args.append('-v')
|
|
elif generator == 'Unix Makefiles':
|
|
if add_dashes:
|
|
extra_args.append('--')
|
|
extra_args.append('VERBOSE=1')
|