Repository: conan-io/conan

Issue:
[feature] CMakeToolchain + Android POC
Implement a proof of concept of using the new CMakeToolchain feature for building:

- Android projects (from Linux/OSX)
- From Windows

Notes:
- Android NDK can be assumed installed
- 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



Discussion:

Resolving patch:
diff --git a/conans/client/toolchain/cmake/android.py b/conans/client/toolchain/cmake/android.py
--- a/conans/client/toolchain/cmake/android.py
+++ b/conans/client/toolchain/cmake/android.py
@@ -1,5 +1,93 @@
+import os
+import textwrap
+
+from conans.client.tools.files import which
+from conans.errors import ConanException
 from .base import CMakeToolchainBase
 
 
 class CMakeAndroidToolchain(CMakeToolchainBase):
-    pass
+    _toolchain_tpl = textwrap.dedent("""
+        {% extends 'base_toolchain' %}
+
+        {% block before_try_compile %}
+            {{ super() }}
+
+            set(CMAKE_SYSTEM_NAME {{ CMAKE_SYSTEM_NAME }})
+            set(CMAKE_SYSTEM_VERSION {{ CMAKE_SYSTEM_VERSION }})
+            set(CMAKE_ANDROID_ARCH_ABI {{ CMAKE_ANDROID_ARCH_ABI }})
+            set(CMAKE_ANDROID_STL_TYPE {{ CMAKE_ANDROID_STL_TYPE }})
+            set(CMAKE_ANDROID_NDK {{ CMAKE_ANDROID_NDK }})
+        {% endblock %}
+
+        {% block main %}
+            {{ super() }}
+
+            {% if shared_libs -%}
+            message(STATUS "Conan toolchain: Setting BUILD_SHARED_LIBS= {{ shared_libs }}")
+            set(BUILD_SHARED_LIBS {{ shared_libs }})
+            {%- endif %}
+
+            {% if parallel -%}
+            set(CONAN_CXX_FLAGS "${CONAN_CXX_FLAGS} {{ parallel }}")
+            set(CONAN_C_FLAGS "${CONAN_C_FLAGS} {{ parallel }}")
+            {%- endif %}
+
+            {% if cppstd -%}
+            message(STATUS "Conan C++ Standard {{ cppstd }} with extensions {{ cppstd_extensions }}}")
+            set(CMAKE_CXX_STANDARD {{ cppstd }})
+            set(CMAKE_CXX_EXTENSIONS {{ cppstd_extensions }})
+            {%- endif %}
+
+            set(CMAKE_CXX_FLAGS_INIT "${CONAN_CXX_FLAGS}" CACHE STRING "" FORCE)
+            set(CMAKE_C_FLAGS_INIT "${CONAN_C_FLAGS}" CACHE STRING "" FORCE)
+            set(CMAKE_SHARED_LINKER_FLAGS_INIT "${CONAN_SHARED_LINKER_FLAGS}" CACHE STRING "" FORCE)
+            set(CMAKE_EXE_LINKER_FLAGS_INIT "${CONAN_EXE_LINKER_FLAGS}" CACHE STRING "" FORCE)
+        {% endblock %}
+    """)
+
+    # TODO: fPIC, fPIE
+    # TODO: RPATH, cross-compiling to Android?
+    # TODO: libcxx, only libc++ https://developer.android.com/ndk/guides/cpp-support
+
+    def __init__(self, conanfile, build_type=None, **kwargs):
+        super(CMakeAndroidToolchain, self).__init__(conanfile, build_type=build_type, **kwargs)
+        self.build_type = build_type or self._conanfile.settings.get_safe("build_type")
+
+    def _get_templates(self):
+        templates = super(CMakeAndroidToolchain, self)._get_templates()
+        templates.update({
+            CMakeToolchainBase.filename: self._toolchain_tpl,
+        })
+        return templates
+
+    def _get_android_abi(self):
+        return {"x86": "x86",
+                "x86_64": "x86_64",
+                "armv7": "armeabi-v7a",
+                "armv8": "arm64-v8a"}.get(str(self._conanfile.settings.arch))
+
+    def _get_android_stl(self):
+        libcxx_str = str(self._conanfile.settings.compiler.libcxx)
+        return libcxx_str  # TODO: only 'c++_shared' y 'c++_static' supported?
+
+    def _guess_android_ndk(self):
+        # TODO: Do not use envvar! This has to be provided by the user somehow
+        android_ndk = os.getenv("CONAN_CMAKE_ANDROID_NDK")
+        if not android_ndk:
+            android_ndk = which('ndk-build')
+            android_ndk = os.path.dirname(android_ndk) if android_ndk else None
+        if not android_ndk:
+            raise ConanException('Cannot find ANDROID_NDK (ndk-build) in the PATH')
+        return android_ndk
+
+    def _get_template_context_data(self):
+        ctxt_toolchain, _ = super(CMakeAndroidToolchain, self)._get_template_context_data()
+        ctxt_toolchain.update({
+            'CMAKE_SYSTEM_NAME': 'Android',
+            'CMAKE_SYSTEM_VERSION': self._conanfile.settings.os.api_level,
+            'CMAKE_ANDROID_ARCH_ABI': self._get_android_abi(),
+            'CMAKE_ANDROID_STL_TYPE': self._get_android_stl(),
+            'CMAKE_ANDROID_NDK': self._guess_android_ndk(),
+        })
+        return ctxt_toolchain, {}


Test patch:
diff --git a/conans/test/integration/toolchains/__init__.py b/conans/test/integration/toolchains/__init__.py
new file mode 100644
diff --git a/conans/test/integration/toolchains/android/__init__.py b/conans/test/integration/toolchains/android/__init__.py
new file mode 100644
diff --git a/conans/test/integration/toolchains/android/_utils.py b/conans/test/integration/toolchains/android/_utils.py
new file mode 100644
--- /dev/null
+++ b/conans/test/integration/toolchains/android/_utils.py
@@ -0,0 +1,38 @@
+import textwrap
+
+lib_h = textwrap.dedent("""
+    int some_function(int value);
+""")
+
+lib_cpp = textwrap.dedent("""
+    #include "lib.h"
+    #include <iostream>
+
+    int some_function(int value) {
+        std::cout << "some_function(value=" << value << ")" << std::endl;
+        return 42;
+    }
+""")
+
+cmakelists = textwrap.dedent("""
+    cmake_minimum_required(VERSION 2.8.12)  # TODO: Define minimun required here
+    project(AndroidLibrary CXX)
+
+    add_library(library lib.h lib.cpp)
+    set_target_properties(library PROPERTIES PUBLIC_HEADER lib.h)
+
+    install(TARGETS library
+        RUNTIME DESTINATION bin
+        LIBRARY DESTINATION lib
+        ARCHIVE DESTINATION lib
+        PUBLIC_HEADER DESTINATION include
+    )
+""")
+
+
+def create_library(client):
+    client.save({
+        'lib.h': lib_h,
+        'lib.cpp': lib_cpp,
+        'CMakeLists.txt': cmakelists
+    })
diff --git a/conans/test/integration/toolchains/android/test_using_cmake.py b/conans/test/integration/toolchains/android/test_using_cmake.py
new file mode 100644
--- /dev/null
+++ b/conans/test/integration/toolchains/android/test_using_cmake.py
@@ -0,0 +1,70 @@
+import textwrap
+import unittest
+
+from conans.client.toolchain.cmake.base import CMakeToolchainBase
+from conans.client.tools import which
+from conans.test.utils.tools import TestClient
+from ._utils import create_library
+
+
+class AndroidToolchainTestCase(unittest.TestCase):
+    # This test assumes that 'CMake' and 'AndroidNDK' are available in the system
+    #
+    # Guidelines: https://developer.android.com/ndk/guides/cmake#command-line
+
+    @classmethod
+    def setUpClass(cls):
+        if not which('cmake'):
+            raise unittest.SkipTest("CMake expected in PATH")
+        if not which('ndk-build'):
+            raise unittest.SkipTest("ANDROID_NDK (ndk-build) expected in PATH")
+
+    def setUp(self):
+        self.t = TestClient()
+        create_library(self.t)
+        self.t.save({
+            'conanfile.py': textwrap.dedent("""
+                from conans import ConanFile, CMake, CMakeToolchain
+
+                class Library(ConanFile):
+                    name = 'library'
+                    settings = 'os', 'arch', 'compiler', 'build_type'
+                    exports_sources = "CMakeLists.txt", "lib.h", "lib.cpp"
+                    options = {'shared': [True, False]}
+                    default_options = {'shared': False}
+
+                    def toolchain(self):
+                        tc = CMakeToolchain(self)
+                        tc.write_toolchain_files()
+
+                    def build(self):
+                        cmake = CMake(self)
+                        cmake.configure()
+
+                    def package(self):
+                        cmake = CMake(self)
+                        cmake.install()
+                """),
+            'profile_host': textwrap.dedent("""
+                [settings]
+                os=Android
+                os.api_level=23
+                arch=x86_64
+                compiler=clang
+                compiler.version=9
+                compiler.libcxx=c++_shared
+                build_type=Release
+            """)
+        })
+
+    def test_use_cmake_toolchain(self):
+        """ This is the naive approach, we follow instruction from CMake in its documentation
+            https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling-for-android
+        """
+        # Build in the cache
+        self.t.run('create . library/version@ --profile:host=profile_host --profile:build=default')
+
+        # Build locally
+        self.t.run('install . library/version@ --profile:host=profile_host --profile:build=default')
+        self.t.run_command('cmake . -DCMAKE_TOOLCHAIN_FILE={}'.format(CMakeToolchainBase.filename))
+        self.t.run_command('cmake --build .')