Skip to content

Commit ccaab64

Browse files
committed
Windows runtime library_path alternative
Windows has no runtime_library path that's practical for us - require user to set PATH specificaly before running python - require user to use os.add_dll_directory() before `import python` - require user to load libzim DLL (via `ctypes.CDLL()`) in memory before `import python` - require us to change our API to use of of those trick before importing wrapper Most practical solution found was thus to install our companion DLLs (libzim and libicu) next to the wrapper one. This is done via a new command that edits the built wheel Added conditional support for _DEBUG as this is required to link against debug libzim DLLs
1 parent 8559275 commit ccaab64

5 files changed

Lines changed: 82 additions & 18 deletions

File tree

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ exclude *.egg-info/*
1515
exclude libzim/*.dylib
1616
exclude libzim/*.so
1717
exclude libzim/*.so.*
18+
exclude libzim/*.dll

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = [ "setuptools == 68.2.2", "wheel == 0.41.3", "cython == 3.0.5" ]
2+
requires = [ "setuptools == 68.2.2", "wheel == 0.41.3", "cython == 3.0.5", "delocate==0.12.0" ]
33
build-backend = "setuptools.build_meta"
44

55
[tool.black]
@@ -24,6 +24,9 @@ manylinux-aarch64-image = "manylinux_2_28"
2424
manylinux-pypy_x86_64-image = "manylinux_2_28"
2525
manylinux-pypy_aarch64-image = "manylinux_2_28"
2626

27+
[tool.cibuildwheel.windows]
28+
repair-wheel-command = "python.exe setup.py repair_win_wheel --destdir={dest_dir} --wheel={wheel}"
29+
2730
[tool.cibuildwheel.linux]
2831
archs = ["x86_64", "aarch64"]
2932

setup.cfg

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ test_requires =
5050
libzim =
5151
libzim.9.dylib
5252
libzim.so.9
53+
zim-9.dll
54+
icuuc74.dll
55+
icutu74.dll
56+
icuio74.dll
57+
icuin74.dll
58+
icudt74.dll
5359

5460
[isort]
5561
profile = black

setup.py

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
1010
The Cython and Cythonize compilation is done automatically by the build backend"""
1111

12+
from __future__ import annotations
13+
1214
import os
1315
import pathlib
1416
import platform as sysplatform
@@ -21,10 +23,10 @@
2123
import urllib.request
2224
from ctypes.util import find_library
2325
from pathlib import Path
24-
from typing import Optional, Tuple
2526

2627
from Cython.Build import cythonize
2728
from Cython.Distutils.build_ext import new_build_ext as build_ext
29+
from delocate.wheeltools import InWheel
2830
from setuptools import Command, Extension, setup
2931

3032

@@ -42,7 +44,10 @@ class Config:
4244
apple_signing_keychain: str = os.getenv("APPLE_SIGNING_KEYCHAIN_PATH")
4345
apple_signing_keychain_profile: str = os.getenv("APPLE_SIGNING_KEYCHAIN_PROFILE")
4446

45-
supported_platforms = {
47+
# windows
48+
_msvc_debug: bool = bool(os.getenv("MSVC_DEBUG"))
49+
50+
supported_platforms = { # noqa: RUF012
4651
"Darwin": ["x86_64", "arm64"],
4752
"Linux": ["x86_64", "aarch64"],
4853
"Linux-musl": ["x86_64", "aarch64"],
@@ -52,8 +57,9 @@ class Config:
5257
base_dir: pathlib.Path = Path(__file__).parent
5358

5459
# Avoid running cythonize on `setup.py clean` and similar
55-
buildless_commands: Tuple[str] = (
60+
buildless_commands: tuple[str, ...] = (
5661
"clean",
62+
"repair_win_wheel",
5763
"--help",
5864
"egg_info",
5965
"--version",
@@ -153,7 +159,15 @@ def wants_universal(self) -> bool:
153159
"universal2"
154160
)
155161

156-
def get_download_filename(self, arch: Optional[str] = None) -> str:
162+
@property
163+
def use_msvc_debug(self) -> bool:
164+
"""whether to add _DEBUG define to compilation
165+
166+
requires having python debug binaries installed.
167+
mandatory for compiling against libzim nighlies"""
168+
return self._msvc_debug or self.is_nightly
169+
170+
def get_download_filename(self, arch: str | None = None) -> str:
157171
"""filename to download to get binary libzim for platform/arch"""
158172
arch = arch or self.arch
159173

@@ -276,11 +290,11 @@ def _install_from(self, folder: pathlib.Path):
276290
print(f"{fpath} -> {libzim_dir / fpath.name}")
277291
os.replace(fpath, libzim_dir / fpath.name)
278292
# windows has different folder and name
279-
for fpath in folder.joinpath("bin").rglob("zim-*.dll"):
280-
print(f"{fpath} -> {libzim_dir / fpath.name}")
281-
os.replace(fpath, libzim_dir / fpath.name)
282-
# windows again, not sure its required at all
283-
for fpath in folder.joinpath("lib").rglob("zim.lib"):
293+
for fpath in (
294+
list(folder.joinpath("bin").rglob("zim-*.dll"))
295+
+ list(folder.joinpath("bin").rglob("icu*.dll"))
296+
+ list(folder.joinpath("lib").rglob("zim.lib"))
297+
):
284298
print(f"{fpath} -> {libzim_dir / fpath.name}")
285299
os.replace(fpath, libzim_dir / fpath.name)
286300

@@ -320,6 +334,18 @@ def cleanup(self):
320334
print("removing downloaded headers")
321335
shutil.rmtree(self.header_file.parent, ignore_errors=True)
322336

337+
def repair_windows_wheel(self, wheel: Path, dest_dir: Path):
338+
"""opens windows wheels in target folder and moves all DLLs files inside
339+
subdirectories of the wheel to the root one (where wrapper is expected)"""
340+
341+
dest_wheel = dest_dir / wheel.name
342+
with InWheel(str(wheel), str(dest_wheel)) as wheel_dir_path:
343+
print(f"repairing {wheel.name} for Windows (DLLs next to wrapper)")
344+
wheel_dir = Path(wheel_dir_path)
345+
for dll in wheel_dir.joinpath("libzim").rglob("*.dll"):
346+
print(f"> moving {dll} using {dll.relative_to(wheel_dir).parent}")
347+
dll.replace(wheel_dir / dll.name)
348+
323349
@property
324350
def header_file(self) -> pathlib.Path:
325351
return self.base_dir / "include" / "zim" / "zim.h"
@@ -381,14 +407,19 @@ def get_cython_extension():
381407
print("Using local libzim binary. Set `USE_SYSTEM_LIBZIM` otherwise.")
382408
include_dirs.append("include")
383409
library_dirs = ["libzim"]
384-
runtime_library_dirs = (
385-
[f"@loader_path/libzim/{config.libzim_fname}"]
386-
if sysplatform == "Darwin"
387-
else ["$ORIGIN/libzim/"]
388-
)
410+
411+
if config.platform != "Windows":
412+
runtime_library_dirs = (
413+
[f"@loader_path/libzim/{config.libzim_fname}"]
414+
if sysplatform == "Darwin"
415+
else ["$ORIGIN/libzim/"]
416+
)
389417

390418
extra_compile_args = ["-std=c++11", "-Wall"]
391-
if config.platform != "Windows":
419+
if config.platform == "Windows":
420+
extra_compile_args.append("/MDd" if config.use_msvc_debug else "/MD")
421+
...
422+
else:
392423
extra_compile_args.append("-Wextra")
393424

394425
wrapper_extension = Extension(
@@ -542,7 +573,29 @@ def run(self):
542573
config.cleanup()
543574

544575

545-
if len(sys.argv) == 2 and sys.argv[1] in config.buildless_commands:
576+
class RepairWindowsWheel(Command):
577+
user_options = [ # noqa: RUF012
578+
("wheel=", None, "Wheel to repair"),
579+
("destdir=", None, "Destination folder for repaired wheels"),
580+
]
581+
582+
def initialize_options(self):
583+
self.wheel = None
584+
self.destdir = None
585+
586+
def finalize_options(self):
587+
assert Path(self.wheel).exists(), "wheel file does not exists"
588+
assert (
589+
Path(self.destdir).exists() and Path(self.destdir).is_dir()
590+
), "dest_dir does not exists"
591+
592+
def run(self):
593+
config.repair_windows_wheel(wheel=Path(self.wheel), dest_dir=Path(self.destdir))
594+
595+
596+
if len(sys.argv) == 1 or (
597+
len(sys.argv) == 2 and sys.argv[1] in config.buildless_commands
598+
):
546599
ext_modules = None
547600
else:
548601
ext_modules = get_cython_extension()
@@ -552,6 +605,7 @@ def run(self):
552605
"build_ext": LibzimBuildExt,
553606
"download_libzim": DownloadLibzim,
554607
"clean": LibzimClean,
608+
"repair_win_wheel": RepairWindowsWheel,
555609
},
556610
ext_modules=ext_modules,
557611
)

tests/test_libzim_creator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def get_creator_output(fpath, verbose):
168168
return ps.stdout
169169

170170

171-
@pytest.mark.parametrize("verbose", [(True, False)])
171+
@pytest.mark.parametrize("verbose", [True, False])
172172
def test_creator_verbose(fpath, verbose):
173173
output = get_creator_output(fpath, verbose).strip()
174174
lines = output.splitlines()

0 commit comments

Comments
 (0)