Repository: conan-io/conan
Pull request: [poc] CMake + iOS
Changelog: Feature: Add POC on a toolchain for iOS (using CMake XCode generator).
Docs: https://github.com/conan-io/docs/pull/1906

Only working with XCode generator for the moment: https://cmake.org/cmake/help/v3.18/manual/cmake-toolchains.7.html#cross-compiling-for-ios-tvos-or-watchos

Close https://github.com/conan-io/conan/issues/7810


Problem statement:
[feature] CMakeToolchain + iOS projects PoC
Implement a proof of concept of using the new CMakeToolchain feature for building iOS projects

Notes:
- Toolchain 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 packages, the ``cmake_find_package_multi`` generator must be used

Hints from issue discussion:

Commit message: minor changes

Patch to review:
diff --git a/conans/client/toolchain/cmake/__init__.py b/conans/client/toolchain/cmake/__init__.py
index d9020743b4f..2e04e7cb575 100644
--- a/conans/client/toolchain/cmake/__init__.py
+++ b/conans/client/toolchain/cmake/__init__.py
@@ -1,4 +1,5 @@
 from .android import CMakeAndroidToolchain
+from .ios import CMakeiOSToolchain
 from .generic import CMakeGenericToolchain
 
 
@@ -7,5 +8,7 @@ def CMakeToolchain(conanfile, *args, **kwargs):
     if os_ == 'Android':
         # assert cross_building(conanfile)  # FIXME: Conan v2.0, two-profiles approach by default
         return CMakeAndroidToolchain(conanfile, *args, **kwargs)
+    if os_ == 'iOS':
+        return CMakeiOSToolchain(conanfile, *args, **kwargs)
     else:
         return CMakeGenericToolchain(conanfile, *args, **kwargs)
diff --git a/conans/client/toolchain/cmake/ios.py b/conans/client/toolchain/cmake/ios.py
new file mode 100644
index 00000000000..1f925be695e
--- /dev/null
+++ b/conans/client/toolchain/cmake/ios.py
@@ -0,0 +1,133 @@
+import textwrap
+
+from .base import CMakeToolchainBase
+
+
+class CMakeiOSToolchain(CMakeToolchainBase):
+    _template_project_include = ''
+
+    _template_toolchain = textwrap.dedent("""
+        # Conan automatically generated toolchain file
+        # DO NOT EDIT MANUALLY, it will be overwritten
+        # Avoid including toolchain file several times (bad if appending to variables like
+        #   CMAKE_CXX_FLAGS. See https://github.com/android/ndk/issues/323
+        if(CONAN_TOOLCHAIN_INCLUDED)
+          return()
+        endif()
+        set(CONAN_TOOLCHAIN_INCLUDED TRUE)
+        # build_type (Release, Debug, etc) is only defined for single-config generators
+        {%- if build_type %}
+        set(CMAKE_BUILD_TYPE "{{ build_type }}" CACHE STRING "Choose the type of build." FORCE)
+        {%- endif %}
+        get_property( _CMAKE_IN_TRY_COMPILE GLOBAL PROPERTY IN_TRY_COMPILE )
+        if(_CMAKE_IN_TRY_COMPILE)
+            message(STATUS "Running toolchain IN_TRY_COMPILE")
+            return()
+        endif()
+        message("Using Conan toolchain through ${CMAKE_TOOLCHAIN_FILE}.")
+        # We are going to adjust automagically many things as requested by Conan
+        #   these are the things done by 'conan_basic_setup()'
+        set(CMAKE_EXPORT_NO_PACKAGE_REGISTRY ON)
+        # To support the cmake_find_package generators
+        {% if cmake_module_path -%}
+        set(CMAKE_MODULE_PATH {{ cmake_module_path }} ${CMAKE_MODULE_PATH})
+        {%- endif %}
+        {% if cmake_prefix_path -%}
+        set(CMAKE_PREFIX_PATH {{ cmake_prefix_path }} ${CMAKE_PREFIX_PATH})
+        {%- endif %}
+        # shared libs
+        {% if shared_libs -%}
+        message(STATUS "Conan toolchain: Setting BUILD_SHARED_LIBS= {{ shared_libs }}")
+        set(BUILD_SHARED_LIBS {{ shared_libs }})
+        {%- endif %}
+
+        # C++ Standard
+        {% if cppstd -%}
+        message(STATUS "Conan C++ Standard {{ cppstd }} with extensions {{ cppstd_extensions }}}")
+        set(CMAKE_CXX_STANDARD {{ cppstd }})
+        set(CMAKE_CXX_EXTENSIONS {{ cppstd_extensions }})
+        {%- endif %}
+        # Install prefix
+        {% if install_prefix -%}
+        set(CMAKE_INSTALL_PREFIX "{{install_prefix}}" CACHE STRING "" FORCE)
+        {%- endif %}
+
+        # iOS stuff
+        # conan vars
+        set(CONAN_SETTINGS_HOST_ARCH "{{host_architecture}}")
+        set(CONAN_SETTINGS_HOST_OS "{{host_os}}") # CMAKE_SYSTEM_NAME
+        set(CONAN_SETTINGS_HOST_OS_VERSION "{{host_os_version}}") # SDK_VERSION
+        set(CONAN_SDK_NAME "{{host_sdk_name}}")
+        # TODO: add logic to calc the deployment target
+        set(CONAN_SETTINGS_HOST_MIN_OS_VERSION "{{host_os_min_version}}") # DEPLOYMENT TARGET
+
+        # set cmake vars
+        set(CMAKE_SYSTEM_NAME ${CONAN_SETTINGS_HOST_OS})
+        set(CMAKE_SYSTEM_VERSION ${CONAN_SETTINGS_HOST_OS_VERSION})
+        set(DEPLOYMENT_TARGET ${CONAN_SETTINGS_HOST_MIN_OS_VERSION})
+        # Set the architectures for which to build.
+        set(CMAKE_OSX_ARCHITECTURES ${CONAN_SETTINGS_HOST_ARCH})
+        # Setting CMAKE_OSX_SYSROOT SDK, when using Xcode generator the name is enough
+        # but full path is necessary for others
+        set(CMAKE_OSX_SYSROOT "${CONAN_SDK_NAME}")
+        if(NOT DEFINED CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM)
+          set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "123456789A" CACHE INTERNAL "")
+        endif()
+    """)
+
+    def __init__(self, conanfile, build_type=None, **kwargs):
+        super(CMakeiOSToolchain, self).__init__(conanfile, build_type=build_type, **kwargs)
+        self.build_type = build_type or self._conanfile.settings.get_safe("build_type")
+        self.host_architecture = self._get_architecture()
+        self.host_os = self._conanfile.settings.get_safe("os")
+        self.host_os_version = self._conanfile.settings.get_safe("os.version")
+        self.host_sdk_name = self._get_sdk_name(self.host_architecture)
+        self.host_os_min_version = "9.0"
+        self.libcxx = self._conanfile.settings.get_safe("compiler.libcxx")
+        self.cppstd = self._conanfile.settings.get_safe("compiler.cppstd")
+
+        try:
+            # This is only defined in the cache, not in the local flow
+            self.install_prefix = self._conanfile.package_folder.replace("\\", "/")
+        except AttributeError:
+            # FIXME: In the local flow, we don't know the package_folder
+            self.install_prefix = None
+
+    def _get_architecture(self):
+        # check valid combinations of architecture - os ?
+        # for iOS a FAT library valid for simulator and device
+        # can be generated if multiple archs are specified:
+        # "-DCMAKE_OSX_ARCHITECTURES=armv7;armv7s;arm64;i386;x86_64"
+        arch = self._conanfile.settings.get_safe("arch")
+        return {"x86": "i386",
+                "x86_64": "x86_64",
+                "armv8": "arm64",
+                "armv8_32": "arm64_32"}.get(arch, arch)
+        return None
+
+    def _get_sdk_name(self, architecture):
+        os_name = self._conanfile.settings.get_safe("os")
+        if "arm" in architecture:
+            return {"iOS": "iphoneos",
+                    "watchOS": "appletvos",
+                    "tvOS": "watchos"}.get(os_name)
+        else:
+            return {"iOS": "iphonesimulator",
+                    "watchOS": "appletvsimulator",
+                    "tvOS": "watchsimulator"}.get(os_name)
+        return None
+
+    def _get_template_context_data(self):
+        tpl_toolchain_context, tpl_project_include_context = \
+            super(CMakeiOSToolchain, self)._get_template_context_data()
+        tpl_toolchain_context.update({
+            "host_architecture": self.host_architecture,
+            "host_os": self.host_os,
+            "host_os_version": self.host_os_version,
+            "host_sdk_name": self.host_sdk_name,
+            "host_os_min_version": self.host_os_min_version,
+            "install_prefix": self.install_prefix,
+            "set_libcxx": self.libcxx,
+            "cppstd": self.cppstd
+        })
+        return tpl_toolchain_context, tpl_project_include_context
diff --git a/conans/test/integration/toolchains/__init__.py b/conans/test/integration/toolchains/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/conans/test/integration/toolchains/ios/__init__.py b/conans/test/integration/toolchains/ios/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/conans/test/integration/toolchains/ios/_utils.py b/conans/test/integration/toolchains/ios/_utils.py
new file mode 100644
index 00000000000..fd25c959d0b
--- /dev/null
+++ b/conans/test/integration/toolchains/ios/_utils.py
@@ -0,0 +1,72 @@
+import textwrap
+
+lib_h = textwrap.dedent("""
+    #pragma once
+    #include <string>
+    class HelloLib {
+    public:
+        void hello(const std::string& name);
+    };
+""")
+
+lib_cpp = textwrap.dedent("""
+    #include "hello.h"
+    #include <iostream>
+    using namespace std;
+    void HelloLib::hello(const std::string& name) {
+        #ifdef DEBUG
+        std::cout << "Hello " << name << " Debug!" <<std::endl;
+        #else
+        std::cout << "Hello " << name << " Release!" <<std::endl;
+        #endif
+    }
+""")
+
+cpp_wrapper_h = textwrap.dedent("""
+    #import <Foundation/Foundation.h>
+    @interface CPP_Wrapper : NSObject
+    - (void)hello_cpp_wrapped:(NSString *)name;
+    @end
+""")
+
+cpp_wrapper_mm = textwrap.dedent("""
+    #import "cpp-wrapper.h"
+    #include "hello.h"
+    @implementation CPP_Wrapper
+    - (void)hello_cpp_wrapped:(NSString *)name {
+        HelloLib hello_lib;
+        hello_lib.hello([name cStringUsingEncoding:NSUTF8StringEncoding]);
+    }
+    @end
+""")
+
+cmakelists = textwrap.dedent("""
+    cmake_minimum_required(VERSION 3.1)
+    project(MyHello CXX)
+    set(SOURCES
+      hello.cpp
+      cpp-wrapper.mm
+    )
+    set(HEADERS
+        hello.h
+        cpp-wrapper.h
+    )
+    add_library (hello ${SOURCES} ${HEADERS})
+    set_target_properties(hello PROPERTIES PUBLIC_HEADER "${HEADERS}")
+    install(TARGETS hello
+        RUNTIME DESTINATION bin
+        LIBRARY DESTINATION lib
+        ARCHIVE DESTINATION lib
+        PUBLIC_HEADER DESTINATION include
+    )
+""")
+
+
+def create_library(client):
+    client.save({
+        'hello.h': lib_h,
+        'hello.cpp': lib_cpp,
+        'cpp-wrapper.h': cpp_wrapper_h,
+        'cpp-wrapper.mm': cpp_wrapper_mm,
+        'CMakeLists.txt': cmakelists
+    })
diff --git a/conans/test/integration/toolchains/ios/test_using_cmake.py b/conans/test/integration/toolchains/ios/test_using_cmake.py
new file mode 100644
index 00000000000..b7eeaa88102
--- /dev/null
+++ b/conans/test/integration/toolchains/ios/test_using_cmake.py
@@ -0,0 +1,80 @@
+import platform
+import textwrap
+import unittest
+
+from conans.client.toolchain.cmake.base import CMakeToolchainBase
+from conans.test.utils.tools import TestClient
+from ._utils import create_library
+
+
+@unittest.skipUnless(platform.system() == "Darwin", "Requires XCode")
+class ToolchainiOSTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.t = TestClient()
+        create_library(self.t)
+        self._conanfile = textwrap.dedent("""
+            from conans import ConanFile, CMake, CMakeToolchain
+
+
+            class Library(ConanFile):
+                name = 'hello'
+                version = '1.0'
+                settings = 'os', 'arch', 'compiler', 'build_type'
+                exports_sources = 'hello.h', 'hello.cpp', 'cpp-wrapper.h', 'cpp-wrapper.mm', 'CMakeLists.txt'
+                options = {{'shared': [True, False]}}
+                default_options = {{'shared': False}}
+                _cmake = None
+
+                def _configure_cmake(self):
+                    if not self._cmake:
+                        self._cmake = CMake(self, generator={generator}, parallel=False)
+                        self._cmake.configure()
+                    return self._cmake
+
+                def toolchain(self):
+                    tc = CMakeToolchain(self)
+                    tc.write_toolchain_files()
+
+                def build(self):
+                    cmake = self._configure_cmake()
+                    cmake.configure()
+                    cmake.build()
+                    self.run("lipo -info Release-iphoneos/libhello.a")
+
+                def package(self):
+                    cmake = self._configure_cmake()
+                    cmake.install()
+            """)
+
+        self.t.save({
+            'ios_profile': textwrap.dedent("""
+                [settings]
+                os=iOS
+                os.version=12.0
+                arch=armv8
+                compiler=apple-clang
+                compiler.version=12.0
+                compiler.libcxx=libc++
+                build_type=Release
+            """)
+        })
+
+    def test_xcode_generator(self):
+        """ Simplest approach:
+            https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling-for-ios-tvos-or-watchos
+        """
+        self.t.save({'conanfile.py': self._conanfile.format(generator='"Xcode"')})
+
+        # Build in the cache
+        self.t.run('create . --profile:build=default --profile:host=ios_profile')
+        self.assertIn("Non-fat file: Release-iphoneos/libhello.a is architecture: arm64",
+                      self.t.out)
+
+        # Build locally
+        self.t.run('install . --profile:host=ios_profile --profile:build=default')
+        self.t.run_command('cmake . -G"Xcode" -DCMAKE_TOOLCHAIN_FILE={}'.format(CMakeToolchainBase.filename))
+        self.t.run_command('cmake --build . --config Release')
+
+    def test_unix_makefiles_generator(self):
+        pass


Code review comments:
diff_hunk: @@ -0,0 +1,72 @@
+import textwrap
+
+lib_h = textwrap.dedent("""
+    #pragma once
+    #include <string>
+    class HelloLib {
+    public:
+        void hello(const std::string& name);
+    };
+""")
+
+lib_cpp = textwrap.dedent("""
+    #include "hello.h"
+    #include <iostream>
+    using namespace std;
+    void HelloLib::hello(const std::string& name) {
+        #ifdef DEBUG
+        std::cout << "Hello " << name << " Debug!" <<std::endl;
+        #else
+        std::cout << "Hello " << name << " Release!" <<std::endl;
+        #endif
+    }
+""")
+
+cpp_wrapper_h = textwrap.dedent("""
path: conans/test/integration/toolchains/ios/_utils.py
text: @user1:
Isn't it possible to have  a pure C++ library compiled for iOS?

That would be much better, if the testing code would be generic?. If we could be building exactly the same "hello world" C++ library with all toolchains, that would be great.

@author:
It is not strictly needed, I just liked the idea of having also the wrapper there, that you will need if you want to call the library from Swift. If if makes it cleaner I'll remove it 👍

diff_hunk: @@ -0,0 +1,133 @@
+import textwrap
+
+from .base import CMakeToolchainBase
+
+
+class CMakeiOSToolchain(CMakeToolchainBase):
+    _template_project_include = ''
+
+    _template_toolchain = textwrap.dedent("""
+        # Conan automatically generated toolchain file
+        # DO NOT EDIT MANUALLY, it will be overwritten
+        # Avoid including toolchain file several times (bad if appending to variables like
+        #   CMAKE_CXX_FLAGS. See https://github.com/android/ndk/issues/323
+        if(CONAN_TOOLCHAIN_INCLUDED)
+          return()
+        endif()
+        set(CONAN_TOOLCHAIN_INCLUDED TRUE)
+        # build_type (Release, Debug, etc) is only defined for single-config generators
+        {%- if build_type %}
+        set(CMAKE_BUILD_TYPE "{{ build_type }}" CACHE STRING "Choose the type of build." FORCE)
+        {%- endif %}
+        get_property( _CMAKE_IN_TRY_COMPILE GLOBAL PROPERTY IN_TRY_COMPILE )
+        if(_CMAKE_IN_TRY_COMPILE)
+            message(STATUS "Running toolchain IN_TRY_COMPILE")
+            return()
+        endif()
+        message("Using Conan toolchain through ${CMAKE_TOOLCHAIN_FILE}.")
+        # We are going to adjust automagically many things as requested by Conan
+        #   these are the things done by 'conan_basic_setup()'
+        set(CMAKE_EXPORT_NO_PACKAGE_REGISTRY ON)
+        # To support the cmake_find_package generators
+        {% if cmake_module_path -%}
+        set(CMAKE_MODULE_PATH {{ cmake_module_path }} ${CMAKE_MODULE_PATH})
+        {%- endif %}
+        {% if cmake_prefix_path -%}
+        set(CMAKE_PREFIX_PATH {{ cmake_prefix_path }} ${CMAKE_PREFIX_PATH})
+        {%- endif %}
+        # shared libs
+        {% if shared_libs -%}
+        message(STATUS "Conan toolchain: Setting BUILD_SHARED_LIBS= {{ shared_libs }}")
+        set(BUILD_SHARED_LIBS {{ shared_libs }})
+        {%- endif %}
+
+        # C++ Standard
+        {% if cppstd -%}
+        message(STATUS "Conan C++ Standard {{ cppstd }} with extensions {{ cppstd_extensions }}}")
+        set(CMAKE_CXX_STANDARD {{ cppstd }})
+        set(CMAKE_CXX_EXTENSIONS {{ cppstd_extensions }})
+        {%- endif %}
+        # Install prefix
+        {% if install_prefix -%}
+        set(CMAKE_INSTALL_PREFIX "{{install_prefix}}" CACHE STRING "" FORCE)
+        {%- endif %}
+
+        # iOS stuff
+        # conan vars
+        set(CONAN_SETTINGS_HOST_ARCH "{{host_architecture}}")
+        set(CONAN_SETTINGS_HOST_OS "{{host_os}}") # CMAKE_SYSTEM_NAME
+        set(CONAN_SETTINGS_HOST_OS_VERSION "{{host_os_version}}") # SDK_VERSION
+        set(CONAN_SDK_NAME "{{host_sdk_name}}")
+        # TODO: add logic to calc the deployment target
+        set(CONAN_SETTINGS_HOST_MIN_OS_VERSION "{{host_os_min_version}}") # DEPLOYMENT TARGET
+
+        # set cmake vars
+        set(CMAKE_SYSTEM_NAME ${CONAN_SETTINGS_HOST_OS})
+        set(CMAKE_SYSTEM_VERSION ${CONAN_SETTINGS_HOST_OS_VERSION})
+        set(DEPLOYMENT_TARGET ${CONAN_SETTINGS_HOST_MIN_OS_VERSION})
+        # Set the architectures for which to build.
+        set(CMAKE_OSX_ARCHITECTURES ${CONAN_SETTINGS_HOST_ARCH})
+        # Setting CMAKE_OSX_SYSROOT SDK, when using Xcode generator the name is enough
+        # but full path is necessary for others
+        set(CMAKE_OSX_SYSROOT "${CONAN_SDK_NAME}")
+        if(NOT DEFINED CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM)
+          set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "123456789A" CACHE INTERNAL "")
+        endif()
+    """)
+
+    def __init__(self, conanfile, build_type=None, **kwargs):
+        super(CMakeiOSToolchain, self).__init__(conanfile, build_type=build_type, **kwargs)
+        self.build_type = build_type or self._conanfile.settings.get_safe("build_type")
+        self.host_architecture = self._get_architecture()
+        self.host_os = self._conanfile.settings.get_safe("os")
+        self.host_os_version = self._conanfile.settings.get_safe("os.version")
+        self.host_sdk_name = self._get_sdk_name(self.host_architecture)
+        self.host_os_min_version = "9.0"
path: conans/client/toolchain/cmake/ios.py
text: @user1:
This constant "9.0" is a bit concerning, please add at least an explanation, a warning or something.