Repository: ARMmbed/mbed-tools

Issue:
Missing `--app-config` option
**Describe the bug**
Mbed CLI 1 offers an `--app-config` option to let users specify the application configuration JSON instead of assuming `mbed_app.json`. This is not currently provided by mbed-tools, but useful when
* one application provides multiple configurations for different use cases
* different applications or tests share one common configuration (e.g. [experimental.json](https://github.com/ARMmbed/mbed-os/blob/master/TESTS/configs/experimental.json))

**To Reproduce**
Try to build an application (e.g. blinky) with `--app-config`, for example
```
mbed-tools compile -t GCC_ARM -m K64F --app-config mbed-os/TESTS/configs/experimental.json
```
and the error is
```
Error: no such option: --app-config
```

**Expected behavior**
An option `--app-config` is available to both `mbed-tools configure` and `mbed-tools compile`. The specified JSON is used for generating `mbed_config.cmake`.

**Screenshots**
N/A

**Desktop (please complete the following information):**
 - OS: Any
 - Version: Any

**Mbed (please complete the following information):**
 - Device: Any
 - Mbed OS Version: 6.11.0
 - Mbed CLI 2 Version: 7.23.0

**Additional context**
N/A


Comments:

Patch:
diff --git a/news/291.bugfix b/news/291.bugfix
new file mode 100644
index 0000000..c7487e4
--- /dev/null
+++ b/news/291.bugfix
@@ -0,0 +1,1 @@
+Add an option `--app-config` to `configure` and `build` commands to allow users to specify an application configuration file.
diff --git a/src/mbed_tools/cli/build.py b/src/mbed_tools/cli/build.py
index f548d60..60d2e06 100644
--- a/src/mbed_tools/cli/build.py
+++ b/src/mbed_tools/cli/build.py
@@ -22,9 +22,10 @@ from mbed_tools.sterm import terminal
     "-t",
     "--toolchain",
     type=click.Choice(["ARM", "GCC_ARM"], case_sensitive=False),
+    required=True,
     help="The toolchain you are using to build your app.",
 )
-@click.option("-m", "--mbed-target", help="A build target for an Mbed-enabled device, e.g. K64F.")
+@click.option("-m", "--mbed-target", required=True, help="A build target for an Mbed-enabled device, e.g. K64F.")
 @click.option("-b", "--profile", default="develop", help="The build type (release, develop or debug).")
 @click.option("-c", "--clean", is_flag=True, default=False, help="Perform a clean build.")
 @click.option(
@@ -39,6 +40,9 @@ from mbed_tools.sterm import terminal
 @click.option(
     "--custom-targets-json", type=click.Path(), default=None, help="Path to custom_targets.json.",
 )
+@click.option(
+    "--app-config", type=click.Path(), default=None, help="Path to application configuration file.",
+)
 @click.option(
     "-f", "--flash", is_flag=True, default=False, help="Flash the binary onto a device",
 )
@@ -54,14 +58,15 @@ from mbed_tools.sterm import terminal
 def build(
     program_path: str,
     profile: str,
-    toolchain: str = "",
-    mbed_target: str = "",
-    clean: bool = False,
-    flash: bool = False,
-    sterm: bool = False,
-    baudrate: int = 9600,
-    mbed_os_path: str = None,
-    custom_targets_json: str = None,
+    toolchain: str,
+    mbed_target: str,
+    clean: bool,
+    flash: bool,
+    sterm: bool,
+    baudrate: int,
+    mbed_os_path: str,
+    custom_targets_json: str,
+    app_config: str,
 ) -> None:
     """Configure and build an Mbed project using CMake and Ninja.
 
@@ -75,12 +80,12 @@ def build(
        custom_targets_json: Path to custom_targets.json.
        toolchain: The toolchain to use for the build.
        mbed_target: The name of the Mbed target to build for.
+       app_config: the path to the application configuration file
        clean: Perform a clean build.
        flash: Flash the binary onto a device.
        sterm: Open a serial terminal to the connected target.
        baudrate: Change the serial baud rate (ignored unless --sterm is also given).
     """
-    _validate_target_and_toolchain_args(mbed_target, toolchain)
     mbed_target, target_id = _get_target_id(mbed_target)
 
     cmake_build_subdir = pathlib.Path(mbed_target.upper(), profile.lower(), toolchain.upper())
@@ -95,6 +100,8 @@ def build(
     click.echo("Configuring project and generating build system...")
     if custom_targets_json is not None:
         program.files.custom_targets_json = pathlib.Path(custom_targets_json)
+    if app_config is not None:
+        program.files.app_config_file = pathlib.Path(app_config)
     config, _ = generate_config(mbed_target.upper(), toolchain, program)
     generate_build_system(program.root, build_tree, profile)
 
@@ -124,13 +131,6 @@ def build(
         terminal.run(dev.serial_port, baudrate)
 
 
-def _validate_target_and_toolchain_args(target: str, toolchain: str) -> None:
-    if not all([toolchain, target]):
-        raise click.UsageError(
-            "Both --toolchain and --mbed-target arguments are required when using the compile subcommand."
-        )
-
-
 def _get_target_id(target: str) -> Tuple[str, Optional[int]]:
     if "[" in target:
         target_name, target_id = target.replace("]", "").split("[", maxsplit=1)
diff --git a/src/mbed_tools/cli/configure.py b/src/mbed_tools/cli/configure.py
index e7279d6..360c389 100644
--- a/src/mbed_tools/cli/configure.py
+++ b/src/mbed_tools/cli/configure.py
@@ -36,8 +36,17 @@ from mbed_tools.build import generate_config
 @click.option(
     "--mbed-os-path", type=click.Path(), default=None, help="Path to local Mbed OS directory.",
 )
+@click.option(
+    "--app-config", type=click.Path(), default=None, help="Path to application configuration file.",
+)
 def configure(
-    toolchain: str, mbed_target: str, program_path: str, mbed_os_path: str, output_dir: str, custom_targets_json: str
+    toolchain: str,
+    mbed_target: str,
+    program_path: str,
+    mbed_os_path: str,
+    output_dir: str,
+    custom_targets_json: str,
+    app_config: str
 ) -> None:
     """Exports a mbed_config.cmake file to build directory in the program root.
 
@@ -55,6 +64,7 @@ def configure(
         program_path: the path to the local Mbed program
         mbed_os_path: the path to the local Mbed OS directory
         output_dir: the path to the output directory
+        app_config: the path to the application configuration file
     """
     cmake_build_subdir = pathlib.Path(mbed_target.upper(), "develop", toolchain.upper())
     if mbed_os_path is None:
@@ -65,6 +75,8 @@ def configure(
         program.files.custom_targets_json = pathlib.Path(custom_targets_json)
     if output_dir is not None:
         program.files.cmake_build_dir = pathlib.Path(output_dir)
+    if app_config is not None:
+        program.files.app_config_file = pathlib.Path(app_config)
 
     mbed_target = mbed_target.upper()
     _, output_path = generate_config(mbed_target, toolchain, program)


Test patch:
diff --git a/tests/cli/test_build.py b/tests/cli/test_build.py
index 860d275..d680ee9 100644
--- a/tests/cli/test_build.py
+++ b/tests/cli/test_build.py
@@ -116,18 +116,6 @@ class TestBuildCommand(TestCase):
             self.assertIsNotNone(result.exception)
             self.assertRegex(result.output, "--mbed-target")
 
-    def test_raises_if_gen_config_target_toolchain_not_passed(
-        self, generate_config, mbed_program, build_project, generate_build_system
-    ):
-        program = mbed_program.from_existing()
-        with mock_project_directory(program):
-            runner = CliRunner()
-            result = runner.invoke(build)
-
-            self.assertIsNotNone(result.exception)
-            self.assertRegex(result.output, "--mbed-target")
-            self.assertRegex(result.output, "--toolchain")
-
     def test_raises_if_target_identifier_not_int(
         self, generate_config, mbed_program, build_project, generate_build_system
     ):
@@ -183,6 +171,21 @@ class TestBuildCommand(TestCase):
             generate_config.assert_called_once_with(target.upper(), toolchain.upper(), program)
             self.assertEqual(program.files.custom_targets_json, custom_targets_json_path)
 
+    def test_app_config_used_when_passed(
+        self, generate_config, mbed_program, build_project, generate_build_system
+    ):
+        program = mbed_program.from_existing()
+        with mock_project_directory(program, mbed_config_exists=True, build_tree_exists=True):
+            toolchain = "gcc_arm"
+            target = "k64f"
+            app_config_path = pathlib.Path("alternative_config.json")
+
+            runner = CliRunner()
+            runner.invoke(build, ["-t", toolchain, "-m", target, "--app-config", app_config_path])
+
+            generate_config.assert_called_once_with(target.upper(), toolchain.upper(), program)
+            self.assertEqual(program.files.app_config_file, app_config_path)
+
     def test_build_folder_removed_when_clean_flag_passed(
         self, generate_config, mbed_program, build_project, generate_build_system
     ):
diff --git a/tests/cli/test_configure.py b/tests/cli/test_configure.py
index a0c61fd..2ae90b1 100644
--- a/tests/cli/test_configure.py
+++ b/tests/cli/test_configure.py
@@ -47,3 +47,15 @@ class TestConfigureCommand(TestCase):
 
         generate_config.assert_called_once_with("K64F", "GCC_ARM", program)
         self.assertEqual(program.files.cmake_build_dir, output_dir)
+
+    @mock.patch("mbed_tools.cli.configure.generate_config")
+    @mock.patch("mbed_tools.cli.configure.MbedProgram")
+    def test_app_config_used_when_passed(self, program, generate_config):
+        program = program.from_existing()
+        app_config_path = pathlib.Path("alternative_config.json")
+        CliRunner().invoke(
+            configure, ["-t", "gcc_arm", "-m", "k64f", "--app-config", app_config_path]
+        )
+
+        generate_config.assert_called_once_with("K64F", "GCC_ARM", program)
+        self.assertEqual(program.files.app_config_file, app_config_path)