Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
runs-on: ubuntu-latest

env:
CARGO_TARGET_DIR: /tmp/cargo-target-ldk-node-python-ci
LDK_NODE_PYTHON_DIR: bindings/python

steps:
Expand All @@ -20,6 +21,11 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.12.3"

- name: Install supported Python versions
run: uv python install 3.10 3.14

- name: Generate Python bindings
run: ./scripts/uniffi_bindgen_generate_python.sh
Expand All @@ -35,4 +41,8 @@ jobs:
ESPLORA_ENDPOINT: "http://127.0.0.1:3002"
run: |
cd $LDK_NODE_PYTHON_DIR
uv run --group dev python -m unittest discover -s src/ldk_node
for python_version in 3.10 3.14; do
UV_PROJECT_ENVIRONMENT=".venv-$python_version" \
uv run --python "$python_version" --group dev \
python -m unittest discover -s src/ldk_node
done
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,15 @@ swift.swiftdoc
/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.kt
/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.kt
/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/

# Ignore generated Python bindings and build output
/bindings/python/.venv/
/bindings/python/.venv-*/
/bindings/python/dist/
/bindings/python/uv.lock
/bindings/python/wheelhouse/
/bindings/python/src/ldk_node/ldk_node.py
/bindings/python/src/ldk_node/libldk_node.so
/bindings/python/src/ldk_node/libldk_node.dylib
/bindings/python/__pycache__/
/bindings/python/src/ldk_node/__pycache__/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![Crate](https://img.shields.io/crates/v/ldk-node.svg?logo=rust)](https://crates.io/crates/ldk-node)
[![Documentation](https://img.shields.io/static/v1?logo=read-the-docs&label=docs.rs&message=ldk-node&color=informational)](https://docs.rs/ldk-node)
[![PyPI](https://img.shields.io/pypi/v/ldk-node.svg?logo=python)](https://pypi.org/project/ldk-node/)
[![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)
[![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)
[![Security Audit](https://github.com/lightningdevkit/ldk-node/actions/workflows/audit.yml/badge.svg)](https://github.com/lightningdevkit/ldk-node/actions/workflows/audit.yml)
Expand Down
1 change: 1 addition & 0 deletions bindings/python/LICENSE-APACHE
1 change: 1 addition & 0 deletions bindings/python/LICENSE-MIT
4 changes: 3 additions & 1 deletion bindings/python/hatch_build.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sysconfig

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

class CustomBuildHook(BuildHookInterface):
def initialize(self, version, build_data):
platform_tag = sysconfig.get_platform().replace("-", "_").replace(".", "_")
build_data["pure_python"] = False
build_data["infer_tag"] = True
build_data["tag"] = f"py3-none-{platform_tag}"
if self.target_name == "sdist":
build_data["force_include"][readme_path(self.root)] = "README.md"
45 changes: 41 additions & 4 deletions bindings/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["hatchling"]
requires = ["hatchling==1.32.0"]
build-backend = "hatchling.build"

[project]
Expand All @@ -9,14 +9,21 @@ authors = [
{ name="Elias Rohrer", email="dev@tnull.de" },
]
description = "A ready-to-go Lightning node library built using LDK and BDK."
license = "MIT OR Apache-2.0"
license-files = ["LICENSE-APACHE", "LICENSE-MIT"]
dynamic = ["readme"]
requires-python = ">=3.8"
requires-python = ">=3.10"
classifiers = [
"Topic :: Software Development :: Libraries",
"Topic :: Security :: Cryptography",
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: Apache Software License",
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]

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

[tool.hatch.build.targets.wheel]
packages = ["src/ldk_node"]
exclude = ["src/ldk_node/test_ldk_node.py"]

[tool.hatch.metadata.hooks.custom]

[tool.hatch.build.hooks.custom]

[tool.cibuildwheel]
archs = ["native"]
build = "cp3{10,11,12,13,14}-{manylinux,macosx}_*"
build-frontend = "uv"
test-command = 'python -c "import ldk_node; ldk_node.default_config()"'

[tool.cibuildwheel.linux]
before-all = [
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.95.0",
'. "$HOME/.cargo/env"',
"cd {package}/../.. && ./scripts/uniffi_bindgen_generate_python.sh",
]
environment-pass = ["CARGO_TARGET_DIR", "SOURCE_DATE_EPOCH"]
manylinux-x86_64-image = "manylinux_2_28"
manylinux-aarch64-image = "manylinux_2_28"

[tool.cibuildwheel.macos]
archs = ["x86_64", "arm64"]
before-all = "rustup target add x86_64-apple-darwin aarch64-apple-darwin --toolchain 1.95.0"
before-build = "cd {package}/../.. && RUSTUP_TOOLCHAIN=1.95.0 ./scripts/uniffi_bindgen_generate_python.sh"

[[tool.cibuildwheel.overrides]]
select = "*-macosx_x86_64"
environment = { MACOSX_DEPLOYMENT_TARGET = "10.12" }

[[tool.cibuildwheel.overrides]]
select = "*-macosx_arm64"
environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" }
196 changes: 174 additions & 22 deletions scripts/python_build_wheel.sh
Original file line number Diff line number Diff line change
@@ -1,31 +1,183 @@
#!/bin/bash
# Build a Python wheel for the current platform.
# Build and test native Python wheels on the current host.
#
# This script compiles the Rust library, generates Python bindings via UniFFI,
# and builds a platform-specific wheel using uv + hatchling.
#
# Run this on each target platform (Linux, macOS) to collect wheels, then use
# scripts/python_publish_package.sh to publish them.
# Run this script once on each supported build host:
# Linux x86_64, Linux aarch64, macOS arm64.
# Each Linux host builds its native wheel. The macOS arm64 host builds both
# arm64 and x86_64 wheels and requires Rust 1.95.0 and Rosetta 2.

set -e
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUTPUT_DIR="$REPO_ROOT/bindings/python/wheelhouse"
ALLOW_DIRTY=false
CIBUILDWHEEL_VERSION="4.1.0"

usage() {
echo "Usage: $0 [--output-dir DIR] [--allow-dirty]"
}

while [[ $# -gt 0 ]]; do
case "$1" in
--output-dir)
[[ $# -ge 2 ]] || { usage >&2; exit 2; }
OUTPUT_DIR="$2"
shift 2
;;
--allow-dirty)
ALLOW_DIRTY=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done

case "$OUTPUT_DIR" in
/*) ;;
*) OUTPUT_DIR="$REPO_ROOT/$OUTPUT_DIR" ;;
esac

for command_name in git uv; do
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "Required command not found: $command_name" >&2
exit 1
fi
done

cd "$REPO_ROOT"

# Generate bindings and compile the native library
echo "Generating Python bindings..."
./scripts/uniffi_bindgen_generate_python.sh

# Build the wheel
echo "Building wheel..."
cd bindings/python
uv build --wheel

echo ""
echo "Wheel built successfully:"
ls -1 dist/*.whl
echo ""
echo "Collect wheels from all target platforms into dist/, then run:"
echo " ./scripts/python_publish_package.sh"
HOST_OS="$(uname -s)"
HOST_ARCH="$(uname -m)"

case "$HOST_OS:$HOST_ARCH" in
Linux:x86_64|Linux:amd64|Linux:aarch64|Linux:arm64|Darwin:aarch64|Darwin:arm64) ;;
*)
echo "Unsupported build host: $HOST_OS $HOST_ARCH" >&2
exit 1
;;
esac

if [[ "$ALLOW_DIRTY" == false ]] && [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then
echo "Refusing to build from a dirty worktree; commit the release first." >&2
echo "Use --allow-dirty only while developing the build configuration." >&2
exit 1
fi

case "$HOST_OS" in
Linux)
EXPECTED_WHEEL_COUNT=1
case "${CIBW_CONTAINER_ENGINE:-}" in
podman*) CONTAINER_COMMAND=podman ;;
docker*|"") CONTAINER_COMMAND=docker ;;
*)
echo "Unsupported CIBW_CONTAINER_ENGINE: $CIBW_CONTAINER_ENGINE" >&2
exit 1
;;
esac

if ! command -v "$CONTAINER_COMMAND" >/dev/null 2>&1; then
if [[ -z "${CIBW_CONTAINER_ENGINE:-}" ]] && command -v podman >/dev/null 2>&1; then
export CIBW_CONTAINER_ENGINE=podman
else
echo "Linux wheel builds require Docker or Podman." >&2
exit 1
fi
fi
;;
Darwin)
EXPECTED_WHEEL_COUNT=2
if ! command -v rustup >/dev/null 2>&1 || \
! rustup run 1.95.0 rustc --version >/dev/null 2>&1; then
echo "macOS wheel builds require the Rust 1.95.0 toolchain." >&2
echo "Install it with: rustup toolchain install 1.95.0 --profile minimal" >&2
exit 1
fi
if ! /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
echo "macOS x86_64 wheel tests require Rosetta 2." >&2
echo "Install it with: softwareupdate --install-rosetta" >&2
exit 1
fi
;;
*)
echo "Unsupported operating system: $HOST_OS" >&2
exit 1
;;
esac

mkdir -p "$OUTPUT_DIR"
shopt -s nullglob
existing_wheels=("$OUTPUT_DIR"/*.whl)
if [[ ${#existing_wheels[@]} -ne 0 ]]; then
echo "Output directory already contains wheels: $OUTPUT_DIR" >&2
exit 1
fi

CARGO_TARGET_DIR="$(mktemp -d /tmp/cargo-target-ldk-node-python-wheels.XXXXXX)"
export CARGO_TARGET_DIR
BUILD_SOURCE_DIR="$(mktemp -d /tmp/ldk-node-python-wheels-source.XXXXXX)"
SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)"
export SOURCE_DATE_EPOCH

cleanup() {
case "$CARGO_TARGET_DIR" in
/tmp/cargo-target-ldk-node-python-wheels.*)
rm -rf -- "$CARGO_TARGET_DIR"
;;
esac
case "$BUILD_SOURCE_DIR" in
/tmp/ldk-node-python-wheels-source.*)
rm -rf -- "$BUILD_SOURCE_DIR"
;;
esac
}
trap cleanup EXIT

git ls-files --cached --others --exclude-standard -z | while IFS= read -r -d '' source_path; do
if [[ -e "$source_path" || -L "$source_path" ]]; then
printf '%s\0' "$source_path"
fi
done | tar --null --files-from=- -cf - | tar -xf - -C "$BUILD_SOURCE_DIR"

echo "Building Python wheel from commit $(git rev-parse HEAD)"
echo "Build host: $HOST_OS $HOST_ARCH"

(
cd "$BUILD_SOURCE_DIR"
uv tool run --python 3.11 --from "cibuildwheel[uv]==$CIBUILDWHEEL_VERSION" cibuildwheel \
bindings/python \
--config-file bindings/python/pyproject.toml \
--output-dir "$OUTPUT_DIR"
)

built_wheels=("$OUTPUT_DIR"/*.whl)
if [[ ${#built_wheels[@]} -ne $EXPECTED_WHEEL_COUNT ]]; then
echo "Expected $EXPECTED_WHEEL_COUNT wheels, found ${#built_wheels[@]}." >&2
exit 1
fi

for wheel_path in "${built_wheels[@]}"; do
wheel_name="$(basename "$wheel_path")"
(
cd "$OUTPUT_DIR"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$wheel_name"
else
shasum -a 256 "$wheel_name"
fi
) > "$wheel_path.sha256"
done

echo "Wheels built and tested successfully:"
for wheel_path in "${built_wheels[@]}"; do
echo " $wheel_path"
echo " $wheel_path.sha256"
done
Loading
Loading