Skip to content

Commit 7b7ab01

Browse files
Add macOS universal2 wheel building support (#115)
* Add macOS universal2 wheel building support * Add universal2 examples and test on CI * Apply suggestions from code review Co-authored-by: David Hewitt <[email protected]> * Use ARCHFLAGS to set universal2 build Co-authored-by: David Hewitt <[email protected]>
1 parent f9c2bf0 commit 7b7ab01

File tree

6 files changed

+181
-48
lines changed

6 files changed

+181
-48
lines changed

.github/workflows/ci.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
matrix:
1515
python-version: [3.6, 3.7, 3.8, 3.9, pypy3]
1616
platform: [
17-
{ os: "macOS-latest", python-architecture: "x64", rust-target: "x86_64-apple-darwin" },
17+
{ os: "macos-latest", python-architecture: "x64", rust-target: "x86_64-apple-darwin" },
1818
{ os: "ubuntu-latest", python-architecture: "x64", rust-target: "x86_64-unknown-linux-gnu" },
1919
{ os: "windows-latest", python-architecture: "x64", rust-target: "x86_64-pc-windows-msvc" },
2020
{ os: "windows-latest", python-architecture: "x86", rust-target: "i686-pc-windows-msvc" },
@@ -44,6 +44,10 @@ jobs:
4444
profile: minimal
4545
default: true
4646

47+
- name: Install Rust aarch64-apple-darwin target
48+
if: matrix.platform.os == 'macos-latest'
49+
run: rustup target add aarch64-apple-darwin
50+
4751
- name: Install test dependencies
4852
run: pip install --upgrade tox setuptools
4953

@@ -74,6 +78,24 @@ jobs:
7478
tox -c $example_dir -e py
7579
done
7680
81+
- name: Test macOS universal2
82+
if: matrix.platform.os == 'macos-latest'
83+
shell: bash
84+
env:
85+
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
86+
MACOSX_DEPLOYMENT_TARGET: '10.9'
87+
ARCHFLAGS: -arch x86_64 -arch arm64
88+
PYO3_CROSS_LIB_DIR: /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.8/lib
89+
run: |
90+
cd examples/namespace_package
91+
pip install wheel
92+
python setup.py bdist_wheel
93+
ls -l dist/
94+
pip install --force-reinstall dist/namespace_package*_universal2.whl
95+
cd -
96+
python -c "from namespace_package import rust; assert rust.rust_func() == 14"
97+
python -c "from namespace_package import python; assert python.python_func() == 15"
98+
7799
test-abi3:
78100
runs-on: ${{ matrix.os }}
79101
strategy:
@@ -92,6 +114,10 @@ jobs:
92114
toolchain: stable
93115
override: true
94116

117+
- name: Install Rust aarch64-apple-darwin target
118+
if: matrix.os == 'macos-latest'
119+
run: rustup target add aarch64-apple-darwin
120+
95121
- name: Build package
96122
run: pip install -e .
97123

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
### Added
88
- Support building x86-64 wheel on arm64 macOS machine. [#114](https://github.com/PyO3/setuptools-rust/pull/114)
9+
- Add macOS universal2 wheel building support. [#115](https://github.com/PyO3/setuptools-rust/pull/115)
910

1011
### Changed
1112
- Respect `PYO3_PYTHON` and `PYTHON_SYS_EXECUTABLE` environment variables if set. [#96](https://github.com/PyO3/setuptools-rust/pull/96)

README.md

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,29 @@ It is possible to use any of the `manylinux` docker images: `manylinux1`, `manyl
133133

134134
You can define rust extension with RustExtension class:
135135

136-
RustExtension(name, path, args=None, features=None,
137-
rust\_version=None, quiet=False, debug=False)
136+
```python
137+
RustExtension(
138+
name,
139+
path="Cargo.toml",
140+
args=None,
141+
features=None,
142+
rustc_flags=None,
143+
rust_version=None,
144+
quiet=False,
145+
debug=None,
146+
binding=Binding.PyO3,
147+
strip=Strip.No,
148+
script=False,
149+
native=False,
150+
optional=False,
151+
py_limited_api=False,
152+
)
153+
```
138154

139155
The class for creating rust extensions.
140156

141-
- param str name
157+
- param str `name`
158+
142159
the full name of the extension, including any packages -- ie.
143160
*not* a filename or pathname, but Python dotted name. It is
144161
possible to specify multiple binaries, if extension uses
@@ -147,51 +164,71 @@ The class for creating rust extensions.
147164
binaries and values are full name of the executable inside python
148165
package.
149166

150-
- param str path
167+
- param str `path`
168+
151169
path to the Cargo.toml manifest file
152170

153-
- param \[str\] args
171+
- param \[str\] `args`
172+
154173
a list of extra argumenents to be passed to cargo.
155174

156-
- param \[str\] features
175+
- param \[str\] `features`
176+
157177
a list of features to also build
158178

159-
- param \[str\] rustc\_flags
179+
- param \[str\] `rustc_flags`
180+
160181
A list of arguments to pass to rustc, e.g. cargo rustc --features
161182
\<features\> \<args\> -- \<rustc\_flags\>
162183

163-
- param str rust\_version
184+
- param str `rust_version`
185+
164186
sematic version of rust compiler version -- for example
165187
*\>1.14,\<1.16*, default is None
166188

167-
- param bool quiet
168-
Does not echo cargo's output. default is False
189+
- param bool `quiet`
169190

170-
- param bool debug
171-
Controls whether --debug or --release is passed to cargo. If set
191+
Does not echo cargo's output. default is `False`
192+
193+
- param bool `debug`
194+
195+
Controls whether `--debug` or `--release` is passed to cargo. If set
172196
to None then build type is auto-detect. Inplace build is debug
173-
build otherwise release. Default: None
197+
build otherwise release. Default: `None`
198+
199+
- param int `binding`
200+
201+
Controls which python binding is in use.
202+
* `Binding.PyO3` uses PyO3
203+
* `Binding.RustCPython` uses rust-cpython
204+
* `Binding.NoBinding` uses no binding.
205+
* `Binding.Exec` build executable.
174206

175-
- param int binding
176-
Controls which python binding is in use. Binding.PyO3 uses PyO3
177-
Binding.RustCPython uses rust-cpython Binding.NoBinding uses no
178-
binding. Binding.Exec build executable.
207+
- param int `strip`
179208

180-
- param int strip
181209
Strip symbols from final file. Does nothing for debug build.
182-
Strip.No - do not strip symbols (default) Strip.Debug - strip
183-
debug symbols Strip.All - strip all symbols
210+
* `Strip.No` - do not strip symbols (default)
211+
* `Strip.Debug` - strip debug symbols
212+
* `Strip.All` - strip all symbols
184213

185-
- param bool script
186-
Generate console script for executable if Binding.Exec is used.
214+
- param bool `script`
215+
216+
Generate console script for executable if `Binding.Exec` is used.
217+
218+
- param bool `native`
187219

188-
- param bool native
189220
Build extension or executable with "-C target-cpu=native"
190221

191-
- param bool optional
222+
- param bool `optional`
223+
192224
if it is true, a build failure in the extension will not abort the
193225
build process, but instead simply not install the failing
194226
extension.
227+
- param bool `py_limited_api`
228+
229+
Same as `py_limited_api` on `setuptools.Extension`. Note that if you
230+
set this to True, your extension must pass the appropriate feature
231+
flags to pyo3 (ensuring that `abi3` feature is enabled).
195232

196233
## Commands
197234

examples/namespace_package/tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ deps =
88
setuptools-rust @ file://{toxinidir}/../../
99
pytest
1010
commands = pytest {posargs}
11+
passenv = *

setuptools_rust/build.py

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,39 @@ def finalize_options(self):
6262
("inplace", "inplace"),
6363
)
6464

65+
def get_target_triple(self):
66+
# If we are on a 64-bit machine, but running a 32-bit Python, then
67+
# we'll target a 32-bit Rust build.
68+
# Automatic target detection can be overridden via the CARGO_BUILD_TARGET
69+
# environment variable.
70+
if os.getenv("CARGO_BUILD_TARGET"):
71+
return os.environ["CARGO_BUILD_TARGET"]
72+
elif self.plat_name == "win32":
73+
return "i686-pc-windows-msvc"
74+
elif self.plat_name == "win-amd64":
75+
return "x86_64-pc-windows-msvc"
76+
elif self.plat_name.startswith("macosx-") and platform.machine() == "x86_64":
77+
# x86_64 or arm64 macOS targeting x86_64
78+
return "x86_64-apple-darwin"
79+
6580
def run_for_extension(self, ext: RustExtension):
81+
arch_flags = os.getenv("ARCHFLAGS")
82+
universal2 = False
83+
if self.plat_name.startswith("macosx-") and arch_flags:
84+
universal2 = "x86_64" in arch_flags and "arm64" in arch_flags
85+
if universal2:
86+
arm64_dylib_paths = self.build_extension(ext, "aarch64-apple-darwin")
87+
x86_64_dylib_paths = self.build_extension(ext, "x86_64-apple-darwin")
88+
dylib_paths = []
89+
for (target_fname, arm64_dylib), (_, x86_64_dylib) in zip(arm64_dylib_paths, x86_64_dylib_paths):
90+
fat_dylib_path = arm64_dylib.replace("aarch64-apple-darwin/", "")
91+
self.create_universal2_binary(fat_dylib_path, [arm64_dylib, x86_64_dylib])
92+
dylib_paths.append((target_fname, fat_dylib_path))
93+
else:
94+
dylib_paths = self.build_extension(ext)
95+
self.install_extension(ext, dylib_paths)
96+
97+
def build_extension(self, ext: RustExtension, target_triple=None):
6698
executable = ext.binding == Binding.Exec
6799

68100
rust_target_info = get_rust_target_info()
@@ -84,22 +116,8 @@ def run_for_extension(self, ext: RustExtension):
84116
)
85117
rustflags = ""
86118

87-
# If we are on a 64-bit machine, but running a 32-bit Python, then
88-
# we'll target a 32-bit Rust build.
89-
# Automatic target detection can be overridden via the CARGO_BUILD_TARGET
90-
# environment variable.
91-
target_triple = None
119+
target_triple = target_triple or self.get_target_triple()
92120
target_args = []
93-
if os.getenv("CARGO_BUILD_TARGET"):
94-
target_triple = os.environ["CARGO_BUILD_TARGET"]
95-
elif self.plat_name == "win32":
96-
target_triple = "i686-pc-windows-msvc"
97-
elif self.plat_name == "win-amd64":
98-
target_triple = "x86_64-pc-windows-msvc"
99-
elif self.plat_name.startswith("macosx-") and platform.machine() == "x86_64":
100-
# x86_64 or arm64 macOS targeting x86_64
101-
target_triple = "x86_64-apple-darwin"
102-
103121
if target_triple is not None:
104122
target_args = ["--target", target_triple]
105123

@@ -264,7 +282,14 @@ def run_for_extension(self, ext: RustExtension):
264282
raise DistutilsExecError(
265283
f"Rust build failed; unable to find any {wildcard_so} in {artifactsdir}"
266284
)
285+
return dylib_paths
267286

287+
def install_extension(self, ext: RustExtension, dylib_paths):
288+
executable = ext.binding == Binding.Exec
289+
debug_build = ext.debug if ext.debug is not None else self.inplace
290+
debug_build = self.debug if self.debug is not None else debug_build
291+
if self.release:
292+
debug_build = False
268293
# Ask build_ext where the shared library would go if it had built it,
269294
# then copy it there.
270295
build_ext = self.get_finalized_command("build_ext")
@@ -301,7 +326,7 @@ def run_for_extension(self, ext: RustExtension):
301326
args.insert(0, "strip")
302327
args.append(ext_path)
303328
try:
304-
output = subprocess.check_output(args, env=env)
329+
output = subprocess.check_output(args)
305330
except subprocess.CalledProcessError:
306331
pass
307332

@@ -323,3 +348,31 @@ def get_dylib_ext_path(self, ext, target_fname):
323348
return build_ext.get_ext_fullpath(target_fname)
324349
finally:
325350
del build_ext.ext_map[modpath]
351+
352+
@staticmethod
353+
def create_universal2_binary(output_path, input_paths):
354+
# Try lipo first
355+
command = ["lipo", "-create", "-output", output_path, *input_paths]
356+
try:
357+
subprocess.check_output(command)
358+
except subprocess.CalledProcessError as e:
359+
output = e.output
360+
if isinstance(output, bytes):
361+
output = e.output.decode("latin-1").strip()
362+
raise CompileError(
363+
"lipo failed with code: %d\n%s" % (e.returncode, output)
364+
)
365+
except OSError:
366+
# lipo not found, try using the fat-macho library
367+
try:
368+
from fat_macho import FatWriter
369+
except ImportError:
370+
raise DistutilsExecError(
371+
"failed to locate `lipo` or import `fat_macho.FatWriter`. "
372+
"Try installing with `pip install fat-macho` "
373+
)
374+
fat = FatWriter()
375+
for input_path in input_paths:
376+
with open(input_path, "rb") as f:
377+
fat.add(f.read())
378+
fat.write_to(output_path)

setuptools_rust/setuptools_ext.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1-
from abc import ABC, abstractmethod
1+
import os
22
from distutils import log
3-
from distutils.cmd import Command
43
from distutils.command.check import check
54
from distutils.command.clean import clean
6-
from distutils.errors import DistutilsPlatformError
7-
from setuptools.command.install import install
5+
86
from setuptools.command.build_ext import build_ext
7+
from setuptools.command.install import install
98

109
try:
1110
from wheel.bdist_wheel import bdist_wheel
1211
except ImportError:
1312
bdist_wheel = None
1413

15-
from .extension import RustExtension
16-
from .utils import get_rust_version
17-
1814

1915
def add_rust_extension(dist):
2016
build_ext_base_class = dist.cmdclass.get('build_ext', build_ext)
@@ -114,6 +110,25 @@ def finalize_options(self):
114110
self.distribution.entry_points["console_scripts"] = ep_scripts
115111

116112
bdist_wheel_base_class.finalize_options(self)
113+
114+
def get_tag(self):
115+
python, abi, plat = super().get_tag()
116+
arch_flags = os.getenv("ARCHFLAGS")
117+
universal2 = False
118+
if self.plat_name.startswith("macosx-") and arch_flags:
119+
universal2 = "x86_64" in arch_flags and "arm64" in arch_flags
120+
if universal2 and plat.startswith("macosx_"):
121+
from wheel.macosx_libfile import calculate_macosx_platform_tag
122+
123+
macos_target = os.getenv("MACOSX_DEPLOYMENT_TARGET")
124+
if macos_target is None:
125+
# Example: macosx_11_0_arm64
126+
macos_target = '.'.join(plat.split("_")[1:3])
127+
plat = calculate_macosx_platform_tag(
128+
self.bdist_dir,
129+
"macosx-{}-universal2".format(macos_target)
130+
)
131+
return python, abi, plat
117132
dist.cmdclass["bdist_wheel"] = bdist_wheel_rust_extension
118133

119134

0 commit comments

Comments
 (0)