Skip to content

Commit 989d552

Browse files
committed
Prepare Python wheel releases
Build Python-ABI-independent wheels for supported Linux and macOS targets so users can install LDK Node directly from PyPI. Keep build and publication available through local scripts. Require the complete artifact set on TestPyPI before production, and exercise the bindings on the oldest and newest supported CPython versions. Co-Authored-By: HAL 9000
1 parent aae54bc commit 989d552

8 files changed

Lines changed: 458 additions & 53 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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,19 @@ LDK Node does not provide a stable public API until v1.0. Persisted node state i
7373
## Language Support
7474
LDK Node itself is written in [Rust][rust] and may therefore be natively added as a library dependency to any `std` Rust program. However, beyond its Rust API it also offers language bindings for [Swift][swift], [Kotlin][kotlin], and [Python][python] based on the [UniFFI](https://github.com/mozilla/uniffi-rs/).
7575

76+
### Python
77+
78+
Starting with version 0.8.0, Python bindings are available from
79+
[PyPI](https://pypi.org/project/ldk-node/):
80+
81+
```shell
82+
uv add ldk-node
83+
```
84+
85+
The published wheels support CPython 3.10 through 3.14 on Linux x86_64 and
86+
aarch64 with glibc 2.28 or newer, and on macOS x86_64 10.12 or newer and
87+
Apple Silicon 11.0 or newer.
88+
7689
## MSRV
7790
The Minimum Supported Rust Version (MSRV) is currently 1.85.0.
7891

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
@@ -16,5 +17,6 @@ def update(self, metadata):
1617

1718
class CustomBuildHook(BuildHookInterface):
1819
def initialize(self, version, build_data):
20+
platform_tag = sysconfig.get_platform().replace("-", "_").replace(".", "_")
1921
build_data["pure_python"] = False
20-
build_data["infer_tag"] = True
22+
build_data["tag"] = f"py3-none-{platform_tag}"

bindings/python/pyproject.toml

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

55
[project]
66
name = "ldk_node"
7-
version = "0.7.0"
7+
version = "0.8.0"
88
authors = [
99
{ name="Elias Rohrer", email="dev@tnull.de" },
1010
]
1111
description = "A ready-to-go Lightning node library built using LDK and BDK."
1212
dynamic = ["readme"]
13-
requires-python = ">=3.8"
13+
requires-python = ">=3.10"
1414
classifiers = [
1515
"Topic :: Software Development :: Libraries",
1616
"Topic :: Security :: Cryptography",
1717
"License :: OSI Approved :: MIT License",
1818
"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,10 +36,38 @@ 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.build.targets.sdist.force-include]
3442
"../../README.md" = "README.md"
3543

3644
[tool.hatch.metadata.hooks.custom]
3745

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

scripts/python_build_wheel.sh

Lines changed: 145 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,154 @@
11
#!/bin/bash
2-
# Build a Python wheel for the current platform.
2+
# Build and test one native Python wheel 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 native target:
5+
# Linux x86_64, Linux aarch64, macOS x86_64, macOS arm64.
6+
# Linux builds require Docker or Podman. macOS builds require Rust 1.95.0.
97

10-
set -e
8+
set -euo pipefail
119

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

1555
cd "$REPO_ROOT"
1656

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"
57+
if [[ "$ALLOW_DIRTY" == false ]] && [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then
58+
echo "Refusing to build from a dirty worktree; commit the release first." >&2
59+
echo "Use --allow-dirty only while developing the build configuration." >&2
60+
exit 1
61+
fi
62+
63+
case "$(uname -s)" in
64+
Linux)
65+
case "${CIBW_CONTAINER_ENGINE:-}" in
66+
podman*) CONTAINER_COMMAND=podman ;;
67+
docker*|"") CONTAINER_COMMAND=docker ;;
68+
*)
69+
echo "Unsupported CIBW_CONTAINER_ENGINE: $CIBW_CONTAINER_ENGINE" >&2
70+
exit 1
71+
;;
72+
esac
73+
74+
if ! command -v "$CONTAINER_COMMAND" >/dev/null 2>&1; then
75+
if [[ -z "${CIBW_CONTAINER_ENGINE:-}" ]] && command -v podman >/dev/null 2>&1; then
76+
export CIBW_CONTAINER_ENGINE=podman
77+
else
78+
echo "Linux wheel builds require Docker or Podman." >&2
79+
exit 1
80+
fi
81+
fi
82+
;;
83+
Darwin)
84+
if ! command -v rustup >/dev/null 2>&1 || \
85+
! rustup run 1.95.0 rustc --version >/dev/null 2>&1; then
86+
echo "macOS wheel builds require the Rust 1.95.0 toolchain." >&2
87+
echo "Install it with: rustup toolchain install 1.95.0 --profile minimal" >&2
88+
exit 1
89+
fi
90+
;;
91+
*)
92+
echo "Unsupported operating system: $(uname -s)" >&2
93+
exit 1
94+
;;
95+
esac
96+
97+
case "$(uname -m)" in
98+
x86_64|amd64|aarch64|arm64) ;;
99+
*)
100+
echo "Unsupported architecture: $(uname -m)" >&2
101+
exit 1
102+
;;
103+
esac
104+
105+
mkdir -p "$OUTPUT_DIR"
106+
shopt -s nullglob
107+
existing_wheels=("$OUTPUT_DIR"/*.whl)
108+
if [[ ${#existing_wheels[@]} -ne 0 ]]; then
109+
echo "Output directory already contains wheels: $OUTPUT_DIR" >&2
110+
exit 1
111+
fi
112+
113+
CARGO_TARGET_DIR="$(mktemp -d /tmp/cargo-target-ldk-node-python-wheels.XXXXXX)"
114+
export CARGO_TARGET_DIR
115+
SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)"
116+
export SOURCE_DATE_EPOCH
117+
118+
cleanup() {
119+
case "$CARGO_TARGET_DIR" in
120+
/tmp/cargo-target-ldk-node-python-wheels.*)
121+
rm -rf -- "$CARGO_TARGET_DIR"
122+
;;
123+
esac
124+
}
125+
trap cleanup EXIT
126+
127+
echo "Building Python wheel from commit $(git rev-parse HEAD)"
128+
echo "Target: $(uname -s) $(uname -m)"
129+
130+
uv tool run --from "cibuildwheel[uv]==$CIBUILDWHEEL_VERSION" cibuildwheel \
131+
bindings/python \
132+
--config-file bindings/python/pyproject.toml \
133+
--output-dir "$OUTPUT_DIR"
134+
135+
built_wheels=("$OUTPUT_DIR"/*.whl)
136+
if [[ ${#built_wheels[@]} -ne 1 ]]; then
137+
echo "Expected exactly one wheel, found ${#built_wheels[@]}." >&2
138+
exit 1
139+
fi
140+
141+
wheel_path="${built_wheels[0]}"
142+
wheel_name="$(basename "$wheel_path")"
143+
(
144+
cd "$OUTPUT_DIR"
145+
if command -v sha256sum >/dev/null 2>&1; then
146+
sha256sum "$wheel_name"
147+
else
148+
shasum -a 256 "$wheel_name"
149+
fi
150+
) > "$wheel_path.sha256"
151+
152+
echo "Wheel built and tested successfully:"
153+
echo " $wheel_path"
154+
echo " $wheel_path.sha256"

0 commit comments

Comments
 (0)