1# Copyright (c) 2018 Foundries.io
2#
3# SPDX-License-Identifier: Apache-2.0
4
5import argparse
6import os
7import pathlib
8import shlex
9import sys
10import yaml
11
12from west.commands import Verbosity
13from west.configuration import config
14from west.util import west_topdir
15from west.version import __version__
16from zcmake import DEFAULT_CMAKE_GENERATOR, run_cmake, run_build, CMakeCache
17from build_helpers import is_zephyr_build, find_build_dir, load_domains, \
18    FIND_BUILD_DIR_DESCRIPTION
19
20from zephyr_ext_common import Forceable
21
22_ARG_SEPARATOR = '--'
23
24SYSBUILD_PROJ_DIR = pathlib.Path(__file__).resolve().parent.parent.parent \
25                    / pathlib.Path('share/sysbuild')
26
27BUILD_INFO_LOG = 'build_info.yml'
28
29BUILD_USAGE = '''\
30west build [-h] [-b BOARD[@REV]]] [-d BUILD_DIR]
31           [-S SNIPPET] [--shield SHIELD]
32           [-t TARGET] [-p {auto, always, never}] [-c] [--cmake-only]
33           [-n] [-o BUILD_OPT] [-f]
34           [--sysbuild | --no-sysbuild] [--domain DOMAIN]
35           [--extra-conf FILE.conf]
36           [--extra-dtc-overlay FILE.overlay]
37           [source_dir] -- [cmake_opt [cmake_opt ...]]
38'''
39
40BUILD_DESCRIPTION = f'''\
41Convenience wrapper for building Zephyr applications.
42
43{FIND_BUILD_DIR_DESCRIPTION}
44
45positional arguments:
46  source_dir            application source directory
47  cmake_opt             extra options to pass to cmake; implies -c
48                        (these must come after "--" as shown above)
49'''
50
51PRISTINE_DESCRIPTION = """\
52A "pristine" build directory is empty. The -p option controls
53whether the build directory is made pristine before the build
54is done. A bare '--pristine' with no value is the same as
55--pristine=always. Setting --pristine=auto uses heuristics to
56guess if a pristine build may be necessary."""
57
58def config_get(option, fallback):
59    return config.get('build', option, fallback=fallback)
60
61def config_getboolean(option, fallback):
62    return config.getboolean('build', option, fallback=fallback)
63
64class AlwaysIfMissing(argparse.Action):
65
66    def __call__(self, parser, namespace, values, option_string=None):
67        setattr(namespace, self.dest, values or 'always')
68
69class Build(Forceable):
70
71    def __init__(self):
72        super(Build, self).__init__(
73            'build',
74            # Keep this in sync with the string in west-commands.yml.
75            'compile a Zephyr application',
76            BUILD_DESCRIPTION,
77            accepts_unknown_args=True)
78
79        self.source_dir = None
80        '''Source directory for the build, or None on error.'''
81
82        self.build_dir = None
83        '''Final build directory used to run the build, or None on error.'''
84
85        self.created_build_dir = False
86        '''True if the build directory was created; False otherwise.'''
87
88        self.run_cmake = False
89        '''True if CMake was run; False otherwise.
90
91        Note: this only describes CMake runs done by this command. The
92        build system generated by CMake may also update itself due to
93        internal logic.'''
94
95        self.cmake_cache = None
96        '''Final parsed CMake cache for the build, or None on error.'''
97
98    def _banner(self, msg):
99        self.inf('-- west build: ' + msg, colorize=True)
100
101    def do_add_parser(self, parser_adder):
102        parser = parser_adder.add_parser(
103            self.name,
104            help=self.help,
105            formatter_class=argparse.RawDescriptionHelpFormatter,
106            description=self.description,
107            usage=BUILD_USAGE)
108
109        # Remember to update west-completion.bash if you add or remove
110        # flags
111
112        parser.add_argument('-b', '--board',
113                        help='board to build for with optional board revision')
114        # Hidden option for backwards compatibility
115        parser.add_argument('-s', '--source-dir', help=argparse.SUPPRESS)
116        parser.add_argument('-d', '--build-dir',
117                            help='build directory to create or use')
118        self.add_force_arg(parser)
119
120        group = parser.add_argument_group('cmake and build tool')
121        group.add_argument('-c', '--cmake', action='store_true',
122                           help='force a cmake run')
123        group.add_argument('--cmake-only', action='store_true',
124                           help="just run cmake; don't build (implies -c)")
125        group.add_argument('--domain', action='append',
126                           help='''execute build tool (make or ninja) only for
127                           given domain''')
128        group.add_argument('-t', '--target',
129                           help='''run build system target TARGET
130                           (try "-t usage")''')
131        group.add_argument('-T', '--test-item',
132                           help='''Build based on test data in testcase.yaml
133                           or sample.yaml. If source directory is not used
134                           an argument has to be defined as
135                           SOURCE_PATH/TEST_NAME.
136                           E.g. samples/hello_world/sample.basic.helloworld.
137                           If source directory is passed
138                           then "TEST_NAME" is enough.''')
139        group.add_argument('-o', '--build-opt', default=[], action='append',
140                           help='''options to pass to the build tool
141                           (make or ninja); may be given more than once''')
142        group.add_argument('-n', '--just-print', '--dry-run', '--recon',
143                            dest='dry_run', action='store_true',
144                            help="just print build commands; don't run them")
145        group.add_argument('-S', '--snippet', dest='snippets', metavar='SNIPPET',
146                           action='append', default=[],
147                           help='''add the argument to SNIPPET; may be given
148                           multiple times. Forces CMake to run again if given.
149                           Do not use this option with manually specified
150                           -DSNIPPET... cmake arguments: the results are
151                           undefined''')
152        group.add_argument('--shield', dest='shields', metavar='SHIELD',
153                           action='append', default=[],
154                           help='''add the argument to SHIELD; may be given
155                           multiple times. Forces CMake to run again if given.
156                           Do not use this option with manually specified
157                           -DSHIELD... cmake arguments: the results are
158                           undefined''')
159        group.add_argument('--extra-conf', dest='extra_conf_files', metavar='EXTRA_CONF_FILE',
160                           action='append', default=[],
161                           help='''add the argument to EXTRA_CONF_FILE; may be given
162                           multiple times. Forces CMake to run again if given.
163                           Do not use this option with manually specified
164                           -DEXTRA_CONF_FILE... cmake arguments: the results are
165                           undefined''')
166        group.add_argument('--extra-dtc-overlay', dest='extra_dtc_overlay_files',
167                           metavar='EXTRA_DTC_OVERLAY_FILE', action='append', default=[],
168                           help='''add the argument to EXTRA_DTC_OVERLAY_FILE; may be given
169                           multiple times. Forces CMake to run again if given.
170                           Do not use this option with manually specified
171                           -DEXTRA_DTC_OVERLAY_FILE... cmake arguments: the results are
172                           undefined''')
173
174        group = parser.add_mutually_exclusive_group()
175        group.add_argument('--sysbuild', action='store_true',
176                           help='''create multi domain build system''')
177        group.add_argument('--no-sysbuild', action='store_true',
178                           help='''do not create multi domain build system
179                                   (default)''')
180
181        group = parser.add_argument_group('pristine builds',
182                                          PRISTINE_DESCRIPTION)
183        group.add_argument('-p', '--pristine', choices=['auto', 'always',
184                            'never'], action=AlwaysIfMissing, nargs='?',
185                            help='pristine build folder setting')
186
187        return parser
188
189    def do_run(self, args, remainder):
190        self.args = args        # Avoid having to pass them around
191        self.config_board = config_get('board', None)
192        self.dbg('args: {} remainder: {}'.format(args, remainder),
193                level=Verbosity.DBG_EXTREME)
194        # Store legacy -s option locally
195        source_dir = self.args.source_dir
196        self._parse_remainder(remainder)
197        # Parse testcase.yaml or sample.yaml files for additional options.
198        if self.args.test_item:
199            # we get path + testitem
200            item = os.path.basename(self.args.test_item)
201            if self.args.source_dir:
202                test_path = self.args.source_dir
203            else:
204                test_path = os.path.dirname(self.args.test_item)
205            if test_path and os.path.exists(test_path):
206                self.args.source_dir = test_path
207                if not self._parse_test_item(item):
208                    self.die("No test metadata found")
209            else:
210                self.die("test item path does not exist")
211
212        if source_dir:
213            if self.args.source_dir:
214                self.die("source directory specified twice:({} and {})".format(
215                                            source_dir, self.args.source_dir))
216            self.args.source_dir = source_dir
217        self.dbg('source_dir: {} cmake_opts: {}'.format(self.args.source_dir,
218                                                       self.args.cmake_opts),
219                level=Verbosity.DBG_EXTREME)
220        self._sanity_precheck()
221        self._setup_build_dir()
222
223        if args.pristine is not None:
224            pristine = args.pristine
225        else:
226            # Load the pristine={auto, always, never} configuration value
227            pristine = config_get('pristine', 'never')
228            if pristine not in ['auto', 'always', 'never']:
229                self.wrn(
230                    'treating unknown build.pristine value "{}" as "never"'.
231                    format(pristine))
232                pristine = 'never'
233        self.auto_pristine = pristine == 'auto'
234
235        self.dbg('pristine: {} auto_pristine: {}'.format(pristine,
236                                                        self.auto_pristine),
237                level=Verbosity.DBG_MORE)
238        if is_zephyr_build(self.build_dir):
239            if pristine == 'always':
240                self._run_pristine()
241                self.run_cmake = True
242            else:
243                self._update_cache()
244                if (self.args.cmake or self.args.cmake_opts or
245                        self.args.cmake_only or self.args.snippets or
246                        self.args.shields or self.args.extra_conf_files or
247                        self.args.extra_dtc_overlay_files):
248                    self.run_cmake = True
249        else:
250            self.run_cmake = True
251
252        self.source_dir = self._find_source_dir()
253        self._sanity_check()
254
255        build_info_path = self.build_dir
256        build_info_file = os.path.join(build_info_path, BUILD_INFO_LOG)
257        west_workspace = west_topdir(self.source_dir)
258        if not os.path.exists(build_info_path):
259            os.makedirs(build_info_path)
260        if not os.path.exists(build_info_file):
261            build_command = {'west': {'command': ' '.join(sys.argv[:]),
262                                     'topdir': str(west_workspace),
263                                     'version': str(__version__)}}
264            try:
265                with open(build_info_file, "w") as f:
266                    yaml.dump(build_command, f, default_flow_style=False)
267            except Exception as e:
268                self.wrn(f'Failed to create info file: {build_info_file},', e)
269
270        board, origin = self._find_board()
271        self._run_cmake(board, origin, self.args.cmake_opts)
272        if args.cmake_only:
273            return
274
275        self._sanity_check()
276        self._update_cache()
277        self.domains = load_domains(self.build_dir)
278
279        self._run_build(args.target, args.domain)
280
281    def _find_board(self):
282        board, origin = None, None
283        if self.cmake_cache:
284            board, origin = (self.cmake_cache.get('CACHED_BOARD'),
285                             'CMakeCache.txt')
286
287            # A malformed CMake cache may exist, but not have a board.
288            # This happens if there's a build error from a previous run.
289            if board is not None:
290                return (board, origin)
291
292        if self.args.board:
293            board, origin = self.args.board, 'command line'
294        elif 'BOARD' in os.environ:
295            board, origin = os.environ['BOARD'], 'env'
296        elif self.config_board is not None:
297            board, origin = self.config_board, 'configfile'
298        return board, origin
299
300    def _parse_remainder(self, remainder):
301        self.args.source_dir = None
302        self.args.cmake_opts = None
303
304        try:
305            # Only one source_dir is allowed, as the first positional arg
306            if remainder[0] != _ARG_SEPARATOR:
307                self.args.source_dir = remainder[0]
308                remainder = remainder[1:]
309            # Only the first argument separator is consumed, the rest are
310            # passed on to CMake
311            if remainder[0] == _ARG_SEPARATOR:
312                remainder = remainder[1:]
313            if remainder:
314                self.args.cmake_opts = remainder
315        except IndexError:
316            pass
317
318    def _parse_test_item(self, test_item):
319        found_test_metadata = False
320        for yp in ['sample.yaml', 'testcase.yaml']:
321            yf = os.path.join(self.args.source_dir, yp)
322            if not os.path.exists(yf):
323                continue
324            found_test_metadata = True
325            with open(yf, 'r') as stream:
326                try:
327                    y = yaml.safe_load(stream)
328                except yaml.YAMLError as exc:
329                    self.die(exc)
330            common = y.get('common')
331            tests = y.get('tests')
332            if not tests:
333                self.die(f"No tests found in {yf}")
334            if test_item not in tests:
335                self.die(f"Test item {test_item} not found in {yf}")
336            item = tests.get(test_item)
337
338            sysbuild = False
339            extra_dtc_overlay_files = []
340            extra_overlay_confs = []
341            extra_conf_files = []
342            required_snippets = []
343            for section in [common, item]:
344                if not section:
345                    continue
346                sysbuild = section.get('sysbuild', sysbuild)
347                for data in [
348                        'extra_args',
349                        'extra_configs',
350                        'extra_conf_files',
351                        'extra_overlay_confs',
352                        'extra_dtc_overlay_files',
353                        'required_snippets'
354                        ]:
355                    extra = section.get(data)
356                    if not extra:
357                        continue
358                    if isinstance(extra, str):
359                        arg_list = extra.split(" ")
360                    else:
361                        arg_list = extra
362
363                    if data == 'extra_configs':
364                        args = []
365                        for arg in arg_list:
366                            equals = arg.find('=')
367                            colon = arg.rfind(':', 0, equals)
368                            if colon != -1:
369                                # conditional configs (xxx:yyy:CONFIG_FOO=bar)
370                                # are not supported by 'west build'
371                                self.wrn('"west build" does not support '
372                                         'conditional config "{}". Add "-D{}" '
373                                         'to the supplied CMake arguments if '
374                                         'desired.'.format(arg, arg[colon+1:]))
375                                continue
376                            args.append("-D{}".format(arg.replace('"', '\"')))
377                    elif data == 'extra_args':
378                        # Retain quotes around config options
379                        config_options = [arg for arg in arg_list if arg.startswith("CONFIG_")]
380                        non_config_options = [arg for arg in arg_list if not arg.startswith("CONFIG_")]
381                        args = ["-D{}".format(a.replace('"', '\"')) for a in config_options]
382                        args.extend(["-D{}".format(arg.replace('"', '')) for arg in non_config_options])
383                    elif data == 'extra_conf_files':
384                        extra_conf_files.extend(arg_list)
385                        continue
386                    elif data == 'extra_overlay_confs':
387                        extra_overlay_confs.extend(arg_list)
388                        continue
389                    elif data == 'extra_dtc_overlay_files':
390                        extra_dtc_overlay_files.extend(arg_list)
391                        continue
392                    elif data == 'required_snippets':
393                        required_snippets.extend(arg_list)
394                        continue
395
396                    if self.args.cmake_opts:
397                        self.args.cmake_opts.extend(args)
398                    else:
399                        self.args.cmake_opts = args
400
401            self.args.sysbuild = sysbuild
402
403        if found_test_metadata:
404            args = []
405            if extra_conf_files:
406                args.append(f"CONF_FILE=\"{';'.join(extra_conf_files)}\"")
407
408            if extra_dtc_overlay_files:
409                args.append(f"DTC_OVERLAY_FILE=\"{';'.join(extra_dtc_overlay_files)}\"")
410
411            if extra_overlay_confs:
412                args.append(f"OVERLAY_CONFIG=\"{';'.join(extra_overlay_confs)}\"")
413
414            if required_snippets:
415                args.append(f"SNIPPET=\"{';'.join(required_snippets)}\"")
416
417            # Build the final argument list
418            args_expanded = ["-D{}".format(a.replace('"', '')) for a in args]
419
420            if self.args.cmake_opts:
421                self.args.cmake_opts.extend(args_expanded)
422            else:
423                self.args.cmake_opts = args_expanded
424
425        return found_test_metadata
426
427    def _sanity_precheck(self):
428        app = self.args.source_dir
429        if app:
430            self.check_force(
431                os.path.isdir(app),
432                'source directory {} does not exist'.format(app))
433            self.check_force(
434                'CMakeLists.txt' in os.listdir(app),
435                "{} doesn't contain a CMakeLists.txt".format(app))
436
437    def _update_cache(self):
438        try:
439            self.cmake_cache = CMakeCache.from_build_dir(self.build_dir)
440        except FileNotFoundError:
441            pass
442
443    def _setup_build_dir(self):
444        # Initialize build_dir and created_build_dir attributes.
445        # If we created the build directory, we must run CMake.
446        self.dbg('setting up build directory', level=Verbosity.DBG_EXTREME)
447        # The CMake Cache has not been loaded yet, so this is safe
448        board, _ = self._find_board()
449        source_dir = self._find_source_dir()
450        app = os.path.split(source_dir)[1]
451        build_dir = find_build_dir(self.args.build_dir, board=board,
452                                   source_dir=source_dir, app=app)
453        if not build_dir:
454            self.die('Unable to determine a default build folder. Check '
455                    'your build.dir-fmt configuration option')
456
457        if os.path.exists(build_dir):
458            if not os.path.isdir(build_dir):
459                self.die('build directory {} exists and is not a directory'.
460                        format(build_dir))
461        else:
462            os.makedirs(build_dir, exist_ok=False)
463            self.created_build_dir = True
464            self.run_cmake = True
465
466        self.build_dir = build_dir
467
468    def _find_source_dir(self):
469        # Initialize source_dir attribute, either from command line argument,
470        # implicitly from the build directory's CMake cache, or using the
471        # default (current working directory).
472        self.dbg('setting up source directory', level=Verbosity.DBG_EXTREME)
473        if self.args.source_dir:
474            source_dir = self.args.source_dir
475        elif self.cmake_cache:
476            source_dir = self.cmake_cache.get('APP_DIR')
477
478            if not source_dir:
479                source_dir = self.cmake_cache.get('APPLICATION_SOURCE_DIR')
480
481            if not source_dir:
482                source_dir = self.cmake_cache.get('CMAKE_HOME_DIRECTORY')
483
484            if not source_dir:
485                # This really ought to be there. The build directory
486                # must be corrupted somehow. Let's see what we can do.
487                self.die('build directory', self.build_dir,
488                        'CMake cache has no CMAKE_HOME_DIRECTORY;',
489                        'please give a source_dir')
490        else:
491            source_dir = os.getcwd()
492        return os.path.abspath(source_dir)
493
494    def _sanity_check_source_dir(self):
495        if self.source_dir == self.build_dir:
496            # There's no forcing this.
497            self.die('source and build directory {} cannot be the same; '
498                    'use --build-dir {} to specify a build directory'.
499                    format(self.source_dir, self.build_dir))
500
501        srcrel = os.path.relpath(self.source_dir)
502        self.check_force(
503            not is_zephyr_build(self.source_dir),
504            'it looks like {srcrel} is a build directory: '
505            'did you mean --build-dir {srcrel} instead?'.
506            format(srcrel=srcrel))
507        self.check_force(
508            'CMakeLists.txt' in os.listdir(self.source_dir),
509            'source directory "{srcrel}" does not contain '
510            'a CMakeLists.txt; is this really what you '
511            'want to build? (Use -s SOURCE_DIR to specify '
512            'the application source directory)'.
513            format(srcrel=srcrel))
514
515    def _sanity_check(self):
516        # Sanity check the build configuration.
517        # Side effect: may update cmake_cache attribute.
518        self.dbg('sanity checking the build', level=Verbosity.DBG_EXTREME)
519        self._sanity_check_source_dir()
520
521        if not self.cmake_cache:
522            return          # That's all we can check without a cache.
523
524        if "CMAKE_PROJECT_NAME" not in self.cmake_cache:
525            # This happens sometimes when a build system is not
526            # completely generated due to an error during the
527            # CMake configuration phase.
528            self.run_cmake = True
529
530        cached_proj = self.cmake_cache.get('APPLICATION_SOURCE_DIR')
531        cached_app = self.cmake_cache.get('APP_DIR')
532        # if APP_DIR is None but APPLICATION_SOURCE_DIR is set, that indicates
533        # an older build folder, this still requires pristine.
534        if cached_app is None and cached_proj:
535            cached_app = cached_proj
536
537        self.dbg('APP_DIR:', cached_app, level=Verbosity.DBG_EXTREME)
538        source_abs = (os.path.abspath(self.args.source_dir)
539                      if self.args.source_dir else None)
540        cached_abs = os.path.abspath(cached_app) if cached_app else None
541
542        self.dbg('pristine:', self.auto_pristine, level=Verbosity.DBG_EXTREME)
543
544        # If the build directory specifies a source app, make sure it's
545        # consistent with --source-dir.
546        apps_mismatched = (source_abs and cached_abs and
547            pathlib.Path(source_abs).resolve() != pathlib.Path(cached_abs).resolve())
548
549        self.check_force(
550            not apps_mismatched or self.auto_pristine,
551            'Build directory "{}" is for application "{}", but source '
552            'directory "{}" was specified; please clean it, use --pristine, '
553            'or use --build-dir to set another build directory'.
554            format(self.build_dir, cached_abs, source_abs))
555
556        if apps_mismatched:
557            self.run_cmake = True  # If they insist, we need to re-run cmake.
558
559        # If CACHED_BOARD is not defined, we need some other way to
560        # find the board.
561        cached_board = self.cmake_cache.get('CACHED_BOARD')
562        self.dbg('CACHED_BOARD:', cached_board, level=Verbosity.DBG_EXTREME)
563        # If apps_mismatched and self.auto_pristine are true, we will
564        # run pristine on the build, invalidating the cached
565        # board. In that case, we need some way of getting the board.
566        self.check_force((cached_board and
567                          not (apps_mismatched and self.auto_pristine))
568                         or self.args.board or self.config_board or
569                         os.environ.get('BOARD'),
570                         'Cached board not defined, please provide it '
571                         '(provide --board, set default with '
572                         '"west config build.board <BOARD>", or set '
573                         'BOARD in the environment)')
574
575        # Check consistency between cached board and --board.
576        boards_mismatched = (self.args.board and cached_board and
577                             self.args.board != cached_board)
578        self.check_force(
579            not boards_mismatched or self.auto_pristine,
580            'Build directory {} targets board {}, but board {} was specified. '
581            '(Clean the directory, use --pristine, or use --build-dir to '
582            'specify a different one.)'.
583            format(self.build_dir, cached_board, self.args.board))
584
585        if self.auto_pristine and (apps_mismatched or boards_mismatched):
586            self._run_pristine()
587            self.cmake_cache = None
588            self.dbg('run_cmake:', True, level=Verbosity.DBG_EXTREME)
589            self.run_cmake = True
590
591            # Tricky corner-case: The user has not specified a build folder but
592            # there was one in the CMake cache. Since this is going to be
593            # invalidated, reset to CWD and re-run the basic tests.
594            if ((boards_mismatched and not apps_mismatched) and
595                    (not source_abs and cached_abs)):
596                self.source_dir = self._find_source_dir()
597                self._sanity_check_source_dir()
598
599    def _run_cmake(self, board, origin, cmake_opts):
600        if board is None and config_getboolean('board_warn', True):
601            self.wrn('This looks like a fresh build and BOARD is unknown;',
602                    "so it probably won't work. To fix, use",
603                    '--board=<your-board>.')
604            self.inf('Note: to silence the above message, run',
605                    "'west config build.board_warn false'")
606
607        if not self.run_cmake:
608            return
609
610        self._banner('generating a build system')
611
612        if board is not None and origin != 'CMakeCache.txt':
613            cmake_opts = ['-DBOARD={}'.format(board)]
614        else:
615            cmake_opts = []
616        if self.args.cmake_opts:
617            cmake_opts.extend(self.args.cmake_opts)
618        if self.args.snippets:
619            cmake_opts.append(f'-DSNIPPET={";".join(self.args.snippets)}')
620        if self.args.shields:
621            cmake_opts.append(f'-DSHIELD={";".join(self.args.shields)}')
622        if self.args.extra_conf_files:
623            cmake_opts.append(f'-DEXTRA_CONF_FILE={";".join(self.args.extra_conf_files)}')
624        if self.args.extra_dtc_overlay_files:
625            cmake_opts.append(
626                f'-DEXTRA_DTC_OVERLAY_FILE='
627                f'{";".join(self.args.extra_dtc_overlay_files)}'
628            )
629
630        user_args = config_get('cmake-args', None)
631        if user_args:
632            cmake_opts.extend(shlex.split(user_args))
633
634        config_sysbuild = config_getboolean('sysbuild', False)
635        if self.args.sysbuild or (config_sysbuild and not self.args.no_sysbuild):
636            cmake_opts.extend(['-S{}'.format(SYSBUILD_PROJ_DIR),
637                               '-DAPP_DIR:PATH={}'.format(self.source_dir)])
638        else:
639            # self.args.no_sysbuild == True or config sysbuild False
640            cmake_opts.extend(['-S{}'.format(self.source_dir)])
641
642        # Invoke CMake from the current working directory using the
643        # -S and -B options (officially introduced in CMake 3.13.0).
644        # This is important because users expect invocations like this
645        # to Just Work:
646        #
647        # west build -- -DOVERLAY_CONFIG=relative-path.conf
648        final_cmake_args = ['-DWEST_PYTHON={}'.format(pathlib.Path(sys.executable).as_posix()),
649                            '-B{}'.format(self.build_dir),
650                            '-G{}'.format(config_get('generator',
651                                                     DEFAULT_CMAKE_GENERATOR))]
652        if cmake_opts:
653            final_cmake_args.extend(cmake_opts)
654        run_cmake(final_cmake_args, dry_run=self.args.dry_run)
655
656    def _run_pristine(self):
657        self._banner('making build dir {} pristine'.format(self.build_dir))
658        if not is_zephyr_build(self.build_dir):
659            self.die('Refusing to run pristine on a folder that is not a '
660                     'Zephyr build system')
661
662        cache = CMakeCache.from_build_dir(self.build_dir)
663
664        app_src_dir = cache.get('APPLICATION_SOURCE_DIR')
665        app_bin_dir = cache.get('APPLICATION_BINARY_DIR')
666
667        cmake_args = [f'-DBINARY_DIR={app_bin_dir}',
668                      f'-DSOURCE_DIR={app_src_dir}',
669                      '-P', cache['ZEPHYR_BASE'] + '/cmake/pristine.cmake']
670        run_cmake(cmake_args, cwd=self.build_dir, dry_run=self.args.dry_run)
671
672    def _run_build(self, target, domain):
673        if target:
674            self._banner('running target {}'.format(target))
675        elif self.run_cmake:
676            self._banner('building application')
677        extra_args = ['--target', target] if target else []
678        if self.args.build_opt:
679            extra_args.append('--')
680            extra_args.extend(self.args.build_opt)
681        if self.args.verbose:
682            self._append_verbose_args(extra_args,
683                                      not bool(self.args.build_opt))
684
685        domains = load_domains(self.build_dir)
686        build_dir_list = []
687
688        if domain is None:
689            # If no domain is specified, we just build top build dir as that
690            # will build all domains.
691            build_dir_list = [domains.get_top_build_dir()]
692        else:
693            self._banner('building domain(s): {}'.format(' '.join(domain)))
694            domain_list = domains.get_domains(domain)
695            for d in domain_list:
696                build_dir_list.append(d.build_dir)
697
698        for b in build_dir_list:
699            run_build(b, extra_args=extra_args,
700                      dry_run=self.args.dry_run)
701
702    def _append_verbose_args(self, extra_args, add_dashes):
703        # These hacks are only needed for CMake versions earlier than
704        # 3.14. When Zephyr's minimum version is at least that, we can
705        # drop this nonsense and just run "cmake --build BUILD -v".
706        self._update_cache()
707        if not self.cmake_cache:
708            return
709        generator = self.cmake_cache.get('CMAKE_GENERATOR')
710        if not generator:
711            return
712        # Substring matching is for things like "Eclipse CDT4 - Ninja".
713        if 'Ninja' in generator:
714            if add_dashes:
715                extra_args.append('--')
716            extra_args.append('-v')
717        elif generator == 'Unix Makefiles':
718            if add_dashes:
719                extra_args.append('--')
720            extra_args.append('VERBOSE=1')
721