diff --git a/conan/internal/internal_tools.py b/conan/internal/internal_tools.py new file mode 100644 index 00000000000..03a4cc0c01b --- /dev/null +++ b/conan/internal/internal_tools.py @@ -0,0 +1,18 @@ +from conans.errors import ConanException + +universal_arch_separator = '|' + + +def is_universal_arch(settings_value, valid_definitions): + if settings_value is None or valid_definitions is None or universal_arch_separator not in settings_value: + return False + + parts = settings_value.split(universal_arch_separator) + + if parts != sorted(parts): + raise ConanException(f"Architectures must be in alphabetical order separated by " + f"{universal_arch_separator}") + + valid_macos_values = [val for val in valid_definitions if ("arm" in val or "x86" in val)] + + return all(part in valid_macos_values for part in parts) diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py index 01f5dc07437..c1c68a4de9f 100644 --- a/conan/tools/cmake/toolchain/blocks.py +++ b/conan/tools/cmake/toolchain/blocks.py @@ -5,7 +5,8 @@ from jinja2 import Template -from conan.tools.apple.apple import get_apple_sdk_fullname +from conan.internal.internal_tools import universal_arch_separator, is_universal_arch +from conan.tools.apple.apple import get_apple_sdk_fullname, _to_apple_arch from conan.tools.android.utils import android_abi from conan.tools.apple.apple import is_apple_os, to_apple_arch from conan.tools.build import build_jobs @@ -355,10 +356,19 @@ def context(self): if not is_apple_os(self._conanfile): return None + def to_apple_archs(conanfile, default=None): + f"""converts conan-style architectures into Apple-style archs + to be used by CMake also supports multiple architectures + separated by '{universal_arch_separator}'""" + arch_ = conanfile.settings.get_safe("arch") if conanfile else None + if arch_ is not None: + return ";".join([_to_apple_arch(arch, default) for arch in + arch_.split(universal_arch_separator)]) + # 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" - host_architecture = to_apple_arch(self._conanfile) + host_architecture = to_apple_archs(self._conanfile) host_os_version = self._conanfile.settings.get_safe("os.version") host_sdk_name = self._conanfile.conf.get("tools.apple:sdk_path") or get_apple_sdk_fullname(self._conanfile) @@ -815,6 +825,11 @@ def _get_generic_system_name(self): return cmake_system_name_map.get(os_host, os_host) def _is_apple_cross_building(self): + + if is_universal_arch(self._conanfile.settings.get_safe("arch"), + self._conanfile.settings.possible_values().get("arch")): + return False + os_host = self._conanfile.settings.get_safe("os") arch_host = self._conanfile.settings.get_safe("arch") arch_build = self._conanfile.settings_build.get_safe("arch") @@ -829,7 +844,9 @@ def _get_cross_build(self): system_version = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_version") system_processor = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_processor") - if not user_toolchain: # try to detect automatically + # try to detect automatically + if not user_toolchain and not is_universal_arch(self._conanfile.settings.get_safe("arch"), + self._conanfile.settings.possible_values().get("arch")): os_host = self._conanfile.settings.get_safe("os") arch_host = self._conanfile.settings.get_safe("arch") if arch_host == "armv8": diff --git a/conans/model/settings.py b/conans/model/settings.py index c7b3837aa3b..d9cf3e8ed7b 100644 --- a/conans/model/settings.py +++ b/conans/model/settings.py @@ -1,5 +1,6 @@ import yaml +from conan.internal.internal_tools import is_universal_arch from conans.errors import ConanException @@ -98,7 +99,8 @@ def __delattr__(self, item): def _validate(self, value): value = str(value) if value is not None else None - if "ANY" not in self._definition and value not in self._definition: + is_universal = is_universal_arch(value, self._definition) if self._name == "settings.arch" else False + if "ANY" not in self._definition and value not in self._definition and not is_universal: raise ConanException(bad_value_msg(self._name, value, self._definition)) return value diff --git a/conans/test/functional/toolchains/cmake/test_universal_binaries.py b/conans/test/functional/toolchains/cmake/test_universal_binaries.py new file mode 100644 index 00000000000..b4743c93030 --- /dev/null +++ b/conans/test/functional/toolchains/cmake/test_universal_binaries.py @@ -0,0 +1,101 @@ +import os +import platform +import textwrap + +import pytest + +from conans.test.utils.tools import TestClient +from conans.util.files import rmdir + + +@pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") +@pytest.mark.tool("cmake", "3.23") +def test_create_universal_binary(): + client = TestClient() + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + class mylibraryRecipe(ConanFile): + package_type = "library" + generators = "CMakeToolchain" + settings = "os", "compiler", "build_type", "arch" + options = {"shared": [True, False], "fPIC": [True, False]} + default_options = {"shared": False, "fPIC": True} + exports_sources = "CMakeLists.txt", "src/*", "include/*" + + def layout(self): + cmake_layout(self) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + self.run("lipo -info libmylibrary.a") + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.libs = ["mylibrary"] + """) + + test_conanfile = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + from conan.tools.build import can_run + + class mylibraryTestConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeDeps", "CMakeToolchain" + + def requirements(self): + self.requires(self.tested_reference_str) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def layout(self): + cmake_layout(self) + + def test(self): + exe = os.path.join(self.cpp.build.bindir, "example") + self.run(f"lipo {exe} -info", env="conanrun") + """) + + client.run("new cmake_lib -d name=mylibrary -d version=1.0") + client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile}) + + client.run('create . --name=mylibrary --version=1.0 ' + '-s="arch=armv8|armv8.3|x86_64" --build=missing -tf=""') + + assert "libmylibrary.a are: x86_64 arm64 arm64e" in client.out + + client.run('test test_package mylibrary/1.0 -s="arch=armv8|armv8.3|x86_64"') + + assert "example are: x86_64 arm64 arm64e" in client.out + + client.run('new cmake_exe -d name=foo -d version=1.0 -d requires=mylibrary/1.0 --force') + + client.run('install . -s="arch=armv8|armv8.3|x86_64"') + + client.run_command("cmake --preset conan-release") + client.run_command("cmake --build --preset conan-release") + client.run_command("lipo -info ./build/Release/foo") + + assert "foo are: x86_64 arm64 arm64e" in client.out + + rmdir(os.path.join(client.current_folder, "build")) + + client.run('install . -s="arch=armv8|armv8.3|x86_64" ' + '-c tools.cmake.cmake_layout:build_folder_vars=\'["settings.arch"]\'') + + client.run_command("cmake --preset \"conan-armv8|armv8.3|x86_64-release\" ") + client.run_command("cmake --build --preset \"conan-armv8|armv8.3|x86_64-release\" ") + client.run_command("lipo -info './build/armv8|armv8.3|x86_64/Release/foo'") + + assert "foo are: x86_64 arm64 arm64e" in client.out diff --git a/conans/test/unittests/tools/apple/test_apple_tools.py b/conans/test/unittests/tools/apple/test_apple_tools.py index 50e410e3756..ee8586b713f 100644 --- a/conans/test/unittests/tools/apple/test_apple_tools.py +++ b/conans/test/unittests/tools/apple/test_apple_tools.py @@ -2,10 +2,13 @@ import pytest import textwrap +from conan.internal.internal_tools import is_universal_arch +from conans.errors import ConanException from conans.test.utils.mocks import ConanFileMock, MockSettings, MockOptions from conans.test.utils.test_files import temp_folder from conan.tools.apple import is_apple_os, to_apple_arch, fix_apple_shared_install_name, XCRun -from conan.tools.apple.apple import _get_dylib_install_name # testing private function +from conan.tools.apple.apple import _get_dylib_install_name + def test_tools_apple_is_apple_os(): conanfile = ConanFileMock() @@ -51,6 +54,7 @@ def test_xcrun_public_settings(): assert settings.os == "watchOS" + def test_get_dylib_install_name(): # https://github.com/conan-io/conan/issues/13014 single_arch = textwrap.dedent(""" @@ -70,3 +74,25 @@ def test_get_dylib_install_name(): mock_output_runner.return_value = mock_output install_name = _get_dylib_install_name("otool", "/path/to/libwebp.7.dylib") assert "/absolute/path/lib/libwebp.7.dylib" == install_name + + +@pytest.mark.parametrize("settings_value,valid_definitions,result", [ + ("arm64|x86_64", ["arm64", "x86_64", "armv7", "x86"], True), + ("x86_64|arm64", ["arm64", "x86_64", "armv7", "x86"], None), + ("armv7|x86", ["arm64", "x86_64", "armv7", "x86"], True), + ("x86|armv7", ["arm64", "x86_64", "armv7", "x86"], None), + (None, ["arm64", "x86_64", "armv7", "x86"], False), + ("arm64|armv7|x86_64", ["arm64", "x86_64", "armv7", "x86"], True), + ("x86|arm64", ["arm64", "x86_64", "armv7", "x86"], None), + ("arm64|ppc32", None, False), + (None, None, False), + ("armv7|x86", None, False), + ("arm64", ["arm64", "x86_64"], False), +]) +# None is for the exception case +def test_is_universal_arch(settings_value, valid_definitions, result): + if result is None: + with pytest.raises(ConanException): + is_universal_arch(settings_value, valid_definitions) + else: + assert is_universal_arch(settings_value, valid_definitions) == result