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
79 changes: 79 additions & 0 deletions .github/workflows/prebuilt-binaries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Publish Prebuilt Binaries

# Publishes the prebuilt binaries of the dart_only_prebuilt_binaries example
# to a GitHub release, so they can be consumed via the (opt-in)
# prebuilt binaries support of native_toolchain_rust.
# See examples/dart_only_prebuilt_binaries/README.md for more information.

on:
workflow_dispatch:

env:
example_dir: examples/dart_only_prebuilt_binaries

permissions:
contents: write

jobs:
create_release:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Determine the release tag from the example's pubspec version
id: tag
run: |
version=$(sed -n 's/^version: //p' "$example_dir/pubspec.yaml")
echo "tag=dart_only_prebuilt_binaries_example-v$version" >> "$GITHUB_OUTPUT"
- name: Create the release, if it does not exist yet
env:
GH_TOKEN: ${{ github.token }}
tag: ${{ steps.tag.outputs.tag }}
run: |
gh release view "$tag" --repo "$GITHUB_REPOSITORY" ||
gh release create "$tag" --repo "$GITHUB_REPOSITORY" \
--title "$tag" \
--notes "Prebuilt binaries for the dart_only_prebuilt_binaries example."

upload_binaries:
needs: create_release
strategy:
matrix:
# More targets (Android, iOS, ...) can be added analogously.
# They are omitted here to keep this example workflow simple.
include:
- os: macos-latest
target: aarch64-apple-darwin
binary: libdart_only_prebuilt_binaries_example.dylib
- os: macos-latest
target: x86_64-apple-darwin
binary: libdart_only_prebuilt_binaries_example.dylib
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary: libdart_only_prebuilt_binaries_example.so
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
binary: libdart_only_prebuilt_binaries_example.so
- os: windows-latest
target: x86_64-pc-windows-msvc
binary: dart_only_prebuilt_binaries_example.dll
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v5
- name: Install the Rust target
working-directory: ${{ env.example_dir }}/rust
run: rustup target add ${{ matrix.target }}
- name: Build the cdylib
working-directory: ${{ env.example_dir }}/rust
run: cargo build --release --target ${{ matrix.target }}
- name: Rename the binary to its target-specific asset name
run: cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "dart_only_prebuilt_binaries_example-${{ matrix.target }}.bin"
- name: Upload the binary to the release
env:
GH_TOKEN: ${{ github.token }}
tag: ${{ needs.create_release.outputs.tag }}
run: gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber "dart_only_prebuilt_binaries_example-${{ matrix.target }}.bin"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ coverage/
/target/
/Cargo.lock

# Prebuilt binaries opt-in config (see examples/dart_only_prebuilt_binaries):
# by ignoring it, CI always builds the Rust code from source.
native_toolchain_rust.toml

.direnv/
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[workspace]
resolver = "3"
members = ["examples/dart_only/rust", "examples/flutter/rust"]
members = [
"examples/dart_only/rust",
"examples/dart_only_prebuilt_binaries/rust",
"examples/flutter/rust",
]
102 changes: 101 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Rust support for Dart's [build hooks](https://dart.dev/tools/hooks).

## Getting Started
1. Install [rustup](https://rustup.rs), for Rust, on your development computer
(if you are a library author, consumers of your package will have to do the same)
(if you are a library author, consumers of your package will have to do the same,
unless they opt in to [Prebuilt Binaries](#prebuilt-binaries))
2. Run `flutter pub add native_toolchain_rust hooks` for Flutter or `dart pub add native_toolchain_rust hooks` for Dart-only
3. See [Code Setup](#code-setup)

Expand Down Expand Up @@ -89,3 +90,102 @@ targets = [
"x86_64-apple-darwin",
]
```


## Prebuilt Binaries
Normally, consumers of a package built with `native_toolchain_rust` need rustup installed
so the Rust code can be compiled from source on their machine.
As an alternative, application developers can *opt in* to downloading prebuilt binaries
on a per-package basis, which skips the Rust build entirely (no rustup required).

To opt in, create a `native_toolchain_rust.toml` file in the root of your project
(for [pub workspaces](https://dart.dev/tools/pub/workspaces), the workspace root):

```toml
[prebuilt-binaries.some_package_name]
url = "https://github.com/some-user/some-repo/releases/download/v{version}/my_crate-{target}.bin"
```

The `url` is a template for where to download the binary from,
and supports the following placeholders:

| Placeholder | Description | Example |
| --- | --- | --- |
| `{version}` | The version of the Dart package being built | `1.2.3` |
| `{target}` | The Rust target triple being built for | `aarch64-apple-darwin` |
| `{lib-name}` | The platform-specific dynamic library file name | `libmy_crate.so` |

Some notes:
- The downloaded binary must match the link mode of the build.
It must be a `cdylib` for dynamic linking, which is what Dart/Flutter uses on all platforms as of now.
- The downloaded binary is saved locally under the correct platform-specific library name,
so the remote file name in the URL template does not matter.
- Downloaded binaries are cached in your project's `.dart_tool` directory,
keyed by their resolved download URL.
Thus, a binary is only downloaded once per package version and target,
until the cache is cleared (e.g., by a `flutter clean`).
- Only ever download binaries from a source you trust!
You are responsible for ensuring the binaries you download are safe
and were built from the package's actual source code.

### Building From Source in CI
Add `native_toolchain_rust.toml` to your project's `.gitignore`:
since the file will then not exist in CI checkouts,
your CI will always build the Rust code from source,
while local development machines (with the file present) use the prebuilt binaries.

### Publishing Prebuilt Binaries via GitHub Releases
The URL template works great with GitHub release assets,
which are downloadable (for public repositories) without any authentication:
```
https://github.com/<owner>/<repo>/releases/download/<tag>/<asset-name>
```

For example, package authors can attach a binary per target triple to each release
in a GitHub Actions workflow:
```yaml
jobs:
upload-prebuilt-binaries:
strategy:
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
binary: libmy_crate.dylib
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary: libmy_crate.so
- os: windows-latest
target: x86_64-pc-windows-msvc
binary: my_crate.dll
# ...and any other targets you wish to support
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- run: rustup target add ${{ matrix.target }}
working-directory: rust
- run: cargo build --release --target ${{ matrix.target }}
working-directory: rust
# NOTE: release asset download URLs use the uploaded *file* name,
# so rename the binary to its target-specific asset name before uploading.
- run: cp "rust/target/${{ matrix.target }}/release/${{ matrix.binary }}" "my_crate-${{ matrix.target }}.bin"
- run: gh release upload ${{ github.ref_name }} "my_crate-${{ matrix.target }}.bin"
env:
GH_TOKEN: ${{ github.token }}
```

For a complete working example, see the
[dart_only_prebuilt_binaries example](examples/dart_only_prebuilt_binaries)
and its [prebuilt-binaries workflow](.github/workflows/prebuilt-binaries.yml).

Downstream users can then consume those binaries with the URL template from above:
```toml
[prebuilt-binaries.some_package_name]
url = "https://github.com/some-user/some-repo/releases/download/v{version}/my_crate-{target}.bin"
```

38 changes: 38 additions & 0 deletions examples/dart_only_prebuilt_binaries/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Dart-Only Prebuilt Binaries Example

This example showcases [Prebuilt Binaries](../../README.md#prebuilt-binaries):
it is identical in spirit to the [dart_only](../dart_only) example,
except that its Rust binaries are also published to GitHub Releases
(via the [prebuilt-binaries workflow](../../.github/workflows/prebuilt-binaries.yml)),
so that consumers can opt in to downloading them instead of building from source.

## Trying It Out
Prebuilt binaries are opt-in:
without any extra configuration, running `dart test` in this directory
builds the Rust code from source (requiring rustup), just like the other examples.

To opt in to the prebuilt binaries instead, create a `native_toolchain_rust.toml`
in the root of *your* project.
Since this repository is a [pub workspace](https://dart.dev/tools/pub/workspaces),
the root project is the repository root, so create the file there:

```toml
[prebuilt-binaries.dart_only_prebuilt_binaries_example]
url = "https://github.com/GregoryConrad/native_toolchain_rust/releases/download/dart_only_prebuilt_binaries_example-v{version}/dart_only_prebuilt_binaries_example-{target}.bin"
```

Then, `dart test` will download the prebuilt binary for your machine
and skip the Rust build entirely (no rustup required!).

Note that `native_toolchain_rust.toml` is intentionally listed
in this repository's `.gitignore`:
since CI checkouts will never contain the file,
CI always builds (and tests) the Rust code from source.

## How the Binaries Are Published
The [prebuilt-binaries workflow](../../.github/workflows/prebuilt-binaries.yml)
builds the cdylib for a matrix of targets and uploads each one
as an asset of the `dart_only_prebuilt_binaries_example-v<version>` GitHub release,
where `<version>` is the `version` in this example's [pubspec.yaml](pubspec.yaml).
The asset names embed the Rust target triple,
matching the `{target}` placeholder in the URL template above.
11 changes: 11 additions & 0 deletions examples/dart_only_prebuilt_binaries/hook/build.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_rust/native_toolchain_rust.dart';

void main(List<String> args) async {
await build(args, (input, output) async {
await const RustBuilder(assetName: 'src/ffi.g.dart').run(
input: input,
output: output,
);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'package:dart_only_prebuilt_binaries_example/src/ffi.g.dart'
show rust_multiply;
11 changes: 11 additions & 0 deletions examples/dart_only_prebuilt_binaries/lib/src/ffi.g.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// AUTO GENERATED FILE, DO NOT EDIT.
//
// Generated by `package:ffigen`.
// ignore_for_file: type=lint, unused_import
import 'dart:ffi' as ffi;

@ffi.Native<ffi.Int32 Function(ffi.Int32, ffi.Int32)>()
external int rust_multiply(
int a,
int b,
);
17 changes: 17 additions & 0 deletions examples/dart_only_prebuilt_binaries/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: dart_only_prebuilt_binaries_example
publish_to: none
resolution: workspace
# NOTE: the version is used to resolve `{version}` in the prebuilt binary
# download URL; see the README for more information.
version: 0.1.0

environment:
sdk: ^3.9.0

dependencies:
hooks: ^2.0.2
native_toolchain_rust: ^1.0.4+0

dev_dependencies:
ffigen: ^20.1.0
test: ^1.29.0
9 changes: 9 additions & 0 deletions examples/dart_only_prebuilt_binaries/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "dart-only-prebuilt-binaries-example"
edition = "2024"

[lib]
crate-type = ["staticlib", "cdylib"]

[build-dependencies]
cbindgen = "0.29.0"
11 changes: 11 additions & 0 deletions examples/dart_only_prebuilt_binaries/rust/bindings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
This file is automatically generated by build.rs; DO NOT MANUALLY EDIT!
This header is designed to be consumed by ffigen over on the Dart side.
*/

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

int32_t rust_multiply(int32_t a, int32_t b);
22 changes: 22 additions & 0 deletions examples/dart_only_prebuilt_binaries/rust/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::env;

fn main() {
create_rust_to_dart_bindings();
}

fn create_rust_to_dart_bindings() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::C)
.with_autogen_warning(
"/*
This file is automatically generated by build.rs; DO NOT MANUALLY EDIT!
This header is designed to be consumed by ffigen over on the Dart side.
*/",
)
.generate()
.expect("Unable to generate bindings")
.write_to_file("bindings.h");
}
28 changes: 28 additions & 0 deletions examples/dart_only_prebuilt_binaries/rust/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[toolchain]
channel = "1.90.0"
profile = "default"
components = ["rust-analyzer"]

targets = [
# Android
"armv7-linux-androideabi",
"aarch64-linux-android",
"x86_64-linux-android",

# iOS (device + simulator)
"aarch64-apple-ios",
"aarch64-apple-ios-sim",
"x86_64-apple-ios",

# Windows
"aarch64-pc-windows-msvc",
"x86_64-pc-windows-msvc",

# Linux
"aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-gnu",

# macOS
"aarch64-apple-darwin",
"x86_64-apple-darwin",
]
12 changes: 12 additions & 0 deletions examples/dart_only_prebuilt_binaries/rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#[unsafe(no_mangle)]
pub extern "C" fn rust_multiply(a: i32, b: i32) -> i32 {
a * b
}

#[cfg(test)]
mod tests {
#[test]
fn test_rust_multiply() {
assert_eq!(crate::rust_multiply(2, 3), 6);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:dart_only_prebuilt_binaries_example/dart_only_prebuilt_binaries_example.dart';
import 'package:test/test.dart';

void main() {
test('rust_multiply correctly multiplies', () {
expect(rust_multiply(2, 3), equals(6));
});
}
Loading