Skip to content

Commit 4159cc3

Browse files
czoidomemsharded
andauthored
Minimal proof of concept of universal binaries support for CMakeToolchain (#15775)
* superbasic support for universal * wip * minor changes * minor changes * add mark * minor changes * improve function * minor changes * wip * wip * wip * minor changes * fix test * put mark again * review * protect for None arch * Update conans/test/unittests/tools/apple/test_apple_tools.py --------- Co-authored-by: James <[email protected]>
1 parent 6dcc150 commit 4159cc3

File tree

5 files changed

+169
-5
lines changed

5 files changed

+169
-5
lines changed

conan/internal/internal_tools.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from conans.errors import ConanException
2+
3+
universal_arch_separator = '|'
4+
5+
6+
def is_universal_arch(settings_value, valid_definitions):
7+
if settings_value is None or valid_definitions is None or universal_arch_separator not in settings_value:
8+
return False
9+
10+
parts = settings_value.split(universal_arch_separator)
11+
12+
if parts != sorted(parts):
13+
raise ConanException(f"Architectures must be in alphabetical order separated by "
14+
f"{universal_arch_separator}")
15+
16+
valid_macos_values = [val for val in valid_definitions if ("arm" in val or "x86" in val)]
17+
18+
return all(part in valid_macos_values for part in parts)

conan/tools/cmake/toolchain/blocks.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
from jinja2 import Template
77

8-
from conan.tools.apple.apple import get_apple_sdk_fullname
8+
from conan.internal.internal_tools import universal_arch_separator, is_universal_arch
9+
from conan.tools.apple.apple import get_apple_sdk_fullname, _to_apple_arch
910
from conan.tools.android.utils import android_abi
1011
from conan.tools.apple.apple import is_apple_os, to_apple_arch
1112
from conan.tools.build import build_jobs
@@ -348,10 +349,19 @@ def context(self):
348349
if not is_apple_os(self._conanfile):
349350
return None
350351

352+
def to_apple_archs(conanfile, default=None):
353+
f"""converts conan-style architectures into Apple-style archs
354+
to be used by CMake also supports multiple architectures
355+
separated by '{universal_arch_separator}'"""
356+
arch_ = conanfile.settings.get_safe("arch") if conanfile else None
357+
if arch_ is not None:
358+
return ";".join([_to_apple_arch(arch, default) for arch in
359+
arch_.split(universal_arch_separator)])
360+
351361
# check valid combinations of architecture - os ?
352362
# for iOS a FAT library valid for simulator and device can be generated
353363
# if multiple archs are specified "-DCMAKE_OSX_ARCHITECTURES=armv7;armv7s;arm64;i386;x86_64"
354-
host_architecture = to_apple_arch(self._conanfile)
364+
host_architecture = to_apple_archs(self._conanfile)
355365

356366
host_os_version = self._conanfile.settings.get_safe("os.version")
357367
host_sdk_name = self._conanfile.conf.get("tools.apple:sdk_path") or get_apple_sdk_fullname(self._conanfile)
@@ -807,6 +817,11 @@ def _get_generic_system_name(self):
807817
return cmake_system_name_map.get(os_host, os_host)
808818

809819
def _is_apple_cross_building(self):
820+
821+
if is_universal_arch(self._conanfile.settings.get_safe("arch"),
822+
self._conanfile.settings.possible_values().get("arch")):
823+
return False
824+
810825
os_host = self._conanfile.settings.get_safe("os")
811826
arch_host = self._conanfile.settings.get_safe("arch")
812827
arch_build = self._conanfile.settings_build.get_safe("arch")
@@ -821,7 +836,9 @@ def _get_cross_build(self):
821836
system_version = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_version")
822837
system_processor = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_processor")
823838

824-
if not user_toolchain: # try to detect automatically
839+
# try to detect automatically
840+
if not user_toolchain and not is_universal_arch(self._conanfile.settings.get_safe("arch"),
841+
self._conanfile.settings.possible_values().get("arch")):
825842
os_host = self._conanfile.settings.get_safe("os")
826843
arch_host = self._conanfile.settings.get_safe("arch")
827844
if arch_host == "armv8":

conans/model/settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import yaml
22

3+
from conan.internal.internal_tools import is_universal_arch
34
from conans.errors import ConanException
45

56

@@ -98,7 +99,8 @@ def __delattr__(self, item):
9899

99100
def _validate(self, value):
100101
value = str(value) if value is not None else None
101-
if "ANY" not in self._definition and value not in self._definition:
102+
is_universal = is_universal_arch(value, self._definition) if self._name == "settings.arch" else False
103+
if "ANY" not in self._definition and value not in self._definition and not is_universal:
102104
raise ConanException(bad_value_msg(self._name, value, self._definition))
103105
return value
104106

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import os
2+
import platform
3+
import textwrap
4+
5+
import pytest
6+
7+
from conans.test.utils.tools import TestClient
8+
from conans.util.files import rmdir
9+
10+
11+
@pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX")
12+
@pytest.mark.tool("cmake", "3.23")
13+
def test_create_universal_binary():
14+
client = TestClient()
15+
16+
conanfile = textwrap.dedent("""
17+
from conan import ConanFile
18+
from conan.tools.cmake import CMake, cmake_layout
19+
class mylibraryRecipe(ConanFile):
20+
package_type = "library"
21+
generators = "CMakeToolchain"
22+
settings = "os", "compiler", "build_type", "arch"
23+
options = {"shared": [True, False], "fPIC": [True, False]}
24+
default_options = {"shared": False, "fPIC": True}
25+
exports_sources = "CMakeLists.txt", "src/*", "include/*"
26+
27+
def layout(self):
28+
cmake_layout(self)
29+
30+
def build(self):
31+
cmake = CMake(self)
32+
cmake.configure()
33+
cmake.build()
34+
self.run("lipo -info libmylibrary.a")
35+
36+
def package(self):
37+
cmake = CMake(self)
38+
cmake.install()
39+
40+
def package_info(self):
41+
self.cpp_info.libs = ["mylibrary"]
42+
""")
43+
44+
test_conanfile = textwrap.dedent("""
45+
import os
46+
from conan import ConanFile
47+
from conan.tools.cmake import CMake, cmake_layout
48+
from conan.tools.build import can_run
49+
50+
class mylibraryTestConan(ConanFile):
51+
settings = "os", "compiler", "build_type", "arch"
52+
generators = "CMakeDeps", "CMakeToolchain"
53+
54+
def requirements(self):
55+
self.requires(self.tested_reference_str)
56+
57+
def build(self):
58+
cmake = CMake(self)
59+
cmake.configure()
60+
cmake.build()
61+
62+
def layout(self):
63+
cmake_layout(self)
64+
65+
def test(self):
66+
exe = os.path.join(self.cpp.build.bindir, "example")
67+
self.run(f"lipo {exe} -info", env="conanrun")
68+
""")
69+
70+
client.run("new cmake_lib -d name=mylibrary -d version=1.0")
71+
client.save({"conanfile.py": conanfile, "test_package/conanfile.py": test_conanfile})
72+
73+
client.run('create . --name=mylibrary --version=1.0 '
74+
'-s="arch=armv8|armv8.3|x86_64" --build=missing -tf=""')
75+
76+
assert "libmylibrary.a are: x86_64 arm64 arm64e" in client.out
77+
78+
client.run('test test_package mylibrary/1.0 -s="arch=armv8|armv8.3|x86_64"')
79+
80+
assert "example are: x86_64 arm64 arm64e" in client.out
81+
82+
client.run('new cmake_exe -d name=foo -d version=1.0 -d requires=mylibrary/1.0 --force')
83+
84+
client.run('install . -s="arch=armv8|armv8.3|x86_64"')
85+
86+
client.run_command("cmake --preset conan-release")
87+
client.run_command("cmake --build --preset conan-release")
88+
client.run_command("lipo -info ./build/Release/foo")
89+
90+
assert "foo are: x86_64 arm64 arm64e" in client.out
91+
92+
rmdir(os.path.join(client.current_folder, "build"))
93+
94+
client.run('install . -s="arch=armv8|armv8.3|x86_64" '
95+
'-c tools.cmake.cmake_layout:build_folder_vars=\'["settings.arch"]\'')
96+
97+
client.run_command("cmake --preset \"conan-armv8|armv8.3|x86_64-release\" ")
98+
client.run_command("cmake --build --preset \"conan-armv8|armv8.3|x86_64-release\" ")
99+
client.run_command("lipo -info './build/armv8|armv8.3|x86_64/Release/foo'")
100+
101+
assert "foo are: x86_64 arm64 arm64e" in client.out

conans/test/unittests/tools/apple/test_apple_tools.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
import pytest
33
import textwrap
44

5+
from conan.internal.internal_tools import is_universal_arch
6+
from conans.errors import ConanException
57
from conans.test.utils.mocks import ConanFileMock, MockSettings, MockOptions
68
from conans.test.utils.test_files import temp_folder
79
from conan.tools.apple import is_apple_os, to_apple_arch, fix_apple_shared_install_name, XCRun
8-
from conan.tools.apple.apple import _get_dylib_install_name # testing private function
10+
from conan.tools.apple.apple import _get_dylib_install_name
11+
912

1013
def test_tools_apple_is_apple_os():
1114
conanfile = ConanFileMock()
@@ -51,6 +54,7 @@ def test_xcrun_public_settings():
5154

5255
assert settings.os == "watchOS"
5356

57+
5458
def test_get_dylib_install_name():
5559
# https://github.com/conan-io/conan/issues/13014
5660
single_arch = textwrap.dedent("""
@@ -70,3 +74,25 @@ def test_get_dylib_install_name():
7074
mock_output_runner.return_value = mock_output
7175
install_name = _get_dylib_install_name("otool", "/path/to/libwebp.7.dylib")
7276
assert "/absolute/path/lib/libwebp.7.dylib" == install_name
77+
78+
79+
@pytest.mark.parametrize("settings_value,valid_definitions,result", [
80+
("arm64|x86_64", ["arm64", "x86_64", "armv7", "x86"], True),
81+
("x86_64|arm64", ["arm64", "x86_64", "armv7", "x86"], None),
82+
("armv7|x86", ["arm64", "x86_64", "armv7", "x86"], True),
83+
("x86|armv7", ["arm64", "x86_64", "armv7", "x86"], None),
84+
(None, ["arm64", "x86_64", "armv7", "x86"], False),
85+
("arm64|armv7|x86_64", ["arm64", "x86_64", "armv7", "x86"], True),
86+
("x86|arm64", ["arm64", "x86_64", "armv7", "x86"], None),
87+
("arm64|ppc32", None, False),
88+
(None, None, False),
89+
("armv7|x86", None, False),
90+
("arm64", ["arm64", "x86_64"], False),
91+
])
92+
# None is for the exception case
93+
def test_is_universal_arch(settings_value, valid_definitions, result):
94+
if result is None:
95+
with pytest.raises(ConanException):
96+
is_universal_arch(settings_value, valid_definitions)
97+
else:
98+
assert is_universal_arch(settings_value, valid_definitions) == result

0 commit comments

Comments
 (0)