Rust

Once can read an existing Cargo.toml, derive a typed build graph, and cache each workspace package and locked dependency separately. You can build, run, and test the project without first translating it into once.toml.

Start With an Existing Cargo Project#

Check the Toolchain#

Once invokes rustc and cargo from the selected toolchain. Confirm that the versions expected by the project are available:

bash
rustc --version
cargo --version

If the repository uses mise to pin them, install and activate that configuration first:

bash
mise install
mise exec -- rustc --version
mise exec -- cargo --version

Host binaries and tests also need the platform linker selected by the Rust compiler. Cross-compiled and mobile outputs require the linker and Rust target support for their destination platform.

Materialize Locked Dependencies#

Once does not download dependencies or change Cargo.lock. If the project has a current lockfile, populate Cargo's local source cache from its exact resolution:

bash
cargo fetch --locked

Run cargo generate-lockfile once when the lockfile needs to be created or deliberately updated. Review that change before continuing. Once requires a concrete lockfile when a project declares external dependencies. Library maintainers who do not commit Cargo.lock can generate one locally, but repeatable builds on another machine require the same resolution.

Preview the Derived Graph#

From the directory that contains the root Cargo.toml, inspect the match and the first-party targets:

bash
once query native-projects
once query targets --kind rust_binary
once query targets --kind rust_library
once query targets --kind rust_test

No once.toml is required, and these commands do not write one. If a repository contains several independent Cargo projects, select one explicitly:

bash
once query native-project cargo --package tools/linter

The cargo_workspace seed runs locked, offline Cargo metadata. It emits first-party libraries, binaries, procedural macros, unit and integration test targets, build-script edges, and locked external packages.

The complete preview includes locked external packages, so it can be large. Request it only when you need to inspect the full expansion:

bash
once query native-project cargo

The identifiers printed by once query targets are the source of truth. Generated first-party identifiers carry their package and Cargo target role. For a package named hello with a binary also named hello, discovery normally derives cargo_hello_bin_hello and cargo_hello_bin_hello_unit_tests.

Cargo workspaces use their shallowest matching Cargo.toml, so member manifests do not create duplicate native project seeds. Cargo remains authoritative for workspace membership, default members, features, target metadata, and resolved versions. Local path packages outside the workspace remain dependency targets instead of becoming first-party workspace members.

Once snapshots external package trees from Cargo's local cache into target outputs. It preserves files, executable modes, and symbolic links without copying sources into the repository. It does not reuse or modify a repository's vendor directory.

An explicit vendor_dir remains available for repositories that already manage pre-vendored Cargo sources. Graph loading never acquires sources or changes Cargo.lock.

Targets gated by Cargo required-features appear only when every required feature is selected. Generated test targets include the package's development dependencies, and hyphenated Cargo target names are normalized for the Rust compiler automatically. Multi-output libraries expose each declared Rust library crate type as a separate generated target.

Build, Run, and Test#

Use a generated identifier through the same commands as every other Once graph:

bash
once build cargo_hello_bin_hello
once run cargo_hello_bin_hello
once test cargo_hello_bin_hello_unit_tests

Ask Once about a target before invoking it when its role is not obvious:

bash
once query capabilities cargo_hello_bin_hello

Outputs are materialized under .once/out/<target>/. The rust_binary reference and rust_test reference list their executable, log, and test-result outputs.

Confirm Caching#

Run the same build twice without changing an input:

bash
once build cargo_hello_bin_hello
once build cargo_hello_bin_hello

The second invocation should restore unchanged actions from the configured cache. Changing one locked dependency invalidates that package and its consumers. Changing one workspace package invalidates that package and its dependants. Changing only a test file reruns the affected test without recompiling an unchanged library.

Dependency fetching is outside the Once graph. Once caches compilation after Cargo's local cache contains the sources selected by Cargo.lock. Continue with Caching to configure a shared remote cache.

Record the Project Selection#

Initialization is optional. Use it when the repository should make its native project selection explicit:

bash
once edit init-native-project cargo

This writes only the cargo_workspace seed. The detailed targets remain derived from Cargo.toml and Cargo.lock, so they do not become duplicated configuration. Commit the seed when reviewers and continuous integration should see the selection.

After initialization, the seed can select Cargo features or a compilation target without copying the generated package graph into the manifest:

toml
[target.attrs]
features = ["workspace-package/feature-name"]
target = "aarch64-unknown-linux-gnu"

Use the cargo_workspace reference for the complete seed contract.

Workspaces and Troubleshooting#

  • If graph loading reports a missing source, run cargo fetch --locked with the same Cargo home and retry.
  • If a binary or example is absent, inspect its required-features, initialize the seed, and select those features explicitly.
  • Use once query native-project cargo --package <path> when more than one independent Cargo.toml matches the workspace.
  • Build scripts that invoke unsupported host tools fail as ordinary declared actions. Add the required toolchain or move the exceptional operation into an explicit target.
  • Cargo configuration files participate in graph resolution. Keep the repository's source replacement and target configuration available when loading the graph.

Choose How Much Configuration to Own#

Native discovery, manifests, and Starlark modules are composable layers. A project does not need to migrate away from Cargo.toml to gain more control.

LayerRepository changeUse it when
Native Cargo projectNoneThe graph derived from Cargo.toml already describes the build.
Recorded native seedOne cargo_workspace target in once.tomlThe repository should pin project selection, features, target configuration, caching, or execution infrastructure.
Explicit typed targetsAdditional targets in once.tomlA package needs a custom boundary, cross-language dependency, mobile artifact, or operation that Cargo metadata does not describe.
Project Starlark moduleA registered reusable target kindSeveral targets need the same new behavior and an existing target kind cannot express it.

Start with native discovery and stop there when it is sufficient. Add an annotated script for a one-off repository task. Move data into once.toml when the task needs typed dependencies, inputs, outputs, or platform selection. Move behavior into a Starlark target kind only when the rule should be reusable.

The initialized cargo_workspace seed can live beside additional explicit targets, so Cargo can continue to own its package graph while Once owns only the exceptional edges. Do not restate every generated Cargo package in once.toml.

When a custom target kind is necessary, keep project-specific Cargo behavior inside that Starlark module and build it from generic action primitives. The shared Rust execution layer remains independent of Cargo and other build systems. The Modules reference covers target kind schemas, resolvers, actions, validation, and module registration.

Author a Graph When You Need More Control#

Hand-authored targets are useful for a standalone Rust library without Cargo, cross-language links, native mobile outputs, or a boundary that should be cached independently.

Declare a Library, Binary, and Test#

Create apps/hello/once.toml:

toml
[[target]]
name = "greeting"
kind = "rust_library"
srcs = ["src/lib.rs"]
[target.attrs]
crate_name = "greeting"
edition = "2021"
[[target]]
name = "hello"
kind = "rust_binary"
srcs = ["src/main.rs"]
deps = ["./greeting"]
[target.attrs]
crate_name = "hello"
edition = "2021"
[[target]]
name = "greeting_tests"
kind = "rust_test"
srcs = ["tests/greeting_test.rs"]
deps = ["./greeting"]
[target.attrs]
crate_name = "greeting_tests"
crate_root = "tests/greeting_test.rs"
edition = "2021"
labels = ["unit"]

Use this source layout:

plaintext
apps/hello/
├── once.toml
├── src/
│ ├── lib.rs
│ └── main.rs
└── tests/
└── greeting_test.rs

The library crate name is greeting, so the binary and test can refer to it as greeting in Rust source. Their ./greeting dependency gives the compiler the matching built crate.

Query Before Building#

Inspect the three targets and their capabilities:

bash
once query targets --kind rust_library
once query capabilities apps/hello/greeting
once query capabilities apps/hello/hello
once query capabilities apps/hello/greeting_tests
once query schema rust_binary

The library exposes build, the binary exposes build and run, and the test target exposes build and test.

Build, Run, and Test#

Build the binary. Once builds greeting first because the binary depends on it:

bash
once build apps/hello/hello

Run that same binary:

bash
once run apps/hello/hello

Run the test target:

bash
once test apps/hello/greeting_tests

Outputs are materialized under .once/out/<target>/. The rust_binary reference and rust_test reference list their executable, log, and test-result outputs.

rust_binary accepts args, run_env, and env_inherit for runtime configuration. data files become declared run inputs, while compile_data files affect compilation. Keeping those roles separate makes cache behavior visible.

Keep Cargo Dependencies When Authoring Targets#

Skip this section when the native cargo_workspace graph is sufficient. That seed already imports the complete locked dependency graph. Use cargo_dependencies when hand-authored Rust targets should keep Cargo as the authority for third-party packages.

Keep third-party requirements in Cargo.toml and exact versions in Cargo.lock. A root cargo_dependencies target lets Cargo resolve the packages while Once builds the resolved crates as graph dependencies. The bundled starter omits metadata_file and resolves live in locked, offline mode so the same example remains portable across compiler hosts. To opt into a checked snapshot instead, include it in the resolver inputs and set metadata_file:

toml
[[target]]
name = "cargo_dependencies"
kind = "cargo_dependencies"
srcs = [
"Cargo.toml",
"Cargo.lock",
".cargo/config.toml",
"cargo-metadata.json",
"apps/*/Cargo.toml",
]
[target.attrs]
manifest = "Cargo.toml"
lockfile = "Cargo.lock"
resolver_inputs = [
"Cargo.toml",
"Cargo.lock",
".cargo/config.toml",
"cargo-metadata.json",
"apps/*/Cargo.toml",
]
metadata_file = "cargo-metadata.json"
vendor_dir = "third_party/rust/vendor"
packages = ["itoa"]

Add that target to a first-party Rust target and identify the matching Cargo package:

toml
[[target]]
name = "hello"
kind = "rust_binary"
srcs = ["src/main.rs"]
deps = ["./greeting", "cargo_dependencies"]
[target.attrs]
crate_name = "hello"
edition = "2021"
[target.attrs.rustc_env]
CARGO_MANIFEST_DIR = "apps/hello"
CARGO_PKG_NAME = "hello"
CARGO_PKG_VERSION = "0.0.0"

With this configuration, the dependency target reads the checked-in Cargo metadata snapshot while loading the graph. If metadata_file is omitted, it runs cargo metadata --locked --offline instead. Registry and Git packages come from the configured vendor directory. Workspace and path packages remain first-party Once targets. The Cargo manifests and lockfile stay authoritative for package names, versions, active features, renamed dependencies, procedural macros, and build dependencies. Every external metadata package must match an exact name, version, and source entry in Cargo.lock; registry entries must also carry a lockfile checksum. Checked-in metadata also carries once_snapshot provenance with the exact resolver input text, feature and target selection, and the compiler host triple. A manifest, configuration, feature, target, or compiler host change therefore rejects stale metadata during graph loading. Once asks the selected Rust compiler for its host triple before accepting a native snapshot.

For live offline metadata, configure Cargo to use the same vendored sources:

toml
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "third_party/rust/vendor"

Keep that configuration in resolver_inputs with the manifests and lockfile. Generated crate targets include the complete vendored package tree, which covers files read through Rust source inclusion macros as well as package data.

Each resolved package becomes a synthetic rust_crate or rust_proc_macro target. Normal Cargo edges use deps; build-script edges use the named build_deps role. The cargo_dependencies target only aggregates their providers, so Once can schedule independent crate builds concurrently instead of compiling the locked package list inside one analysis implementation.

Inspect the imported packages, then build and run the first-party consumer:

bash
once query targets --kind rust_crate
once query targets --kind rust_proc_macro
once build apps/hello/hello
once run apps/hello/hello

The bundled Cargo starter uses itoa from the locked graph and prints 42. That final run verifies more than graph loading: the local binary compiled and linked against the provider emitted by the synthetic crate target.

Use once query schema cargo_dependencies before adding feature or target filters. For a cross-compiled binary, Once asks Cargo for destination metadata and host metadata. Destination crates retain the requested Rust target, while procedural macros, build dependencies, and their required host variants compile for the execution host.

Refresh a native snapshot when dependency inputs change:

bash
cargo metadata --format-version 1 --locked --offline > cargo-metadata.json

Use the same feature flags declared by cargo_dependencies and pass its target through --filter-platform. Add the once_snapshot input and selection provenance documented in the cargo_dependencies reference. For any snapshot target that sets target, record a second snapshot for the execution host, mark its selection with host = true, and set host_metadata_file.

Use Build Scripts and Advanced Compiler Inputs#

Rust targets can set build_script to compile and run a Cargo-style build script before the main crate. Once provides OUT_DIR and consumes common compiler configuration, environment, link argument, link library, and link search directives printed by the script. Dependency link-search outputs and Cargo links metadata are available to downstream targets and build scripts.

Rust libraries, binaries, tests, crates, and procedural macros can also depend on c_library targets. Static and dynamic library paths plus transitive linker options flow through intermediate Rust crates and are applied by the final Rust link action. Native provider fields remain available to Apple and Android consumers of Rust outputs.

Use named dependency roles when the relationship has compile-time semantics that differ from an ordinary Rust crate:

toml
[[target]]
name = "hello"
kind = "rust_binary"
srcs = ["src/main.rs"]
deps = ["./greeting"]
[target.dependencies]
proc_macro_deps = ["./derive_greeting"]
link_deps = ["./native_support"]

proc_macro_deps accepts rust_proc_macro providers built for the execution host. link_deps accepts c_provider records and applies their libraries and linker options to final artifacts. Existing targets may continue placing these providers in deps, but named roles make the contract explicit and allow Once to diagnose a provider in the wrong role before analysis.

The target kind reference also documents compiler flags, environment files, linker settings, crate aliases, feature selection, and host-specific dependency selection. Add these only when the simple library edge above is not enough, and query the schema before choosing an attribute.

Produce Native Mobile Libraries#

Use rust_mobile_library when the same sources feed both Apple and Android consumers:

toml
[[target]]
name = "SharedRust"
kind = "rust_mobile_library"
deps = ["./SharedCore"]
srcs = ["src/shared/**/*.rs"]
[target.attrs]
crate_name = "shared_rust"
apple_target = "aarch64-apple-ios"
android_target = "aarch64-linux-android"
android_abi = "arm64-v8a"
android_api = 24
[[target]]
name = "SharedCore"
kind = "rust_mobile_library"
srcs = ["src/core/**/*.rs"]
[target.attrs]
crate_name = "shared_core"
apple_target = "aarch64-apple-ios"
android_target = "aarch64-linux-android"
android_abi = "arm64-v8a"

An Apple consumer requests a static library. An Android consumer requests a shared library and packages it for the configured Application Binary Interface. Android linking requires the Android Native Development Kit, found through ANDROID_NDK_HOME or android_ndk.

Dependencies between rust_mobile_library targets are compiled recursively for the platform requested by the Apple or Android consumer. Use explicit platform-specific rust_library targets only when a dependency must expose a host or single-target rlib instead of the deferred mobile provider.

Supported Target Kinds and Limitations#

Use the target kind reference for each role:

Rust tests run only host-target executables. A cross-target test can be built, but running it requires a platform runner that this target kind does not provide. Compatibility attributes listed as unsupported in the target kind reference fail validation when set to a non-empty value.

Continue with Memory once the binary builds and tests. It shows how Once records durable context about graph work. For Apple or Android consumers of the Rust library, follow the relevant application guide first, then add the native dependency after the application works independently.