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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Reproducible builds need identical bytes on every platform: never convert
# line endings on checkout or commit.
* -text
26 changes: 26 additions & 0 deletions .github/workflows/native.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Builds the Rust library on every reference host, fails if any host or the
# committed dnix.lock disagrees, and publishes the release the lock names
# when it reaches the default branch.
name: Native library

on:
push:
branches: [main]
pull_request:

permissions:
contents: write

# A newer push supersedes runs on a branch, never on main: a run there may
# be publishing.
concurrency:
group: native-${{ github.ref }}
cancel-in-progress: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }}

jobs:
native:
# The same dnix commit as pubspec.yaml and flake.lock.
uses: slopbot9k/dnix/.github/workflows/native.yml@57efe93b683e77d86a326a3a96ff217e6d6a4283
with:
# Xcode 27 lives on its own public-preview image, not on macos-26.
macos-runner: xcode-27
50 changes: 46 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,66 @@ name: Test native assets

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-latest, windows-latest]
os: [ubuntu-24.04, ubuntu-24.04-arm, xcode-27, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 90
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: dart-lang/setup-dart@v1
with:
sdk: '3.13.0'
- uses: dtolnay/rust-toolchain@1.99.1
if: matrix.os == 'ubuntu-24.04'
# The hook builds through the Nix flake; before the first native
# release that is the only path, and it is unavailable on Windows.
- uses: DeterminateSystems/nix-installer-action@v23
if: runner.os != 'Windows'
- run: dart pub get --no-example
- run: dart analyze lib test hook
- run: dart test
- run: dart analyze lib test hook tool
# Prebuilt libraries exist once the lock's release is published from the
# default branch of the repository dnix.json names; until then only
# hosts with Nix can build them.
- name: Check whether the lock's release is published
id: release
shell: bash
run: |
repository=$(jq -r .repository dnix.json | sed 's/\.git$//; s#/$##')
tag="native-$(jq -r .release dnix.lock)"
url="$repository/releases/tag/$tag"
echo "published=$([ "$(curl -s -o /dev/null -w '%{http_code}' "$url")" = 200 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
# Prebuilt once published; until then Linux and macOS verify the source
# build against the lock, and Windows, without Nix, skips the tests.
- name: Choose the build mode
shell: bash
run: echo '${{ steps.release.outputs.published == 'true' && 'prebuilt' || 'source' }}' > .dnix-build
# On failure the last lines become an annotation, readable without
# access to the logs.
- name: Run dart test
if: runner.os != 'Windows' || steps.release.outputs.published == 'true'
shell: bash
run: |
if ! dart test 2>&1 | tee test.log; then
echo "::error::$(tail -c 1500 test.log | tr '\n' ' ')"
exit 1
fi
- name: Rust tests
if: matrix.os == 'ubuntu-24.04'
working-directory: src/serai/hrf
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ build/

.flutter-plugins
.flutter-plugins-dependencies

# Nix build outputs
/result
/result-*
# Per-checkout dnix build mode override (see README).
.dnix-build
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## Unreleased

* The build hook, the Nix build, and the workflows now come from the shared `dnix` package; the package keeps `dnix.json`, `dnix.lock`, a two-line hook, and a two-line flake.
* The hook obtains the native library in one of three modes: `prebuilt` downloads the library of the pinned native release and verifies its hash, `source` builds it with the Nix flake and verifies the same hash, and `dev` builds with Nix without the Xcode pin or hash check. Static linking is no longer offered.
* `dnix.lock` is a source file: `update_lock` records what the Rust source builds to and names the release, derived from the hashes, that holds those bytes. CI fails when source and lock disagree and publishes the release when the lock reaches the default branch; nothing is committed by CI.
* Reproducible builds: source paths are remapped onto canonical prefixes, Apple dylibs get an `@rpath` install name with header padding and are linked without a debug map instead of stripped afterwards, PE timestamps derive from content, C objects carry no debug info or compiler identification, and deployment targets are fixed at the floors in `dnix.json`.
* Nix flake with sandboxed reference builds of the Linux, Android, and Windows targets from hash-pinned toolchains, plus an Apple build under a pinned Xcode.
* Cross-host identity: cargo gets a fixed host triple through a rustc wrapper so crate metadata hashes do not depend on the build host, and the Windows target compiles its C code with zig's clang like the Linux targets.
* CI builds the reference targets on four hosts, rebuilds each from scratch, and fails on any cross-host difference.
* Pin Rust 1.99.1. Earlier toolchains strip Apple dynamic libraries with a misaligned `__LINKEDIT` string pool, which iOS 27 and macOS 27 refuse to load.

## 0.3.0
Expand Down
82 changes: 77 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# frostdart

Dart bindings for the Serai FROST multisig implementation. Rust sources live in
`src/serai`; native assets build and bundle them automatically.
`src/serai`; the package ships the compiled library through native assets.

Requires Dart 3.13+ (or a Flutter SDK containing it), Rust installed with rustup,
and the platform's C build tools: Xcode for Apple, the Android NDK, a Linux C
compiler, or Visual Studio's C++ build tools on Windows. Put `rustup` on PATH.
The hook locates the pinned Rust 1.99.1 tools and installs the target as needed.
Requires Dart 3.13+ (or a Flutter SDK containing it). Nothing else is needed
to use the package: the build hook downloads the library built for this
version and checks its hash.

```sh
dart pub get --no-example
Expand All @@ -17,3 +16,76 @@ Consuming Flutter apps use normal `flutter run` / `flutter build` commands.
The example requires iOS 15+ or macOS 13+ for Apple builds.

Regenerate bindings with `dart run ffigen --config ffigen.yaml` (requires libclang).

## Native library

The hook obtains the Rust library in one of three modes, chosen with the
`build` user define in the consuming app's `pubspec.yaml`, or for this
checkout with a `.dnix-build` file holding the mode:

```yaml
hooks:
user_defines:
frostdart:
build: prebuilt # the default; or source, or dev
```

- `prebuilt` downloads the library of the release `dnix.lock` names and
verifies it against the hash recorded there. A mismatch is an error, never
a fallback. Downloads are cached by hash.
- `source` builds the library with Nix, the same build CI uses, and verifies
it against the same hash. Needs Nix on Linux or an Apple Silicon Mac; the
Apple targets need Xcode 27.0.
- `dev` builds with Nix but skips the Xcode pin and the hash check, for
working on the Rust code. The hook notices a `.dnix-build` file from its
next run on, so touch `dnix.json` after creating it.

Only dynamic libraries are provided, built for macOS 12, iOS 13 (the arm64
simulator 14), Android API 24, and glibc 2.28 at minimum. iOS stays at 13
because Flutter 3.47 asks build hooks for iOS 13 and writes 13.0 into each
native asset's framework as its minimum. The floors, the Rust and Xcode
versions, and the location of the Rust workspace live in `dnix.json`.

### Releases

`dnix.lock` records the hash of every library the Rust source builds to and
names the GitHub release, `native-<release>`, that holds those bytes. After
changing the Rust code, run `dart tool/update_lock.dart` on a Mac with Nix
and Xcode 27.0 and commit the lock with the change; without a Mac, push and
copy the lock CI prints once every host is green. CI
(`.github/workflows/native.yml`) builds on four hosts, fails if any host or
the lock disagrees, and publishes the release when the lock reaches the
default branch. Nothing is ever committed by CI, and old releases stay.

Anyone can rebuild one platform with Nix and compare:
`dart tool/verify_release.dart linux` (or android, windows, macos, ios, or a
target triple), on Windows inside WSL2 for the non-Apple platforms. The Apple
platforms use the Xcode 27.0 bundle under `/Applications`, whatever
`xcode-select` points at.

### How the build stays reproducible

The build comes from the shared [dnix](https://github.com/cypherstack/dnix)
package: `flake.nix` hands it `dnix.json`, and dnix builds every non-Apple
target in a Nix sandbox from toolchains pinned by hash, with the same bytes
on Linux, macOS, and WSL2, and the Apple targets with the pinned Xcode. See
dnix's documentation for how the output is kept independent of the host.

```sh
nix build .#x86_64-unknown-linux-gnu # one target, output in result/lib
nix build .#all # every target this host can build
cat result/SHA256SUMS
nix develop .#apple --command dnix-apple-build out # Apple targets, pinned Xcode
```

Bumping Rust means changing `toolchains.rust` in `dnix.json`, plus
`src/serai/hrf/rust-toolchain.toml` and the toolchain step in
`.github/workflows/release.yml`, which serve `cargo test`. Bumping dnix means
the same commit in `pubspec.yaml`, `flake.lock`, and
`.github/workflows/native.yml`. Either is followed by `update_lock`.

### Tests

`.github/workflows/release.yml` runs the Dart tests on every OS: against the
prebuilt library once the lock's release is published, from source before
that (Windows waits for the release), and the Rust tests on Linux.
33 changes: 33 additions & 0 deletions dnix.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "frostdart",
"repository": "https://github.com/cypherstack/frostdart",
"library": "hrf_api",
"asset": "frostdart_bindings_generated.dart",
"rust": {
"workspace": "src/serai",
"crate": "hrf-api"
},
"toolchains": {
"rust": "1.99.1",
"xcode": "27.0"
},
"floors": {
"macos": 12,
"ios": 13,
"android": 24,
"glibc": "2.28"
},
"targets": [
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"aarch64-apple-ios",
"aarch64-apple-ios-sim",
"x86_64-apple-ios",
"aarch64-linux-android",
"armv7-linux-androideabi",
"x86_64-linux-android",
"aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc"
]
}
17 changes: 17 additions & 0 deletions dnix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"release": "780746010c71",
"date": "2026-10-23",
"files": {
"aarch64-apple-darwin/libhrf_api.dylib": "62b494f6ddccd034e1c8d16f28ded45b1737e367df5869e2fb0dbec0ee59fdef",
"aarch64-apple-ios-sim/libhrf_api.dylib": "7fcddfeaab9cd9dfc7f75d734a78144adc2e7ff6cb09f19a06a65938447f86de",
"aarch64-apple-ios/libhrf_api.dylib": "f80ea3d4bfbab97683a0ca45928bf99cb2741fa681d61048193ad278ca0255ac",
"aarch64-linux-android/libhrf_api.so": "17c4aaa8659efc92ee37abd8d040e73b4cca34fc404776ac266bfe6be2150525",
"aarch64-unknown-linux-gnu/libhrf_api.so": "f2c083c7bf06b3c03fc268136bd0e1a9b3f6d683041106eac38af322d897fa38",
"armv7-linux-androideabi/libhrf_api.so": "6a0f91a0d6f1964744011cf8e68258f9123b7cce335fc5edbe1aa2546ce02a84",
"x86_64-apple-darwin/libhrf_api.dylib": "14adca4a9ba9711c868c229dc8386d1fb38814746374df6e23d9e971f4966dce",
"x86_64-apple-ios/libhrf_api.dylib": "a9304f7c22b244135029ba1c02e453c3d092db95df23a83e4343266f0ca9123f",
"x86_64-linux-android/libhrf_api.so": "d3cd02cd9a0b7de631cca5ec9f68d1a59e6ddca7c545c7e10b186d1bc08a2058",
"x86_64-pc-windows-msvc/hrf_api.dll": "461731d3c79d1e99fe9472ef20165e793476b6de1092f07673735bdac14a5278",
"x86_64-unknown-linux-gnu/libhrf_api.so": "73e8812fa3b39cd034b83e3c4e0b6f5a2d621f97dd962d6777bf731c35ad9e23"
}
}
9 changes: 9 additions & 0 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
dnix:
dependency: transitive
description:
path: "."
ref: "57efe93b683e77d86a326a3a96ff217e6d6a4283"
resolved-ref: "57efe93b683e77d86a326a3a96ff217e6d6a4283"
url: "https://github.com/slopbot9k/dnix.git"
source: git
version: "0.1.0"
fake_async:
dependency: transitive
description:
Expand Down
Loading
Loading