|  | # Copyright (c) 2018 Foundries.io | 
|  | # | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  |  | 
|  | import argparse | 
|  | import contextlib | 
|  | import os | 
|  | import pathlib | 
|  | import shlex | 
|  | import sys | 
|  |  | 
|  | import yaml | 
|  | from build_helpers import FIND_BUILD_DIR_DESCRIPTION, find_build_dir, is_zephyr_build, load_domains | 
|  | from west.commands import Verbosity | 
|  | from west.configuration import config | 
|  | from west.util import west_topdir | 
|  | from west.version import __version__ | 
|  | from zcmake import DEFAULT_CMAKE_GENERATOR, CMakeCache, run_build, run_cmake | 
|  | from zephyr_ext_common import Forceable | 
|  |  | 
|  | _ARG_SEPARATOR = '--' | 
|  |  | 
|  | SYSBUILD_PROJ_DIR = pathlib.Path(__file__).resolve().parent.parent.parent \ | 
|  | / pathlib.Path('share/sysbuild') | 
|  |  | 
|  | BUILD_INFO_LOG = 'build_info.yml' | 
|  |  | 
|  | BUILD_USAGE = '''\ | 
|  | west build [-h] [-b BOARD[@REV]]] [-d BUILD_DIR] | 
|  | [-S SNIPPET] [--shield SHIELD] | 
|  | [-t TARGET] [-p {auto, always, never}] [-c] [--cmake-only] | 
|  | [-n] [-o BUILD_OPT] [-f] | 
|  | [--sysbuild | --no-sysbuild] [--domain DOMAIN] | 
|  | [--extra-conf FILE.conf] | 
|  | [--extra-dtc-overlay FILE.overlay] | 
|  | [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 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().__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 _banner(self, msg): | 
|  | self.inf('-- west build: ' + msg, colorize=True) | 
|  |  | 
|  | 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. If source directory is not used | 
|  | an argument has to be defined as | 
|  | SOURCE_PATH/TEST_NAME. | 
|  | E.g. samples/hello_world/sample.basic.helloworld. | 
|  | If source directory is passed | 
|  | then "TEST_NAME" is enough.''') | 
|  | 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', metavar='SNIPPET', | 
|  | 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.add_argument('--shield', dest='shields', metavar='SHIELD', | 
|  | action='append', default=[], | 
|  | help='''add the argument to SHIELD; may be given | 
|  | multiple times. Forces CMake to run again if given. | 
|  | Do not use this option with manually specified | 
|  | -DSHIELD... cmake arguments: the results are | 
|  | undefined''') | 
|  | group.add_argument('--extra-conf', dest='extra_conf_files', metavar='EXTRA_CONF_FILE', | 
|  | action='append', default=[], | 
|  | help='''add the argument to EXTRA_CONF_FILE; may be given | 
|  | multiple times. Forces CMake to run again if given. | 
|  | Do not use this option with manually specified | 
|  | -DEXTRA_CONF_FILE... cmake arguments: the results are | 
|  | undefined''') | 
|  | group.add_argument('--extra-dtc-overlay', dest='extra_dtc_overlay_files', | 
|  | metavar='EXTRA_DTC_OVERLAY_FILE', action='append', default=[], | 
|  | help='''add the argument to EXTRA_DTC_OVERLAY_FILE; may be given | 
|  | multiple times. Forces CMake to run again if given. | 
|  | Do not use this option with manually specified | 
|  | -DEXTRA_DTC_OVERLAY_FILE... 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) | 
|  | self.dbg(f'args: {args} remainder: {remainder}', | 
|  | level=Verbosity.DBG_EXTREME) | 
|  | # Store legacy -s option locally | 
|  | source_dir = self.args.source_dir | 
|  | self._parse_remainder(remainder) | 
|  |  | 
|  | if source_dir: | 
|  | if self.args.source_dir: | 
|  | self.die( | 
|  | f"source directory specified twice:({source_dir} and {self.args.source_dir})") | 
|  | self.args.source_dir = source_dir | 
|  | self.dbg(f'source_dir: {self.args.source_dir} cmake_opts: {self.args.cmake_opts}', | 
|  | level=Verbosity.DBG_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']: | 
|  | self.wrn( | 
|  | f'treating unknown build.pristine value "{pristine}" as "never"') | 
|  | pristine = 'never' | 
|  | self.auto_pristine = pristine == 'auto' | 
|  |  | 
|  | self.dbg(f'pristine: {pristine} auto_pristine: {self.auto_pristine}', | 
|  | level=Verbosity.DBG_MORE) | 
|  | 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 or | 
|  | self.args.shields or self.args.extra_conf_files or | 
|  | self.args.extra_dtc_overlay_files): | 
|  | self.run_cmake = True | 
|  | else: | 
|  | self.run_cmake = True | 
|  |  | 
|  | self.source_dir = self._find_source_dir() | 
|  | self._sanity_check() | 
|  |  | 
|  | build_info_path = self.build_dir | 
|  | build_info_file = os.path.join(build_info_path, BUILD_INFO_LOG) | 
|  | west_workspace = west_topdir(self.source_dir) | 
|  | if not os.path.exists(build_info_path): | 
|  | os.makedirs(build_info_path) | 
|  | if not os.path.exists(build_info_file): | 
|  | build_command = {'west': {'command': ' '.join(sys.argv[:]), | 
|  | 'topdir': str(west_workspace), | 
|  | 'version': str(__version__)}} | 
|  | try: | 
|  | with open(build_info_file, "w") as f: | 
|  | yaml.dump(build_command, f, default_flow_style=False) | 
|  | except Exception as e: | 
|  | self.wrn(f'Failed to create info file: {build_info_file},', e) | 
|  |  | 
|  | board, origin = self._find_board() | 
|  |  | 
|  | # 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) | 
|  | if self.args.source_dir: | 
|  | test_path = self.args.source_dir | 
|  | else: | 
|  | test_path = os.path.dirname(self.args.test_item) | 
|  | if test_path and os.path.exists(test_path): | 
|  | self.args.source_dir = test_path | 
|  | if not self._parse_test_item(item, board): | 
|  | self.die("No test metadata found") | 
|  | else: | 
|  | self.die("test item path does not exist") | 
|  |  | 
|  | 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: | 
|  | pass | 
|  |  | 
|  | def _parse_extra_entry(self, arg, board): | 
|  | equals = arg.find('=') | 
|  | colon = arg.rfind(':', 0, equals) | 
|  | if colon != -1: | 
|  | elem = arg.split(':') | 
|  | if elem[0] != 'platform': | 
|  | # conditional configs other than platform | 
|  | # (xxx:yyy:CONFIG_FOO=bar) are not supported by 'west build' | 
|  | self.wrn(f'"west build" does not support conditional config "{arg}". Add ' | 
|  | f'"-D{arg[colon+1:]}" to the supplied CMake arguments if desired.') | 
|  | return None | 
|  | elif elem[1] not in board: | 
|  | return None | 
|  | return elem[2] | 
|  | else: | 
|  | return arg | 
|  |  | 
|  | def _parse_test_item(self, test_item, board): | 
|  | 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) as stream: | 
|  | try: | 
|  | y = yaml.safe_load(stream) | 
|  | except yaml.YAMLError as exc: | 
|  | self.die(exc) | 
|  | common = y.get('common') | 
|  | tests = y.get('tests') | 
|  | if not tests: | 
|  | self.die(f"No tests found in {yf}") | 
|  | if test_item not in tests: | 
|  | self.die(f"Test item {test_item} not found in {yf}") | 
|  | item = tests.get(test_item) | 
|  |  | 
|  | sysbuild = False | 
|  | extra_dtc_overlay_files = [] | 
|  | extra_overlay_confs = [] | 
|  | extra_conf_files = [] | 
|  | required_snippets = [] | 
|  | for section in [common, item]: | 
|  | if not section: | 
|  | continue | 
|  | sysbuild = section.get('sysbuild', sysbuild) | 
|  | for data in [ | 
|  | 'extra_args', | 
|  | 'extra_configs', | 
|  | 'extra_conf_files', | 
|  | 'extra_overlay_confs', | 
|  | 'extra_dtc_overlay_files', | 
|  | 'required_snippets' | 
|  | ]: | 
|  | extra = section.get(data) | 
|  | if not extra: | 
|  | continue | 
|  | if isinstance(extra, str): | 
|  | arg_list = extra.split(" ") | 
|  | else: | 
|  | arg_list = extra | 
|  |  | 
|  | if data == 'extra_configs': | 
|  | args = [] | 
|  | for arg in arg_list: | 
|  | entry = self._parse_extra_entry(arg, board) | 
|  | if entry is not None: | 
|  | args.append("-D{}".format(entry.replace('"', '\"'))) | 
|  | elif data == 'extra_args': | 
|  | # Retain quotes around config options | 
|  | config_options = [arg for arg in arg_list if arg.startswith("CONFIG_")] | 
|  | non_config_option_candidates = [ | 
|  | arg for arg in arg_list if not arg.startswith("CONFIG_") | 
|  | ] | 
|  | args = ["-D{}".format(a.replace('"', '\"')) for a in config_options] | 
|  |  | 
|  | non_config_options = [] | 
|  | for arg in non_config_option_candidates: | 
|  | entry = self._parse_extra_entry(arg, board) | 
|  | if entry is not None: | 
|  | non_config_options.append(entry) | 
|  |  | 
|  | args.extend([ | 
|  | "-D{}".format(arg.replace('"', '')) for arg in non_config_options | 
|  | ]) | 
|  | elif data == 'extra_conf_files': | 
|  | extra_conf_files.extend(arg_list) | 
|  | continue | 
|  | elif data == 'extra_overlay_confs': | 
|  | extra_overlay_confs.extend(arg_list) | 
|  | continue | 
|  | elif data == 'extra_dtc_overlay_files': | 
|  | extra_dtc_overlay_files.extend(arg_list) | 
|  | continue | 
|  | elif data == 'required_snippets': | 
|  | required_snippets.extend(arg_list) | 
|  | continue | 
|  |  | 
|  | if self.args.cmake_opts: | 
|  | self.args.cmake_opts.extend(args) | 
|  | else: | 
|  | self.args.cmake_opts = args | 
|  |  | 
|  | self.args.sysbuild = sysbuild | 
|  |  | 
|  | if found_test_metadata: | 
|  | args = [] | 
|  | if extra_conf_files: | 
|  | args.append(f"CONF_FILE=\"{';'.join(extra_conf_files)}\"") | 
|  |  | 
|  | if extra_dtc_overlay_files: | 
|  | args.append(f"DTC_OVERLAY_FILE=\"{';'.join(extra_dtc_overlay_files)}\"") | 
|  |  | 
|  | if extra_overlay_confs: | 
|  | args.append(f"OVERLAY_CONFIG=\"{';'.join(extra_overlay_confs)}\"") | 
|  |  | 
|  | if required_snippets: | 
|  | args.append(f"SNIPPET=\"{';'.join(required_snippets)}\"") | 
|  |  | 
|  | # Build the final argument list | 
|  | args_expanded = ["-D{}".format(a.replace('"', '')) for a in args] | 
|  |  | 
|  | if self.args.cmake_opts: | 
|  | self.args.cmake_opts.extend(args_expanded) | 
|  | else: | 
|  | self.args.cmake_opts = args_expanded | 
|  |  | 
|  | return found_test_metadata | 
|  |  | 
|  | def _sanity_precheck(self): | 
|  | app = self.args.source_dir | 
|  | if app: | 
|  | self.check_force( | 
|  | os.path.isdir(app), | 
|  | f'source directory {app} does not exist') | 
|  | self.check_force( | 
|  | 'CMakeLists.txt' in os.listdir(app), | 
|  | f"{app} doesn't contain a CMakeLists.txt") | 
|  |  | 
|  | def _update_cache(self): | 
|  | with contextlib.suppress(FileNotFoundError): | 
|  | self.cmake_cache = CMakeCache.from_build_dir(self.build_dir) | 
|  |  | 
|  | def _setup_build_dir(self): | 
|  | # Initialize build_dir and created_build_dir attributes. | 
|  | # If we created the build directory, we must run CMake. | 
|  | self.dbg('setting up build directory', level=Verbosity.DBG_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: | 
|  | self.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): | 
|  | self.die(f'build directory {build_dir} exists and is not a directory') | 
|  | 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). | 
|  | self.dbg('setting up source directory', level=Verbosity.DBG_EXTREME) | 
|  | if self.args.source_dir: | 
|  | source_dir = self.args.source_dir | 
|  | elif self.cmake_cache: | 
|  | source_dir = self.cmake_cache.get('APP_DIR') | 
|  |  | 
|  | if not source_dir: | 
|  | source_dir = self.cmake_cache.get('APPLICATION_SOURCE_DIR') | 
|  |  | 
|  | if not source_dir: | 
|  | 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. | 
|  | self.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. | 
|  | self.die(f'source and build directory {self.source_dir} cannot be the same; ' | 
|  | f'use --build-dir {self.build_dir} to specify a build directory') | 
|  |  | 
|  | srcrel = os.path.relpath(self.source_dir) | 
|  | self.check_force( | 
|  | not is_zephyr_build(self.source_dir), | 
|  | f'it looks like {srcrel} is a build directory: ' | 
|  | f'did you mean --build-dir {srcrel} instead?') | 
|  | self.check_force( | 
|  | 'CMakeLists.txt' in os.listdir(self.source_dir), | 
|  | f'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)') | 
|  |  | 
|  | def _sanity_check(self): | 
|  | # Sanity check the build configuration. | 
|  | # Side effect: may update cmake_cache attribute. | 
|  | self.dbg('sanity checking the build', level=Verbosity.DBG_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 | 
|  |  | 
|  | self.dbg('APP_DIR:', cached_app, level=Verbosity.DBG_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 | 
|  |  | 
|  | self.dbg('pristine:', self.auto_pristine, level=Verbosity.DBG_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, | 
|  | f'Build directory "{self.build_dir}" is for application "{cached_abs}", but source ' | 
|  | f'directory "{source_abs}" was specified; please clean it, use --pristine, ' | 
|  | 'or use --build-dir to set another build directory') | 
|  |  | 
|  | 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') | 
|  | self.dbg('CACHED_BOARD:', cached_board, level=Verbosity.DBG_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, | 
|  | f'Build directory {self.build_dir} targets board {cached_board}, ' | 
|  | f'but board {self.args.board} was specified. ' | 
|  | '(Clean the directory, use --pristine, or use --build-dir to ' | 
|  | 'specify a different one.)') | 
|  |  | 
|  | if self.auto_pristine and (apps_mismatched or boards_mismatched): | 
|  | self._run_pristine() | 
|  | self.cmake_cache = None | 
|  | self.dbg('run_cmake:', True, level=Verbosity.DBG_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): | 
|  | self.wrn('This looks like a fresh build and BOARD is unknown;', | 
|  | "so it probably won't work. To fix, use", | 
|  | '--board=<your-board>.') | 
|  | self.inf('Note: to silence the above message, run', | 
|  | "'west config build.board_warn false'") | 
|  |  | 
|  | if not self.run_cmake: | 
|  | return | 
|  |  | 
|  | cmake_env = None | 
|  |  | 
|  | self._banner('generating a build system') | 
|  |  | 
|  | if board is not None and origin != 'CMakeCache.txt': | 
|  | cmake_opts = [f'-DBOARD={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)}') | 
|  | if self.args.shields: | 
|  | cmake_opts.append(f'-DSHIELD={";".join(self.args.shields)}') | 
|  | if self.args.extra_conf_files: | 
|  | cmake_opts.append(f'-DEXTRA_CONF_FILE={";".join(self.args.extra_conf_files)}') | 
|  | if self.args.extra_dtc_overlay_files: | 
|  | cmake_opts.append( | 
|  | f'-DEXTRA_DTC_OVERLAY_FILE=' | 
|  | f'{";".join(self.args.extra_dtc_overlay_files)}' | 
|  | ) | 
|  |  | 
|  | 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([f'-S{SYSBUILD_PROJ_DIR}']) | 
|  | cmake_env = os.environ.copy() | 
|  | cmake_env["APP_DIR"] = str(self.source_dir) | 
|  | else: | 
|  | # self.args.no_sysbuild == True or config sysbuild False | 
|  | cmake_opts.extend([f'-S{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 = [f'-DWEST_PYTHON={pathlib.Path(sys.executable).as_posix()}', | 
|  | f'-B{self.build_dir}', | 
|  | f'-G{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, env=cmake_env) | 
|  |  | 
|  | def _run_pristine(self): | 
|  | self._banner(f'making build dir {self.build_dir} pristine') | 
|  | if not is_zephyr_build(self.build_dir): | 
|  | self.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: | 
|  | self._banner(f'running target {target}') | 
|  | elif self.run_cmake: | 
|  | self._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: | 
|  | self._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') |