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