| # Copyright (c) 2018 Foundries.io |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import argparse |
| import os |
| import pathlib |
| import shlex |
| import sys |
| import yaml |
| |
| 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, load_domains, \ |
| FIND_BUILD_DIR_DESCRIPTION |
| |
| from zephyr_ext_common import Forceable |
| |
| _ARG_SEPARATOR = '--' |
| |
| SYSBUILD_PROJ_DIR = pathlib.Path(__file__).resolve().parent.parent.parent \ |
| / pathlib.Path('share/sysbuild') |
| |
| BUILD_USAGE = '''\ |
| west build [-h] [-b BOARD[@REV]]] [-d BUILD_DIR] |
| [-t TARGET] [-p {auto, always, never}] [-c] [--cmake-only] |
| [-n] [-o BUILD_OPT] [-f] |
| [--sysbuild | --no-sysbuild] [--domain DOMAIN] |
| [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 west-completion.bash if you add or remove |
| # flags |
| |
| parser.add_argument('-b', '--board', |
| help='board to build for with optional board revision') |
| # 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('--domain', action='append', |
| help='''execute build tool (make or ninja) only for |
| given domain''') |
| group.add_argument('-t', '--target', |
| help='''run build system target TARGET |
| (try "-t usage")''') |
| group.add_argument('-T', '--test-item', |
| help='''Build based on test data in testcase.yaml |
| or sample.yaml''') |
| 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.add_argument('-S', '--snippet', dest='snippets', |
| action='append', default=[], |
| help='''add the argument to SNIPPET; may be given |
| multiple times. Forces CMake to run again if given. |
| Do not use this option with manually specified |
| -DSNIPPET... cmake arguments: the results are |
| undefined''') |
| |
| group = parser.add_mutually_exclusive_group() |
| group.add_argument('--sysbuild', action='store_true', |
| help='''create multi domain build system''') |
| group.add_argument('--no-sysbuild', action='store_true', |
| help='''do not create multi domain build system |
| (default)''') |
| |
| 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) |
| # Parse testcase.yaml or sample.yaml files for additional options. |
| if self.args.test_item: |
| # we get path + testitem |
| item = os.path.basename(self.args.test_item) |
| test_path = os.path.dirname(self.args.test_item) |
| if test_path: |
| self.args.source_dir = test_path |
| if not self._parse_test_item(item): |
| log.die("No test metadata found") |
| 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', 'never') |
| 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 or self.args.snippets): |
| 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.domains = load_domains(self.build_dir) |
| |
| self._run_build(args.target, args.domain) |
| |
| def _find_board(self): |
| board, origin = None, None |
| if self.cmake_cache: |
| board, origin = (self.cmake_cache.get('CACHED_BOARD'), |
| 'CMakeCache.txt') |
| |
| # A malformed CMake cache may exist, but not have a board. |
| # This happens if there's a build error from a previous run. |
| if board is not None: |
| return (board, origin) |
| |
| if 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 _parse_test_item(self, test_item): |
| found_test_metadata = False |
| for yp in ['sample.yaml', 'testcase.yaml']: |
| yf = os.path.join(self.args.source_dir, yp) |
| if not os.path.exists(yf): |
| continue |
| found_test_metadata = True |
| with open(yf, 'r') as stream: |
| try: |
| y = yaml.safe_load(stream) |
| except yaml.YAMLError as exc: |
| log.die(exc) |
| tests = y.get('tests') |
| if not tests: |
| log.die(f"No tests found in {yf}") |
| item = tests.get(test_item) |
| if not item: |
| log.die(f"Test item {test_item} not found in {yf}") |
| |
| for data in ['extra_args', 'extra_configs']: |
| extra = item.get(data) |
| if not extra: |
| continue |
| if isinstance(extra, str): |
| arg_list = extra.split(" ") |
| else: |
| arg_list = extra |
| args = ["-D{}".format(arg.replace('"', '\"')) for arg in arg_list] |
| if self.args.cmake_opts: |
| self.args.cmake_opts.extend(args) |
| else: |
| self.args.cmake_opts = args |
| return found_test_metadata |
| |
| 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_proj = self.cmake_cache.get('APPLICATION_SOURCE_DIR') |
| cached_app = self.cmake_cache.get('APP_DIR') |
| # if APP_DIR is None but APPLICATION_SOURCE_DIR is set, that indicates |
| # an older build folder, this still requires pristine. |
| if cached_app is None and cached_proj: |
| cached_app = cached_proj |
| |
| log.dbg('APP_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.Path(source_abs).resolve() != pathlib.Path(cached_abs).resolve()) |
| |
| 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) |
| if self.args.snippets: |
| cmake_opts.append(f'-DSNIPPET={";".join(self.args.snippets)}') |
| |
| user_args = config_get('cmake-args', None) |
| if user_args: |
| cmake_opts.extend(shlex.split(user_args)) |
| |
| config_sysbuild = config_getboolean('sysbuild', False) |
| if self.args.sysbuild or (config_sysbuild and not self.args.no_sysbuild): |
| cmake_opts.extend(['-S{}'.format(SYSBUILD_PROJ_DIR), |
| '-DAPP_DIR:PATH={}'.format(self.source_dir)]) |
| else: |
| # self.args.no_sysbuild == True or config sysbuild False |
| cmake_opts.extend(['-S{}'.format(self.source_dir)]) |
| |
| # 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(pathlib.Path(sys.executable).as_posix()), |
| '-B{}'.format(self.build_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) |
| |
| app_src_dir = cache.get('APPLICATION_SOURCE_DIR') |
| app_bin_dir = cache.get('APPLICATION_BINARY_DIR') |
| |
| cmake_args = [f'-DBINARY_DIR={app_bin_dir}', |
| f'-DSOURCE_DIR={app_src_dir}', |
| '-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, domain): |
| 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)) |
| |
| domains = load_domains(self.build_dir) |
| build_dir_list = [] |
| |
| if domain is None: |
| # If no domain is specified, we just build top build dir as that |
| # will build all domains. |
| build_dir_list = [domains.get_top_build_dir()] |
| else: |
| _banner('building domain(s): {}'.format(' '.join(domain))) |
| domain_list = domains.get_domains(domain) |
| for d in domain_list: |
| build_dir_list.append(d.build_dir) |
| |
| for b in build_dir_list: |
| run_build(b, 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') |