Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .github/workflows/cron-daily-bindings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ jobs:
uses: $/.github/workflows/dart.yml
kotlin:
uses: $/.github/workflows/kotlin.yml
native:
uses: $/.github/workflows/native.yml
73 changes: 14 additions & 59 deletions .github/workflows/csharp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
if: github.event_name != 'pull_request'
runs-on: windows-latest
env:
RUSTUP_TOOLCHAIN: 1.85
RUSTUP_TOOLCHAIN: 1.85.0
defaults:
run:
working-directory: payjoin-ffi/csharp
Expand Down Expand Up @@ -107,22 +107,12 @@ jobs:
run: dotnet test Payjoin.Tests.csproj --logger "console;verbosity=minimal"

build-nuget-native:
# Every RID cross-compiles from Linux: cargo-zigbuild links the Linux targets
# against a pinned glibc floor and the macOS targets against zig's bundled
# libSystem stubs; cargo-xwin links MSVC-ABI Windows targets against the
# Microsoft CRT/SDK. The per-RID smoke jobs verify each produced asset on
# real hardware.
# Every RID builds on Linux with the native build contract's script, the
# one the native workflow checks for reproducibility. The per-RID smoke
# jobs verify each produced asset on real hardware.
name: "Build NuGet native asset"
if: github.event_name != 'pull_request'
runs-on: ubuntu-26.04
env:
# Must match the dtolnay/rust-toolchain version exactly: the cross-target
# std components are installed for that toolchain, and a partial version
# here selects a different rustup toolchain entry without them (E0463).
RUSTUP_TOOLCHAIN: 1.85.0
defaults:
run:
working-directory: payjoin-ffi/csharp
strategy:
# Let every RID report its own result; a failure in one target should not
# cancel the signal from the others.
Expand All @@ -147,54 +137,19 @@ jobs:
with:
persist-credentials: false # unneeded: no git auth beyond checkout

- name: Install Rust 1.85.0
uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1
with:
toolchain: 1.85.0
targets: ${{ matrix.target }}

- name: Use cache
uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
lookup-only: ${{ startsWith(github.ref, 'refs/tags/') }}

- name: Install cross toolchain
# Version- and hash-pinned: these tools produce the shipped binaries.
run: pip install --require-hashes -r scripts/cross-requirements.txt

- name: Install MSVC toolchain frontends (clang-cl, llvm-lib)
if: startsWith(matrix.rid, 'win-')
# cargo-xwin compiles C dependencies with clang-cl and archives them with
# llvm-lib. The runner image ships LLVM with versioned binary names only,
# so expose the unversioned names cc-rs looks for.
run: |
for tool in clang-cl llvm-lib; do
if command -v "$tool" >/dev/null; then
continue
fi
latest=$(ls -1 /usr/bin/"$tool"-* 2>/dev/null | sort -V | tail -n 1)
if [ -z "$latest" ]; then
sudo apt-get update -q
sudo apt-get install -y -q llvm clang
latest=$(ls -1 /usr/bin/"$tool"-* 2>/dev/null | sort -V | tail -n 1)
fi
if [ -z "$latest" ]; then
echo "$tool not found on the runner" >&2
exit 1
fi
sudo ln -s "$latest" /usr/local/bin/"$tool"
done
command -v clang-cl llvm-lib
- name: Set up nix
uses: $/.github/actions/setup-nix

- name: Build release native asset
run: |
PAYJOIN_FFI_CROSS=1 PAYJOIN_FFI_RID=${{ matrix.rid }} bash ./scripts/build_nuget_native.sh
env:
TARGET: ${{ matrix.target }}
run: nix develop .#native -c contrib/native/build.sh "$TARGET" "$RUNNER_TEMP/out"

- name: Upload NuGet native asset
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ matrix.rid }}
path: payjoin-ffi/csharp/artifacts/runtimes/${{ matrix.rid }}/native/*
path: ${{ runner.temp }}/out/${{ matrix.target }}/*
if-no-files-found: error

pack-nuget:
Expand All @@ -204,8 +159,8 @@ jobs:
needs: build-nuget-native
env:
# Exact version so the packaged bindings build with the toolchain the
# action installs, not an implicitly-installed latest 1.85.x.
RUSTUP_TOOLCHAIN: 1.85.0
# action installs, the release toolchain of the native build contract.
RUSTUP_TOOLCHAIN: 1.85.1
defaults:
run:
working-directory: payjoin-ffi/csharp
Expand All @@ -215,10 +170,10 @@ jobs:
with:
persist-credentials: false # unneeded: no git auth beyond checkout

- name: Install Rust 1.85.0
- name: Install Rust 1.85.1
uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1
with:
toolchain: 1.85.0
toolchain: 1.85.1

- name: Use cache
uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
Expand Down
187 changes: 187 additions & 0 deletions .github/workflows/native.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
name: Native libraries

# Builds the payjoin-ffi native library for each target twice, on separate
# runners from different paths, and fails unless both builds are
# byte-identical. The libraries only ever come from Linux. The macOS job
# checks the Apple ones against Xcode and produces nothing.

on:
workflow_call:

permissions:
contents: read

jobs:
build:
name: Build ${{ matrix.target }} (${{ matrix.build }})
runs-on: ubuntu-26.04
timeout-minutes: 60
strategy:
matrix:
target:
- aarch64-apple-ios
- aarch64-apple-ios-sim
- aarch64-unknown-linux-gnu
- x86_64-unknown-linux-gnu
- aarch64-apple-darwin
- x86_64-apple-darwin
- aarch64-pc-windows-msvc
- x86_64-pc-windows-msvc
- aarch64-linux-android
- armv7-linux-androideabi
- x86_64-linux-android
build: [first, second]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false # unneeded: no git auth beyond checkout
- name: Set up nix
uses: $/.github/actions/setup-nix
- name: Build
env:
TARGET: ${{ matrix.target }}
BUILD: ${{ matrix.build }}
run: |
set -euo pipefail
# The second build runs from other paths, with a space in them, so
# any path that leaks into the library breaks the match. Its own
# home moves the caches the toolchain keeps there, such as zig's.
repo=$GITHUB_WORKSPACE
if [[ $BUILD == second ]]; then
repo="$RUNNER_TEMP/second build/payjoin"
mkdir -p "$repo"
cp -R . "$repo"
export HOME="$RUNNER_TEMP/second build/home" CARGO_HOME="$RUNNER_TEMP/second build/cargo"
fi
shell=native
if [[ $TARGET == *-android* ]]; then
shell=native-android
fi
nix develop ".#$shell" -c "$repo/contrib/native/build.sh" "$TARGET" "$RUNNER_TEMP/out"
- name: Upload the library
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: native-${{ matrix.build }}-${{ matrix.target }}
path: ${{ runner.temp }}/out
if-no-files-found: error

compare:
name: Compare the two builds
needs: build
runs-on: ubuntu-26.04
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false # unneeded: no git auth beyond checkout
- name: Set up nix
uses: $/.github/actions/setup-nix
- name: Download the first build
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: native-first-*
merge-multiple: true
path: ${{ runner.temp }}/first
- name: Download the second build
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: native-second-*
merge-multiple: true
path: ${{ runner.temp }}/second
- name: Compare
working-directory: ${{ runner.temp }}
run: |
set -euo pipefail
(cd first && sha256sum -- */*) | tee SHA256SUMS
(cd second && sha256sum -c ../SHA256SUMS)
# Both slices just matched, so the file derived from them needs no
# second build of its own.
- name: Derive the universal2 macOS library
run: |
set -euo pipefail
dir="$RUNNER_TEMP/first"
mkdir -p "$dir/universal2-apple-darwin"
nix shell --inputs-from . nixpkgs-unstable#llvmPackages_21.llvm -c llvm-lipo -create \
"$dir/aarch64-apple-darwin/libpayjoin_ffi.dylib" \
"$dir/x86_64-apple-darwin/libpayjoin_ffi.dylib" \
-output "$dir/universal2-apple-darwin/libpayjoin_ffi.dylib"
- name: Upload the libraries
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: payjoin-native
path: ${{ runner.temp }}/first
if-no-files-found: error

verify-apple:
name: Verify the Apple libraries on macOS
needs: compare
runs-on: macos-latest
timeout-minutes: 60
env:
RUSTUP_TOOLCHAIN: 1.85.0
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false # unneeded: no git auth beyond checkout
- name: Download the libraries
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: payjoin-native
path: ${{ runner.temp }}/native
- name: Check the imports against Xcode's SDKs
working-directory: ${{ runner.temp }}/native
run: |
set -euo pipefail
# Apple's linker resolves every symbol a library imports against
# the SDK of the platform it runs on, and fails on any it lacks.
check() {
local triple=$1 target=$2 sdk=$3
nm -u -j "$triple/libpayjoin_ffi.dylib" | sed 's/^/-Wl,-u,/' >"$triple.args"
xcrun --sdk "$sdk" clang -target "$target" -dynamiclib -x c /dev/null \
-liconv @"$triple.args" -o "$RUNNER_TEMP/$triple-imports.dylib"
}
check aarch64-apple-ios arm64-apple-ios13.0 iphoneos
check aarch64-apple-ios-sim arm64-apple-ios13.0-simulator iphonesimulator
check aarch64-apple-darwin arm64-apple-macos11.0 macosx
check x86_64-apple-darwin x86_64-apple-macos10.15 macosx
- name: Rename and sign each library the way Flutter embeds it
run: |
set -euo pipefail
lipo "$RUNNER_TEMP/native/universal2-apple-darwin/libpayjoin_ffi.dylib" -verify_arch arm64 x86_64
for lib in "$RUNNER_TEMP"/native/*/libpayjoin_ffi.dylib; do
cp "$lib" "$RUNNER_TEMP/payjoin_ffi"
install_name_tool -id @rpath/payjoin_ffi.framework/payjoin_ffi "$RUNNER_TEMP/payjoin_ffi"
codesign --force --sign - "$RUNNER_TEMP/payjoin_ffi"
codesign --verify --verbose "$RUNNER_TEMP/payjoin_ffi"
done
- name: "Install Rust 1.85.0"
uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1
with:
toolchain: 1.85.0
- name: Set up Flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
with:
channel: stable
flutter-version: 3.44.9
- name: Generate the Dart bindings
run: |
set -euo pipefail
cp Cargo-recent.lock Cargo.lock
cd payjoin-ffi/dart
# The library carries no features, so neither may the bindings.
PAYJOIN_FFI_FEATURES='' bash scripts/generate_bindings.sh
- name: Run a smoke test in the iOS simulator
working-directory: payjoin-ffi/dart
run: |
set -euo pipefail
# Bundle the libraries built on Linux instead of building one.
cp contrib/ios-simulator/prebuilt_hook.dart hook/build.dart
cp -R "$RUNNER_TEMP/native" prebuilt
cd contrib/ios-simulator
flutter create --platforms ios .
udid=$(xcrun simctl list devices available --json |
jq -r '[.devices[][] | select(.name | startswith("iPhone"))][0].udid')
xcrun simctl boot "$udid"
flutter test integration_test -d "$udid"
25 changes: 22 additions & 3 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ jobs:
build-wheel:
# The wheel bundles a prebuilt native library, so each supported
# platform packs its own wheel: x86_64 on Linux (retagged manylinux by
# auditwheel) and a fat universal2 dylib for macOS. Both wheels build
# on a Linux host. The smoke jobs below verify each wheel on real hardware.
# auditwheel) and a fat universal2 dylib for macOS. Both libraries build
# on Linux with the native build contract's script, and the universal2
# file is combined from its two slices the way the native workflow does
# it. The smoke jobs below verify each wheel on real hardware.
name: "Build wheel"
if: github.event_name != 'pull_request'
runs-on: ubuntu-26.04
Expand All @@ -89,9 +91,26 @@ jobs:
- name: Set up nix
uses: $/.github/actions/setup-nix
- name: Build the wheel
run: nix develop .#python --command bash ./payjoin-ffi/python/contrib/build-wheel.sh
env:
PAYJOIN_WHEEL_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
out=$RUNNER_TEMP/out
if [[ $PAYJOIN_WHEEL_PLATFORM == linux-x64 ]]; then
nix develop .#native -c contrib/native/build.sh x86_64-unknown-linux-gnu "$out"
lib=$out/x86_64-unknown-linux-gnu/libpayjoin_ffi.so
else
for target in aarch64-apple-darwin x86_64-apple-darwin; do
nix develop .#native -c contrib/native/build.sh "$target" "$out"
done
lib=$out/universal2-apple-darwin/libpayjoin_ffi.dylib
mkdir -p "$(dirname "$lib")"
nix develop .#native -c llvm-lipo -create \
"$out/aarch64-apple-darwin/libpayjoin_ffi.dylib" \
"$out/x86_64-apple-darwin/libpayjoin_ffi.dylib" \
-output "$lib"
fi
nix develop .#python --command bash ./payjoin-ffi/python/contrib/build-wheel.sh "$lib"
- name: Upload wheel
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
Expand Down
40 changes: 40 additions & 0 deletions contrib/native/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Native build contract

The payjoin-ffi native libraries are built on Linux, by one script, in one
devshell. The language bindings move onto these libraries one at a time.

```sh
nix develop .#native -c contrib/native/build.sh <triple> <out-dir>
```

The Android targets need the NDK, which only `.#native-android` provides.

The library lands in `<out-dir>/<triple>/`. The
[native workflow](../../.github/workflows/native.yml) builds every target
twice, on separate runners from different paths, and fails unless both
builds are byte-identical. A macOS runner checks the Apple libraries
against Xcode and never produces any.

| Invariant | Rule |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Toolchain | Rust 1.85.1. CI keeps testing the 1.85.0 MSRV. |
| Lockfile | `Cargo-recent.lock`, built `--locked` |
| Features | None. Bindings are generated separately, so the library is the same for every language. |
| Paths | The repository is remapped to `/payjoin`, the target dir to `/payjoin/target`, the cargo home to `/cargo`, the rustup home to `/rustup`, the NDK to `/ndk` and the Windows SDK to `/xwin`, for rustc and the C compiler |
| Profile | Cargo's release defaults |
| Linux | `cargo zigbuild` against glibc 2.17 |
| macOS | zig through `apple-cc.sh` against the stub SDK, deployment target 10.15 on x86_64 and 11.0 on arm64, Flutter's floor and the first macOS on Apple silicon. `universal2-apple-darwin` is the two slices combined with `llvm-lipo`. |
| iOS | zig through `apple-cc.sh` against the stub SDK, minimum iOS 13.0. Device and arm64 simulator only. |
| Windows | `xwin` fetches MSVC's CRT 14.44.17.14 and Windows SDK 10.0.26100 from the Visual Studio 17.14.37824.6 manifest pinned in `vs-channel.json`. LLVM 21's clang-cl, llvm-lib and lld-link build against them, linked with `/Brepro` and without a PDB. |
| Android | NDK 29.0.14206865 at API 21, 16 KB aligned on 64-bit by the NDK's default |

The Apple dylibs are linked with an `@rpath/libpayjoin_ffi.dylib` install
name, `-headerpad_max_install_names` for Flutter's `install_name_tool`, and
`-S`, which keeps object paths and mtimes out of the stabs.
`apple-stubs.sh` generates the stub SDK from the macOS `libSystem.tbd` and
Darwin headers that zig ships, so no Apple SDK file is used or committed.

`x86_64-apple-ios`, the Intel simulator, is not built. Rust 1.85.1's std
for it targets iOS 10.0, so its objects carry `LC_VERSION_MIN_IPHONEOS`,
which zig 0.16 reads as a device platform and rejects for a simulator
link.
Loading
Loading