Skip to content

fix: pack one binary per platform into python wheels#1181

Merged
mrexox merged 3 commits intoevilmartians:masterfrom
danfimov:fix-pypi-distribution
Jan 21, 2026
Merged

fix: pack one binary per platform into python wheels#1181
mrexox merged 3 commits intoevilmartians:masterfrom
danfimov:fix-pypi-distribution

Conversation

@danfimov
Copy link
Contributor

@danfimov danfimov commented Nov 2, 2025

Closes #971

Context

Previously, the PyPI package was being built as a single wheel containing binaries for all architectures. This meant every user, regardless of their platform, was downloading a 50+ MB wheel with binaries they would never use. This is inefficient and not the proper way to distribute platform-specific packages on PyPI.

Changes

The fix implements platform-specific wheel generation, where each wheel contains only the binary for its target architecture.

I also replaced license classifier with license_files parameter, because this classifier is deprecated:

/Users/danfimov/Documents/fun/lefthook/packaging/pypi/.venv/lib/python3.12/site-packages/setuptools/config/_apply_pyprojecttoml.py:61: SetuptoolsDeprecationWarning: License classifiers are deprecated.
!!

        ********************************************************************************
        Please consider removing the following classifiers in favor of a SPDX license expression:

        License :: OSI Approved :: MIT License

        See https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license for details.
        ********************************************************************************

!!

I tested building process with this commands:

  • Go to directory with setup.py: cd packaging/pypi
  • Call setup.py to build wheel for my machine: python setup.py bdist_wheel
  • To test with different architecture, I added env vars: LEFTHOOK_TARGET_PLATFORM=linux LEFTHOOK_TARGET_ARCH=arm64 python setup.py bdist_wheel

P.S. I'm familiar with Python, but not with Ruby, so please review part with pack.rb changes carefully)

Copilot AI review requested due to automatic review settings November 2, 2025 13:12
@danfimov danfimov requested a review from mrexox as a code owner November 2, 2025 13:12
@danfimov danfimov force-pushed the fix-pypi-distribution branch from 8aa2b73 to 9bf9812 Compare November 2, 2025 13:15
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the PyPI packaging to generate platform-specific wheels instead of bundling all platform binaries in a single universal wheel. The change creates separate wheel distributions for each OS/architecture combination (Linux, Darwin, FreeBSD, OpenBSD, Windows × x86_64, arm64).

  • Introduces platform detection logic and custom wheel building to package only the relevant binary per platform
  • Updates the Ruby packaging script to build separate wheels for each platform combination
  • Removes the MIT License classifier from setup.py while adding license_files directive

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
packaging/pypi/setup.py Adds platform detection, custom wheel class, and dynamic package_data generation to create platform-specific wheels
packaging/pack.rb Updates PyPI publishing to build separate wheels for each platform using environment variables

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@danfimov
Copy link
Contributor Author

danfimov commented Nov 2, 2025

Maybe it will be even better to migrate from setup.py completely and use pyproject.toml with build instead:

But let's think about it in a different MR after fixing this issue with platform-specific binaries)

@mrexox
Copy link
Member

mrexox commented Nov 5, 2025

Thank you for preparing this PR! I will review it later this week 👍

@mrexox
Copy link
Member

mrexox commented Nov 13, 2025

I have slightly changed the script and built the packages

PYTHON_PLATFORMS = ["linux", "darwin", "freebsd", "openbsd", "windows"].product(["x86_64", "arm64"])

# ...

    PYTHON_PLATFORMS.each do |os, arch|
      puts "Building wheel for #{os}-#{arch}..."

      cd(pypi_dir)
      ENV["LEFTHOOK_TARGET_PLATFORM"] = os
      ENV["LEFTHOOK_TARGET_ARCH"] = arch
      system("python setup.py bdist_wheel", exception: true)
    end

    # cd(pypi_dir)
    # system("python -m twine upload --verbose --repository lefthook dist/*", exception: true)

However when I try to install a package it fails:

pip install packaging/pypi/dist/lefthook-2.0.2-py3-none-darwin_arm64.whl
ERROR: lefthook-2.0.2-py3-none-darwin_arm64.whl is not a supported wheel on this platform.

Also I noticed that logs print info about adding all binaries:

Building wheel for openbsd-arm64...
running bdist_wheel
running build
running build_py
copying lefthook/__init__.py -> build/lib/lefthook
copying lefthook/main.py -> build/lib/lefthook
copying lefthook/__main__.py -> build/lib/lefthook
creating build/lib/lefthook/bin/lefthook-openbsd-arm64
copying lefthook/bin/lefthook-openbsd-arm64/lefthook -> build/lib/lefthook/bin/lefthook-openbsd-arm64
/Users/ian/.local/share/mise/installs/python/3.11.9/lib/python3.11/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
!!

        ********************************************************************************
        Please avoid running ``setup.py`` directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details.
        ********************************************************************************

!!
  self.initialize_options()
installing to build/bdist.macosx-11.0-arm64/wheel
running install
running install_lib
creating build/bdist.macosx-11.0-arm64/wheel
creating build/bdist.macosx-11.0-arm64/wheel/lefthook
creating build/bdist.macosx-11.0-arm64/wheel/lefthook/bin
creating build/bdist.macosx-11.0-arm64/wheel/lefthook/bin/lefthook-openbsd-arm64
copying build/lib/lefthook/bin/lefthook-openbsd-arm64/lefthook -> build/bdist.macosx-11.0-arm64/wheel/./lefthook/bin/lefthook-openbsd-arm64
creating build/bdist.macosx-11.0-arm64/wheel/lefthook/bin/lefthook-linux-x86_64
copying build/lib/lefthook/bin/lefthook-linux-x86_64/lefthook -> build/bdist.macosx-11.0-arm64/wheel/./lefthook/bin/lefthook-linux-x86_64
creating build/bdist.macosx-11.0-arm64/wheel/lefthook/bin/lefthook-linux-arm64
copying build/lib/lefthook/bin/lefthook-linux-arm64/lefthook -> build/bdist.macosx-11.0-arm64/wheel/./lefthook/bin/lefthook-linux-arm64
creating build/bdist.macosx-11.0-arm64/wheel/lefthook/bin/lefthook-freebsd-arm64
copying build/lib/lefthook/bin/lefthook-freebsd-arm64/lefthook -> build/bdist.macosx-11.0-arm64/wheel/./lefthook/bin/lefthook-freebsd-arm64
creating build/bdist.macosx-11.0-arm64/wheel/lefthook/bin/lefthook-darwin-x86_64
copying build/lib/lefthook/bin/lefthook-darwin-x86_64/lefthook -> build/bdist.macosx-11.0-arm64/wheel/./lefthook/bin/lefthook-darwin-x86_64
creating build/bdist.macosx-11.0-arm64/wheel/lefthook/bin/lefthook-darwin-arm64
copying build/lib/lefthook/bin/lefthook-darwin-arm64/lefthook -> build/bdist.macosx-11.0-arm64/wheel/./lefthook/bin/lefthook-darwin-arm64
creating build/bdist.macosx-11.0-arm64/wheel/lefthook/bin/lefthook-openbsd-x86_64
copying build/lib/lefthook/bin/lefthook-openbsd-x86_64/lefthook -> build/bdist.macosx-11.0-arm64/wheel/./lefthook/bin/lefthook-openbsd-x86_64
creating build/bdist.macosx-11.0-arm64/wheel/lefthook/bin/lefthook-freebsd-x86_64
copying build/lib/lefthook/bin/lefthook-freebsd-x86_64/lefthook -> build/bdist.macosx-11.0-arm64/wheel/./lefthook/bin/lefthook-freebsd-x86_64
copying build/lib/lefthook/__init__.py -> build/bdist.macosx-11.0-arm64/wheel/./lefthook
copying build/lib/lefthook/main.py -> build/bdist.macosx-11.0-arm64/wheel/./lefthook
copying build/lib/lefthook/__main__.py -> build/bdist.macosx-11.0-arm64/wheel/./lefthook
running install_egg_info
running egg_info
writing lefthook.egg-info/PKG-INFO
writing dependency_links to lefthook.egg-info/dependency_links.txt
writing entry points to lefthook.egg-info/entry_points.txt
writing top-level names to lefthook.egg-info/top_level.txt
reading manifest file 'lefthook.egg-info/SOURCES.txt'
adding license file 'LICENSE'
writing manifest file 'lefthook.egg-info/SOURCES.txt'
Copying lefthook.egg-info to build/bdist.macosx-11.0-arm64/wheel/./lefthook-2.0.2-py3.11.egg-info
running install_scripts
creating build/bdist.macosx-11.0-arm64/wheel/lefthook-2.0.2.dist-info/WHEEL
creating 'dist/lefthook-2.0.2-py3-none-openbsd_arm64.whl' and adding 'build/bdist.macosx-11.0-arm64/wheel' to it
adding 'lefthook/__init__.py'
adding 'lefthook/__main__.py'
adding 'lefthook/main.py'
adding 'lefthook/bin/lefthook-darwin-arm64/lefthook'
adding 'lefthook/bin/lefthook-darwin-x86_64/lefthook'
adding 'lefthook/bin/lefthook-freebsd-arm64/lefthook'
adding 'lefthook/bin/lefthook-freebsd-x86_64/lefthook'
adding 'lefthook/bin/lefthook-linux-arm64/lefthook'
adding 'lefthook/bin/lefthook-linux-x86_64/lefthook'
adding 'lefthook/bin/lefthook-openbsd-arm64/lefthook'
adding 'lefthook/bin/lefthook-openbsd-x86_64/lefthook'
adding 'lefthook-2.0.2.dist-info/LICENSE'
adding 'lefthook-2.0.2.dist-info/METADATA'
adding 'lefthook-2.0.2.dist-info/WHEEL'
adding 'lefthook-2.0.2.dist-info/entry_points.txt'
adding 'lefthook-2.0.2.dist-info/top_level.txt'
adding 'lefthook-2.0.2.dist-info/RECORD'
removing build/bdist.macosx-11.0-arm64/wheel

Is there anything wrong? I'm not quite familiar with Python build process and will be able to take a look later. But maybe it's clear for you what went wrong 👀

@danfimov danfimov force-pushed the fix-pypi-distribution branch 2 times, most recently from d7506c1 to 20001fb Compare November 30, 2025 12:35
@danfimov
Copy link
Contributor Author

danfimov commented Nov 30, 2025

After some more hours of debug I decided to switch to uv and hatch for package build.

Now it should work. You can test build process like that:

  • Delete or comment string with system("uv publish", exception: true) in pack.rb
  • Call ruby pack.rb prepare and ruby pack.rb publish_pypi
  • There should be separate wheel for every platform + universal wheel

And you can check installation of right wheel from pypi using this package from test pypi: https://test.pypi.org/project/lefthook-test/#files (I will delete it after this MR will be merrged)


There is one problem: I can't build and push to pypi wheels for freebsd / openbsd (only for linux/macos/windows). It happens becase pypi has limited number of supported platform tags (source for list of supported tags). Example of issue:

Uploading lefthook_test-2.0.4-py3-none-freebsd_13_0_aarch64.whl (4.6MiB)
error: Failed to publish `dist/lefthook_test-2.0.4-py3-none-freebsd_13_0_aarch64.whl` to https://test.pypi.org/legacy/
  Caused by: Upload failed with status code 400 Bad Request. Server says: 400 Binary wheel 'lefthook_test-2.0.4-py3-none-freebsd_13_0_aarch64.whl' has an unsupported platform tag 'freebsd_13_0_aarch64'.

So I decided to build platform-specific wheels with one binary for supported platforms + one universal wheel with every binary for other platforms. If someone will try to install lefthook on linux, he will get wheel lefthook-2.0.4-py3-none-manylinux_2_17_x86_64.whl with one binary of lefthook for his platform. If someone will try to install lefthook from pypi on freebsd, he will get universal wheel with binaries for all platforms. You can check this behavior by installing lefthook in some test project:

> uv pip install lefthook-test==2.0.5 --index-url https://test.pypi.org/simple/
Audited 1 package in 0.39ms
> cd .venv/lib/python3.14/site-packages/lefthook/bin/
> ls -la
Permissions Size User     Date Modified Name
drwxrwxr-x     - danfimov 30 Nov 13:27   lefthook-linux-x86_64
.rw-rw-r--     0 danfimov 30 Nov 13:23   .keep

It's not a perfect solution (freebsd user still need to download 50Mb of binaries), but I see only one alternative - write in docs something like "we don't support installation from pypi for platforms other than linux/windows/macos" and just not build universal wheel. And I don't really like to do that. What you think we should do @mrexox?

@mrexox
Copy link
Member

mrexox commented Dec 1, 2025

I think it's fine to keep 50MB giant for FreeBSD and OpenBSD. I'll try to find a solution if someone complains, but the goal is to deliver lefthook even with lots of odd binaries.

I will check the changes later this week. Thank you for putting an effort into this!

@danfimov danfimov force-pushed the fix-pypi-distribution branch from 20001fb to 3950fa1 Compare December 6, 2025 10:35
Copy link
Member

@mrexox mrexox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good. Will merge after rebasing and removing openbsd and freebsd platforms

@mrexox
Copy link
Member

mrexox commented Jan 20, 2026

@danfimov Sorry for a long delay. Could you please rebase the PR? I'm going to merge it after that

@danfimov danfimov force-pushed the fix-pypi-distribution branch from 81d1bba to fff9b27 Compare January 20, 2026 18:04
@danfimov
Copy link
Contributor Author

Rebased the PR and removed unsupported platforms. Should be good to merge now, @mrexox.

@danfimov danfimov force-pushed the fix-pypi-distribution branch from fff9b27 to 8decbd0 Compare January 20, 2026 18:08
@mrexox mrexox merged commit 2096d4c into evilmartians:master Jan 21, 2026
13 checks passed
@mrexox
Copy link
Member

mrexox commented Jan 21, 2026

Awesome! Thank you for taking care for such a long time!

@mrexox
Copy link
Member

mrexox commented Jan 27, 2026

Something isn't working well. You can see that in publishing logs the side of the uploaded pacakges is a few Kb: https://github.com/evilmartians/lefthook/actions/runs/21388823016/job/61571056398

It should've been ~10Mb instead. Do you have an idea of what could go wrong?

@npnh2
Copy link

npnh2 commented Feb 12, 2026

@mrexox
Hatch excludes the binaries because they are listed in .gitignore. This can be fixed by updating packaging/pypi/pyproject.toml:

[tool.hatch.build.targets.wheel]
packages = ["lefthook"]
artifacts = ["lefthook/bin"]      # add artifacts

Reference:
https://hatch.pypa.io/latest/config/build/#artifacts

@npnh2
Copy link

npnh2 commented Feb 13, 2026

@mrexox
My mistake, It seems that artifacts = ["lefthook/bin"] won't include lefthook/main.py, but artifacts = ["lefthook"] will.

@mrexox
Copy link
Member

mrexox commented Feb 13, 2026

Thank you @npnh2! I'll test this in next release :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Installing in Python environment downloads executables for all platforms

4 participants