Skip to content

Commit 93c06bc

Browse files
authored
Merge pull request #1041 from tnull/2026-08-python-wheels
Prepare Python wheel releases
2 parents d9b8f7b + 6c51028 commit 93c06bc

10 files changed

Lines changed: 497 additions & 54 deletions

File tree

.github/workflows/python.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212
runs-on: ubuntu-latest
1313

1414
env:
15+
CARGO_TARGET_DIR: /tmp/cargo-target-ldk-node-python-ci
1516
LDK_NODE_PYTHON_DIR: bindings/python
1617

1718
steps:
@@ -20,6 +21,11 @@ jobs:
2021

2122
- name: Install uv
2223
uses: astral-sh/setup-uv@v7
24+
with:
25+
version: "0.12.3"
26+
27+
- name: Install supported Python versions
28+
run: uv python install 3.10 3.14
2329

2430
- name: Generate Python bindings
2531
run: ./scripts/uniffi_bindgen_generate_python.sh
@@ -35,4 +41,8 @@ jobs:
3541
ESPLORA_ENDPOINT: "http://127.0.0.1:3002"
3642
run: |
3743
cd $LDK_NODE_PYTHON_DIR
38-
uv run --group dev python -m unittest discover -s src/ldk_node
44+
for python_version in 3.10 3.14; do
45+
UV_PROJECT_ENVIRONMENT=".venv-$python_version" \
46+
uv run --python "$python_version" --group dev \
47+
python -m unittest discover -s src/ldk_node
48+
done

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,15 @@ swift.swiftdoc
2727
/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.kt
2828
/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.kt
2929
/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/
30+
31+
# Ignore generated Python bindings and build output
32+
/bindings/python/.venv/
33+
/bindings/python/.venv-*/
34+
/bindings/python/dist/
35+
/bindings/python/uv.lock
36+
/bindings/python/wheelhouse/
37+
/bindings/python/src/ldk_node/ldk_node.py
38+
/bindings/python/src/ldk_node/libldk_node.so
39+
/bindings/python/src/ldk_node/libldk_node.dylib
40+
/bindings/python/__pycache__/
41+
/bindings/python/src/ldk_node/__pycache__/

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
[![Crate](https://img.shields.io/crates/v/ldk-node.svg?logo=rust)](https://crates.io/crates/ldk-node)
44
[![Documentation](https://img.shields.io/static/v1?logo=read-the-docs&label=docs.rs&message=ldk-node&color=informational)](https://docs.rs/ldk-node)
5+
[![PyPI](https://img.shields.io/pypi/v/ldk-node.svg?logo=python)](https://pypi.org/project/ldk-node/)
56
[![Maven Central Android](https://img.shields.io/maven-central/v/org.lightningdevkit/ldk-node-android)](https://central.sonatype.com/artifact/org.lightningdevkit/ldk-node-android)
67
[![Maven Central JVM](https://img.shields.io/maven-central/v/org.lightningdevkit/ldk-node-jvm)](https://central.sonatype.com/artifact/org.lightningdevkit/ldk-node-jvm)
78
[![Security Audit](https://github.com/lightningdevkit/ldk-node/actions/workflows/audit.yml/badge.svg)](https://github.com/lightningdevkit/ldk-node/actions/workflows/audit.yml)

bindings/python/LICENSE-APACHE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE-APACHE

bindings/python/LICENSE-MIT

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE-MIT

bindings/python/hatch_build.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sysconfig
23

34
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
45
from hatchling.metadata.plugin.interface import MetadataHookInterface
@@ -20,7 +21,8 @@ def update(self, metadata):
2021

2122
class CustomBuildHook(BuildHookInterface):
2223
def initialize(self, version, build_data):
24+
platform_tag = sysconfig.get_platform().replace("-", "_").replace(".", "_")
2325
build_data["pure_python"] = False
24-
build_data["infer_tag"] = True
26+
build_data["tag"] = f"py3-none-{platform_tag}"
2527
if self.target_name == "sdist":
2628
build_data["force_include"][readme_path(self.root)] = "README.md"

bindings/python/pyproject.toml

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = ["hatchling"]
2+
requires = ["hatchling==1.32.0"]
33
build-backend = "hatchling.build"
44

55
[project]
@@ -9,14 +9,21 @@ authors = [
99
{ name="Elias Rohrer", email="dev@tnull.de" },
1010
]
1111
description = "A ready-to-go Lightning node library built using LDK and BDK."
12+
license = "MIT OR Apache-2.0"
13+
license-files = ["LICENSE-APACHE", "LICENSE-MIT"]
1214
dynamic = ["readme"]
13-
requires-python = ">=3.8"
15+
requires-python = ">=3.10"
1416
classifiers = [
1517
"Topic :: Software Development :: Libraries",
1618
"Topic :: Security :: Cryptography",
17-
"License :: OSI Approved :: MIT License",
18-
"License :: OSI Approved :: Apache Software License",
19+
"Operating System :: MacOS",
20+
"Operating System :: POSIX :: Linux",
1921
"Programming Language :: Python :: 3",
22+
"Programming Language :: Python :: 3.10",
23+
"Programming Language :: Python :: 3.11",
24+
"Programming Language :: Python :: 3.12",
25+
"Programming Language :: Python :: 3.13",
26+
"Programming Language :: Python :: 3.14",
2027
]
2128

2229
[project.urls]
@@ -29,7 +36,37 @@ dev = ["requests"]
2936

3037
[tool.hatch.build.targets.wheel]
3138
packages = ["src/ldk_node"]
39+
exclude = ["src/ldk_node/test_ldk_node.py"]
3240

3341
[tool.hatch.metadata.hooks.custom]
3442

3543
[tool.hatch.build.hooks.custom]
44+
45+
[tool.cibuildwheel]
46+
archs = ["native"]
47+
build = "cp3{10,11,12,13,14}-{manylinux,macosx}_*"
48+
build-frontend = "uv"
49+
test-command = 'python -c "import ldk_node; ldk_node.default_config()"'
50+
51+
[tool.cibuildwheel.linux]
52+
before-all = [
53+
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.95.0",
54+
'. "$HOME/.cargo/env"',
55+
"cd {package}/../.. && ./scripts/uniffi_bindgen_generate_python.sh",
56+
]
57+
environment-pass = ["CARGO_TARGET_DIR", "SOURCE_DATE_EPOCH"]
58+
manylinux-x86_64-image = "manylinux_2_28"
59+
manylinux-aarch64-image = "manylinux_2_28"
60+
61+
[tool.cibuildwheel.macos]
62+
archs = ["x86_64", "arm64"]
63+
before-all = "rustup target add x86_64-apple-darwin aarch64-apple-darwin --toolchain 1.95.0"
64+
before-build = "cd {package}/../.. && RUSTUP_TOOLCHAIN=1.95.0 ./scripts/uniffi_bindgen_generate_python.sh"
65+
66+
[[tool.cibuildwheel.overrides]]
67+
select = "*-macosx_x86_64"
68+
environment = { MACOSX_DEPLOYMENT_TARGET = "10.12" }
69+
70+
[[tool.cibuildwheel.overrides]]
71+
select = "*-macosx_arm64"
72+
environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" }

scripts/python_build_wheel.sh

Lines changed: 174 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,183 @@
11
#!/bin/bash
2-
# Build a Python wheel for the current platform.
2+
# Build and test native Python wheels on the current host.
33
#
4-
# This script compiles the Rust library, generates Python bindings via UniFFI,
5-
# and builds a platform-specific wheel using uv + hatchling.
6-
#
7-
# Run this on each target platform (Linux, macOS) to collect wheels, then use
8-
# scripts/python_publish_package.sh to publish them.
4+
# Run this script once on each supported build host:
5+
# Linux x86_64, Linux aarch64, macOS arm64.
6+
# Each Linux host builds its native wheel. The macOS arm64 host builds both
7+
# arm64 and x86_64 wheels and requires Rust 1.95.0 and Rosetta 2.
98

10-
set -e
9+
set -euo pipefail
1110

1211
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1312
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
13+
OUTPUT_DIR="$REPO_ROOT/bindings/python/wheelhouse"
14+
ALLOW_DIRTY=false
15+
CIBUILDWHEEL_VERSION="4.1.0"
16+
17+
usage() {
18+
echo "Usage: $0 [--output-dir DIR] [--allow-dirty]"
19+
}
20+
21+
while [[ $# -gt 0 ]]; do
22+
case "$1" in
23+
--output-dir)
24+
[[ $# -ge 2 ]] || { usage >&2; exit 2; }
25+
OUTPUT_DIR="$2"
26+
shift 2
27+
;;
28+
--allow-dirty)
29+
ALLOW_DIRTY=true
30+
shift
31+
;;
32+
-h|--help)
33+
usage
34+
exit 0
35+
;;
36+
*)
37+
echo "Unknown argument: $1" >&2
38+
usage >&2
39+
exit 2
40+
;;
41+
esac
42+
done
43+
44+
case "$OUTPUT_DIR" in
45+
/*) ;;
46+
*) OUTPUT_DIR="$REPO_ROOT/$OUTPUT_DIR" ;;
47+
esac
48+
49+
for command_name in git uv; do
50+
if ! command -v "$command_name" >/dev/null 2>&1; then
51+
echo "Required command not found: $command_name" >&2
52+
exit 1
53+
fi
54+
done
1455

1556
cd "$REPO_ROOT"
1657

17-
# Generate bindings and compile the native library
18-
echo "Generating Python bindings..."
19-
./scripts/uniffi_bindgen_generate_python.sh
20-
21-
# Build the wheel
22-
echo "Building wheel..."
23-
cd bindings/python
24-
uv build --wheel
25-
26-
echo ""
27-
echo "Wheel built successfully:"
28-
ls -1 dist/*.whl
29-
echo ""
30-
echo "Collect wheels from all target platforms into dist/, then run:"
31-
echo " ./scripts/python_publish_package.sh"
58+
HOST_OS="$(uname -s)"
59+
HOST_ARCH="$(uname -m)"
60+
61+
case "$HOST_OS:$HOST_ARCH" in
62+
Linux:x86_64|Linux:amd64|Linux:aarch64|Linux:arm64|Darwin:aarch64|Darwin:arm64) ;;
63+
*)
64+
echo "Unsupported build host: $HOST_OS $HOST_ARCH" >&2
65+
exit 1
66+
;;
67+
esac
68+
69+
if [[ "$ALLOW_DIRTY" == false ]] && [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then
70+
echo "Refusing to build from a dirty worktree; commit the release first." >&2
71+
echo "Use --allow-dirty only while developing the build configuration." >&2
72+
exit 1
73+
fi
74+
75+
case "$HOST_OS" in
76+
Linux)
77+
EXPECTED_WHEEL_COUNT=1
78+
case "${CIBW_CONTAINER_ENGINE:-}" in
79+
podman*) CONTAINER_COMMAND=podman ;;
80+
docker*|"") CONTAINER_COMMAND=docker ;;
81+
*)
82+
echo "Unsupported CIBW_CONTAINER_ENGINE: $CIBW_CONTAINER_ENGINE" >&2
83+
exit 1
84+
;;
85+
esac
86+
87+
if ! command -v "$CONTAINER_COMMAND" >/dev/null 2>&1; then
88+
if [[ -z "${CIBW_CONTAINER_ENGINE:-}" ]] && command -v podman >/dev/null 2>&1; then
89+
export CIBW_CONTAINER_ENGINE=podman
90+
else
91+
echo "Linux wheel builds require Docker or Podman." >&2
92+
exit 1
93+
fi
94+
fi
95+
;;
96+
Darwin)
97+
EXPECTED_WHEEL_COUNT=2
98+
if ! command -v rustup >/dev/null 2>&1 || \
99+
! rustup run 1.95.0 rustc --version >/dev/null 2>&1; then
100+
echo "macOS wheel builds require the Rust 1.95.0 toolchain." >&2
101+
echo "Install it with: rustup toolchain install 1.95.0 --profile minimal" >&2
102+
exit 1
103+
fi
104+
if ! /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
105+
echo "macOS x86_64 wheel tests require Rosetta 2." >&2
106+
echo "Install it with: softwareupdate --install-rosetta" >&2
107+
exit 1
108+
fi
109+
;;
110+
*)
111+
echo "Unsupported operating system: $HOST_OS" >&2
112+
exit 1
113+
;;
114+
esac
115+
116+
mkdir -p "$OUTPUT_DIR"
117+
shopt -s nullglob
118+
existing_wheels=("$OUTPUT_DIR"/*.whl)
119+
if [[ ${#existing_wheels[@]} -ne 0 ]]; then
120+
echo "Output directory already contains wheels: $OUTPUT_DIR" >&2
121+
exit 1
122+
fi
123+
124+
CARGO_TARGET_DIR="$(mktemp -d /tmp/cargo-target-ldk-node-python-wheels.XXXXXX)"
125+
export CARGO_TARGET_DIR
126+
BUILD_SOURCE_DIR="$(mktemp -d /tmp/ldk-node-python-wheels-source.XXXXXX)"
127+
SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)"
128+
export SOURCE_DATE_EPOCH
129+
130+
cleanup() {
131+
case "$CARGO_TARGET_DIR" in
132+
/tmp/cargo-target-ldk-node-python-wheels.*)
133+
rm -rf -- "$CARGO_TARGET_DIR"
134+
;;
135+
esac
136+
case "$BUILD_SOURCE_DIR" in
137+
/tmp/ldk-node-python-wheels-source.*)
138+
rm -rf -- "$BUILD_SOURCE_DIR"
139+
;;
140+
esac
141+
}
142+
trap cleanup EXIT
143+
144+
git ls-files --cached --others --exclude-standard -z | while IFS= read -r -d '' source_path; do
145+
if [[ -e "$source_path" || -L "$source_path" ]]; then
146+
printf '%s\0' "$source_path"
147+
fi
148+
done | tar --null --files-from=- -cf - | tar -xf - -C "$BUILD_SOURCE_DIR"
149+
150+
echo "Building Python wheel from commit $(git rev-parse HEAD)"
151+
echo "Build host: $HOST_OS $HOST_ARCH"
152+
153+
(
154+
cd "$BUILD_SOURCE_DIR"
155+
uv tool run --python 3.11 --from "cibuildwheel[uv]==$CIBUILDWHEEL_VERSION" cibuildwheel \
156+
bindings/python \
157+
--config-file bindings/python/pyproject.toml \
158+
--output-dir "$OUTPUT_DIR"
159+
)
160+
161+
built_wheels=("$OUTPUT_DIR"/*.whl)
162+
if [[ ${#built_wheels[@]} -ne $EXPECTED_WHEEL_COUNT ]]; then
163+
echo "Expected $EXPECTED_WHEEL_COUNT wheels, found ${#built_wheels[@]}." >&2
164+
exit 1
165+
fi
166+
167+
for wheel_path in "${built_wheels[@]}"; do
168+
wheel_name="$(basename "$wheel_path")"
169+
(
170+
cd "$OUTPUT_DIR"
171+
if command -v sha256sum >/dev/null 2>&1; then
172+
sha256sum "$wheel_name"
173+
else
174+
shasum -a 256 "$wheel_name"
175+
fi
176+
) > "$wheel_path.sha256"
177+
done
178+
179+
echo "Wheels built and tested successfully:"
180+
for wheel_path in "${built_wheels[@]}"; do
181+
echo " $wheel_path"
182+
echo " $wheel_path.sha256"
183+
done

0 commit comments

Comments
 (0)