diff --git a/.cargo/config.toml b/.cargo/config.toml index f9884cb..d29d6c3 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,9 +1,3 @@ -[unstable] -codegen-backend = true - -[profile.dev] -codegen-backend = "cranelift" - [target.x86_64-unknown-linux-gnu] linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f32332..fc6ce9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,8 +77,15 @@ jobs: else cargo binstall --no-confirm --locked "whitaker-installer@${WHITAKER_INSTALLER_VERSION}" fi - # --cranelift provisions rustc-codegen-cranelift on Whitaker's pinned - # toolchain; this project builds with the Cranelift debug backend. + # --cranelift provisions rustc-codegen-cranelift on Whitaker's own + # pinned toolchain, kept separate from this project's toolchain + # pin. This project's debug builds use Cranelift only via the + # opt-in tools/dev-fast/config.toml fragment, which the + # build/test/lint/typecheck Makefile targets pass explicitly with + # --config; .cargo/config.toml no longer sets a codegen backend + # (it configures the mold linker only). The Lint step's Whitaker + # invocation does not pass --config, so it lints under the + # default LLVM backend regardless of this provisioning. whitaker-installer --cranelift - name: Audit dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a005a9..d863a6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,8 +56,9 @@ jobs: key: cross-${{ env.CROSS_REVISION }} - name: Install cross env: - # Clear repository build flags from .cargo/config.toml (mold linker - # and Cranelift codegen-backend) before installing cross from CROSS_REVISION. + # .cargo/config.toml contains the Linux mold linker configuration + # only (no codegen backend); clear its RUSTFLAGS before installing + # cross from CROSS_REVISION. RUSTFLAGS: "" run: | if [ -x "$HOME/.cargo/bin/cross" ]; then @@ -74,11 +75,14 @@ jobs: restore-keys: | ${{ runner.os }}-cargo-${{ matrix.target }}- - name: Build release binary - # Use +stable to override rust-toolchain.toml (which specifies nightly - # with Cranelift for development) and ensure release builds use stable. + # Use +stable to override rust-toolchain.toml's nightly pin and + # ensure release builds use stable, regardless of local dev tooling. env: - # Build release artifacts without repository-local linker flags, - # including mold rustflags from .cargo/config.toml. + # Build release artifacts without repository-local linker flags: + # .cargo/config.toml's mold rustflags are Linux-only and never + # apply here. Cranelift lives solely in the opt-in + # tools/dev-fast/config.toml fragment, which this build never + # reads, so there is nothing to isolate it from. RUSTFLAGS: "" run: cross +stable build --release --target ${{ matrix.target }} - name: Prepare artifact diff --git a/.rustfmt.toml b/.rustfmt.toml index f7ad026..7c6d17b 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,3 +1,10 @@ +# Canonical rustfmt configuration for the estate. In a consuming +# repository this file lives at the root as `.rustfmt.toml`; keep it +# there, aligned with canon (the rust-rustfmt-baseline rule FMT-001 +# compares the parsed keys). +# +# `unstable_features = true` means rustfmt must run on the nightly channel; +# the rust-rustfmt-baseline rule (FMT-002) checks for that evidence. unstable_features = true comment_width = 100 format_code_in_doc_comments = true diff --git a/AGENTS.md b/AGENTS.md index 87bef35..308cca8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -378,3 +378,27 @@ 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. + +## dev-fast is the standard development path + +The standard `make build`, `make test`, `make lint`, and `make typecheck` +targets already pass `--config tools/dev-fast/config.toml` to every cargo +invocation they make; this is not limited to the opt-in `dev-build`/ +`dev-test` targets above. An agent or human who calls `cargo build`, +`cargo test`, `cargo clippy`, `cargo check`, or `cargo doc` directly for a +development build, test, lint, or typecheck run must pass +`--config tools/dev-fast/config.toml` too, or use the Makefile targets +instead of raw `cargo`. The fragment must never be applied to coverage, +release, or verification builds. Mixing direct-cargo and `make` +invocations without the flag thrashes the incremental build cache, since +Cargo fingerprints a build differently depending on which codegen backend +and linker configuration produced it. diff --git a/Cargo.toml b/Cargo.toml index de4e262..5e2106d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,10 @@ allow_attributes = "deny" allow_attributes_without_reason = "deny" blanket_clippy_restriction_lints = "deny" cognitive_complexity = "deny" +disallowed_methods = "deny" needless_pass_by_value = "deny" implicit_hasher = "deny" +missing_assert_message = "deny" # 2. debugging leftovers dbg_macro = "deny" @@ -83,7 +85,21 @@ error_impl_error = "deny" result_large_err = "deny" [lints.rust] +unknown_lints = "deny" +renamed_and_removed_lints = "deny" +unsafe_code = "forbid" missing_docs = "deny" [lints.rustdoc] missing_crate_level_docs = "deny" +broken_intra_doc_links = "deny" +private_intra_doc_links = "deny" +bare_urls = "deny" +invalid_html_tags = "deny" +invalid_codeblock_attributes = "deny" +unescaped_backticks = "deny" + +[dev-dependencies] +camino = "1.2.5" +cap-std = { version = "4.0.2", features = ["fs_utf8"] } +rstest = "0.26.1" diff --git a/Makefile b/Makefile index 1edb31e..1f30905 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,12 @@ UV_ENV = UV_CACHE_DIR=.uv-cache UV_TOOL_DIR=.uv-tools TYPOS_VERSION ?= 1.48.0 TYPOS = $(UV) tool run typos@$(TYPOS_VERSION) +# The standard build, test, lint, and typecheck targets below use the +# opt-in dev-fast profile (Cranelift plus mold) defined in the Wave 1 +# block near the end of this file. Declared here too so the standard +# targets can see it regardless of where in the file they sit. +DEV_FAST_CONFIG ?= tools/dev-fast/config.toml + build: target/debug/$(TARGET) ## Build debug binary release: target/release/$(TARGET) ## Build release binary @@ -36,14 +42,14 @@ clean: ## Remove build artifacts $(CARGO) clean test: ## Run tests with warnings treated as errors - RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) - RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) test --doc --workspace --all-features + RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) --config "$(DEV_FAST_CONFIG)" $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) + RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) --config "$(DEV_FAST_CONFIG)" test --doc --workspace --all-features test-workflow-contracts: ## Validate the mutation-testing caller contract uv run --with 'pytest>=8' --with 'pyyaml>=6' pytest tests/workflow_contracts -q target/%/$(TARGET): ## Build binary in debug or release mode - $(CARGO) build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release) --bin $(TARGET) + $(CARGO) $(if $(findstring release,$(@)),,--config "$(DEV_FAST_CONFIG)") build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release) --bin $(TARGET) coverage: ## Generate lcov coverage with lld for llvm-tools compatibility @echo "coverage linker flags: $(COVERAGE_LINKER_FLAGS)" @@ -54,12 +60,12 @@ coverage: ## Generate lcov coverage with lld for llvm-tools compatibility $(CARGO) llvm-cov --lcov --output-path lcov.info $(TEST_FLAGS) lint: ## Run rustdoc, Clippy, and the Whitaker Dylint suite with warnings denied - RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" $(CARGO) doc --no-deps - $(CARGO) clippy $(CLIPPY_FLAGS) + RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" $(CARGO) --config "$(DEV_FAST_CONFIG)" doc --no-deps + $(CARGO) --config "$(DEV_FAST_CONFIG)" clippy $(CLIPPY_FLAGS) RUSTFLAGS="$(RUST_FLAGS)" $(WHITAKER) --all -- $(CARGO_FLAGS) typecheck: ## Type-check without building - RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) check $(CARGO_FLAGS) + RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) --config "$(DEV_FAST_CONFIG)" check $(CARGO_FLAGS) fmt: ## Format Rust and Markdown sources $(CARGO) +nightly fmt --all @@ -111,3 +117,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/clippy.toml b/clippy.toml index 0e76365..70d64a8 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,4 +1,6 @@ -# Align with CodeScene’s ceiling +# Canonical clippy configuration for the estate. +# +# The thresholds align with CodeScene's code-health ceilings. cognitive-complexity-threshold = 9 # default is 25 too-many-arguments-threshold = 4 # default is 7 too-many-lines-threshold = 70 # default is 100 @@ -6,3 +8,17 @@ excessive-nesting-threshold = 4 # default is off allow-expect-in-tests = true +# Enforce the environment-injection mandate. The reason strings surface in +# the diagnostic, so a contributor who trips one is told what to do instead. +# +# Sanctioned sites carry `#[expect(clippy::disallowed_methods, reason = "..")]` +# rather than `allow`, so the expectation goes unfulfilled — and warns — once +# the site is migrated. The backlog removes itself instead of rotting. +disallowed-methods = [ + { path = "std::env::var", reason = "inject an environment reader" }, + { path = "std::env::var_os", reason = "inject an environment reader" }, + { path = "std::env::vars", reason = "inject an environment reader" }, + { path = "std::env::vars_os", reason = "inject an environment reader" }, + { path = "std::env::set_var", reason = "use a stub environment in tests" }, + { path = "std::env::remove_var", reason = "use a stub environment in tests" }, +] diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 01e4a1e..d91a4af 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -16,6 +16,41 @@ The main `.github/workflows/ci.yml` workflow deliberately does not run `make test WITH_ACT=1`; the separate Act workflow runs those slower container-backed checks in parallel. +## Lint baseline + +`Cargo.toml`'s `[lints.clippy]`, `[lints.rust]`, and `[lints.rustdoc]` +tables hold this repository's lint baseline. `evert` is a single crate +with no `[workspace]` table, so the tables live directly on the crate +manifest rather than under `[workspace.lints]` with per-member +inheritance. `Cargo.toml` is authoritative for the exact entries; this +section summarizes intent rather than duplicating the list. + +The baseline follows the estate's phase 2 Rust conventions: hygiene +and panic-prone operations are denied outright (`unwrap_used`, +`indexing_slicing`, `unreachable`, and similar), `pedantic` is enabled +as a warning tier, and `missing_docs` and `missing_crate_level_docs` +require real documentation rather than suppression. + +Where a lint violation is a genuine, tracked deferral rather than a +bug, annotate the site with +`#[expect(clippy::, reason = "...")]`, never `allow`. An +`#[expect]` only suppresses the warning while the violation remains; +once the site is fixed, the unfulfilled expectation itself warns, so +the deferral surfaces for removal instead of rotting silently in the +codebase. + +`clippy.toml` carries the numeric thresholds behind the baseline +(cognitive complexity, argument count, function length, nesting +depth) and the `disallowed-methods` list that blocks direct +`std::env::var`/`var_os`/`vars`/`vars_os`/`set_var`/`remove_var` +calls. Each disallowed method's `reason` tells the contributor what to +do instead: inject an environment reader in production code, or use a +stub environment in tests. + +The pinned nightly toolchain in `rust-toolchain.toml` supplies the +`rustfmt`, `clippy`, and `rust-analyzer` components the baseline and +this workflow depend on. + ## Spelling policy `make all` and `make markdownlint` enforce en-GB-oxendict spelling with the @@ -77,10 +112,20 @@ as a test assertion on the SHA string. ## Tooling -Development builds use Cranelift for debug code generation. On Linux targets, -`.cargo/config.toml` configures clang to link with `mold` so debug builds link -quickly. Coverage generation uses `lld` because LLVM coverage tooling expects -LLVM-compatible linker behaviour. +Development builds use the standard LLVM backend by default. On Linux +targets, `.cargo/config.toml` configures clang to link with `mold` so debug +builds link quickly. Coverage generation uses `lld` because LLVM coverage +tooling expects LLVM-compatible linker behaviour. + +The pinned nightly toolchain retains the `llvm-tools-preview` and +`rustc-codegen-cranelift-preview` components, so the Cranelift backend and +LLVM coverage tooling are always installed; `tools/dev-fast/config.toml` is +what actually controls the repository-local opt-in activation. The opt-in +accelerated path, `make dev-build` and `make dev-test`, applies the +Cranelift codegen backend alongside `mold` via that fragment. It requires a +nightly toolchain and is never applied to release, coverage, or +verification builds; see [Fast development +builds](../AGENTS.md#fast-development-builds) in `AGENTS.md`. Install `clang`, `lld`, `mold`, `python3`, and `cargo-audit` before running the full generated workflow locally on Linux. diff --git a/docs/repository-layout.md b/docs/repository-layout.md index 20bdae4..52e1f5b 100644 --- a/docs/repository-layout.md +++ b/docs/repository-layout.md @@ -34,6 +34,9 @@ compact and omits build output such as `target/`. ├── tests/ │ └── stub.rs +├── tools/ +│ └── dev-fast/ +│ └── config.toml ├── AGENTS.md ├── Cargo.toml ├── LICENSE @@ -47,7 +50,7 @@ compact and omits build output such as `target/`. ## Path responsibilities - `.cargo/config.toml`: Configures Cargo defaults for local development, - including Linux linker and code-generation settings. + including Linux linker settings. - `.github/dependabot.yml`: Configures automated dependency update checks. - `.github/workflows/act-validation.yml`: Runs the generated workflow validation through `act` separately from main CI. @@ -77,6 +80,9 @@ compact and omits build output such as `target/`. behaviour. - `tests/stub.rs`: Keeps the generated test directory valid until real tests replace it. +- `tools/dev-fast/config.toml`: Configures the opt-in accelerated debug build + (Cranelift plus `mold`), applied explicitly via `make dev-build` and + `make dev-test`; never merged into `.cargo/config.toml`. - `AGENTS.md`: Provides repository-specific working instructions for agents and contributors. - `Cargo.toml`: Defines package metadata, dependencies, lint policy, and Cargo diff --git a/docs/users-guide.md b/docs/users-guide.md index 39ca14e..f542d30 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -10,10 +10,8 @@ settings, and documented starter code. Library projects render `src/lib.rs`. Application projects render `src/main.rs`, `src/lib.rs`, release automation, and `[package.metadata.binstall]` metadata for binary installation. -Development builds use Cranelift for debug code generation. On Linux targets, -`.cargo/config.toml` configures clang to link with `mold` so local debug builds -link quickly. Coverage generation uses `lld` instead because LLVM coverage -tools expect LLVM-compatible linker behaviour. +See the [developers' guide](developers-guide.md) for local build tooling, +including the linker configuration and the opt-in accelerated build path. ## Makefile Targets @@ -25,6 +23,10 @@ The generated `Makefile` exposes these public targets: - `make test` runs `cargo nextest run` when cargo-nextest is installed and falls back to `cargo test` otherwise. All projects also run doctests. - `make build` builds the debug target. +- `make dev-build` builds the debug target using the opt-in accelerated + build configuration described in the + [developers' guide](developers-guide.md). +- `make dev-test` runs tests using the same configuration. - `make release` builds the release target. - `make coverage` writes `lcov.info` using `cargo llvm-cov` and `lld`. - `make audit` derives the Rust workspace root with `cargo metadata` and runs @@ -32,5 +34,7 @@ The generated `Makefile` exposes these public targets: - `make markdownlint` checks Markdown files. - `make nixie` validates Mermaid diagrams. -Install `clang`, `lld`, `mold`, `python3`, and `cargo-audit` before running the -full generated workflow locally on Linux. +Install `clang`, `lld`, `python3`, and `cargo-audit` before running the full +generated workflow locally on Linux. See the +[developers' guide](developers-guide.md) for the additional tooling the +opt-in accelerated build path requires. diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 803cd3f..8c6c4ba 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -3,6 +3,7 @@ channel = "nightly-2026-05-28" components = [ "clippy", "llvm-tools-preview", + "rust-analyzer", "rustc-codegen-cranelift-preview", "rustfmt", ] diff --git a/tests/makefile_contract.rs b/tests/makefile_contract.rs new file mode 100644 index 0000000..acb3127 --- /dev/null +++ b/tests/makefile_contract.rs @@ -0,0 +1,218 @@ +//! Contract test asserting the Makefile wires the dev-fast profile into +//! the standard development targets and keeps it out of coverage. +//! +//! Operation Parabellum's Wave 2+3 sponsor decision makes the dev-fast +//! profile (Cranelift plus mold, `tools/dev-fast/config.toml`) the +//! standard local development path: `make build`, `make test`, +//! `make lint`, and `make typecheck` must pass `--config` pointing at +//! that fragment to every cargo invocation they make, while +//! `make coverage` must keep the supported LLVM backend and platform +//! linker. This test reads the repository's own `Makefile` textually +//! and fails fast if a future edit drops the wiring, before the +//! estate-wide audit (concordat's forthcoming DF-004 rule) would ever +//! catch it. See AGENTS.md's "dev-fast is the standard development +//! path" section for the convention this test enforces. +//! +//! Assertions check each cargo-invoking recipe *line* individually +//! rather than the recipe block as a whole: a target with several +//! cargo lines (nextest plus doc-tests, `cargo doc` plus `cargo +//! clippy`) would otherwise pass a whole-block substring match even +//! when only one of those lines carries `--config`. + +use std::{io, process::Command}; + +use camino::Utf8Path; +use cap_std::{ambient_authority, fs_utf8::Dir}; +use rstest::rstest; + +/// Opens the crate manifest directory as a capability directory handle, +/// so file access below stays scoped to the checkout rather than +/// touching the ambient working directory via `std::fs`. +fn manifest_dir() -> io::Result { + Dir::open_ambient_dir(env!("CARGO_MANIFEST_DIR"), ambient_authority()) +} + +/// Reads the repository's `Makefile` as UTF-8 text. +fn read_makefile() -> io::Result { manifest_dir()?.read_to_string("Makefile") } + +/// Returns whether `relative` exists as a file beneath the crate +/// manifest directory. +fn manifest_has_file(relative: &Utf8Path) -> io::Result { + Ok(manifest_dir()?.is_file(relative)) +} + +/// Returns the recipe lines following an unindented `:` line, up +/// to (but excluding) the next unindented line. `None` means the target +/// itself was not found; `Some(String::new())` means the target exists +/// but has no recipe of its own (a dependency-only forwarding rule). +fn recipe_block(makefile: &str, target: &str) -> Option { + let header = format!("{target}:"); + let mut found_header = false; + let mut block = Vec::new(); + for line in makefile.lines() { + if found_header { + if line.starts_with('\t') || line.starts_with(' ') { + block.push(line); + } else { + break; + } + } else if line.starts_with(&header) { + found_header = true; + } + } + found_header.then(|| block.join("\n")) +} + +/// Matches the estate's `(?i)dev[-_]fast` convention without pulling in +/// the `regex` crate for a single fixed pattern. +fn mentions_dev_fast(text: &str) -> bool { + let lower = text.to_lowercase(); + lower.contains("dev-fast") || lower.contains("dev_fast") +} + +/// Returns the lines within `block` that actually invoke cargo via the +/// Makefile's `$(CARGO)` macro, as distinct from unrelated recipe lines +/// such as the Whitaker Dylint invocation, which is not cargo and is +/// not expected to carry `--config`. +fn cargo_invocation_lines(block: &str) -> Vec<&str> { + block + .lines() + .filter(|line| line.contains("$(CARGO)")) + .collect() +} + +/// Confirms every cargo invocation in a standard development target's +/// recipe routes through the dev-fast fragment. +/// +/// The `#[case]` pairs the target a developer invokes with the Makefile +/// rule whose recipe text should be checked. `build`'s own rule has no +/// recipe of its own: it depends on the `target/%/$(TARGET)` pattern +/// rule that actually invokes cargo (shared with `release`, which must +/// stay excluded), so that is the block resolved and checked here. +#[rstest] +#[case("build", "target/%/$(TARGET)")] +#[case("test", "test")] +#[case("lint", "lint")] +#[case("typecheck", "typecheck")] +fn standard_targets_use_dev_fast(#[case] invoked_target: &str, #[case] recipe_target: &str) { + let makefile = read_makefile().expect("Makefile must be readable"); + let block = recipe_block(&makefile, recipe_target).unwrap_or_else(|| { + panic!( + "Makefile target `{recipe_target}` (backing `make {invoked_target}`) was not found; \ + the dev-fast standard-path convention requires it to exist and route cargo through \ + --config tools/dev-fast/config.toml, per AGENTS.md's \"dev-fast is the standard \ + development path\" section" + ) + }); + let cargo_lines = cargo_invocation_lines(&block); + assert!( + !cargo_lines.is_empty(), + "Makefile target `{recipe_target}` (backing `make {invoked_target}`) has no $(CARGO) \ + invocation to check; the dev-fast standard-path convention expects at least one, per \ + AGENTS.md's \"dev-fast is the standard development path\" section" + ); + for line in cargo_lines { + assert!( + line.contains("--config"), + "Makefile target `{recipe_target}` (backing `make {invoked_target}`) has a cargo \ + invocation that does not pass --config: `{line}`; every cargo line must use the \ + dev-fast profile, per AGENTS.md's \"dev-fast is the standard development path\" \ + section" + ); + assert!( + mentions_dev_fast(line), + "Makefile target `{recipe_target}` (backing `make {invoked_target}`) has a cargo \ + invocation whose --config does not reference the dev-fast fragment: `{line}`, per \ + AGENTS.md's \"dev-fast is the standard development path\" section" + ); + } +} + +/// Confirms the coverage target keeps the supported LLVM backend and +/// platform linker rather than picking up the dev-fast fragment. +#[test] +fn coverage_target_excludes_dev_fast() { + let makefile = read_makefile().expect("Makefile must be readable"); + let Some(block) = recipe_block(&makefile, "coverage") else { + return; // No coverage target in this repository; nothing to guard. + }; + assert!( + !mentions_dev_fast(&block), + "Makefile target `coverage` must not reference the dev-fast fragment: coverage builds \ + require the supported LLVM backend and platform linker, per AGENTS.md's \"dev-fast is \ + the standard development path\" section" + ); +} + +/// Confirms the dev-fast fragment the Makefile wiring depends on exists. +#[test] +fn dev_fast_fragment_exists() { + let relative = Utf8Path::new("tools/dev-fast/config.toml"); + let found = manifest_has_file(relative).expect("manifest directory must be readable"); + assert!( + found, + "{relative} must exist: the standard build/test/lint/typecheck targets pass --config \ + pointing at it" + ); +} + +/// Runs `make --dry-run CARGO=` in the crate +/// root and returns its captured stdout. `--dry-run` prints the recipe +/// Make would run without executing it, so this needs neither a +/// nightly toolchain nor `mold` installed. +fn make_dry_run(target: &str, cargo_override: &str) -> io::Result { + let output = Command::new("make") + .arg("--dry-run") + .arg(target) + .arg(format!("CARGO={cargo_override}")) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .output()?; + String::from_utf8(output.stdout) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error)) +} + +/// Confirms the opt-in `dev-build`/`dev-test` targets honour a CARGO +/// override, proving they invoke `$(CARGO)` rather than a hard-coded +/// `cargo`. Overriding CARGO on the command line must show up ahead of +/// `--config`, which must in turn come before the dev-fast fragment +/// reference, in the emitted recipe. +#[rstest] +#[case("dev-build")] +#[case("dev-test")] +fn dev_fast_targets_honour_cargo_override(#[case] target: &str) { + let output = make_dry_run(target, "probe-cargo").expect("make --dry-run must run and succeed"); + let cargo_pos = output.find("probe-cargo").unwrap_or_else(|| { + panic!( + "`make --dry-run {target} CARGO=probe-cargo` did not emit \"probe-cargo\"; the recipe \ + must invoke $(CARGO) rather than a hard-coded cargo, per AGENTS.md's \"dev-fast is \ + the standard development path\" section. Output: {output:?}" + ) + }); + let config_pos = output.find("--config").unwrap_or_else(|| { + panic!( + "`make --dry-run {target} CARGO=probe-cargo` did not emit \"--config\". Output: \ + {output:?}" + ) + }); + assert!( + cargo_pos < config_pos, + "`make --dry-run {target} CARGO=probe-cargo` must emit the substituted cargo binary \ + before --config; got: {output:?}" + ); + let lower = output.to_lowercase(); + let dev_fast_pos = ["dev-fast", "dev_fast"] + .into_iter() + .filter_map(|needle| lower.find(needle)) + .min() + .unwrap_or_else(|| { + panic!( + "`make --dry-run {target} CARGO=probe-cargo` did not reference the dev-fast \ + fragment. Output: {output:?}" + ) + }); + assert!( + config_pos < dev_fast_pos, + "`make --dry-run {target} CARGO=probe-cargo` must emit --config before the dev-fast \ + fragment reference; got: {output:?}" + ); +} diff --git a/tests/stub.rs b/tests/stub.rs index 28c8a7f..594d1f4 100644 --- a/tests/stub.rs +++ b/tests/stub.rs @@ -6,8 +6,11 @@ #[test] fn replace_this_stub_when_real_tests_exist() { + // `option_env!` resolves at compile time, so this sidesteps the + // environment-injection mandate (`clippy::disallowed_methods`) that + // applies to runtime `std::env` reads. assert!( - std::env::var_os("CARGO_MANIFEST_DIR").is_some(), + option_env!("CARGO_MANIFEST_DIR").is_some(), "CARGO_MANIFEST_DIR should be set by Cargo when running tests" ); } 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 25a8719..af8d953 100644 --- a/typos.local.toml +++ b/typos.local.toml @@ -12,7 +12,22 @@ accepted = ["Center", "Flavored"] [words.corrections] [patterns] -ignore = [] +# Inline code spans quote identifiers verbatim, so they are not en-GB +# prose and must stay exempt (for example, documentation-style-guide.md's +# deliberate `color` API-spelling example). +# +# The mold-linker phrases below are scoped exemptions, not a blanket +# "mold" correction: mold (https://github.com/rui314/mold) is a linker +# name, not a misspelling of "mould", but only in these specific +# phrasings. A stray "mold" elsewhere in prose should still be caught. +ignore = [ + "`[^`\\n]+`", + "-fuse-ld=mold", + "mold linker", + "Cranelift \\+ mold", + "Cranelift and mold", + "`mold`", +] [files] exclude = [] diff --git a/typos.toml b/typos.toml index 387b261..1c29451 100644 --- a/typos.toml +++ b/typos.toml @@ -31,10 +31,17 @@ extend-exclude = [ locale = "en-gb" extend-ignore-re = [ "(?s)```.*?```", + "-fuse-ld=mold", + "Cranelift \\+ mold", + "Cranelift and mold", + "\\brust-analyzer\\b", "`[^`\\n]+`", + "`mold`", + "mold linker", ] [default.extend-words] +"ASO" = "ASO" "Center" = "Center" "Flavored" = "Flavored" "absolutisable" = "absolutizable" @@ -145,8 +152,6 @@ extend-ignore-re = [ "apologizers" = "apologizers" "apologizes" = "apologizes" "apologizing" = "apologizing" -"artifact" = "artifact" -"artifacts" = "artifacts" "atomisable" = "atomizable" "atomisation" = "atomization" "atomisations" = "atomizations" @@ -833,6 +838,7 @@ extend-ignore-re = [ "globalizers" = "globalizers" "globalizes" = "globalizes" "globalizing" = "globalizing" +"handwritten" = "handwritten" "harmonisable" = "harmonizable" "harmonisation" = "harmonization" "harmonisations" = "harmonizations" @@ -1013,6 +1019,24 @@ extend-ignore-re = [ "internationalizers" = "internationalizers" "internationalizes" = "internationalizes" "internationalizing" = "internationalizing" +"italicisable" = "italicizable" +"italicisation" = "italicization" +"italicisations" = "italicizations" +"italicise" = "italicize" +"italicised" = "italicized" +"italiciser" = "italicizer" +"italicisers" = "italicizers" +"italicises" = "italicizes" +"italicising" = "italicizing" +"italicizable" = "italicizable" +"italicization" = "italicization" +"italicizations" = "italicizations" +"italicize" = "italicize" +"italicized" = "italicized" +"italicizer" = "italicizer" +"italicizers" = "italicizers" +"italicizes" = "italicizes" +"italicizing" = "italicizing" "itemisable" = "itemizable" "itemisation" = "itemization" "itemisations" = "itemizations" @@ -1682,6 +1706,24 @@ extend-ignore-re = [ "pluralizers" = "pluralizers" "pluralizes" = "pluralizes" "pluralizing" = "pluralizing" +"polymerisable" = "polymerizable" +"polymerisation" = "polymerization" +"polymerisations" = "polymerizations" +"polymerise" = "polymerize" +"polymerised" = "polymerized" +"polymeriser" = "polymerizer" +"polymerisers" = "polymerizers" +"polymerises" = "polymerizes" +"polymerising" = "polymerizing" +"polymerizable" = "polymerizable" +"polymerization" = "polymerization" +"polymerizations" = "polymerizations" +"polymerize" = "polymerize" +"polymerized" = "polymerized" +"polymerizer" = "polymerizer" +"polymerizers" = "polymerizers" +"polymerizes" = "polymerizes" +"polymerizing" = "polymerizing" "popularisable" = "popularizable" "popularisation" = "popularization" "popularisations" = "popularizations" @@ -2438,6 +2480,24 @@ extend-ignore-re = [ "uncategorizers" = "uncategorizers" "uncategorizes" = "uncategorizes" "uncategorizing" = "uncategorizing" +"underutilisable" = "underutilizable" +"underutilisation" = "underutilization" +"underutilisations" = "underutilizations" +"underutilise" = "underutilize" +"underutilised" = "underutilized" +"underutiliser" = "underutilizer" +"underutilisers" = "underutilizers" +"underutilises" = "underutilizes" +"underutilising" = "underutilizing" +"underutilizable" = "underutilizable" +"underutilization" = "underutilization" +"underutilizations" = "underutilizations" +"underutilize" = "underutilize" +"underutilized" = "underutilized" +"underutilizer" = "underutilizer" +"underutilizers" = "underutilizers" +"underutilizes" = "underutilizes" +"underutilizing" = "underutilizing" "uninitialisable" = "uninitializable" "uninitialisation" = "uninitialization" "uninitialisations" = "uninitializations"