Repository: ARMmbed/mbed-tools

Issue:
README.md : miss cmake and ninja information
### Description

Hi
This morning, I spent some time on a new PC to install this new mbed tool,
This was not working, and I got several not friendly messages...
... till I remembered that I didn't install cmake yet...

So my request would be:
- to update tools when cmake is not installed with some friendly message "please install cmake"
- same for ninja
- to update README.md to add information how to install cmake and ninja

Thx

@0xc0170 
@MarceloSalazar 
@JeanMarcR





### Issue request type

<!--
Please add only one `x` to one of the following types. Do not fill multiple types (split the issue otherwise).


For questions please use https://forums.mbed.com/
-->

- [x] Enhancement
- [ ] Bug


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/ARMmbed/mbed-tools/pull/154?src=pr&el=h1) Report
> Merging [#154](https://codecov.io/gh/ARMmbed/mbed-tools/pull/154?src=pr&el=desc) (78ae5c4) into [master](https://codecov.io/gh/ARMmbed/mbed-tools/commit/9ad9be8ebe4c5cb07a88a7bd151ca845340a2cfe?el=desc) (9ad9be8) will **increase** coverage by `0.08%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/ARMmbed/mbed-tools/pull/154/graphs/tree.svg?width=650&height=150&src=pr&token=QFRIdRv91G)](https://codecov.io/gh/ARMmbed/mbed-tools/pull/154?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #154      +/-   ##
==========================================
+ Coverage   96.50%   96.58%   +0.08%     
==========================================
  Files          95       95              
  Lines        2659     2668       +9     
==========================================
+ Hits         2566     2577      +11     
+ Misses         93       91       -2     
```


| [Impacted Files](https://codecov.io/gh/ARMmbed/mbed-tools/pull/154?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/mbed\_tools/build/build.py](https://codecov.io/gh/ARMmbed/mbed-tools/pull/154/diff?src=pr&el=tree#diff-c3JjL21iZWRfdG9vbHMvYnVpbGQvYnVpbGQucHk=) | `100.00% <100.00%> (ø)` | |
| [...ools/devices/\_internal/detect\_candidate\_devices.py](https://codecov.io/gh/ARMmbed/mbed-tools/pull/154/diff?src=pr&el=tree#diff-c3JjL21iZWRfdG9vbHMvZGV2aWNlcy9faW50ZXJuYWwvZGV0ZWN0X2NhbmRpZGF0ZV9kZXZpY2VzLnB5) | `100.00% <0.00%> (+11.76%)` | :arrow_up: |

jeromecoutant: No update/information in README?
rwalton-arm: > No update/information in README?

We already have [instructions for installing CMake and Ninja](https://github.com/ARMmbed/mbed-os-5-docs/blob/development/docs/tools/mbed_cli_2/install.md) in our user facing documentation. However, we should probably also add them to our README.md, so I've updated it in commit 582c455 
jeromecoutant: > We already have [instructions for installing CMake and Ninja](https://github.com/ARMmbed/mbed-os-5-docs/blob/development/docs/tools/mbed_cli_2/install.md) in our user facing documentation.

So to avoid duplication, you could just add this link in the Installation paragraph ?


Patch:
diff --git a/README.md b/README.md
index fdd2e05..eff3449 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,10 @@ follows:
 
 ## Installation
 
+`mbed-tools` relies on the Ninja build system and CMake.
+- CMake. [Install version 3.19.0 or newer for all operating systems](https://cmake.org/install/).
+- Ninja. [Install version 1.0 or newer for all operating systems](https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages).
+
 We recommend installing `mbed-tools` in a Python virtual environment to avoid dependency conflicts.
 
 To install the most recent production quality release use:
diff --git a/news/20201210131204.bugfix b/news/20201210131204.bugfix
new file mode 100644
index 0000000..65ae014
--- /dev/null
+++ b/news/20201210131204.bugfix
@@ -0,0 +1,1 @@
+Emit more useful error messages if CMake or Ninja aren't found in PATH.
diff --git a/src/mbed_tools/build/build.py b/src/mbed_tools/build/build.py
index 66822bc..2334bc4 100644
--- a/src/mbed_tools/build/build.py
+++ b/src/mbed_tools/build/build.py
@@ -22,6 +22,7 @@ def build_project(build_dir: pathlib.Path, target: Optional[str] = None) -> None
         build_dir: Path to the CMake build tree.
         target: The CMake target to build (e.g 'install')
     """
+    _check_ninja_found()
     target_flag = ["--target", target] if target is not None else []
     _cmake_wrapper("--build", str(build_dir), *target_flag)
 
@@ -34,6 +35,7 @@ def generate_build_system(source_dir: pathlib.Path, build_dir: pathlib.Path, pro
         build_dir: Path to the CMake build tree.
         profile: The Mbed build profile (develop, debug or release).
     """
+    _check_ninja_found()
     _cmake_wrapper("-S", str(source_dir), "-B", str(build_dir), "-GNinja", f"-DCMAKE_BUILD_TYPE={profile}")
 
 
@@ -41,5 +43,16 @@ def _cmake_wrapper(*cmake_args: str) -> None:
     try:
         logger.debug("Running CMake with args: %s", cmake_args)
         subprocess.run(["cmake", *cmake_args], check=True)
+    except FileNotFoundError:
+        raise MbedBuildError("Could not find CMake. Please ensure CMake is installed and added to PATH.")
     except subprocess.CalledProcessError:
         raise MbedBuildError("CMake invocation failed!")
+
+
+def _check_ninja_found() -> None:
+    try:
+        subprocess.run(["ninja", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    except FileNotFoundError:
+        raise MbedBuildError(
+            "Could not find the 'Ninja' build program. Please ensure 'Ninja' is installed and added to PATH."
+        )


Test patch:
diff --git a/tests/build/test_build.py b/tests/build/test_build.py
index b9d32af..5293966 100644
--- a/tests/build/test_build.py
+++ b/tests/build/test_build.py
@@ -2,45 +2,60 @@
 # Copyright (C) 2020 Arm Mbed. All rights reserved.
 # SPDX-License-Identifier: Apache-2.0
 #
-import pathlib
+import subprocess
 
-from tempfile import TemporaryDirectory
-from unittest import TestCase, mock
+from unittest import mock
+
+import pytest
 
 from mbed_tools.build.build import build_project, generate_build_system
 from mbed_tools.build.exceptions import MbedBuildError
 
 
-class TestBuildProject(TestCase):
-    @mock.patch("mbed_tools.build.build._cmake_wrapper")
-    def test_invokes_cmake_with_correct_args(self, cmake_wrapper):
+@pytest.fixture
+def subprocess_run():
+    with mock.patch("mbed_tools.build.build.subprocess.run", autospec=True) as subproc:
+        yield subproc
+
+
+class TestBuildProject:
+    def test_invokes_cmake_with_correct_args(self, subprocess_run):
         build_project(build_dir="cmake_build", target="install")
 
-        cmake_wrapper.assert_called_once_with("--build", "cmake_build", "--target", "install")
+        subprocess_run.assert_called_with(["cmake", "--build", "cmake_build", "--target", "install"], check=True)
 
-    @mock.patch("mbed_tools.build.build._cmake_wrapper")
-    def test_invokes_cmake_with_correct_args_if_no_target_passed(self, cmake_wrapper):
+    def test_invokes_cmake_with_correct_args_if_no_target_passed(self, subprocess_run):
         build_project(build_dir="cmake_build")
 
-        cmake_wrapper.assert_called_once_with("--build", "cmake_build")
+        subprocess_run.assert_called_with(["cmake", "--build", "cmake_build"], check=True)
 
-    def test_raises_build_error_if_build_dir_doesnt_exist(self):
-        with TemporaryDirectory() as tmp_dir:
-            nonexistent_build_dir = pathlib.Path(tmp_dir, "cmake_build")
+    def test_raises_build_error_if_cmake_invocation_fails(self, subprocess_run):
+        subprocess_run.side_effect = (None, subprocess.CalledProcessError(1, ""))
 
-            with self.assertRaises(MbedBuildError):
-                build_project(nonexistent_build_dir)
+        with pytest.raises(MbedBuildError, match="CMake invocation failed"):
+            build_project(build_dir="cmake_build")
 
 
-@mock.patch("mbed_tools.build.build._cmake_wrapper")
-class TestConfigureProject(TestCase):
-    def test_invokes_cmake_with_correct_args(self, cmake_wrapper):
+class TestConfigureProject:
+    def test_invokes_cmake_with_correct_args(self, subprocess_run):
         source_dir = "source_dir"
         build_dir = "cmake_build"
         profile = "debug"
 
         generate_build_system(source_dir, build_dir, profile)
 
-        cmake_wrapper.assert_called_once_with(
-            "-S", source_dir, "-B", build_dir, "-GNinja", f"-DCMAKE_BUILD_TYPE={profile}"
+        subprocess_run.assert_called_with(
+            ["cmake", "-S", source_dir, "-B", build_dir, "-GNinja", f"-DCMAKE_BUILD_TYPE={profile}"], check=True
         )
+
+    def test_raises_when_ninja_cannot_be_found(self, subprocess_run):
+        subprocess_run.side_effect = FileNotFoundError
+
+        with pytest.raises(MbedBuildError, match="Ninja"):
+            generate_build_system("", "", "")
+
+    def test_raises_when_cmake_cannot_be_found(self, subprocess_run):
+        subprocess_run.side_effect = (None, FileNotFoundError)
+
+        with pytest.raises(MbedBuildError, match="Could not find CMake"):
+            generate_build_system("", "", "")