Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimal proof of concept of universal binaries support for CMakeToolchain #15775

Merged
merged 19 commits into from
Mar 5, 2024
Merged
18 changes: 18 additions & 0 deletions conan/internal/internal_tools.py
Original file line number Diff line number Diff line change
@@ -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:
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)]
czoido marked this conversation as resolved.
Show resolved Hide resolved

return all(part in valid_macos_values for part in parts)
23 changes: 20 additions & 3 deletions conan/tools/cmake/toolchain/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -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":
Expand Down
4 changes: 3 additions & 1 deletion conans/model/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import yaml

from conan.internal.internal_tools import is_universal_arch
from conans.errors import ConanException


Expand Down Expand Up @@ -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

Expand Down
101 changes: 101 additions & 0 deletions conans/test/functional/toolchains/cmake/test_universal_binaries.py
Original file line number Diff line number Diff line change
@@ -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
26 changes: 25 additions & 1 deletion conans/test/unittests/tools/apple/test_apple_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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("""
Expand All @@ -70,3 +74,23 @@ 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,result", [
("arm64|x86_64", True),
("x86_64|arm64", None),
("armv7|x86", True),
("x86|armv7", None),
(None, False),
("arm64|armv7|x86_64", True),
("x86|arm64", None),
("arm64|ppc32", False),
])
# None is for the exception case
def test_is_universal_arch(settings_value, result):
valid_definitions = ["arm64", "x86_64", "armv7", "x86"]
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