Add build command.
This is an optional convenience wrapper around cmake + ninja (or any other generator supported by Zephyr). It will never be mandatory to use this wrapper. Raw CMake and Ninja/Make/etc. will always be supported. This command attempts to do what you mean when run from a Zephyr application source or a pre-existing build directory: - When "west build" is run from a Zephyr build directory, the source directory is obtained from the CMake cache, and that build directory is re-compiled. - Otherwise, the source directory defaults to the current working directory, so running "west build" from a Zephyr application's source directory compiles it. The source and build directories can be explicitly set with the --source-dir and --build-dir options. The build directory defaults to 'build' if it is not auto-detected. The build directory is always created if it does not exist. This command runs CMake to generate a build system if one is not present in the build directory, then builds the application. Subsequent builds try to avoid re-running CMake; you can force it to run by setting --cmake. To pass additional options to CMake, give them as extra arguments after a '--' For example, "west build -- -DOVERLAY_CONFIG=some.conf" sets an overlay config file. (Doing this forces a CMake run.) A separate helper library is placed in west.build to make adapting flash/debug/debugserver workflows play nicer with build in future patches. Signed-off-by: Marti Bolivar <marti@foundries.io>
This commit is contained in:
parent
53d5bf0e37
commit
ed9f4fe735
|
@ -0,0 +1,42 @@
|
|||
# Copyright 2018 (c) Foundries.io.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'''Common definitions for building Zephyr applications.
|
||||
|
||||
This provides some default settings and convenience wrappers for
|
||||
building Zephyr applications needed by multiple commands.
|
||||
|
||||
See west.cmd.build for the build command itself.
|
||||
'''
|
||||
|
||||
from . import cmake
|
||||
from . import log
|
||||
|
||||
DEFAULT_BUILD_DIR = 'build'
|
||||
'''Name of the default Zephyr build directory.'''
|
||||
|
||||
DEFAULT_CMAKE_GENERATOR = 'Ninja'
|
||||
'''Name of the default CMake generator.'''
|
||||
|
||||
|
||||
def is_zephyr_build(path):
|
||||
'''Return true if and only if `path` appears to be a valid Zephyr
|
||||
build directory.
|
||||
|
||||
"Valid" means the given path is a directory which contains a CMake
|
||||
cache with a 'ZEPHYR_TOOLCHAIN_VARIANT' key.
|
||||
'''
|
||||
try:
|
||||
cache = cmake.CMakeCache.from_build_dir(path)
|
||||
except FileNotFoundError:
|
||||
cache = {}
|
||||
|
||||
if 'ZEPHYR_TOOLCHAIN_VARIANT' in cache:
|
||||
log.dbg('{} is a zephyr build directory'.format(path),
|
||||
level=log.VERBOSE_EXTREME)
|
||||
return True
|
||||
else:
|
||||
log.dbg('{} is NOT a valid zephyr build directory'.format(path),
|
||||
level=log.VERBOSE_EXTREME)
|
||||
return False
|
|
@ -0,0 +1,263 @@
|
|||
# Copyright (c) 2018 Foundries.io
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from ..build import DEFAULT_BUILD_DIR, DEFAULT_CMAKE_GENERATOR, \
|
||||
is_zephyr_build
|
||||
from .. import log
|
||||
from .. import cmake
|
||||
from . import WestCommand
|
||||
|
||||
BUILD_HELP = '''\
|
||||
Convenience wrapper for building Zephyr applications.
|
||||
|
||||
This command attempts to do what you mean when run from a Zephyr
|
||||
application source or a pre-existing build directory:
|
||||
|
||||
- When "west build" is run from a Zephyr build directory, the source
|
||||
directory is obtained from the CMake cache, and that build directory
|
||||
is re-compiled.
|
||||
|
||||
- Otherwise, the source directory defaults to the current working
|
||||
directory, so running "west build" from a Zephyr application's
|
||||
source directory compiles it.
|
||||
|
||||
The source and build directories can be explicitly set with the
|
||||
--source-dir and --build-dir options. The build directory defaults to
|
||||
'build' if it is not auto-detected. The build directory is always
|
||||
created if it does not exist.
|
||||
|
||||
This command runs CMake to generate a build system if one is not
|
||||
present in the build directory, then builds the application.
|
||||
Subsequent builds try to avoid re-running CMake; you can force it
|
||||
to run by setting --cmake.
|
||||
|
||||
To pass additional options to CMake, give them as extra arguments
|
||||
after a '--' For example, "west build -- -DOVERLAY_CONFIG=some.conf" sets
|
||||
an overlay config file. (Doing this forces a CMake run.)'''
|
||||
|
||||
|
||||
class Build(WestCommand):
|
||||
|
||||
def __init__(self):
|
||||
super(Build, self).__init__(
|
||||
'build',
|
||||
BUILD_HELP,
|
||||
accepts_unknown_args=False)
|
||||
|
||||
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.force_cmake = False
|
||||
'''True if a CMake run was forced; 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,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description=self.description)
|
||||
|
||||
parser.add_argument('-b', '--board',
|
||||
help='''board to build for (must be given for the
|
||||
first build, can be omitted later)''')
|
||||
parser.add_argument('-s', '--source-dir',
|
||||
help='''explicitly sets the source directory;
|
||||
if not given, infer it from directory context''')
|
||||
parser.add_argument('-d', '--build-dir',
|
||||
help='''explicitly sets the build directory;
|
||||
if not given, infer it from directory context''')
|
||||
parser.add_argument('-t', '--target',
|
||||
help='''override the build system target (e.g.
|
||||
'clean', 'pristine', etc.)''')
|
||||
parser.add_argument('-c', '--cmake', action='store_true',
|
||||
help='force CMake to run')
|
||||
parser.add_argument('-f', '--force', action='store_true',
|
||||
help='ignore any errors and try to build anyway')
|
||||
parser.add_argument('cmake_opts', nargs='*', metavar='cmake_opt',
|
||||
help='extra option to pass to CMake; implies -c')
|
||||
|
||||
return parser
|
||||
|
||||
def do_run(self, args, ignored):
|
||||
self.args = args # Avoid having to pass them around
|
||||
log.dbg('args:', args, level=log.VERBOSE_EXTREME)
|
||||
self._sanity_precheck()
|
||||
self._setup_build_dir()
|
||||
if is_zephyr_build(self.build_dir):
|
||||
self._update_cache()
|
||||
self.force_cmake = self.args.cmake or self.args.cmake_opts
|
||||
self._setup_source_dir()
|
||||
self._sanity_check()
|
||||
|
||||
log.inf('source directory: {}'.format(self.source_dir))
|
||||
log.inf('build directory: {}{}'.
|
||||
format(self.build_dir,
|
||||
(' (created)' if self.created_build_dir
|
||||
else '')))
|
||||
if self.cmake_cache:
|
||||
board = self.cmake_cache.get('CACHED_BOARD')
|
||||
else:
|
||||
board = 'UNKNOWN' # shouldn't happen
|
||||
log.inf('BOARD:', board)
|
||||
|
||||
self._run_cmake(self.args.cmake_opts)
|
||||
self._sanity_check()
|
||||
self._update_cache()
|
||||
|
||||
extra_args = ['--target', args.target] if args.target else []
|
||||
cmake.run_build(self.build_dir, extra_args=extra_args)
|
||||
|
||||
def _sanity_precheck(self):
|
||||
app = self.args.source_dir
|
||||
if (app and (not os.path.isdir(app) or
|
||||
'CMakeLists.txt' not in os.listdir(app))):
|
||||
self._check_force('{app} is not a directory with CMakeLists.txt; '
|
||||
'did you mean --build-dir {app}?'.
|
||||
format(app=app))
|
||||
|
||||
def _update_cache(self):
|
||||
try:
|
||||
self.cmake_cache = cmake.CMakeCache.from_build_dir(self.build_dir)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _setup_build_dir(self):
|
||||
# Initialize build_dir and created_build_dir attributes.
|
||||
log.dbg('setting up build directory', level=log.VERBOSE_EXTREME)
|
||||
if self.args.build_dir:
|
||||
build_dir = self.args.build_dir
|
||||
else:
|
||||
cwd = os.getcwd()
|
||||
if is_zephyr_build(cwd):
|
||||
build_dir = cwd
|
||||
else:
|
||||
build_dir = DEFAULT_BUILD_DIR
|
||||
build_dir = os.path.abspath(build_dir)
|
||||
|
||||
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.force_cmake = True
|
||||
|
||||
self.build_dir = build_dir
|
||||
|
||||
def _setup_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('APPLICATION_SOURCE_DIR')
|
||||
if not source_dir:
|
||||
# Maybe Zephyr changed the key? Give the user a way
|
||||
# to retry, at least.
|
||||
log.die("can't determine application from build directory "
|
||||
"{}, please specify an application to build".
|
||||
format(self.build_dir))
|
||||
else:
|
||||
source_dir = os.getcwd()
|
||||
self.source_dir = os.path.abspath(source_dir)
|
||||
|
||||
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)
|
||||
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))
|
||||
|
||||
if is_zephyr_build(self.source_dir):
|
||||
self._check_force('it looks like {srcrel} is a build directory: '
|
||||
'did you mean -build-dir {srcrel} instead?'.
|
||||
format(srcrel=os.path.relpath(self.source_dir)))
|
||||
|
||||
if not is_zephyr_build(self.build_dir) and not self.args.board:
|
||||
self._check_force('this looks like a new or clean build, '
|
||||
'please provide --board')
|
||||
|
||||
if not self.cmake_cache:
|
||||
return # That's all we can check without a cache.
|
||||
|
||||
cached_app = self.cmake_cache.get('APPLICATION_SOURCE_DIR')
|
||||
log.dbg('APPLICATION_SOURCE_DIR:', cached_app,
|
||||
level=log.VERBOSE_EXTREME)
|
||||
if self.args.source_dir:
|
||||
source_abs = os.path.abspath(self.args.source_dir)
|
||||
else:
|
||||
source_abs = None
|
||||
if cached_app and source_abs and source_abs != cached_app:
|
||||
self._check_force('build directory "{}" is for application "{}", '
|
||||
'but source directory "{}" was specified; '
|
||||
'please clean it or use --build-dir to set '
|
||||
'another build directory'.
|
||||
format(os.path.relpath(self.build_dir),
|
||||
cached_app,
|
||||
os.path.relpath(self.args.source_dir)))
|
||||
self.force_cmake = True # If they insist, we need to re-run cmake.
|
||||
|
||||
cached_board = self.cmake_cache.get('CACHED_BOARD')
|
||||
log.dbg('CACHED_BOARD:', cached_board, level=log.VERBOSE_EXTREME)
|
||||
if not cached_board and not self.args.board:
|
||||
if self.created_build_dir:
|
||||
self._check_force(
|
||||
'Building for the first time: you must provide --board')
|
||||
else:
|
||||
self._check_force(
|
||||
'Board is missing or unknown, please provide --board')
|
||||
if self.args.board and cached_board and \
|
||||
self.args.board != cached_board:
|
||||
self._check_force('Build directory targets board {}, '
|
||||
'but board {} was specified'.
|
||||
format(cached_board, self.args.board))
|
||||
|
||||
def _check_force(self, msg):
|
||||
if not self.args.force:
|
||||
log.err(msg)
|
||||
log.die('refusing to proceed without --force due to above error')
|
||||
|
||||
def _run_cmake(self, cmake_opts):
|
||||
if not self.force_cmake:
|
||||
log.dbg('not running cmake; build system is present')
|
||||
return
|
||||
|
||||
# It's unfortunate to have to use the undocumented -B and -H
|
||||
# options to set the source and binary directories.
|
||||
#
|
||||
# However, it's the only known way to set that directory and
|
||||
# run CMake from the current working directory. This is
|
||||
# important because users expect invocations like this to Just
|
||||
# Work:
|
||||
#
|
||||
# west build -- -DOVERLAY_CONFIG=relative-path.conf
|
||||
final_cmake_args = ['-B{}'.format(self.build_dir),
|
||||
'-H{}'.format(self.source_dir),
|
||||
'-G{}'.format(DEFAULT_CMAKE_GENERATOR)]
|
||||
if self.args.board:
|
||||
final_cmake_args.append('-DBOARD={}'.format(self.args.board))
|
||||
if cmake_opts:
|
||||
final_cmake_args.extend(cmake_opts)
|
||||
cmake.run_cmake(final_cmake_args)
|
|
@ -14,13 +14,14 @@ from subprocess import CalledProcessError
|
|||
|
||||
from . import log
|
||||
from .cmd import CommandContextError
|
||||
from .cmd.build import Build
|
||||
from .cmd.flash import Flash
|
||||
from .cmd.debug import Debug, DebugServer
|
||||
from .util import quote_sh_list
|
||||
|
||||
|
||||
COMMANDS = (Flash(), Debug(), DebugServer())
|
||||
'''Supported top-level commands.'''
|
||||
COMMANDS = (Build(), Flash(), Debug(), DebugServer())
|
||||
'''Built-in West commands.'''
|
||||
|
||||
|
||||
class InvalidWestContext(RuntimeError):
|
||||
|
|
Loading…
Reference in New Issue