Reusable infrastructure for Dart packages that build, cache, and ship native libraries through Dart hooks.
- Three API paths
- Comparison
- Install
- Source fallback pipeline
- Build steps
- CLI commands
- Platform toolchains
- Caching
- License
For packages with single-stage builds using existing hooks builders:
import 'package:hooks/hooks.dart';
import 'package:native_prebuilt/native_prebuilt.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
void main(List<String> args) async {
await build(args, (input, output) async {
await PrebuiltCodeAssetBuilder(
assetName: 'src/my_package.dart',
libraryStem: 'my_package',
manifest: myPackagePrebuilts,
linkModeResolver: (_) => DynamicLoadingBundled(),
sourceFallback: SourceFallback(
sources: [LocalSource(paths: ['.'])],
builder: HookBuilderSourceBuilder.factory(
(input, source) => CBuilder.library(
name: 'my_package',
packageName: input.packageName,
assetName: 'src/my_package.dart',
sources: const ['src/native/my_package.c'],
),
),
),
).run(input: input, output: output, logger: Logger.root);
});
}For multi-stage builds with caching and cross-compilation, you can write the NativeProject in Dart by hand:
final project = NativeProject(
name: 'tdlib',
asset: const NativeAssetSpec(
assetName: 'src/client/platform/io/tdjson_native.dart',
libraryStem: 'tdjson',
linkMode: DynamicLoadingBundled(),
),
build: NativeBuildDefinition(
recipes: {
OS.linux: StepBuildRecipe(steps: [
CmakeConfigureStep(buildDirectory: 'build'),
CmakeBuildStep(buildDirectory: 'build', targets: ['tdjson']),
ExportArtifactStep(artifactPath: 'build/td/libtdjson.so'),
]),
},
),
);
// hook/build.dart
await runNativeProjectCli(args, project: project);You can define the complete project in native_prebuilt.yaml instead of writing NativeProject code manually. The CLI reads that manifest, validates it, and generates the build graph from it.
assetName is the Dart library path that declares the native code asset (the @Native bindings), not the shared-library filename.
If native_prebuilt.lock.yaml is present next to the config file, detect() overlays it automatically. Use project.copyWith(prebuilts: ...) only for manual overrides in custom code.
| Capability | Hooks Builder callback | Managed recipe |
|---|---|---|
| Prebuilt resolution | Yes | Yes |
| Source fallback | Yes | Yes |
| Standard hook caching | Yes | Yes |
| Existing hooks builders | Yes | Not required |
| Step-level native cache | No | Yes |
| Multi-stage graph | Manual | Yes |
| Standalone CI build | Limited | Yes |
| Central artifact validation | After registration | Yes |
dependencies:
native_prebuilt: ^0.3.1When no prebuilt is available, you can let native_prebuilt resolve source and build from it.
Resolution order:
hooks.user_definesoverride- Local
.prebuilt/directory - Shared cache / release download
- Source build using recipe or callback
native_prebuilt build executes declarative YAML recipes, and falls back to
hook/build.dart when no recipe is declared. If the manifest has an artifact
entry for the target, that entry is used to standardize the staged payload name.
Recipe values are Liquid templates rendered with:
source.path,source.origin, andsource.revisionwork,output,cache, and the equivalentdirectories.*pathsenv.*target.label,target.os,target.architecture,target.sdktarget.rust_target,target.zig_target, and target OS booleanslibrary.name,library.dynamic_name,library.static_name,library.versioned_name, andlibrary.extensionhook.*,project.*, and custombuild.options.*values
The manifest-level variables map defines shared Liquid values. Variables can
refer to the standard recipe values and to one another:
variables:
cargo_manifest: "{{ source.path }}/rust/Cargo.toml"
cargo_target: "{{ target.rust_target }}"
build:
recipes:
- target: {os: linux, architecture: x64}
steps:
- id: build
type: command
commands:
- [cargo, build, --manifest-path, "{{ variables.cargo_manifest }}", --target, "{{ variables.cargo_target }}"]YAML anchors can still share complete step lists. Dynamic artifact payloads are
the default; specify payload: {type: static_library} only for static targets.
Example:
source_directory: "{{ source.path }}/example/android"
build_directory: "{{ work }}/build"
CMAKE_TOOLCHAIN_FILE: "{{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake"Every step has these common YAML keys:
type(required)id(required)needs(optional list of step ids)
| type | Required keys | Optional keys |
|---|---|---|
cmake_configure |
source_directory, build_directory |
needs, generator, toolchain_file, definitions |
cmake_build |
build_directory |
needs, targets, parallel, environment |
command |
commands |
needs, working_directory, environment |
download_archive |
url |
needs, sha256, output_directory |
git_checkout |
repository, revision |
needs, target_directory, submodules |
git_apply_patch |
patch_path |
needs, target_directory |
copy |
source_path, destination_path |
needs, recursive |
strip |
input_path, output_path |
needs, strip_all |
export_artifact |
artifact, primary |
needs, kind |
Export the checked-in schema copy with:
dart run native_prebuilt schema exportThis writes schema/native_prebuilt.schema.json. Point editors at that file,
for example in VS Code:
{
"yaml.schemas": {
"./schema/native_prebuilt.schema.json": "native_prebuilt.yaml"
}
}| Command | Description |
|---|---|
init |
Generate an initial native_prebuilt.yaml scaffold from pubspec.yaml |
plan --target <platform> |
Show build plan and recipe steps |
build --target <platform> --output <dir> |
Build native library from declarative recipes |
cache-key --target <platform> |
Show cache key |
explain-cache --target <platform> |
Explain cache state |
verify --target <platform> |
Verify built artifact |
manifest update |
Generate/refresh Dart manifest or lock YAML |
manifest verify |
Verify manifest hashes |
schema export |
Write the JSON schema copy used by editors |
fetch |
Download prebuilt artifacts |
doctor |
Check build environment |
workflow init |
Generate GitHub/GitLab CI workflows |
init creates a manifest without overwriting an existing file unless --force
is passed. Use --package, --library-stem, --asset-name, --platform, and
source/release options to make the scaffold non-interactive. Add the declarative
build.recipes for the native source build, then use dart run native_prebuilt
for build, manifest, and workflow commands.
workflow init writes 5 GitHub workflow files, or 8 GitLab files by default. Use --gitlab --platform ... to filter GitLab outputs to selected platforms.
Auto-detected from environment:
- Android NDK —
ANDROID_NDK_HOMEor SDK - Apple SDK — Xcode paths
- MSVC — Visual Studio installation
- vcpkg —
VCPKG_ROOT
Build steps are cached using content-based fingerprints:
- Same inputs → cache hit (skip build)
- Changed source/toolchain → cache miss (rebuild)
- Cache stored in
.dart_tool/native_prebuilt/build-cache/
MIT