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:
Marti Bolivar 2018-08-10 16:13:47 -05:00
parent 53d5bf0e37
commit ed9f4fe735
3 changed files with 308 additions and 2 deletions

42
src/west/build.py Normal file
View File

@ -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

263
src/west/cmd/build.py Normal file
View File

@ -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)

View File

@ -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):