Skip to content

Commit 2096d4c

Browse files
authored
pkg: pack one binary per platform into python wheels (#1181)
1 parent a50fcff commit 2096d4c

File tree

5 files changed

+208
-61
lines changed

5 files changed

+208
-61
lines changed

.github/workflows/release.yml

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -129,27 +129,17 @@ jobs:
129129
name: dist
130130
- run: tar -xvf dist.tar
131131

132-
- name: Setup Python
133-
uses: actions/setup-python@v5
132+
- name: Setup uv with python
133+
uses: astral-sh/setup-uv@v7
134134
with:
135-
python-version: '3.12'
136-
- run: python -m pip install --upgrade pip twine wheel setuptools
135+
enable-cache: false
136+
python-version: "3.12"
137+
version: "latest"
137138

138139
- name: Publish to PyPI
139140
env:
140-
PYPI_API_KEY: ${{ secrets.PYPI_API_KEY }}
141+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
141142
run: |
142-
cat << EOF > ~/.pypirc
143-
[distutils]
144-
index-servers =
145-
lefthook
146-
147-
[lefthook]
148-
repository = https://upload.pypi.org/legacy/
149-
username = __token__
150-
password = ${PYPI_API_KEY}
151-
EOF
152-
chmod 0600 ~/.pypirc
153143
cd packaging/
154144
ruby pack.rb prepare
155145
ruby pack.rb publish_pypi

packaging/pack.rb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
ROOT = File.join(__dir__, "..")
1010
DIST = File.join(ROOT, "dist")
1111

12+
PYTHON_PLATFORMS = ["linux", "darwin", "windows"].product(["x86_64", "arm64"])
13+
1214
module Pack
1315
extend FileUtils
1416

@@ -39,7 +41,7 @@ def set_version
3941

4042
replace_in_file("npm/lefthook/package.json", /"(lefthook-.+)": "[\d.]+"/, %{"\\1": "#{VERSION}"})
4143
replace_in_file("rubygems/lefthook.gemspec", /(spec\.version\s+= ).*/, %{\\1"#{VERSION}"})
42-
replace_in_file("pypi/setup.py", /(version+=).*/, %{\\1'#{VERSION}',})
44+
replace_in_file("pypi/pyproject.toml", /(version\s*=\s*)"[^"]+"/, %{\\1"#{VERSION}"})
4345
replace_in_file("aur/lefthook/PKGBUILD", /(pkgver+=).*/, %{\\1#{VERSION}})
4446
replace_in_file("aur/lefthook-bin/PKGBUILD", /(pkgver+=).*/, %{\\1#{VERSION}})
4547
end
@@ -164,9 +166,18 @@ def publish_gem
164166

165167
def publish_pypi
166168
puts "Publishing to PyPI..."
167-
cd(File.join(__dir__, "pypi"))
168-
system("python setup.py sdist bdist_wheel", exception: true)
169-
system("python -m twine upload --verbose --repository lefthook dist/*", exception: true)
169+
pypi_dir = File.join(__dir__, "pypi")
170+
171+
PYTHON_PLATFORMS.each do |os, arch|
172+
puts "Building wheel for #{os}-#{arch}..."
173+
cd(pypi_dir)
174+
ENV["LEFTHOOK_TARGET_PLATFORM"] = os
175+
ENV["LEFTHOOK_TARGET_ARCH"] = arch
176+
system("uv build --wheel", exception: true)
177+
end
178+
179+
puts "Uploading to PyPI..."
180+
system("uv publish", exception: true)
170181
end
171182

172183
def publish_aur_lefthook

packaging/pypi/hatch_build.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import atexit
2+
import os
3+
import platform
4+
import shutil
5+
import sys
6+
import tempfile
7+
from pathlib import Path
8+
9+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
10+
11+
12+
PLATFORM_MAPPING = {
13+
'linux': 'linux',
14+
'linux2': 'linux',
15+
'darwin': 'darwin',
16+
'win32': 'windows',
17+
'windows': 'windows',
18+
'freebsd': 'freebsd',
19+
'openbsd': 'openbsd',
20+
}
21+
22+
ARCH_MAPPING = {
23+
'x86_64': 'x86_64',
24+
'amd64': 'x86_64',
25+
'arm64': 'arm64',
26+
'aarch64': 'arm64',
27+
}
28+
29+
30+
PEP425_TAGS = {
31+
("linux", "x86_64"): "py3-none-manylinux_2_17_x86_64",
32+
("linux", "arm64"): "py3-none-manylinux_2_17_aarch64",
33+
("darwin", "x86_64"): "py3-none-macosx_10_15_x86_64",
34+
("darwin", "arm64"): "py3-none-macosx_11_0_arm64",
35+
("windows", "x86_64"): "py3-none-win_amd64",
36+
("windows", "arm64"): "py3-none-win_arm64",
37+
}
38+
39+
40+
def normalize_platform(value: str) -> str:
41+
if not value:
42+
return value
43+
return PLATFORM_MAPPING.get(value.lower(), value.lower())
44+
45+
46+
def normalize_arch(value: str) -> str:
47+
if not value:
48+
return value
49+
return ARCH_MAPPING.get(value.lower(), value.lower())
50+
51+
52+
def get_platform_info():
53+
target_platform = os.environ.get('LEFTHOOK_TARGET_PLATFORM')
54+
target_arch = os.environ.get('LEFTHOOK_TARGET_ARCH')
55+
56+
if target_platform and target_arch:
57+
normalized_platform = normalize_platform(target_platform)
58+
normalized_arch = normalize_arch(target_arch)
59+
print(f"[HOOK] Using target: {normalized_platform}-{normalized_arch}")
60+
return normalized_platform, normalized_arch
61+
62+
system = normalize_platform(sys.platform) or normalize_platform(platform.system())
63+
machine = normalize_arch(platform.machine())
64+
result = system, machine
65+
print(f"[HOOK] Auto-detected: {result[0]}-{result[1]}")
66+
return result
67+
68+
69+
class CustomBuildHook(BuildHookInterface):
70+
PLUGIN_NAME = "custom"
71+
72+
def __init__(self, *args, **kwargs) -> None:
73+
super().__init__(*args, **kwargs)
74+
self.target_platform = None
75+
self.target_arch = None
76+
self._temp_dir = None
77+
self._moved_entries = []
78+
self._restore_registered = False
79+
80+
def initialize(self, version, build_data):
81+
target_platform, target_arch = get_platform_info()
82+
self.target_platform = target_platform
83+
self.target_arch = target_arch
84+
85+
tag = PEP425_TAGS.get((target_platform, target_arch))
86+
if tag:
87+
build_data["tag"] = tag
88+
self._prune_binaries()
89+
if not self._restore_registered:
90+
atexit.register(self._restore_binaries)
91+
self._restore_registered = True
92+
print(f"[HOOK] Building platform wheel {tag}")
93+
else:
94+
print(
95+
"[HOOK] No PEP425 tag for "
96+
f"{target_platform}-{target_arch}; building universal wheel."
97+
)
98+
99+
print(f"[HOOK] Initialized for {target_platform}-{target_arch}")
100+
101+
def finalize(self, version, build_data, artifact_path) -> None:
102+
print(f"[HOOK] Built artifact: {artifact_path}")
103+
self._restore_binaries()
104+
105+
def _prune_binaries(self):
106+
if not self.target_platform or not self.target_arch:
107+
raise RuntimeError("Target platform is not set before pruning binaries.")
108+
109+
bin_dir = Path(self.root) / "lefthook" / "bin"
110+
if not bin_dir.is_dir():
111+
raise RuntimeError(f"Bin directory not found: {bin_dir}")
112+
113+
target_dir_name = f"lefthook-{self.target_platform}-{self.target_arch}"
114+
target_dir = bin_dir / target_dir_name
115+
if not target_dir.exists():
116+
available = ", ".join(sorted(p.name for p in bin_dir.iterdir() if p.is_dir()))
117+
raise FileNotFoundError(
118+
f"Binary folder '{target_dir_name}' is missing. Available: {available or 'none'}"
119+
)
120+
121+
binaries = list(target_dir.glob("lefthook*"))
122+
if not binaries:
123+
raise FileNotFoundError(
124+
f"No lefthook binary found under {target_dir}."
125+
)
126+
127+
self._temp_dir = Path(tempfile.mkdtemp(prefix="lefthook-bin-backup-"))
128+
preserved = {target_dir_name, ".keep"}
129+
130+
for entry in bin_dir.iterdir():
131+
if entry.name in preserved:
132+
continue
133+
destination = self._temp_dir / entry.name
134+
shutil.move(str(entry), str(destination))
135+
self._moved_entries.append((destination, entry))
136+
137+
print(f"[HOOK] Shipped binaries: {target_dir_name}")
138+
139+
def _restore_binaries(self):
140+
while self._moved_entries:
141+
backup_path, original_path = self._moved_entries.pop()
142+
if backup_path.exists():
143+
shutil.move(str(backup_path), str(original_path))
144+
if self._temp_dir and self._temp_dir.exists():
145+
shutil.rmtree(self._temp_dir, ignore_errors=True)
146+
self._temp_dir = None

packaging/pypi/pyproject.toml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[project]
2+
name = "lefthook"
3+
version = "2.0.15"
4+
description = "Git hooks manager. Fast, powerful, simple."
5+
readme = "README.md"
6+
license = "MIT"
7+
license-files = ["LICENSE"]
8+
authors = [
9+
{name = "Evil Martians", email = "lefthook@evilmartians.com"}
10+
]
11+
requires-python = ">=3.6"
12+
classifiers = [
13+
"Operating System :: OS Independent",
14+
"Topic :: Software Development :: Version Control :: Git",
15+
]
16+
17+
[project.urls]
18+
Homepage = "https://github.com/evilmartians/lefthook"
19+
20+
[project.scripts]
21+
lefthook = "lefthook.main:main"
22+
23+
[build-system]
24+
requires = ["hatchling"]
25+
build-backend = "hatchling.build"
26+
27+
[tool.hatch.build.targets.wheel]
28+
packages = ["lefthook"]
29+
30+
[tool.hatch.build.hooks.custom]
31+
path = "hatch_build.py"
32+
33+
[tool.hatch.build.targets.sdist]
34+
include = [
35+
"lefthook/",
36+
]
37+
38+
[[tool.uv.index]]
39+
name = "pypi"
40+
url = "https://pypi.org/simple/"
41+
default = true

packaging/pypi/setup.py

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)