Skip to content

macOS wheels claim Intel support but bundle an arm64-only libjpeg #653

Description

@kalwalt

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions