Repository: conan-io/conan
Pull request: [toolchain] CMake + Ninja
Changelog: Omit
Docs: omit
closes #7814

Testing CMakeToolchain with Ninja generator

CMake + Ninja only uses a specific Ninja var: [CMAKE_NINJA_OUTPUT_PATH_PREFIX](https://cmake.org/cmake/help/v3.7/variable/CMAKE_NINJA_OUTPUT_PATH_PREFIX.html), but it doesn't affect our jobs. It is used for extra ninja files dir, by default is present in CMakeFiles/ folder.

More info about CMake and Ninja: https://cmake.org/cmake/help/latest/generator/Ninja.html


- [ ] Refer to the issue that supports this Pull Request.
- [ ] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [ ] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [ ] I've followed the PEP8 style guides for Python code.
- [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one. 

<sup>**Note:** By default this PR will skip the slower tests and will use a limited set of python versions. Check [here](https://github.com/conan-io/conan/blob/develop/.github/PR_INCREASE_TESTING.md) how to increase the testing level by writing some tags in the current PR body text.</sup>


Problem statement:
[feature] CMakeToolchain + Ninja PoC
Implement a proof of concept of using the new CMakeToolchain feature for building with Ninja:

- From Linux
- From Windows

Notes:
- Ninja can be assumed installed. 
- Opt-in to use Ninja instead of the default (MSBuild, Makefiles) build system, or hardcoded in the recipe, investigate alternatives
- Focused on creating/building 1 package, both local flow (conan install + native cmake) and ``conan create`` flow.
- Integration test.
- Test can be skipped for CI, but should work locally, annotate assumptions and installation details
- No env-var configuration at all
- All code must be private and local to the toolchains package
- New cross-build model with contexts must be used if necessary, not the old os_build, arch_build
- If enough time to implement reuse of a package, the ``cmake_find_package_multi`` generator must be used

Hints from issue discussion:
I would like to take this one.

Commit message: #7814 Add new integration test for cmake-ninja

Signed-off-by: Uilian Ries <uilianries@gmail.com>

Patch to review:
diff --git a/conans/test/integration/toolchains/cmake/__init__.py b/conans/test/integration/toolchains/cmake/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/conans/test/integration/toolchains/cmake/test_ninja.py b/conans/test/integration/toolchains/cmake/test_ninja.py
new file mode 100644
index 00000000000..15ba28580df
--- /dev/null
+++ b/conans/test/integration/toolchains/cmake/test_ninja.py
@@ -0,0 +1,127 @@
+import shutil
+import textwrap
+import unittest
+import os
+
+from conans.test.utils.tools import TestClient
+from conans.test.utils.test_files import temp_folder
+from conans.client.tools import environment_append
+
+
+class CppProject(object):
+
+    header = textwrap.dedent("""
+        #include <string>
+        int bar(const std::string& str);
+    """)
+
+    source = textwrap.dedent("""
+        #include "foobar.hpp"
+        #include <iostream>
+        int bar(const std::string& str) {
+            std::cout << "(BAR): " << str << std::endl;
+            return 0;
+        }
+    """)
+
+    cmakefile = textwrap.dedent("""
+        cmake_minimum_required(VERSION 2.8.12)
+        project(foobar CXX)
+        add_library(${CMAKE_PROJECT_NAME} foobar.hpp foobar.cpp)
+        set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES PUBLIC_HEADER foobar.hpp)
+        install(TARGETS ${CMAKE_PROJECT_NAME}
+            RUNTIME DESTINATION bin
+            LIBRARY DESTINATION lib
+            ARCHIVE DESTINATION lib
+            PUBLIC_HEADER DESTINATION include
+        )
+    """)
+
+    def create_project(self, testclient):
+        testclient.save({
+            "foobar.hpp": CppProject.header,
+            "foobar.cpp": CppProject.source,
+            "CMakeLists.txt": CppProject.cmakefile
+        })
+
+
+class CMakeNinjaTestCase(unittest.TestCase):
+    # This test assumes that 'CMake' and 'Ninja' are available in the system
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile, CMake, CMakeToolchain
+
+        class Foobar(ConanFile):
+            name = "foobar"
+            settings = "os", "arch", "compiler", "build_type"
+            exports_sources = "CMakeLists.txt", "foobar.hpp", "foobar.cpp"
+            options = {"shared": [True, False]}
+            default_options = {"shared": False}
+
+            def toolchain(self):
+                tc = CMakeToolchain(self)
+                # tc.preprocessor_definitions["CMAKE_NINJA_OUTPUT_PATH_PREFIX"] = "MyValue"
+                tc.write_toolchain_files()
+
+            def build(self):
+                cmake = CMake(self)
+                cmake.configure()
+                cmake.build()
+
+            def package(self):
+                cmake = CMake(self)
+
+                cmake.configure()
+                cmake.install()
+    """)
+
+    @classmethod
+    def setUpClass(cls):
+        if not shutil.which("ninja"):
+            raise unittest.SkipTest("Ninja expected in PATH")
+
+    def setUp(self):
+        folder = temp_folder(False)
+        cpp_project = CppProject()
+        self.client = TestClient(current_folder=folder)
+        cpp_project.create_project(self.client)
+        self.client.save({
+            "conanfile.py": CMakeNinjaTestCase.conanfile,
+        })
+
+    def test_regular_build(self):
+        """ Ninja build must proceed using default profile and conan create
+        """
+        with environment_append({"CONAN_CMAKE_GENERATOR": "Ninja"}):
+            self.client.run("create . foobar/0.1.0@")
+            self.assertIn('CMake command: cmake -G "Ninja" '
+                          '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"', self.client.out)
+
+        conanfile = CMakeNinjaTestCase.conanfile.replace("(self)", "(self, generator='Ninja')")
+        self.client.save({
+            "conanfile.py": conanfile,
+        })
+        self.client.run("create . foobar/0.1.0@")
+        self.assertIn('CMake command: cmake -G "Ninja" '
+                      '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"', self.client.out)
+
+    def test_devflow_build(self):
+        """ Ninja build must proceed using default profile and conan development flow
+        """
+        conanfile = CMakeNinjaTestCase.conanfile.replace("(self)", "(self, generator='Ninja')")
+        self.client.save({
+            "conanfile.py": conanfile,
+        })
+
+        build_folder = os.path.join(self.client.current_folder, "build")
+        package_folder = os.path.join(self.client.current_folder, "pkg")
+        with environment_append({"CONAN_PRINT_RUN_COMMANDS": "1"}):
+            self.client.run("export . foobar/0.1.0@")
+            self.client.run("install . --install-folder={}".format(build_folder))
+            self.client.run("build . --build-folder={}".format(build_folder))
+            self.assertIn('CMake command: cmake -G "Ninja" '
+                          '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"', self.client.out)
+            # FIXME: conan package tries to install on /usr/local. CMAKE_PREFIX_PATH is empty
+            self.client.run("package . --build-folder={} --package-folder={}"
+                            .format(build_folder, package_folder), assert_error=True)
+            self.assertIn('Permission denied.', self.client.out)


Code review comments:
diff_hunk: @@ -0,0 +1,127 @@
+import shutil
+import textwrap
+import unittest
+import os
+
+from conans.test.utils.tools import TestClient
+from conans.test.utils.test_files import temp_folder
+from conans.client.tools import environment_append
+
+
+class CppProject(object):
+
+    header = textwrap.dedent("""
+        #include <string>
+        int bar(const std::string& str);
+    """)
+
+    source = textwrap.dedent("""
+        #include "foobar.hpp"
+        #include <iostream>
+        int bar(const std::string& str) {
+            std::cout << "(BAR): " << str << std::endl;
+            return 0;
+        }
+    """)
+
+    cmakefile = textwrap.dedent("""
+        cmake_minimum_required(VERSION 2.8.12)
+        project(foobar CXX)
+        add_library(${CMAKE_PROJECT_NAME} foobar.hpp foobar.cpp)
+        set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES PUBLIC_HEADER foobar.hpp)
+        install(TARGETS ${CMAKE_PROJECT_NAME}
+            RUNTIME DESTINATION bin
+            LIBRARY DESTINATION lib
+            ARCHIVE DESTINATION lib
+            PUBLIC_HEADER DESTINATION include
+        )
+    """)
+
+    def create_project(self, testclient):
+        testclient.save({
+            "foobar.hpp": CppProject.header,
+            "foobar.cpp": CppProject.source,
+            "CMakeLists.txt": CppProject.cmakefile
+        })
+
+
+class CMakeNinjaTestCase(unittest.TestCase):
+    # This test assumes that 'CMake' and 'Ninja' are available in the system
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile, CMake, CMakeToolchain
+
+        class Foobar(ConanFile):
+            name = "foobar"
+            settings = "os", "arch", "compiler", "build_type"
+            exports_sources = "CMakeLists.txt", "foobar.hpp", "foobar.cpp"
+            options = {"shared": [True, False]}
+            default_options = {"shared": False}
+
+            def toolchain(self):
+                tc = CMakeToolchain(self)
+                # tc.preprocessor_definitions["CMAKE_NINJA_OUTPUT_PATH_PREFIX"] = "MyValue"
+                tc.write_toolchain_files()
+
+            def build(self):
+                cmake = CMake(self)
+                cmake.configure()
+                cmake.build()
+
+            def package(self):
+                cmake = CMake(self)
+
+                cmake.configure()
+                cmake.install()
+    """)
+
+    @classmethod
+    def setUpClass(cls):
+        if not shutil.which("ninja"):
+            raise unittest.SkipTest("Ninja expected in PATH")
+
+    def setUp(self):
+        folder = temp_folder(False)
+        cpp_project = CppProject()
+        self.client = TestClient(current_folder=folder)
+        cpp_project.create_project(self.client)
+        self.client.save({
+            "conanfile.py": CMakeNinjaTestCase.conanfile,
+        })
+
+    def test_regular_build(self):
+        """ Ninja build must proceed using default profile and conan create
+        """
+        with environment_append({"CONAN_CMAKE_GENERATOR": "Ninja"}):
+            self.client.run("create . foobar/0.1.0@")
+            self.assertIn('CMake command: cmake -G "Ninja" '
+                          '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"', self.client.out)
+
+        conanfile = CMakeNinjaTestCase.conanfile.replace("(self)", "(self, generator='Ninja')")
+        self.client.save({
+            "conanfile.py": conanfile,
+        })
+        self.client.run("create . foobar/0.1.0@")
+        self.assertIn('CMake command: cmake -G "Ninja" '
+                      '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"', self.client.out)
+
+    def test_devflow_build(self):
+        """ Ninja build must proceed using default profile and conan development flow
+        """
+        conanfile = CMakeNinjaTestCase.conanfile.replace("(self)", "(self, generator='Ninja')")
+        self.client.save({
+            "conanfile.py": conanfile,
+        })
+
+        build_folder = os.path.join(self.client.current_folder, "build")
+        package_folder = os.path.join(self.client.current_folder, "pkg")
+        with environment_append({"CONAN_PRINT_RUN_COMMANDS": "1"}):
+            self.client.run("export . foobar/0.1.0@")
+            self.client.run("install . --install-folder={}".format(build_folder))
+            self.client.run("build . --build-folder={}".format(build_folder))
+            self.assertIn('CMake command: cmake -G "Ninja" '
+                          '-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"', self.client.out)
+            # FIXME: conan package tries to install on /usr/local. CMAKE_PREFIX_PATH is empty
+            self.client.run("package . --build-folder={} --package-folder={}"
path: conans/test/integration/toolchains/cmake/test_ninja.py
text: @user1:
Don't worry about local packaging at the moment, this can wait, can be removed.