Expand description
English | 简体中文
§xtask
The repository’s task runner, behind the cargo xtask alias. It wraps the
commands a contributor runs before a commit, so CI and a local run cannot drift
apart, and it builds the example’s web and Android artifacts.
This is a development tool for this repository only, and publish = false. It
is a workspace member, so cargo clippy --workspace and cargo fmt --all
cover it along with the crates it acts on; .cargo/config.toml wires it up as
xtask = "run -p xtask --".
§Tasks
cargo xtask check # typos, tombi, clippy and the doc build, then `cargo fmt --check`
cargo xtask test # nextest over unit and integration tests, then the doctests
cargo xtask print-gpu # print the GPU devices the tests draw with, as JSON
cargo xtask miri # run the ECS's tests under Miri, which is what checks its unsafe
cargo xtask test-wasm # run the GPU tests in a browser, through wasm and WebGL2
cargo xtask run-wasm # build the web examples and serve them for a browser
cargo xtask build-android # build the Android library and the APK around it
cargo xtask publish # publish the workspace's crates to crates.io, in dependency order§cargo xtask check
Runs typos and tombi, then clippy twice, then the doc build, then
cargo fmt --all -- --check. The two tools that need no compiler go
first, so a misspelling or an unsorted feature table costs a second rather than
the minutes clippy takes. Clippy comes next because its diagnostics may leave
the tree in a state rustfmt would rewrite, so formatting is the last word; the
doc build sits between them because a rustdoc warning is a lint like clippy’s,
and a doc comment rustdoc would rewrite is still rustfmt’s to settle. Accepts
--release.
typos covers every file in the repository and tombi every TOML one — lint --error-on-warnings and then format --check — which is what CI’s lint job
runs, so a misspelling or a misformatted Cargo.toml fails here instead of
only there. Both are pointed at the workspace root rather than the working
directory, since that is where typos.toml and tombi.toml live. Neither
ships with cargo: when one is missing the step says which tool, not how to
install it — both are distributed outside cargo.
Clippy runs twice, because each compiles a different set of code:
- The library crates with no default features at all, which is the combination
that proves their general facilities compile and lint on their own — and the
one a false optional feature, whose dependants never really compiled without
it, fails to compile. The crates are named one by one rather than picked with
--workspace, because--no-default-featuresreaches only the packages the command names: for every other package the features come from the dependency edges that reach it, and feature unification applies all of them. Every application crate asksunlit3dforuiandwinit, so a workspace-wide run would compile the libraries with exactly the backends this pass exists to leave out. - The workspace with every feature, which the doc build and the full test run compile.
§cargo xtask test
Runs cargo nextest run --workspace --lib --bins --tests --all-features, then
cargo test --workspace --all-features --doc. Nextest gives every test its own
process, so one test’s device, logger or panic cannot reach another’s; it does
not run doctests, hence the second pass. The targets are named rather than swept
in with --all-targets because the benchmarks are not test binaries.
The nextest pass runs twice: once with the default tier — the WebGPU baseline’s
limits — and once with UNLIT3D_DEVICE_TIER=webgl2, which narrows every headless
device to WebGL2’s limits and missing downlevel capabilities. The second pass is
what exercises the paths a browser takes — the shader’s arrays read through
textures instead of storage buffers, and a mesh’s vertex offset baked into its
indices — on a machine whose own adapter is Vulkan, Metal or DX12. Set the
variable to native to run against the adapter’s own limits instead of the
baseline. Accepts --release, which applies to both passes.
A frame that fails its snapshot comparison is written before the test fails, under
target/snapshot-mismatches for the default pass and
target/snapshot-mismatches-webgl2 for the tier pass — a directory each, so a
frame that differs under both tiers can be told apart. CI uploads them when a
test fails, so a mismatch can be looked at rather than only scored.
§cargo xtask print-gpu
Prints the devices a test run would draw with — the tier, whether an indexed
draw can use base_vertex, and the adapter, features and limits — as JSON. A
test run draws through two tiers, the default one and then webgl2, so both are
printed, one after the other. --tier reports a single one instead: webgpu,
webgl2 or native, spelled the way UNLIT3D_DEVICE_TIER spells them for a
test run.
The device is wgpu’s, and so is the description: AdapterInfo, Features and
Limits are serialized as wgpu declares them, so the output carries the fields
— and the spellings — wgpu-info would print, without a table to keep in step
with a wgpu upgrade. The task runner links no graphics code, so it runs this
through unlit_wgpu_test_util’s gpu-info binary and prints what that says.
CI runs it once per test job, before the suite: a mismatch against a stored
frame is usually one driver disagreeing with another, so the run that reports a
score also records what it ran on. cargo xtask test-wasm prints the same
record for the browser, which is a device no host process can describe.
§cargo xtask miri
Runs unlit_ecs’s tests under Miri, the interpreter that checks the aliasing
the compiler cannot. The world keeps component values in UnsafeCells and hands
out plain references through a shared &World, so a mistake in the borrow
ledger is undefined behaviour rather than a panic, and no ordinary test can see
it — a release build reads the same bytes as a debug one. cargo xtask test
therefore cannot stand in for this, and Miri is the gate for that code.
It interprets every test, which is one to two orders of magnitude slower, so it
names the one crate whose storage is unsafe rather than the workspace. The
toolchain is nightly’s, since Miri is not on stable and its sysroot is built
against the nightly it ships with; rustup component add miri --toolchain nightly installs it. CI runs this as its own job, keyed on the nightly’s commit
hash so a toolchain bump rebuilds the sysroot instead of reusing a stale one.
§cargo xtask test-wasm
Runs the same GPU tests cargo xtask test does, but on
wasm32-unknown-unknown in a real browser, and then starts the example itself. A
wasm binary has no process to start and no exit code to hand back, so the pass is
assembled rather than run:
- The workspace is built for wasm — under the default features, which is what
the browser and the APK ship, and under the
wasm-releasecargo profile — so a change that breaks the example or the library for this target fails here rather than only in its own job. cargo nextest list --list-type binaries-onlybuilds the wasm test binaries and reports where they landed — asked of nextest so the target list, features and names stay the ones the host pass uses.wasm-bindgenturns each into a JS module exportingrun_test.- The test page, which loads one module and calls that export, is copied beside
them, together with a
wasm_paths.jsonmapping a module name to its script. - A Node runner serves that directory and opens the page once per test in a
fresh browser context. A frame that fails its comparison is encoded in the
browser — the page has no filesystem — and handed to this runner, which
writes it under
target/snapshot-mismatches-wasmfor CI to upload. - The same native test binaries, with
UNLIT3D_WASM_TESTset, become a proxy instead of a test suite:cargo nextestdrives them, and each trial asks the runner to run that test in the browser. Nextest’s listing, filtering, reporting and exit code then all work as they do for an ordinary suite.
The verdict travels through sessionStorage, because a browser has no exit code
and a panicking test traps its wasm instance rather than unwinding — which is
also why #[should_panic] is judged by a panic hook rather than by catching the
panic.
Before the tests, the runner also loads each example — the programs a browser
actually runs, which the GPU tests never start — and checks that it boots, finds
a backend and leaves a shaded image on its canvas rather than a blank one. They
are the same pages run-wasm serves, built by the same code, so the two cannot
drift.
The runner’s JS dependencies and a browser are installed on the first run;
CHROME_PATH names an existing browser to reuse instead of downloading one, and
--show opens a visible window to watch a failure happen. The profile the
nextest pass uses is wasm, defined in .config/nextest.toml; that is a nextest
profile, and separate from the wasm-release cargo profile the builds themselves
use.
Everything this task compiles for wasm32-unknown-unknown goes through
wasm-release, as run-wasm does. Every build for the target also enables the
simd128 target feature — set in .cargo/config.toml, which the workflows
extend through the per-target CARGO_TARGET_*_RUSTFLAGS variables rather than a
RUSTFLAGS that would override it wholesale — under which rapier’s physics and
rustysynth’s mixing both vectorize. A debug wasm is the slow thing here: it takes
minutes to link, several times the size to fetch, and then runs the tests on a
code path no browser ever ships.
§cargo xtask run-wasm
The standard pipeline for a wasm32-unknown-unknown binary a browser runs:
build the examples for the target, hand each wasm to wasm-bindgen so it gets a
JS loader, then serve the output behind a built-in static file server — WebGPU
is only available in a secure context, and localhost is one while a
file:// URL is not. The server is built into the task runner, so nothing needs
to be installed.
--app picks what to build: unlit3d, unlit3dx or all, the default. The
build lays the site out the way the Pages workflow publishes it — a landing page
at the root, one directory per example beside it — and the landing page links
whichever examples were built, so --app unlit3dx serves a one-card page rather
than a broken link.
It binds to loopback on port 8000, or the ports after it if that one is taken,
and prints the URL to open. --no-serve builds and runs bindgen without
serving; trailing positional arguments are passed through to the cargo build.
The build always uses the workspace’s wasm-release cargo profile — release’s
settings at a size-oriented opt-level — and enables the simd128 target
feature, as every wasm build does; there is no profile switch to pass: the output
lands under target/generated/wasm-release either way, and a browser is
never handed the debug binary.
The binaries are named explicitly rather than left to the default target
selection, which would also build the *_android crates’ cdylibs — a library
kind only Android loads. The binary is what a browser runs, and wasm-bindgen
is handed its .wasm.
§cargo xtask build-android
Two tools, split along the language boundary. cargo ndk cross-compiles the
unlit3d_examples_android and unlit3dx_examples_android crates for Android
and drops each .so into the app’s jniLibs directory, which is where Gradle
looks for native libraries and packages whatever it finds; Gradle then compiles
the activities and assembles the APK around them. The libraries are built first
because an APK without them is an activity that cannot start.
Those two crates hold the exported android_main and nothing else. The examples
themselves build as plain libraries, because crate-type is the same for every
target: a cdylib in an example would link a shared library on the desktop and
in the browser too, where nothing loads one. Each *_android crate keeps the
example’s library name, which is what the activities ask for.
The APK carries one activity per example, each loading its own library through
its own android.app.lib_name manifest entry — GameActivity reads that name
once per activity, which is why switching examples starts the other activity
rather than reusing the running one. A floating tab bar, drawn by
TabbedExampleActivity, switches between them and finishes the activity it
leaves.
Both tools configure themselves from the environment: cargo ndk reads the NDK
out of ANDROID_NDK_HOME (or the newest one under ANDROID_HOME/ndk) and
Gradle reads the SDK out of ANDROID_HOME (or android/local.properties). A
JDK 17 or newer must be on PATH, which is what AGP requires.
One ABI, arm64-v8a, and one API level, 26, are compiled, matching abiFilters
and minSdk in android/app/build.gradle.kts — those two files are the whole
contract between the Rust and Gradle halves of the build. The default build type
is debug; --release builds a release APK, which is left unsigned because the
project ships no signing configuration.
§cargo xtask publish
Publishes the crates that reach crates.io — unlit_ecs, unlit_wgpu,
unlit3d and unlit3dx — one at a time and in that order. The order is a
requirement rather than a preference: a crate cannot be packaged until the crates it depends on are
in the registry, so publishing unlit3d first fails with
no matching package named unlit_ecs found. cargo publish waits for each
uploaded crate to appear in the index, which is what makes the next one resolve.
cargo publish --workspace would order the crates itself, but its --dry-run
cannot verify the packages (rust-lang/cargo#16525); publishing crate by crate
keeps the verification pass a release is worth.
Every argument after -- is passed through to each cargo publish, so
cargo xtask publish -- --dry-run runs every check without uploading and
-- --allow-dirty publishes an uncommitted worktree. --dry-run is also
recognised when it decides whether a run that stopped partway can be re-run.
The test harness, the example and the task runner are publish = false, so they
are never uploaded. A real publish is not repeatable — cargo refuses a version
that is already on the registry — so a run that stops partway must be resumed at
the crate that failed.
§Layout
src/main.rs argument parsing (argh) and task dispatch
src/check.rs `cargo xtask check`
src/test.rs `cargo xtask test`
src/print_gpu.rs `cargo xtask print-gpu`: the device, read by `gpu-info`
src/miri.rs `cargo xtask miri`: the ECS's tests under Miri
src/test_wasm.rs `cargo xtask test-wasm`: the browser pass and its runner
src/run_wasm.rs `cargo xtask run-wasm`: the wasm build, bindgen and page
src/build_android.rs `cargo xtask build-android`: the cross-build and the APK
src/publish.rs `cargo xtask publish`: the crates and their publish order
src/http.rs the static file server `run-wasm` serves with
src/step.rs running one child command and reporting which step failed§Building it
cargo run -p xtask -- checkBeing a workspace member, it shares the workspace’s Cargo.lock and target/.
cargo xtask <task> from anywhere in the repository is the normal entry point.
§License
Dual-licensed under MIT or Apache-2.0, at your option.
Re-exports§
pub(crate) use argh::FromArgs;🔒pub(crate) use argh::FromArgs;🔒
Modules§
- build_
android 🔒 cargo xtask build-android: build the examples’ shared libraries, then the APK that packages them.- check 🔒
cargo xtask check: the typo and TOML checks, clippy, the doc build, then a formatting check.- http 🔒
- A minimal static file server, so serving the example needs nothing installed.
- miri 🔒
cargo xtask miri: the ECS’s tests under Miri, which is what checks theunsafein its storage.- print_
gpu 🔒 cargo xtask print-gpu: describe the device the tests draw with.- publish 🔒
cargo xtask publish: publish the workspace’s crates in dependency order.- run_
wasm 🔒 cargo xtask run-wasm: build and serve the web examples.- step 🔒
- Running one step of a task as a child command.
- test 🔒
cargo xtask test: nextest over the workspace, then the doctests.- test_
wasm 🔒 cargo xtask test-wasm: run the GPU tests in a real browser.
Structs§
- Args 🔒
- The repository task runner.
- Build
Android 🔒Args - Arguments of
cargo xtask build-android. - Check
Args 🔒 - Arguments of
cargo xtask check. - Miri
Args 🔒 - Arguments of
cargo xtask miri. - Print
GpuArgs 🔒 - Arguments of
cargo xtask print-gpu. - Publish
Args 🔒 - Arguments of
cargo xtask publish. - RunWasm
Args 🔒 - Arguments of
cargo xtask run-wasm. - Test
Args 🔒 - Arguments of
cargo xtask test. - Test
Wasm 🔒Args - Arguments of
cargo xtask test-wasm.