Skip to content

Commit 3fed7c8

Browse files
committed
Added macOS universal support
Although cibuildwheel specifies the target arch for each of its builds, a regular user would attempt to build with `python3 -m build`, defaulting to universal2 This adds support for this use case, by downloading the two archs libzim and creating a universal libzim before building the universal extension.
1 parent 05d9243 commit 3fed7c8

1 file changed

Lines changed: 70 additions & 11 deletions

File tree

setup.py

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616
import shutil
1717
import subprocess
1818
import sys
19+
import sysconfig
1920
import urllib.request
2021
from ctypes.util import find_library
2122
from pathlib import Path
22-
from typing import Tuple
23+
from typing import Optional, Tuple
2324

2425
from Cython.Build import cythonize
2526
from Cython.Distutils.build_ext import new_build_ext as build_ext
@@ -73,10 +74,6 @@ def is_nightly(self) -> bool:
7374
@property
7475
def platform(self) -> str:
7576
"""Platform building for: Darwin, Linux"""
76-
# import json
77-
78-
# with open("environ.txt", "w") as fh:
79-
# json.dump(dict(os.environ), fh, indent=4)
8077
return sysplatform.system()
8178

8279
@property
@@ -90,10 +87,14 @@ def platform_libc(self) -> str:
9087
def arch(self) -> str:
9188
# macOS x86_64|arm64 - linux x86_64|aarch64
9289

93-
# when using cibuildwheel on macOS to cross-compile, `PLAT` contains
90+
# when using cibuildwheel on macOS to cross-compile,
91+
# `_PYTHON_HOST_PLATFORM` contains
9492
# a platform string like macosx-11.0-arm64
9593
# we extract the cross-compile arch from it
96-
return os.getenv("PLAT", "").rsplit("-", 1)[-1] or sysplatform.machine()
94+
return (
95+
os.getenv("_PYTHON_HOST_PLATFORM", "").rsplit("-", 1)[-1]
96+
or sysplatform.machine()
97+
)
9798

9899
def check_platform(self):
99100
if (
@@ -124,20 +125,71 @@ def is_musl(self) -> bool:
124125
return False
125126

126127
@property
127-
def download_filename(self) -> str:
128+
def wants_universal(self) -> bool:
129+
"""whether requesting a macOS universal build"""
130+
return self.platform == "Darwin" and sysconfig.get_platform().endswith(
131+
"universal2"
132+
)
133+
134+
def get_download_filename(self, arch: Optional[str] = None) -> str:
128135
"""filename to download to get binary libzim for platform/arch"""
136+
arch = arch or self.arch
137+
129138
lzplatform = {"Darwin": "macos", "Linux": "linux"}.get(self.platform)
130139
if lzplatform == "linux" and self.is_musl:
131140
lzplatform = "linux_musl"
132141

133142
return pathlib.Path(
134-
f"libzim_{lzplatform}-{self.arch}-{self.libzim_dl_version}.tar.gz"
143+
f"libzim_{lzplatform}-{arch}-{self.libzim_dl_version}.tar.gz"
135144
).name
136145

137146
def download_to_dest(self):
138147
"""download expected libzim binary into libzim/ and libzim/include/ folders"""
139-
libzim_dir = self.base_dir / "libzim"
140-
fpath = self.base_dir / self.download_filename
148+
if self.wants_universal:
149+
folders = {}
150+
for arch in self.supported_platforms["Darwin"]:
151+
folders[arch] = self._download_and_extract(
152+
self.get_download_filename(arch)
153+
)
154+
155+
try:
156+
# duplicate x86_64 tree as placeholder (removing first)
157+
folder = folders["x86_64"].with_name(
158+
folders["x86_64"].name.replace("x86_64", "universal")
159+
)
160+
shutil.rmtree(folder, ignore_errors=True)
161+
shutil.copytree(
162+
folders["x86_64"],
163+
folder,
164+
symlinks=True,
165+
ignore_dangling_symlinks=True,
166+
)
167+
# delete libzim from copied tree
168+
dest = folder / "lib" / self.libzim_fname
169+
dest.unlink()
170+
# create universal from all archs
171+
subprocess.run(
172+
["lipo"]
173+
+ [
174+
str(folder / "lib" / self.libzim_fname)
175+
for folder in folders.values()
176+
]
177+
+ ["-output", str(dest), "-create"],
178+
check=True,
179+
)
180+
finally:
181+
# clean-up temp folders
182+
for _folder in folders.values():
183+
shutil.rmtree(_folder, ignore_errors=True)
184+
else:
185+
folder = self._download_and_extract(self.get_download_filename())
186+
187+
self._install_from(folder)
188+
189+
def _download_and_extract(self, filename: str) -> pathlib.Path:
190+
"""folder it downloaded and extracted libzim dist to"""
191+
192+
fpath = self.base_dir / filename
141193
source_url = "http://download.openzim.org/release/libzim"
142194
if self.is_nightly:
143195
source_url = f"http://download.openzim.org/nightly/{self.libzim_dl_version}"
@@ -158,6 +210,12 @@ def download_to_dest(self):
158210
shutil.unpack_archive(fpath, self.base_dir, "gztar")
159211
folder = fpath.with_name(fpath.name.replace(".tar.gz", ""))
160212

213+
return folder
214+
215+
def _install_from(self, folder: pathlib.Path):
216+
"""move headers and libzim binary from dist folder to expected location"""
217+
libzim_dir = self.base_dir / "libzim"
218+
161219
# remove existing headers if present
162220
self.base_dir.joinpath("include").mkdir(parents=True, exist_ok=True)
163221
shutil.rmtree(self.base_dir / "include" / "zim", ignore_errors=True)
@@ -167,6 +225,7 @@ def download_to_dest(self):
167225

168226
# copy new libs
169227
for fpath in folder.joinpath("lib").rglob("libzim.*"):
228+
print(f"{fpath} -> {libzim_dir / fpath.name}")
170229
os.replace(fpath, libzim_dir / fpath.name)
171230

172231
# remove temp folder

0 commit comments

Comments
 (0)