Skip to content
Joe Rickerby edited this page Feb 27, 2020 · 27 revisions

Wheels are the new standard binary installation format for Python.

Many projects provide wheels now : http://pythonwheels.com

However, only some of these wheels are binary wheels.

Binary wheels on macOS / OSX

In the old days, pip would only install binary wheels on Windows. But, on January 1st 2014, pip started installing matching binary wheels for macOS (the relevant pull request).

Some popular projects that already have such wheels are:

Question: will pip give me a broken wheel?

"Matching binary wheels" are wheels with a filename that matches the installing pip's Python version, Python ABI version, and the platform tag.

At first, some worried that this tag system might cause trouble for people installing macOS binary wheels into unusual Python versions, such as homebrew and macports.

In practice though, the platform tag does a good job of preventing pip from installing a wheel that won't work.

Here's an example wheel filename: numpy-1.8.0-cp27-none-macosx_10_6_intel.whl. The name splits at dashes into:

  • numpy (package name)
  • 1.8.0 (package version)
  • cp27 (CPython 2.7 - Python version)
  • none (Python ABI number - only applies to Pythons >= 3)
  • macosx_10_6_intel (platform tag)

The platform tag comes from the output of python -c "import distutils.util; print(distutils.util.get_platform())" on the platform building the wheel. I (MB) built numpy-1.8.0-cp27-none-macosx_10_6_intel.whl with Python 2.7 from a Python.org binary installer on an macOS 10.9 machine. The macOS platform tag further breaks down to:

  • macosx
  • 10_6 (the version of the SDK used to compile Python)
  • intel (short-hand for a fat binary containing x86_64 and i386 objects)

As you see, the SDK part of the tag is not from the macOS running on the machine I (MB) was building from but from the distutils configuration on the Python I was building for. In this case I was building for a Python.org binary, and that Python.org binary gave macosx-10.6-intel from distutils get_platform().

For pip to accept the wheel as matching its own platform, the platform value has to be compatible with your version of Python and macOS. So, pip will accept wheels specifying 10_6_intel for any version of macOS >= 10.6, with any of i386 or x86_64 or dual-architecture Python. Pip will accept a wheel with platform tags 10_12_x86_64 only on macOS >= 10.12, on either dual-architecture or single-architecture x86_64 Python.

The table below has some values of distutils.util.get_platform() for different Pythons on macOS.

Read 10.x below as referring to the macOS version you are currently running. The real value will be 10.12 or similar.

Up until Python 3.7, Python.org installers were built to be compapatible with macOS 10.6, and were dual architecture (i386 and x86_64 == 32- and 64- bit). Python.org Python 3.7 has an alternative installer compatible with Python 10.9 and above, and only containing x86_64 / 64 bit architecture.

Python source Python version macOS version get_platform()
Python.org 3.7 10.6+ or 10.9+ macosx-10.9-intel or macosx-10.6-intel (64 bit only on 10.9)
Python.org 2.7 ... 3.6 10.6+ macosx-10.6-intel
System Python 2.7 10.x macosx-10.x-intel (always matches)
Macports 2.7 ...3.7 10.x macosx-10.x-x86_64 (matches latest install)
Homebrew 2.7 ... 3.7 10.x macosx-10.x-x86_64 (matches latest install)
Anaconda 2.7 ... 3.7 10.9+ macosx-10.7-x86_64

You get the idea. Most Python.org Pythons use the 10.6 SDK, and have fat (x86_64 and i386) architecture in them; since 3.7 Python.org also provides a 10.9 SDK and x86_64 only version. System Pythons use the SDK for the macOS they ship with, and also have fat (dual) architecture. Homebrew and Macports Python have the SDK for the macOS they were first installed / last updated on, and x86_64 architecture only. Anaconda uses a uniform minimum (currently reported as 10.7, even though they claim 10.9+ support only).

This tells us that wheels built with 10.6 / fat architecture Python.org Python will have the correct architecture and compatible SDK for all the other Pythons listed. Why? Because having a fat binary includes having x86_64, so is compatible with x86_64-only builds. Stuff compiled with the 10.6 SDK should also be compatible with stuff built against later SDK versions (up to and including 10.9). You can demonstrate this to yourself by renaming the wheel above to - for example - numpy-1.8.0-cp27-none-macosx_10_9_x86_64.whl and then installing into a homebrew python on macOS 10.9. Sure enough, it installs, imports and tests without problem.

Warning: Apple is removing support for stdlibc++ after 5 years of depreciation; this means that packages with C++ in them that are built with anything older than a 10.9 target may not work on the most recent macOS versions, like 10.14.

Answer: no, pip will be very careful to give you a matching wheel

Python.org wheels are safe to distribute because the architecture and SDK versions are in fact compatible with system Python, homebrew Python and macports Python.

So - nothing can go wrong with macOS binary wheels?

Strange to say, things can go wrong with wheels as for any binary distribution. Here are some things that can go wrong, and how to fix them:

Linking to external libraries that some machines do not have

All Python extensions link against macOS system libraries, but these are carefully managed to be ABI compatible between macOS versions, and you should not run into problems with these.

You can use the delocate utility to check which libraries you are linking against. For example, this is the result of running delocate-listdeps --all on a binary wheel for the tornado library:

/usr/lib/libSystem.B.dylib

This library is present and ABI compatible for all of macOS versions 10.6 and higher.

If you build a complicated Python extension it may link against some external libraries elsewhere on the system. scipy is one example; it links to the gfortran runtime libraries, whereever it finds them. Here's the output of delocate-listdeps --all for a scipy wheel built naively on a standard macOS 10.9 system using gfortran from homebrew:

/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate
/usr/lib/libSystem.B.dylib
/usr/lib/libstdc++.6.dylib
/usr/local/Cellar/gfortran/4.8.2/gfortran/lib/libgcc_s.1.dylib
/usr/local/Cellar/gfortran/4.8.2/gfortran/lib/libgfortran.3.dylib
/usr/local/Cellar/gfortran/4.8.2/gfortran/lib/libquadmath.0.dylib

Again, the libraries in /System and /usr/lib will be present on macOS >= 10.6, but of course the libraries in /usr/local/Cellar/gfortran will only be present if someone has installed gfortran via homebrew. If I distribute this wheel, it will only work for someone who has installed these libraries. The delocate utility can usually fix this by copying the dynamic libraries into the wheel and relinking the extensions.

Other stuff that might happen but we haven't yet seen

Some people have reported that binaries built with an earlier SDK (such as 10.3) on a later macOS (such as 10.9) do not in fact work on earlier versions of macOS, as they should (see comment on pip PR). I have not myself (MB) run into this problem with the 10.6 SDK. For safety, it is best to build binaries such as wheels on the same macOS versions as the SDK. For example, if you are building wheels targeting the 10.6 SDK, try and build the wheels on a machine running 10.6. I don't know of any reports of problems using these binaries on later macOS versions.

There was some worry on a pip pull-request discussion that it might be possible to get Python confused with wheels built against different C++ runtime libraries. Min RK couldn't make this problem happen with test-cases, so we are currently working on the assumption that this is not an issue. Obviously it doesn't come up if you're not using C++.

The answer is always the same - test

If in doubt - test. For example, put your wheels up on a server somewhere (examples of this are Min RKs machine, the nipy server) and then test the wheels with something like:

NIPY_URL=https://nipy.bic.berkeley.edu/scipy_installers
pip install --find-links $NIPY_URL tornado

Do this in virtualenvs with different Pythons and on different macOS versions. If you run into trouble, let us know via the Python Mac special interest group mailing list and we'll try to help. At very least, we'd really like to know.

Practical example of building wheels for a project

As we've seen, the Python.org Python distributions are the best to build against, because they use the 10.6 SDK (and hence are compatible with macOS versions from 10.6) and they have dual architectures (i386 and x86_64). This makes the resulting wheel compatible with system Python, homebrew and macports.

Install Python.org Pythons

Install:

  • Python.org Python 2.7
  • Python.org Python 3.6
  • Dual-architecture Python.org Python 3.7

Install wheel

$MACPIES/2.7/bin/pip install wheel
$MACPIES/3.6/bin/pip3 install wheel
$MACPIES/3.7/bin/pip3 install wheel

Build wheels

Here I'm building wheels for markupsafe:

cd markupsafe
rm -rf build
$MACPIES/2.7/bin/python setup.py bdist_wheel
rm -rf build
$MACPIES/3.6/bin/python3 setup.py bdist_wheel
rm -rf build
$MACPIES/3.7/bin/python3 setup.py bdist_wheel

You should now have three wheels in your distribution directory (usually dist). In my case:

dist/MarkupSafe-0.23-cp27-none-macosx_10_6_intel.whl
dist/MarkupSafe-0.23-cp36-cp36m-macosx_10_6_intel.whl
dist/MarkupSafe-0.23-cp37-cp37m-macosx_10_6_intel.whl

Check wheels for external dependencies that need shipping

pip install delocate
delocate-listdeps dist/*.whl

Markupsafe wheels have no dependencies outside the system library paths, so you get something like this:

dist/MarkupSafe-0.23-cp27-none-macosx_10_6_intel.whl:
dist/MarkupSafe-0.23-cp36-cp36m-macosx_10_6_intel.whl:
dist/MarkupSafe-0.23-cp37-cp37m-macosx_10_6_intel.whl:

If your project does have some dependencies from the analysis above, then:

mkdir fixed_wheels
delocate-wheel -w fixed_wheels dist/*.whl

Upload to pypi

Finally, you can upload these to pypi, maybe using twine. Then you will go green here: http://pythonwheels.com/

Automating wheel builds with travis

We (the MacPython organization) support some other projects building macOS wheels using travis-ci.org; feel free to contact us if you'd like help too.

See Wheel building for details.

Changing your wheel compatibility

At time of writing this paragraph, macOS 10.6 was long since end of life, and there are a very small number of installations in the wild as old as macOS 10.9.

It can be tiresome to build wheels with the very old 10.6 SDK, including the problems with C++ noted above. It is often annoying to build wheels with 32-bit as well as 64-bit architecture.

As a result, it has become fairly common for people to build wheels with the 10.9 SDK. You can do this by setting:

export MACOSX_DEPLOYMENT_TARGET=10.9

before you do your build. Since wheel 0.34.0, this will produce a wheel whose platform tag matches either this macOS version, or the minimum of any libraries that your wheel pulls in, whichever is later.