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
17 changes: 16 additions & 1 deletion bindings/python/hatch_build.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import os
import sysconfig

from hatchling.builders.hooks.plugin.interface import BuildHookInterface
from hatchling.metadata.plugin.interface import MetadataHookInterface


class CustomMetadataHook(MetadataHookInterface):
def update(self, metadata):
readme_path = os.path.join(self.root, "README.md")
if not os.path.isfile(readme_path):
readme_path = os.path.normpath(os.path.join(self.root, "..", "..", "README.md"))

with open(readme_path, encoding="utf-8") as readme_file:
metadata["readme"] = {"content-type": "text/markdown", "text": readme_file.read()}


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}"
48 changes: 44 additions & 4 deletions bindings/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
[build-system]
requires = ["hatchling"]
requires = ["hatchling==1.32.0"]
build-backend = "hatchling.build"

[project]
name = "ldk_node"
version = "0.7.0"
version = "0.8.0"
authors = [
{ name="Elias Rohrer", email="dev@tnull.de" },
]
description = "A ready-to-go Lightning node library built using LDK and BDK."
readme = "../../README.md"
requires-python = ">=3.8"
dynamic = ["readme"]
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,5 +36,38 @@ dev = ["requests"]

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

[tool.hatch.build.targets.sdist.force-include]
"../../README.md" = "README.md"

[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]
before-all = "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" }
167 changes: 145 additions & 22 deletions scripts/python_build_wheel.sh
Original file line number Diff line number Diff line change
@@ -1,31 +1,154 @@
#!/bin/bash
# Build a Python wheel for the current platform.
# Build and test one native Python wheel 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 native target:
# Linux x86_64, Linux aarch64, macOS x86_64, macOS arm64.
# Linux builds require Docker or Podman. macOS builds require Rust 1.95.0.

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"
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 "$(uname -s)" in
Linux)
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)
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
;;
*)
echo "Unsupported operating system: $(uname -s)" >&2
exit 1
;;
esac

case "$(uname -m)" in
x86_64|amd64|aarch64|arm64) ;;
*)
echo "Unsupported architecture: $(uname -m)" >&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
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
}
trap cleanup EXIT

echo "Building Python wheel from commit $(git rev-parse HEAD)"
echo "Target: $(uname -s) $(uname -m)"

uv tool run --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 1 ]]; then
echo "Expected exactly one wheel, found ${#built_wheels[@]}." >&2
exit 1
fi

wheel_path="${built_wheels[0]}"
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"

echo "Wheel built and tested successfully:"
echo " $wheel_path"
echo " $wheel_path.sha256"
Loading
Loading