scripts: west_commands: refactor build.py

This is a prep work patch for adding another command.  Refactor
build.py to use a new Forceable superclass and find_build_dir() helper
routine. Fix a help string while we are here.

Signed-off-by: Marti Bolivar <marti@foundries.io>
diff --git a/scripts/west_commands/build.py b/scripts/west_commands/build.py
index 2e7b426..78d7668 100644
--- a/scripts/west_commands/build.py
+++ b/scripts/west_commands/build.py
@@ -7,9 +7,10 @@
 
 from west import log
 from west import cmake
-from west.build import DEFAULT_BUILD_DIR, DEFAULT_CMAKE_GENERATOR, \
-    is_zephyr_build
-from west.commands import WestCommand
+from west.build import DEFAULT_CMAKE_GENERATOR, is_zephyr_build
+
+from zephyr_ext_common import find_build_dir, Forceable, BUILD_DIR_DESCRIPTION
+
 
 BUILD_DESCRIPTION = '''\
 Convenience wrapper for building Zephyr applications.
@@ -36,11 +37,14 @@
 to run by setting --cmake.
 
 To pass additional options to CMake, give them as extra arguments
-after a '--' For example, "west build -- -DOVERLAY_CONFIG=some.conf" sets
-an overlay config file. (Doing this forces a CMake run.)'''
+after a '--'. For example, this sets an overlay config file:
+
+west build [...] -- -DOVERLAY_CONFIG=some.conf
+
+(Doing this forces a CMake run.)'''
 
 
-class Build(WestCommand):
+class Build(Forceable):
 
     def __init__(self):
         super(Build, self).__init__(
@@ -89,18 +93,14 @@
                             cache. Otherwise, the current directory is
                             assumed.''')
         parser.add_argument('-d', '--build-dir',
-                            help='''Explicitly sets the build directory.
-                            If not given and the current directory is a Zephyr
-                            build directory, it will be used; otherwise, "{}"
-                            is assumed. The directory will be created if
-                            it doesn't exist.'''.format(DEFAULT_BUILD_DIR))
+                            help=BUILD_DIR_DESCRIPTION +
+                            "The directory is created if it doesn't exist.")
         parser.add_argument('-t', '--target',
                             help='''Override the build system target (e.g.
                             'clean', 'pristine', etc.)''')
         parser.add_argument('-c', '--cmake', action='store_true',
                             help='Force CMake to run')
-        parser.add_argument('-f', '--force', action='store_true',
-                            help='Ignore any errors and try to build anyway')
+        self.add_force_arg(parser)
         parser.add_argument('cmake_opts', nargs='*', metavar='cmake_opt',
                             help='Extra option to pass to CMake; implies -c')
 
@@ -144,12 +144,12 @@
     def _sanity_precheck(self):
         app = self.args.source_dir
         if app:
-            if not os.path.isdir(app):
-                self._check_force('source directory {} does not exist'.
-                                  format(app))
-            elif 'CMakeLists.txt' not in os.listdir(app):
-                self._check_force("{} doesn't contain a CMakeLists.txt".
-                                  format(app))
+            self.check_force(
+                os.path.isdir(app),
+                'source directory {} does not exist'.format(app))
+            self.check_force(
+                'CMakeLists.txt' in os.listdir(app),
+                "{} doesn't contain a CMakeLists.txt".format(app))
 
     def _update_cache(self):
         try:
@@ -159,16 +159,9 @@
 
     def _setup_build_dir(self):
         # Initialize build_dir and created_build_dir attributes.
+        # If we created the build directory, we must run CMake.
         log.dbg('setting up build directory', level=log.VERBOSE_EXTREME)
-        if self.args.build_dir:
-            build_dir = self.args.build_dir
-        else:
-            cwd = os.getcwd()
-            if is_zephyr_build(cwd):
-                build_dir = cwd
-            else:
-                build_dir = DEFAULT_BUILD_DIR
-        build_dir = os.path.abspath(build_dir)
+        build_dir = find_build_dir(self.args.build_dir)
 
         if os.path.exists(build_dir):
             if not os.path.isdir(build_dir):
@@ -211,20 +204,21 @@
                     format(self.source_dir, self.build_dir))
 
         srcrel = os.path.relpath(self.source_dir)
-        if is_zephyr_build(self.source_dir):
-            self._check_force('it looks like {srcrel} is a build directory: '
-                              'did you mean -build-dir {srcrel} instead?'.
-                              format(srcrel=srcrel))
-        elif 'CMakeLists.txt' not in os.listdir(self.source_dir):
-            self._check_force('source directory "{srcrel}" does not contain '
-                              'a CMakeLists.txt; is that really what you '
-                              'want to build? (Use -s SOURCE_DIR to specify '
-                              'the application source directory)'.
-                              format(srcrel=srcrel))
-
-        if not is_zephyr_build(self.build_dir) and not self.args.board:
-            self._check_force('this looks like a new or clean build, '
-                              'please provide --board')
+        self.check_force(
+            not is_zephyr_build(self.source_dir),
+            'it looks like {srcrel} is a build directory: '
+            'did you mean --build-dir {srcrel} instead?'.
+            format(srcrel=srcrel))
+        self.check_force(
+            'CMakeLists.txt' in os.listdir(self.source_dir),
+            'source directory "{srcrel}" does not contain '
+            'a CMakeLists.txt; is this really what you '
+            'want to build? (Use -s SOURCE_DIR to specify '
+            'the application source directory)'.
+            format(srcrel=srcrel))
+        self.check_force(
+            is_zephyr_build(self.build_dir) or self.args.board,
+            'this looks like a new or clean build, please provide --board')
 
         if not self.cmake_cache:
             return          # That's all we can check without a cache.
@@ -235,37 +229,36 @@
         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
-        if cached_abs and source_abs and source_abs != cached_abs:
-            self._check_force('build directory "{}" is for application "{}", '
-                              'but source directory "{}" was specified; '
-                              'please clean it or use --build-dir to set '
-                              'another build directory'.
-                              format(self.build_dir, cached_abs,
-                                     source_abs))
+
+        # If the build directory specifies a source app, make sure it's
+        # consistent with --source-dir.
+        apps_mismatched = (source_abs and cached_abs and
+                           source_abs != cached_abs)
+        self.check_force(
+            not apps_mismatched,
+            'Build directory "{}" is for application "{}", but source '
+            'directory "{}" was specified; please clean it or use --build-dir '
+            'to set another build directory'.
+            format(self.build_dir, cached_abs, source_abs))
+        if apps_mismatched:
             self.run_cmake = True  # If they insist, we need to re-run cmake.
 
+        # If CACHED_BOARD is not defined, we need --board from the
+        # command line.
         cached_board = self.cmake_cache.get('CACHED_BOARD')
         log.dbg('CACHED_BOARD:', cached_board, level=log.VERBOSE_EXTREME)
-        if not cached_board and not self.args.board:
-            if self.created_build_dir:
-                self._check_force(
-                    'Building for the first time: you must provide --board')
-            else:
-                self._check_force(
-                    'Board is missing or unknown, please provide --board')
-        if self.args.board and cached_board and \
-           self.args.board != cached_board:
-            self._check_force('Build directory {} targets board {}, '
-                              'but board {} was specified. (Clean that '
-                              'directory or use --build-dir to specify '
-                              'a different one.)'.
-                              format(self.build_dir, cached_board,
-                                     self.args.board))
+        self.check_force(cached_board or self.args.board,
+                         'Cached board not defined, please provide --board')
 
-    def _check_force(self, msg):
-        if not self.args.force:
-            log.err(msg)
-            log.die('refusing to proceed without --force due to above error')
+        # 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,
+            'Build directory {} targets board {}, but board {} was specified. '
+            '(Clean the directory or use --build-dir to specify a different '
+            'one.)'.
+            format(self.build_dir, cached_board, self.args.board))
 
     def _run_cmake(self, cmake_opts):
         if not self.run_cmake: