diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9b3a19c..e57b900 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,3 +28,10 @@ updates: cooldown: default-days: 7 semver-major-days: 14 + - package-ecosystem: rust-toolchain + directory: / + labels: + - dependencies + - rust-toolchain + schedule: + interval: weekly diff --git a/AGENTS.md b/AGENTS.md index fdd2710..f5e5bc0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -378,3 +378,12 @@ The following tooling is available in this environment: These practices help maintain a high-quality codebase and facilitate collaboration. + +## Fast development builds + +`make dev-build` and `make dev-test` compile with the opt-in Cranelift +backend and the mold linker configured in `tools/dev-fast/config.toml`. +They require a nightly toolchain and, on Linux, a `mold` binary on the +`PATH`. The fragment is passed explicitly with `--config`, so release, +coverage, and verification builds are unaffected; never copy its contents +into `.cargo/config.toml`, which Cargo applies to every build. diff --git a/Cargo.lock b/Cargo.lock index 7b6e2e6..d62705f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,312 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.119", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "statelet" version = "0.1.0" +dependencies = [ + "camino", + "rstest", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index 9d521dd..7005a98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,3 +79,7 @@ missing_docs = "deny" [lints.rustdoc] missing_crate_level_docs = "deny" + +[dev-dependencies] +camino = "1.2.5" +rstest = "0.26.1" diff --git a/Makefile b/Makefile index 31454e6..63ebae7 100644 --- a/Makefile +++ b/Makefile @@ -126,3 +126,14 @@ rust-audit: ## Audit the Rust workspace for known vulnerabilities help: ## Show available targets @grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \ awk 'BEGIN {FS=":"; printf "Available targets:\n"} {printf " %-20s %s\n", $$1, $$2}' + +# Opt-in accelerated debug builds (Cranelift + mold); requires a nightly +# toolchain. See AGENTS.md and tools/dev-fast/config.toml. +DEV_FAST_CONFIG ?= tools/dev-fast/config.toml + +.PHONY: dev-build dev-test +dev-build: ## Build debug binaries with Cranelift and mold + $(CARGO) --config "$(DEV_FAST_CONFIG)" build + +dev-test: ## Run tests with Cranelift and mold + $(CARGO) --config "$(DEV_FAST_CONFIG)" test diff --git a/docs/developers-guide.md b/docs/developers-guide.md index c1026da..406d0a1 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -59,6 +59,31 @@ LLVM-compatible linker behaviour. Install `clang`, `lld`, `mold`, `python3`, and `cargo-audit` before running the full generated workflow locally on Linux. +## Fast development builds + +`make dev-build` and `make dev-test` offer an opt-in, faster iteration loop +for local debug work. `dev-build` compiles debug binaries and `dev-test` runs +the test suite; both use the Cranelift codegen backend and the mold linker +configured in `tools/dev-fast/config.toml`. + +The `DEV_FAST_CONFIG` variable names that fragment, defaulting to +`tools/dev-fast/config.toml`, and both targets pass it to Cargo explicitly +with `--config "$(DEV_FAST_CONFIG)"`. Cargo never auto-discovers this +fragment; it takes effect only when a target invokes it directly, so other +`make` targets are unaffected. + +Using the fragment requires a nightly toolchain, because the Cranelift +codegen backend is unstable. On Linux it also requires the mold linker on +`PATH`; the fragment gates the linker flag behind a `target_os = "linux"` +`cfg` table, so other platforms fall back to their default linker. + +Cranelift configuration must never be copied into `.cargo/config.toml`. +Cargo auto-discovers that file and applies it to every invocation, which +would silently degrade release, coverage, and verification builds to the +faster but less optimizing backend. Keep the fast-build configuration +isolated in `tools/dev-fast/config.toml` and reach it only through +`make dev-build` and `make dev-test`. + ## Spelling policy The tracked `typos.toml` is generated from the shared estate dictionary and the diff --git a/tests/dev_fast_contract.rs b/tests/dev_fast_contract.rs new file mode 100644 index 0000000..6b9a441 --- /dev/null +++ b/tests/dev_fast_contract.rs @@ -0,0 +1,77 @@ +//! Contract tests for the opt-in dev-fast Make targets. +//! +//! `dev-build` and `dev-test` must honour the injectable `CARGO` variable +//! and pass the dev-fast fragment to Cargo explicitly with `--config`, +//! rather than relying on Cargo auto-discovering `.cargo/config.toml`. +//! These tests parse `make --dry-run` output rather than running a real +//! build, so they stay fast and do not require a nightly toolchain or the +//! mold linker. Invoking `make` is process spawning, which Whitaker's +//! capability-based filesystem rules do not constrain; only file access +//! goes through `camino`. + +use std::{io, process::Command}; + +use camino::{Utf8Path, Utf8PathBuf}; +use rstest::rstest; + +/// Returns the workspace root, derived from the manifest directory so the +/// test works regardless of the runner's current directory. +fn workspace_root() -> Utf8PathBuf { Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")) } + +/// Runs `make --dry-run CARGO=probe-cargo` and returns its stdout. +fn dry_run(target: &str) -> Result { + let output = Command::new("make") + .current_dir(workspace_root()) + .args(["--dry-run", target, "CARGO=probe-cargo"]) + .output()?; + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +/// Locates `probe-cargo`, `--config`, and the dev-fast fragment marker on a +/// recipe line, returning their byte offsets if all three are present. +fn substitution_offsets(line: &str) -> Option<(usize, usize, usize)> { + let cargo = line.find("probe-cargo")?; + let config = line.find("--config")?; + let dev_fast = line.find("dev-fast")?; + Some((cargo, config, dev_fast)) +} + +#[rstest] +#[case::dev_build("dev-build")] +#[case::dev_test("dev-test")] +fn dev_fast_target_substitutes_injected_cargo(#[case] target: &str) { + let stdout = dry_run(target).expect("make --dry-run should run"); + + let Some(recipe_line) = stdout.lines().find(|line| line.contains("probe-cargo")) else { + panic!("target {target} did not invoke the injected CARGO override:\n{stdout}"); + }; + + let Some((cargo_pos, config_pos, dev_fast_pos)) = substitution_offsets(recipe_line) else { + panic!( + "target {target} recipe line is missing probe-cargo, --config, or a dev-fast \ + reference:\n{recipe_line}" + ); + }; + + assert!( + cargo_pos < config_pos && config_pos < dev_fast_pos, + "target {target} emitted an out-of-order command:\n{recipe_line}" + ); +} + +#[test] +fn dev_fast_config_fragment_exists() { + let config_path: Utf8PathBuf = [ + workspace_root().as_str(), + "tools", + "dev-fast", + "config.toml", + ] + .iter() + .collect(); + + assert!( + Utf8Path::new(&config_path).exists(), + "{config_path} must exist for the dev-fast targets to function" + ); +} diff --git a/tools/dev-fast/config.toml b/tools/dev-fast/config.toml new file mode 100644 index 0000000..01e7f89 --- /dev/null +++ b/tools/dev-fast/config.toml @@ -0,0 +1,31 @@ +# Canonical opt-in Cargo configuration fragment for accelerated local debug +# builds. +# +# This configuration is deliberately kept out of `.cargo/config.toml`. +# Cargo auto-discovers that path, so anything placed there applies to +# release packaging, coverage, and verification builds, which must keep +# the supported LLVM backend and platform linker. In a consuming +# repository this fragment lives at `tools/dev-fast/config.toml` and is +# passed explicitly with `cargo --config tools/dev-fast/config.toml ...` +# from `make dev-build` and `make dev-test`. +# +# Repositories that set repository-wide `rustflags` in `.cargo/config.toml` +# must restate them in the target table below. Cargo joins the rustflags of +# every matching `[target.*]` entry (target-triple and `cfg` tables alike), +# but the joined target rustflags take precedence over `[build].rustflags` +# rather than merging with it. + +[unstable] +codegen-backend = true + +# Cranelift trades runtime performance for compile speed, so it applies to +# the dev profile only. The `make dev-*` targets never build release +# artefacts. +[profile.dev] +codegen-backend = "cranelift" + +# mold ships for Linux only, so the flag is gated behind a target `cfg`. On +# macOS and Windows the table simply does not apply and the platform default +# linker is used. +[target.'cfg(target_os = "linux")'] +rustflags = ["-Clink-arg=-fuse-ld=mold"] diff --git a/typos.local.toml b/typos.local.toml index 89d67e3..6eb34fc 100644 --- a/typos.local.toml +++ b/typos.local.toml @@ -13,6 +13,13 @@ accepted = [] [patterns] # Preserve complete formal names while allowing Markdown line wrapping. ignore = [ + # "mold" names the linker (https://github.com/rui314/mold) in these + # contexts only; the mould correction stays active elsewhere. + "-fuse-ld=mold", + "mold linker", + "Cranelift \\+ mold", + "Cranelift and mold", + "`mold`", "Azure Architecture\\s+Center\\s+\\|\\s+Microsoft Learn", "GitHub\\s+Flavored\\s+Markdown", ] diff --git a/typos.toml b/typos.toml index 92a5a5b..fb0c159 100644 --- a/typos.toml +++ b/typos.toml @@ -31,10 +31,15 @@ extend-exclude = [ locale = "en-gb" extend-ignore-re = [ "(?s)```.*?```", + "-fuse-ld=mold", "Azure Architecture\\s+Center\\s+\\|\\s+Microsoft Learn", + "Cranelift \\+ mold", + "Cranelift and mold", "GitHub\\s+Flavored\\s+Markdown", "\\brust-analyzer\\b", "`[^`\\n]+`", + "`mold`", + "mold linker", ] [default.extend-words]