The macOS wheels published as 0.0.13 advertise Intel support in their platform tag, but
cannot work on an Intel Mac. pip installs them happily and the import then fails.
Evidence
Every macOS wheel on PyPI, checked by parsing the Mach-O headers:
| wheel |
extension |
bundled libjpeg |
| cp39 – cp313 |
x86_64 + arm64 (universal) |
arm64 only |
artoolkitnft_core.cpython-310-darwin.so magic=0xcafebabe
UNIVERSAL (fat) binary with 2 architectures:
- x86_64 (927776 bytes)
- arm64 (848448 bytes)
artoolkitnft/.dylibs/libjpeg.9.dylib magic=0xcffaedfe
THIN binary, single architecture: arm64
The filename tag is macosx_10_14_x86_64.macosx_15_0_arm64, so pip selects these on Intel.
The extension loads — it has an x86_64 slice — and then the dynamic linker fails to resolve
libjpeg.9.dylib, which has only an arm64 slice.
Why it happens
Two halves of the build disagree about architecture:
- The extension inherits its architectures from the interpreter. CPython from
actions/setup-python is universal2, so distutils builds both slices by default. Nothing in
setup.py asks for this.
- libjpeg is built by
build_libjpeg() with a plain ./configure && make, which produces a
native-only library — arm64 on the arm64 runner.
delocate then bundles the arm64-only libjpeg into a wheel whose extension claims both, and
tags the result from the extension. Neither tool is wrong in isolation.
This was masked while macOS builds ran on macos-13/macos-14 matching the target, and became
visible once the matrix became arm64-only.
Two ways to fix
A. Make the tag honest — arm64 only. Set ARCHFLAGS="-arch arm64" for the macOS build so
the extension is thin. The wheel tags as macosx_*_arm64, and an Intel Mac correctly gets
no matching distribution found rather than a broken install. This matches what both READMEs
already claim.
B. Make the wheel genuinely universal. Build libjpeg for both architectures
(CFLAGS="-arch x86_64 -arch arm64", plus the same for linking) so the bundled dylib is fat.
B is worth considering: it would restore Intel macOS support without the paid
macos-15-intel runner, which is the only reason Intel was dropped in the first place. The
catch is autotools and multi-arch builds, which do not always cooperate — and zlib would need the
same treatment.
Recommend A now so the shipped artefacts stop lying, and evaluate B separately.
What to do about 0.0.13
PyPI does not allow replacing a released version, and individual files cannot be yanked — only
the whole release. Yanking would punish working Linux, Windows and arm64 users for a bug that
affects Intel Macs only, so the better path is to ship 0.0.14 with fix A. Once published,
pip resolves Intel Macs to no matching distribution rather than a broken wheel.
Verification for the fix
lipo -archs on macOS, or without a Mac, parse the Mach-O magic directly: 0xcafebabe is a fat
binary, 0xfeedfacf/0xcffaedfe a thin 64-bit one. Whatever the extension reports, every
bundled dylib under .dylibs/ must report the same set. That check belongs in
.github/scripts/check_wheel.py, which already rejects wheels whose contents disagree with
their tag — it just does not look at architectures yet.
Found while verifying the macOS tag noted as an open question in
specs/2026-09-22-multi-marker-plan.md.
The macOS wheels published as 0.0.13 advertise Intel support in their platform tag, but
cannot work on an Intel Mac.
pipinstalls them happily and the import then fails.Evidence
Every macOS wheel on PyPI, checked by parsing the Mach-O headers:
The filename tag is
macosx_10_14_x86_64.macosx_15_0_arm64, so pip selects these on Intel.The extension loads — it has an x86_64 slice — and then the dynamic linker fails to resolve
libjpeg.9.dylib, which has only an arm64 slice.Why it happens
Two halves of the build disagree about architecture:
actions/setup-pythonis universal2, so distutils builds both slices by default. Nothing insetup.pyasks for this.build_libjpeg()with a plain./configure && make, which produces anative-only library — arm64 on the arm64 runner.
delocatethen bundles the arm64-only libjpeg into a wheel whose extension claims both, andtags the result from the extension. Neither tool is wrong in isolation.
This was masked while macOS builds ran on
macos-13/macos-14matching the target, and becamevisible once the matrix became arm64-only.
Two ways to fix
A. Make the tag honest — arm64 only. Set
ARCHFLAGS="-arch arm64"for the macOS build sothe extension is thin. The wheel tags as
macosx_*_arm64, and an Intel Mac correctly getsno matching distribution foundrather than a broken install. This matches what both READMEsalready claim.
B. Make the wheel genuinely universal. Build libjpeg for both architectures
(
CFLAGS="-arch x86_64 -arch arm64", plus the same for linking) so the bundled dylib is fat.B is worth considering: it would restore Intel macOS support without the paid
macos-15-intelrunner, which is the only reason Intel was dropped in the first place. Thecatch is autotools and multi-arch builds, which do not always cooperate — and zlib would need the
same treatment.
Recommend A now so the shipped artefacts stop lying, and evaluate B separately.
What to do about 0.0.13
PyPI does not allow replacing a released version, and individual files cannot be yanked — only
the whole release. Yanking would punish working Linux, Windows and arm64 users for a bug that
affects Intel Macs only, so the better path is to ship 0.0.14 with fix A. Once published,
pip resolves Intel Macs to
no matching distributionrather than a broken wheel.Verification for the fix
lipo -archson macOS, or without a Mac, parse the Mach-O magic directly:0xcafebabeis a fatbinary,
0xfeedfacf/0xcffaedfea thin 64-bit one. Whatever the extension reports, everybundled dylib under
.dylibs/must report the same set. That check belongs in.github/scripts/check_wheel.py, which already rejects wheels whose contents disagree withtheir tag — it just does not look at architectures yet.
Found while verifying the macOS tag noted as an open question in
specs/2026-09-22-multi-marker-plan.md.