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/release.yml b/.github/workflows/release.yml index 1cd34d7..54c8e3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,8 +56,8 @@ 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. + # Clear the repository's mold linker rustflags from + # .cargo/config.toml before installing cross from CROSS_REVISION. RUSTFLAGS: "" run: | if [ -x "$HOME/.cargo/bin/cross" ]; then @@ -74,8 +74,9 @@ 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 (needed + # for the repository's lint and dev-fast tooling) so release builds + # use stable regardless. env: # Build release artifacts without repository-local linker flags, # including mold rustflags from .cargo/config.toml. 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 02fc2c8..a3341db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -372,3 +372,26 @@ 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. + +## Standard development path + +The dev-fast profile is the standard development path, not a side path: +`make build`, `make test`, `make lint`, and `make typecheck` all pass +`--config tools/dev-fast/config.toml` to Cargo. An agent or human calling +`cargo build`, `cargo test`, `cargo clippy`, `cargo check`, or `cargo doc` +directly for a development build, test, lint, or typecheck run must pass +that same `--config` flag, or the run will use a different codegen +backend and linker than `make` does. 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, because Cargo fingerprints the codegen backend and the two runs +produce different fingerprints for what looks like the same build. diff --git a/Cargo.lock b/Cargo.lock index a197fef..80cfcf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,305 @@ # 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 = "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 = "memoryd" version = "0.1.0" +dependencies = [ + "rstest", +] + +[[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 = "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 c70459d..a27998f 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,19 @@ 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] +rstest = "0.26.1" diff --git a/Makefile b/Makefile index 79b14e0..3059317 100644 --- a/Makefile +++ b/Makefile @@ -31,11 +31,11 @@ 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) --config "$(DEV_FAST_CONFIG)" $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) 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)" @@ -46,13 +46,13 @@ coverage: ## Generate lcov coverage with lld for llvm-tools compatibility $(CARGO) llvm-cov --lcov --output-path lcov.info $(TEST_FLAGS) lint: ## Run Clippy 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) @echo "Whitaker binary: $(WHITAKER)" PATH="$(USER_BIN_PATH):$(PATH)" 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: fmt-tools ## Format Rust and Markdown sources $(CARGO) +nightly fmt --all @@ -73,3 +73,14 @@ nixie: ## Validate Mermaid diagrams 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 0f34caf..ed58e37 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -7,18 +7,56 @@ This guide explains the contributor workflow for the generated Memoryd project. Use `make all` as the public entrypoint for formatting, linting, and tests. `make lint` runs rustdoc, Clippy, and Whitaker. `make test` prefers `cargo nextest run` and falls back to `cargo test` when cargo-nextest is not -available. `make coverage` uses `cargo llvm-cov` with `lld`. +available. `make coverage` uses `cargo llvm-cov` with `lld`. `make dev-build` +and `make dev-test` are opt-in accelerated variants; see below. ## 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. +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. `.cargo/config.toml` +no longer enables the Cranelift codegen backend for debug builds; that opt-in +acceleration now lives in `tools/dev-fast/config.toml` instead, so it applies +only when explicitly requested rather than to every build Cargo discovers. +`rust-toolchain.toml` still pins the `llvm-tools-preview` and +`rustc-codegen-cranelift-preview` components, so both the coverage tooling +and the Cranelift backend itself remain installed; `tools/dev-fast/config.toml` +is what controls whether a given build actually activates Cranelift. + +`make dev-build` and `make dev-test` compile with that Cranelift-plus-mold +fragment, passed explicitly via `cargo --config tools/dev-fast/config.toml`. +They require a nightly toolchain and, on Linux, a `mold` binary on the +`PATH`. Release, coverage, and verification builds are unaffected because +the fragment is never merged into `.cargo/config.toml`. Install `clang`, `lld`, and `mold` before running the full generated workflow locally on Linux. +## Lint baseline + +Memoryd is a single crate with no `[workspace]` table, so its lint tables +live directly under `[lints.clippy]`, `[lints.rust]`, and `[lints.rustdoc]` +in `Cargo.toml`, rather than under `[workspace.lints]` with per-member +inheritance. They implement the estate's phase 2 Rust baseline. `Cargo.toml` +is authoritative for the exact set and level of each lint; this section +summarizes intent rather than duplicating the list. + +Violations must be fixed, not silenced. Where a violation is a genuine, +scheduled deferral, annotate the site with +`#[expect(clippy::, reason = "...")]`, never `#[allow(...)]`: once the +site is fixed, the unfulfilled expectation itself becomes a warning, so the +backlog announces its own shrinkage instead of rotting silently. + +`clippy.toml` carries the numeric thresholds (cognitive complexity, +argument count, function length, nesting depth) and the +`disallowed-methods` list that forbids direct `std::env::var`/`set_var`/ +`remove_var` calls and their `_os`/`vars` siblings; inject an environment +reader instead so environment access stays testable. + +The pinned nightly toolchain in `rust-toolchain.toml` supplies the +`rustfmt`, `clippy`, and `rust-analyzer` components the lint and formatting +gates depend on. + Behavioural tests that describe externally observable workflows should use `rstest-bdd` so Gherkin scenarios, `rstest` fixtures, and Rust assertions run under the standard Cargo test harness. PostgreSQL migration and repository diff --git a/docs/repository-layout.md b/docs/repository-layout.md index 3c004e9..4d143f1 100644 --- a/docs/repository-layout.md +++ b/docs/repository-layout.md @@ -46,7 +46,10 @@ 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 the Linux linker. +- `tools/dev-fast/config.toml`: Opt-in Cranelift-plus-mold configuration + fragment for accelerated local debug builds, applied explicitly by + `make dev-build` and `make dev-test`. - `.github/dependabot.yml`: Configures automated dependency update checks. - `.github/workflows/ci.yml`: Runs Memoryd's continuous integration checks. - `.github/workflows/release.yml`: Builds and publishes binary release diff --git a/docs/users-guide.md b/docs/users-guide.md index 0811009..a51e10c 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`, 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 the local build and +linker configuration. ## Makefile Targets @@ -27,8 +25,10 @@ The generated `Makefile` exposes these public targets: - `make build` builds the debug target. - `make release` builds the release target. - `make coverage` writes `lcov.info` using `cargo llvm-cov` and `lld`. +- `make dev-build` and `make dev-test` provide opt-in accelerated build + variants; see the [developers' guide](developers-guide.md) for details. - `make markdownlint` checks Markdown files. - `make nixie` validates Mermaid diagrams. -Install `clang`, `lld`, and `mold` before running the full generated workflow -locally on Linux. +See the [developers' guide](developers-guide.md) for the toolchain +prerequisites needed to run the full generated workflow locally. diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4c5db19..997b617 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -3,6 +3,7 @@ channel = "nightly-2026-05-21" 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..9969b89 --- /dev/null +++ b/tests/makefile_contract.rs @@ -0,0 +1,199 @@ +//! Contract test for the Makefile's dev-fast wiring. +//! +//! The estate's dev-fast profile (`tools/dev-fast/config.toml`) is the +//! standard development path, not an opt-in side path: every cargo +//! invocation in the `build`, `test`, `lint`, and `typecheck` Makefile +//! targets must pass `--config tools/dev-fast/config.toml`, while +//! `coverage` must never do so, because coverage requires the LLVM +//! codegen backend and the platform linker. See AGENTS.md's "Standard +//! development path" section for the full rationale. This test reads the +//! repository's own Makefile so an over-eager edit that drops the wiring +//! fails locally before the estate-wide DF-004 audit ever runs. +//! +//! Checks operate per cargo-invoking recipe line, not on a recipe's whole +//! text: a target whose recipe carries several cargo lines (the `doc` +//! step plus `clippy` in `lint`) would otherwise pass a whole-block match +//! even when only one of those lines is wired. A separate pair of tests +//! dry-runs `dev-build`/`dev-test` with a substituted `CARGO` value to +//! prove the Wave 1 block's `$(CARGO)` indirection actually reaches the +//! `--config` flag, without needing the nightly toolchain or `mold`. + +use std::{path::Path, process::Command}; + +use rstest::rstest; + +const MAKEFILE: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Makefile")); + +/// Returns the tab-indented recipe lines that follow the first line +/// starting with `header_prefix`, or an empty string if no such rule +/// header exists. +/// +/// This mirrors the convention documented in the dev-fast wiring brief: +/// a target's recipe block is the run of indented lines between its +/// header and the next non-indented line. +fn recipe_after(makefile: &str, header_prefix: &str) -> String { + let Some(start) = makefile + .lines() + .position(|line| line.starts_with(header_prefix)) + else { + return String::new(); + }; + makefile + .lines() + .skip(start + 1) + .take_while(|line| line.starts_with('\t')) + .collect::>() + .join("\n") +} + +/// Returns the recipe used for `make build`, following the delegation to +/// the `target/%/$(TARGET)` pattern rule when `build` itself carries no +/// recipe of its own (it depends on `target/debug/$(TARGET)` instead). +fn build_recipe(makefile: &str) -> String { + let direct = recipe_after(makefile, "build:"); + if direct.is_empty() { + recipe_after(makefile, "target/%/$(TARGET):") + } else { + direct + } +} + +/// Returns the lines within `recipe` that directly invoke `$(CARGO)`, as +/// distinct from lines that invoke some other tool (for example the +/// Whitaker binary, which reuses `$(CARGO_FLAGS)` but not `$(CARGO)` +/// itself). +fn cargo_invocation_lines(recipe: &str) -> Vec<&str> { + recipe + .lines() + .filter(|line| line.contains("$(CARGO)")) + .collect() +} + +/// Reports whether `text` references the dev-fast configuration +/// fragment, matching `dev-fast` or `dev_fast` case-insensitively. +fn mentions_dev_fast(text: &str) -> bool { + let lower = text.to_lowercase(); + lower.contains("dev-fast") || lower.contains("dev_fast") +} + +/// Asserts that every `$(CARGO)` invocation in `recipe` passes `--config` +/// and references the dev-fast fragment, checked line by line so a +/// partially-wired multi-line recipe fails rather than passing on the +/// strength of its other lines. +fn assert_uses_dev_fast_config(target: &str, recipe: &str) { + let cargo_lines = cargo_invocation_lines(recipe); + assert!( + !cargo_lines.is_empty(), + "the `{target}` Makefile target's recipe has no $(CARGO) invocation to check for dev-fast \ + wiring; see AGENTS.md's \"Standard development path\" section", + ); + for line in cargo_lines { + let wired = line.contains("--config") && mentions_dev_fast(line); + assert!( + wired, + "the `{target}` Makefile target's cargo invocation `{line}` must pass `--config \ + tools/dev-fast/config.toml` (the DEV_FAST_CONFIG variable) so it runs on the \ + estate's dev-fast standard development path; see AGENTS.md's \"Standard development \ + path\" section", + ); + } +} + +/// Asserts that no `$(CARGO)` invocation in `recipe` references the +/// dev-fast fragment, checked line by line for the same reason as +/// [`assert_uses_dev_fast_config`]. +fn assert_never_uses_dev_fast_config(target: &str, recipe: &str) { + for line in cargo_invocation_lines(recipe) { + assert!( + !mentions_dev_fast(line), + "the `{target}` Makefile target's cargo invocation `{line}` must never reference the \ + dev-fast configuration fragment: {target} requires the LLVM codegen backend and the \ + platform linker; see AGENTS.md's \"Fast development builds\" section", + ); + } +} + +#[rstest] +#[case::make_test("test")] +#[case::make_lint("lint")] +#[case::make_typecheck("typecheck")] +fn standard_target_uses_dev_fast_config(#[case] target: &str) { + let header = format!("{target}:"); + let recipe = recipe_after(MAKEFILE, &header); + assert_uses_dev_fast_config(target, &recipe); +} + +#[test] +fn build_target_uses_dev_fast_config() { + let recipe = build_recipe(MAKEFILE); + assert_uses_dev_fast_config("build", &recipe); +} + +#[test] +fn coverage_target_never_uses_dev_fast_config() { + let recipe = recipe_after(MAKEFILE, "coverage:"); + assert_never_uses_dev_fast_config("coverage", &recipe); +} + +#[test] +fn dev_fast_config_file_exists() { + let path = Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tools/dev-fast/config.toml" + )); + assert!( + path.exists(), + "tools/dev-fast/config.toml must exist: the Makefile's DEV_FAST_CONFIG variable and the \ + standard development targets depend on it; see AGENTS.md's \"Fast development builds\" \ + section", + ); +} + +/// Runs `make --dry-run CARGO=` in the repository +/// root and returns the printed command, without building or testing +/// anything. Proves the Wave 1 block's `$(CARGO)` indirection actually +/// reaches the recipe, on any toolchain, with no nightly or `mold` +/// dependency. +/// +/// Returns `Err` rather than panicking directly: this is a helper, not a +/// `#[test]` function itself, so `clippy::expect_used` still applies to +/// it even with `allow-expect-in-tests` set. +fn dry_run(target: &str, probe_cargo: &str) -> Result { + let output = Command::new("make") + .arg("--dry-run") + .arg(target) + .arg(format!("CARGO={probe_cargo}")) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .output() + .map_err(|error| { + format!("failed to run `make --dry-run {target} CARGO={probe_cargo}`: {error}") + })?; + String::from_utf8(output.stdout) + .map_err(|error| format!("make dry-run output for `{target}` was not valid UTF-8: {error}")) +} + +/// Asserts that each string in `needles` occurs in `text`, in the given +/// order, by repeatedly splitting off everything before and including +/// the next needle. Names `target` when a needle is missing or +/// out of order. +fn assert_ordered(target: &str, text: &str, needles: &[&str]) { + let mut remaining = text; + for needle in needles { + let Some((_, after)) = remaining.split_once(needle) else { + panic!( + "the `make {target}` dry-run output must contain {needles:?} in order; {needle:?} \ + was missing after the previous marker; full output: {text:?}", + ); + }; + remaining = after; + } +} + +#[rstest] +#[case::probe_dev_build("dev-build")] +#[case::probe_dev_test("dev-test")] +fn dev_fast_target_respects_cargo_substitution(#[case] target: &str) { + let probe_cargo = "probe-cargo"; + let output = dry_run(target, probe_cargo).expect("make --dry-run must succeed"); + assert_ordered(target, &output, &[probe_cargo, "--config", "dev-fast"]); +} diff --git a/tests/stub.rs b/tests/stub.rs index 28c8a7f..af7b7cb 100644 --- a/tests/stub.rs +++ b/tests/stub.rs @@ -7,7 +7,7 @@ #[test] fn replace_this_stub_when_real_tests_exist() { assert!( - std::env::var_os("CARGO_MANIFEST_DIR").is_some(), + !env!("CARGO_MANIFEST_DIR").is_empty(), "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"]