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 import log
13from west.configuration import config
14from zcmake import DEFAULT_CMAKE_GENERATOR, run_cmake, run_build, CMakeCache
15from build_helpers import is_zephyr_build, find_build_dir, \
16    FIND_BUILD_DIR_DESCRIPTION
17
18from zephyr_ext_common import Forceable
19
20_ARG_SEPARATOR = '--'
21
22BUILD_USAGE = '''\
23west build [-h] [-b BOARD] [-d BUILD_DIR]
24           [-t TARGET] [-p {auto, always, never}] [-c] [--cmake-only]
25           [-n] [-o BUILD_OPT] [-f]
26           [source_dir] -- [cmake_opt [cmake_opt ...]]
27'''
28
29BUILD_DESCRIPTION = f'''\
30Convenience wrapper for building Zephyr applications.
31
32{FIND_BUILD_DIR_DESCRIPTION}
33
34positional arguments:
35  source_dir            application source directory
36  cmake_opt             extra options to pass to cmake; implies -c
37                        (these must come after "--" as shown above)
38'''
39
40PRISTINE_DESCRIPTION = """\
41A "pristine" build directory is empty. The -p option controls
42whether the build directory is made pristine before the build
43is done. A bare '--pristine' with no value is the same as
44--pristine=always. Setting --pristine=auto uses heuristics to
45guess if a pristine build may be necessary."""
46
47def _banner(msg):
48    log.inf('-- west build: ' + msg, colorize=True)
49
50def config_get(option, fallback):
51    return config.get('build', option, fallback=fallback)
52
53def config_getboolean(option, fallback):
54    return config.getboolean('build', option, fallback=fallback)
55
56class AlwaysIfMissing(argparse.Action):
57
58    def __call__(self, parser, namespace, values, option_string=None):
59        setattr(namespace, self.dest, values or 'always')
60
61class Build(Forceable):
62
63    def __init__(self):
64        super(Build, self).__init__(
65            'build',
66            # Keep this in sync with the string in west-commands.yml.
67            'compile a Zephyr application',
68            BUILD_DESCRIPTION,
69            accepts_unknown_args=True)
70
71        self.source_dir = None
72        '''Source directory for the build, or None on error.'''
73
74        self.build_dir = None
75        '''Final build directory used to run the build, or None on error.'''
76
77        self.created_build_dir = False
78        '''True if the build directory was created; False otherwise.'''
79
80        self.run_cmake = False
81        '''True if CMake was run; False otherwise.
82
83        Note: this only describes CMake runs done by this command. The
84        build system generated by CMake may also update itself due to
85        internal logic.'''
86
87        self.cmake_cache = None
88        '''Final parsed CMake cache for the build, or None on error.'''
89
90    def do_add_parser(self, parser_adder):
91        parser = parser_adder.add_parser(
92            self.name,
93            help=self.help,
94            formatter_class=argparse.RawDescriptionHelpFormatter,
95            description=self.description,
96            usage=BUILD_USAGE)
97
98        # Remember to update west-completion.bash if you add or remove
99        # flags
100
101        parser.add_argument('-b', '--board', help='board to build for')
102        # Hidden option for backwards compatibility
103        parser.add_argument('-s', '--source-dir', help=argparse.SUPPRESS)
104        parser.add_argument('-d', '--build-dir',
105                            help='build directory to create or use')
106        self.add_force_arg(parser)
107
108        group = parser.add_argument_group('cmake and build tool')
109        group.add_argument('-c', '--cmake', action='store_true',
110                           help='force a cmake run')
111        group.add_argument('--cmake-only', action='store_true',
112                           help="just run cmake; don't build (implies -c)")
113        group.add_argument('-t', '--target',
114                           help='''run build system target TARGET
115                           (try "-t usage")''')
116        group.add_argument('-T', '--test-item',
117                           help='''Build based on test data in testcase.yaml
118                           or sample.yaml''')
119        group.add_argument('-o', '--build-opt', default=[], action='append',
120                           help='''options to pass to the build tool
121                           (make or ninja); may be given more than once''')
122        group.add_argument('-n', '--just-print', '--dry-run', '--recon',
123                            dest='dry_run', action='store_true',
124                            help="just print build commands; don't run them")
125
126        group = parser.add_argument_group('pristine builds',
127                                          PRISTINE_DESCRIPTION)
128        group.add_argument('-p', '--pristine', choices=['auto', 'always',
129                            'never'], action=AlwaysIfMissing, nargs='?',
130                            help='pristine build folder setting')
131
132        return parser
133
134    def do_run(self, args, remainder):
135        self.args = args        # Avoid having to pass them around
136        self.config_board = config_get('board', None)
137        log.dbg('args: {} remainder: {}'.format(args, remainder),
138                level=log.VERBOSE_EXTREME)
139        # Store legacy -s option locally
140        source_dir = self.args.source_dir
141        self._parse_remainder(remainder)
142        # Parse testcase.yaml or sample.yaml files for additional options.
143        if self.args.test_item:
144            self._parse_test_item()
145        if source_dir:
146            if self.args.source_dir:
147                log.die("source directory specified twice:({} and {})".format(
148                                            source_dir, self.args.source_dir))
149            self.args.source_dir = source_dir
150        log.dbg('source_dir: {} cmake_opts: {}'.format(self.args.source_dir,
151                                                       self.args.cmake_opts),
152                level=log.VERBOSE_EXTREME)
153        self._sanity_precheck()
154        self._setup_build_dir()
155
156        if args.pristine is not None:
157            pristine = args.pristine
158        else:
159            # Load the pristine={auto, always, never} configuration value
160            pristine = config_get('pristine', 'never')
161            if pristine not in ['auto', 'always', 'never']:
162                log.wrn(
163                    'treating unknown build.pristine value "{}" as "never"'.
164                    format(pristine))
165                pristine = 'never'
166        self.auto_pristine = (pristine == 'auto')
167
168        log.dbg('pristine: {} auto_pristine: {}'.format(pristine,
169                                                        self.auto_pristine),
170                level=log.VERBOSE_VERY)
171        if is_zephyr_build(self.build_dir):
172            if pristine == 'always':
173                self._run_pristine()
174                self.run_cmake = True
175            else:
176                self._update_cache()
177                if (self.args.cmake or self.args.cmake_opts or
178                        self.args.cmake_only):
179                    self.run_cmake = True
180        else:
181            self.run_cmake = True
182        self.source_dir = self._find_source_dir()
183        self._sanity_check()
184
185        board, origin = self._find_board()
186        self._run_cmake(board, origin, self.args.cmake_opts)
187        if args.cmake_only:
188            return
189
190        self._sanity_check()
191        self._update_cache()
192
193        self._run_build(args.target)
194
195    def _find_board(self):
196        board, origin = None, None
197        if self.cmake_cache:
198            board, origin = (self.cmake_cache.get('CACHED_BOARD'),
199                             'CMakeCache.txt')
200
201            # A malformed CMake cache may exist, but not have a board.
202            # This happens if there's a build error from a previous run.
203            if board is not None:
204                return (board, origin)
205
206        if self.args.board:
207            board, origin = self.args.board, 'command line'
208        elif 'BOARD' in os.environ:
209            board, origin = os.environ['BOARD'], 'env'
210        elif self.config_board is not None:
211            board, origin = self.config_board, 'configfile'
212        return board, origin
213
214    def _parse_remainder(self, remainder):
215        self.args.source_dir = None
216        self.args.cmake_opts = None
217
218        try:
219            # Only one source_dir is allowed, as the first positional arg
220            if remainder[0] != _ARG_SEPARATOR:
221                self.args.source_dir = remainder[0]
222                remainder = remainder[1:]
223            # Only the first argument separator is consumed, the rest are
224            # passed on to CMake
225            if remainder[0] == _ARG_SEPARATOR:
226                remainder = remainder[1:]
227            if remainder:
228                self.args.cmake_opts = remainder
229        except IndexError:
230            return
231
232    def _parse_test_item(self):
233        for yp in ['sample.yaml', 'testcase.yaml']:
234            yf = os.path.join(self.args.source_dir, yp)
235            if not os.path.exists(yf):
236                continue
237            with open(yf, 'r') as stream:
238                try:
239                    y = yaml.safe_load(stream)
240                except yaml.YAMLError as exc:
241                    log.die(exc)
242            tests = y.get('tests')
243            if not tests:
244                continue
245            item = tests.get(self.args.test_item)
246            if not item:
247                continue
248
249            for data in ['extra_args', 'extra_configs']:
250                extra = item.get(data)
251                if not extra:
252                    continue
253                if isinstance(extra, str):
254                    arg_list = extra.split(" ")
255                else:
256                    arg_list = extra
257                args = ["-D{}".format(arg.replace('"', '')) for arg in arg_list]
258                if self.args.cmake_opts:
259                    self.args.cmake_opts.extend(args)
260                else:
261                    self.args.cmake_opts = args
262
263    def _sanity_precheck(self):
264        app = self.args.source_dir
265        if app:
266            self.check_force(
267                os.path.isdir(app),
268                'source directory {} does not exist'.format(app))
269            self.check_force(
270                'CMakeLists.txt' in os.listdir(app),
271                "{} doesn't contain a CMakeLists.txt".format(app))
272
273    def _update_cache(self):
274        try:
275            self.cmake_cache = CMakeCache.from_build_dir(self.build_dir)
276        except FileNotFoundError:
277            pass
278
279    def _setup_build_dir(self):
280        # Initialize build_dir and created_build_dir attributes.
281        # If we created the build directory, we must run CMake.
282        log.dbg('setting up build directory', level=log.VERBOSE_EXTREME)
283        # The CMake Cache has not been loaded yet, so this is safe
284        board, _ = self._find_board()
285        source_dir = self._find_source_dir()
286        app = os.path.split(source_dir)[1]
287        build_dir = find_build_dir(self.args.build_dir, board=board,
288                                   source_dir=source_dir, app=app)
289        if not build_dir:
290            log.die('Unable to determine a default build folder. Check '
291                    'your build.dir-fmt configuration option')
292
293        if os.path.exists(build_dir):
294            if not os.path.isdir(build_dir):
295                log.die('build directory {} exists and is not a directory'.
296                        format(build_dir))
297        else:
298            os.makedirs(build_dir, exist_ok=False)
299            self.created_build_dir = True
300            self.run_cmake = True
301
302        self.build_dir = build_dir
303
304    def _find_source_dir(self):
305        # Initialize source_dir attribute, either from command line argument,
306        # implicitly from the build directory's CMake cache, or using the
307        # default (current working directory).
308        log.dbg('setting up source directory', level=log.VERBOSE_EXTREME)
309        if self.args.source_dir:
310            source_dir = self.args.source_dir
311        elif self.cmake_cache:
312            source_dir = self.cmake_cache.get('CMAKE_HOME_DIRECTORY')
313            if not source_dir:
314                # This really ought to be there. The build directory
315                # must be corrupted somehow. Let's see what we can do.
316                log.die('build directory', self.build_dir,
317                        'CMake cache has no CMAKE_HOME_DIRECTORY;',
318                        'please give a source_dir')
319        else:
320            source_dir = os.getcwd()
321        return os.path.abspath(source_dir)
322
323    def _sanity_check_source_dir(self):
324        if self.source_dir == self.build_dir:
325            # There's no forcing this.
326            log.die('source and build directory {} cannot be the same; '
327                    'use --build-dir {} to specify a build directory'.
328                    format(self.source_dir, self.build_dir))
329
330        srcrel = os.path.relpath(self.source_dir)
331        self.check_force(
332            not is_zephyr_build(self.source_dir),
333            'it looks like {srcrel} is a build directory: '
334            'did you mean --build-dir {srcrel} instead?'.
335            format(srcrel=srcrel))
336        self.check_force(
337            'CMakeLists.txt' in os.listdir(self.source_dir),
338            'source directory "{srcrel}" does not contain '
339            'a CMakeLists.txt; is this really what you '
340            'want to build? (Use -s SOURCE_DIR to specify '
341            'the application source directory)'.
342            format(srcrel=srcrel))
343
344    def _sanity_check(self):
345        # Sanity check the build configuration.
346        # Side effect: may update cmake_cache attribute.
347        log.dbg('sanity checking the build', level=log.VERBOSE_EXTREME)
348        self._sanity_check_source_dir()
349
350        if not self.cmake_cache:
351            return          # That's all we can check without a cache.
352
353        if "CMAKE_PROJECT_NAME" not in self.cmake_cache:
354            # This happens sometimes when a build system is not
355            # completely generated due to an error during the
356            # CMake configuration phase.
357            self.run_cmake = True
358
359        cached_app = self.cmake_cache.get('APPLICATION_SOURCE_DIR')
360        log.dbg('APPLICATION_SOURCE_DIR:', cached_app,
361                level=log.VERBOSE_EXTREME)
362        source_abs = (os.path.abspath(self.args.source_dir)
363                      if self.args.source_dir else None)
364        cached_abs = os.path.abspath(cached_app) if cached_app else None
365
366        log.dbg('pristine:', self.auto_pristine, level=log.VERBOSE_EXTREME)
367
368        # If the build directory specifies a source app, make sure it's
369        # consistent with --source-dir.
370        apps_mismatched = (source_abs and cached_abs and
371            pathlib.PurePath(source_abs) != pathlib.PurePath(cached_abs))
372
373        self.check_force(
374            not apps_mismatched or self.auto_pristine,
375            'Build directory "{}" is for application "{}", but source '
376            'directory "{}" was specified; please clean it, use --pristine, '
377            'or use --build-dir to set another build directory'.
378            format(self.build_dir, cached_abs, source_abs))
379
380        if apps_mismatched:
381            self.run_cmake = True  # If they insist, we need to re-run cmake.
382
383        # If CACHED_BOARD is not defined, we need some other way to
384        # find the board.
385        cached_board = self.cmake_cache.get('CACHED_BOARD')
386        log.dbg('CACHED_BOARD:', cached_board, level=log.VERBOSE_EXTREME)
387        # If apps_mismatched and self.auto_pristine are true, we will
388        # run pristine on the build, invalidating the cached
389        # board. In that case, we need some way of getting the board.
390        self.check_force((cached_board and
391                          not (apps_mismatched and self.auto_pristine))
392                         or self.args.board or self.config_board or
393                         os.environ.get('BOARD'),
394                         'Cached board not defined, please provide it '
395                         '(provide --board, set default with '
396                         '"west config build.board <BOARD>", or set '
397                         'BOARD in the environment)')
398
399        # Check consistency between cached board and --board.
400        boards_mismatched = (self.args.board and cached_board and
401                             self.args.board != cached_board)
402        self.check_force(
403            not boards_mismatched or self.auto_pristine,
404            'Build directory {} targets board {}, but board {} was specified. '
405            '(Clean the directory, use --pristine, or use --build-dir to '
406            'specify a different one.)'.
407            format(self.build_dir, cached_board, self.args.board))
408
409        if self.auto_pristine and (apps_mismatched or boards_mismatched):
410            self._run_pristine()
411            self.cmake_cache = None
412            log.dbg('run_cmake:', True, level=log.VERBOSE_EXTREME)
413            self.run_cmake = True
414
415            # Tricky corner-case: The user has not specified a build folder but
416            # there was one in the CMake cache. Since this is going to be
417            # invalidated, reset to CWD and re-run the basic tests.
418            if ((boards_mismatched and not apps_mismatched) and
419                    (not source_abs and cached_abs)):
420                self.source_dir = self._find_source_dir()
421                self._sanity_check_source_dir()
422
423    def _run_cmake(self, board, origin, cmake_opts):
424        if board is None and config_getboolean('board_warn', True):
425            log.wrn('This looks like a fresh build and BOARD is unknown;',
426                    "so it probably won't work. To fix, use",
427                    '--board=<your-board>.')
428            log.inf('Note: to silence the above message, run',
429                    "'west config build.board_warn false'")
430
431        if not self.run_cmake:
432            return
433
434        _banner('generating a build system')
435
436        if board is not None and origin != 'CMakeCache.txt':
437            cmake_opts = ['-DBOARD={}'.format(board)]
438        else:
439            cmake_opts = []
440        if self.args.cmake_opts:
441            cmake_opts.extend(self.args.cmake_opts)
442
443        user_args = config_get('cmake-args', None)
444        if user_args:
445            cmake_opts.extend(shlex.split(user_args))
446
447        # Invoke CMake from the current working directory using the
448        # -S and -B options (officially introduced in CMake 3.13.0).
449        # This is important because users expect invocations like this
450        # to Just Work:
451        #
452        # west build -- -DOVERLAY_CONFIG=relative-path.conf
453        final_cmake_args = ['-DWEST_PYTHON={}'.format(sys.executable),
454                            '-B{}'.format(self.build_dir),
455                            '-S{}'.format(self.source_dir),
456                            '-G{}'.format(config_get('generator',
457                                                     DEFAULT_CMAKE_GENERATOR))]
458        if cmake_opts:
459            final_cmake_args.extend(cmake_opts)
460        run_cmake(final_cmake_args, dry_run=self.args.dry_run)
461
462    def _run_pristine(self):
463        _banner('making build dir {} pristine'.format(self.build_dir))
464        if not is_zephyr_build(self.build_dir):
465            log.die('Refusing to run pristine on a folder that is not a '
466                    'Zephyr build system')
467
468        cache = CMakeCache.from_build_dir(self.build_dir)
469
470        app_src_dir = cache.get('APPLICATION_SOURCE_DIR')
471        app_bin_dir = cache.get('APPLICATION_BINARY_DIR')
472
473        cmake_args = [f'-DBINARY_DIR={app_bin_dir}',
474                      f'-DSOURCE_DIR={app_src_dir}',
475                      '-P', cache['ZEPHYR_BASE'] + '/cmake/pristine.cmake']
476        run_cmake(cmake_args, cwd=self.build_dir, dry_run=self.args.dry_run)
477
478    def _run_build(self, target):
479        if target:
480            _banner('running target {}'.format(target))
481        elif self.run_cmake:
482            _banner('building application')
483        extra_args = ['--target', target] if target else []
484        if self.args.build_opt:
485            extra_args.append('--')
486            extra_args.extend(self.args.build_opt)
487        if self.args.verbose:
488            self._append_verbose_args(extra_args,
489                                      not bool(self.args.build_opt))
490        run_build(self.build_dir, extra_args=extra_args,
491                  dry_run=self.args.dry_run)
492
493    def _append_verbose_args(self, extra_args, add_dashes):
494        # These hacks are only needed for CMake versions earlier than
495        # 3.14. When Zephyr's minimum version is at least that, we can
496        # drop this nonsense and just run "cmake --build BUILD -v".
497        self._update_cache()
498        if not self.cmake_cache:
499            return
500        generator = self.cmake_cache.get('CMAKE_GENERATOR')
501        if not generator:
502            return
503        # Substring matching is for things like "Eclipse CDT4 - Ninja".
504        if 'Ninja' in generator:
505            if add_dashes:
506                extra_args.append('--')
507            extra_args.append('-v')
508        elif generator == 'Unix Makefiles':
509            if add_dashes:
510                extra_args.append('--')
511            extra_args.append('VERBOSE=1')
512