From 7cfad2c4ccd7a055460ba39b9b89137e362edeb5 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sat, 1 Aug 2026 20:51:27 +0100 Subject: [PATCH 1/5] Add optional Polonius support to generated projects Default Polonius on for applications and off for libraries while allowing an explicit override. Preserve the compiler flag through Cargo target configuration, Make compile paths, coverage, and release workflows so Cranelift, mold, and lld remain coherent. Generate borrow-checker policy documentation and agent guidance. Exercise both configurations and compile a single-lookup borrow-returning accessor. Ignore the local GrepAI index used during semantic exploration. --- .gitignore | 1 + README.md | 3 + copier.yaml | 8 + docs/developers-guide.md | 8 +- docs/users-guide.md | 12 ++ template/.cargo/config.toml.jinja | 14 +- template/.github/workflows/ci.yml.jinja | 4 +- ...erage-main.yml => coverage-main.yml.jinja} | 7 +- ...ur == 'app' %}release.yml{% endif %}.jinja | 12 +- template/AGENTS.md.jinja | 19 ++ template/Makefile.jinja | 29 ++- template/README.md.jinja | 6 + template/docs/contents.md.jinja | 5 +- template/docs/developers-guide.md.jinja | 11 ++ template/docs/repository-layout.md.jinja | 7 + template/docs/users-guide.md.jinja | 9 + ...le_polonius %}polonius.md{% endif %}.jinja | 68 +++++++ template/rust-toolchain.toml.jinja | 4 + template/typos.local.toml | 2 +- template/typos.toml | 81 ++++++++ tests/helpers/rendering.py | 5 +- tests/helpers/tooling_contracts/__init__.py | 4 + tests/helpers/tooling_contracts/polonius.py | 177 ++++++++++++++++++ .../__snapshots__/test_snapshots.ambr | 45 +++-- tests/test_template/test_basic_rendering.py | 12 ++ tests/test_template/test_compilation.py | 34 ++++ tests/test_template/test_tooling_contracts.py | 43 ++++- typos.local.toml | 2 +- typos.toml | 1 + 29 files changed, 583 insertions(+), 50 deletions(-) rename template/.github/workflows/{coverage-main.yml => coverage-main.yml.jinja} (90%) create mode 100644 template/docs/{% if enable_polonius %}polonius.md{% endif %}.jinja create mode 100644 tests/helpers/tooling_contracts/polonius.py diff --git a/.gitignore b/.gitignore index 40b6baa..2475a88 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__/ # Untracked cache of the estate-wide en-GB-oxendict dictionary. .typos-oxendict-base.json .typos-oxendict-base.toml +.grepai/ diff --git a/README.md b/README.md index 6883468..c4de0b7 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ The template requires **Copier 9.0** or later to avoid incompatibilities. enabled【F:template/Cargo.toml†L1-L9】. - **Pinned toolchain** file specifying a configurable nightly release 【F:template/rust-toolchain.toml.jinja†L1-L3】. +- **Optional Polonius support**, recommended and enabled by default for + applications, with coherent Cargo, Makefile, coverage, release, and agent + guidance that can be disabled for wider library compiler compatibility. - **Project metadata prompts** for repository URL, homepage, crates.io keywords, crates.io categories, nightly date, and optional Linux development target. - **Fast generated tooling** including Cranelift debug code generation, Linux diff --git a/copier.yaml b/copier.yaml index a101d8f..4724fb9 100644 --- a/copier.yaml +++ b/copier.yaml @@ -73,6 +73,14 @@ flavour: default: lib help: 'What type of project? (lib = reusable library, app = executable binary)' +enable_polonius: + type: bool + default: "{{ flavour == 'app' }}" + help: >- + Enable the nightly Polonius borrow checker (-Zpolonius=next)? Recommended + for applications; libraries may disable it to retain wider compiler + compatibility. + codescene_project_id: type: str default: '' diff --git a/docs/developers-guide.md b/docs/developers-guide.md index bc91a57..7599b9d 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -19,9 +19,11 @@ so Python test dependencies must be added to that invocation before tests import them. Keep long runs logged through `tee` into `/tmp`, following the example in `AGENTS.md`. -The tests render both library and application projects, run generated public -gates such as `make all`, validate generated Makefiles with `mbake`, and parse -generated `Cargo.toml` files as TOML. +The tests render both library and application projects with Polonius enabled +and disabled, run generated public gates such as `make all`, validate generated +Makefiles with `mbake`, and parse generated Cargo and workflow configuration. +The Polonius contract checks every `RUSTFLAGS` override, including Linux mold +linking, LLVM coverage, and cross-platform application releases. ## Formatting, Linting, and Type Checking diff --git a/docs/users-guide.md b/docs/users-guide.md index b11c596..4f4c7d9 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -11,6 +11,11 @@ metadata used in the generated `Cargo.toml`: - `flavour` selects `lib` or `app` and determines the generated structure and release metadata. +- `enable_polonius` enables the nightly Polonius borrow checker + (`-Zpolonius=next`). It defaults to enabled for applications, where + borrow-centric internal APIs can evolve with the project, and disabled for + libraries, which commonly need wider compiler compatibility. Either default + can be overridden. - `package_description` becomes `[package].description`. - `repository_url` becomes `[package].repository` and is used by generated app projects for cargo-binstall release URLs. @@ -35,6 +40,13 @@ 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. +When Polonius is enabled, the generated Cargo configuration, Makefile, coverage +workflows, and application release workflow preserve `-Zpolonius=next`. The +Linux target-specific `mold` flags repeat it because Cargo selects target +rustflags instead of merging them with `[build].rustflags`. The generated +project also includes `docs/polonius.md` and matching `AGENTS.md` guidance for +borrow-centric APIs. + 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 diff --git a/template/.cargo/config.toml.jinja b/template/.cargo/config.toml.jinja index b9d65a4..46493cf 100644 --- a/template/.cargo/config.toml.jinja +++ b/template/.cargo/config.toml.jinja @@ -1,3 +1,11 @@ +{% if enable_polonius -%} +# Enable the Polonius alpha borrow-checking analysis by default so Cargo, +# rust-analyzer, and verification tools agree about what borrows are legal. +# An inherited RUSTFLAGS value overrides this table; the generated Makefile and +# workflows therefore re-state -Zpolonius=next whenever they set RUSTFLAGS. +[build] +rustflags = ["-Zpolonius=next"] +{% endif -%} [unstable] codegen-backend = true @@ -7,7 +15,11 @@ codegen-backend = "cranelift" {% if dev_target and 'linux' in dev_target -%} [target.{{ dev_target }}] linker = "clang" -rustflags = ["-C", "link-arg=-fuse-ld=mold"] +{% if enable_polonius -%} +# Cargo selects target rustflags instead of merging them with build.rustflags, +# so this target-specific mold configuration must repeat the Polonius flag. +{% endif -%} +rustflags = [{% if enable_polonius %}"-Zpolonius=next", {% endif %}"-C", "link-arg=-fuse-ld=mold"] {% elif dev_target -%} # mold is Linux-only. For faster linking on {{ dev_target }}, add a # platform-specific block here. Common options include lld via clang on macOS diff --git a/template/.github/workflows/ci.yml.jinja b/template/.github/workflows/ci.yml.jinja index 07cdd37..7a9f371 100644 --- a/template/.github/workflows/ci.yml.jinja +++ b/template/.github/workflows/ci.yml.jinja @@ -95,14 +95,14 @@ jobs: - name: Log coverage linker configuration run: | echo "Coverage linker: clang" - echo "Coverage RUSTFLAGS: -C link-arg=-fuse-ld=lld" + echo "Coverage RUSTFLAGS:{% endraw %}{% if enable_polonius %} -Zpolonius=next{% endif %}{% raw %} -C link-arg=-fuse-ld=lld" echo "Coverage CFLAGS: -fuse-ld=lld" echo "Coverage LDFLAGS: -fuse-ld=lld" - name: Test and Measure Coverage uses: leynos/shared-actions/.github/actions/generate-coverage@18bed1ca49a6de3d8882bd72635a32ae3f023d57 env: CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: clang - RUSTFLAGS: -C link-arg=-fuse-ld=lld + RUSTFLAGS:{% endraw %}{% if enable_polonius %} -Zpolonius=next{% endif %}{% raw %} -C link-arg=-fuse-ld=lld CFLAGS: -fuse-ld=lld LDFLAGS: -fuse-ld=lld with: diff --git a/template/.github/workflows/coverage-main.yml b/template/.github/workflows/coverage-main.yml.jinja similarity index 90% rename from template/.github/workflows/coverage-main.yml rename to template/.github/workflows/coverage-main.yml.jinja index cd28b75..c3ede3f 100644 --- a/template/.github/workflows/coverage-main.yml +++ b/template/.github/workflows/coverage-main.yml.jinja @@ -1,4 +1,4 @@ -name: Coverage (main) +{% raw %}name: Coverage (main) # CodeScene accepts `cs-coverage upload` only for analysed branches, so # main-branch coverage is uploaded here on push; pull requests generate @@ -48,14 +48,14 @@ jobs: - name: Log coverage linker configuration run: | echo "Coverage linker: clang" - echo "Coverage RUSTFLAGS: -C link-arg=-fuse-ld=lld" + echo "Coverage RUSTFLAGS:{% endraw %}{% if enable_polonius %} -Zpolonius=next{% endif %}{% raw %} -C link-arg=-fuse-ld=lld" echo "Coverage CFLAGS: -fuse-ld=lld" echo "Coverage LDFLAGS: -fuse-ld=lld" - name: Test and Measure Coverage uses: leynos/shared-actions/.github/actions/generate-coverage@18bed1ca49a6de3d8882bd72635a32ae3f023d57 env: CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: clang - RUSTFLAGS: -C link-arg=-fuse-ld=lld + RUSTFLAGS:{% endraw %}{% if enable_polonius %} -Zpolonius=next{% endif %}{% raw %} -C link-arg=-fuse-ld=lld CFLAGS: -fuse-ld=lld LDFLAGS: -fuse-ld=lld with: @@ -71,3 +71,4 @@ jobs: format: lcov access-token: ${{ env.CS_ACCESS_TOKEN }} installer-checksum: ${{ vars.CODESCENE_CLI_SHA256 }} +{% endraw %} diff --git a/template/.github/workflows/{% if flavour == 'app' %}release.yml{% endif %}.jinja b/template/.github/workflows/{% if flavour == 'app' %}release.yml{% endif %}.jinja index 27d4050..1fb3654 100644 --- a/template/.github/workflows/{% if flavour == 'app' %}release.yml{% endif %}.jinja +++ b/template/.github/workflows/{% if flavour == 'app' %}release.yml{% endif %}.jinja @@ -47,7 +47,7 @@ jobs: persist-credentials: false - uses: leynos/shared-actions/.github/actions/setup-rust@18bed1ca49a6de3d8882bd72635a32ae3f023d57 with: - toolchain: stable + toolchain: {% endraw %}{% if enable_polonius %}nightly-{{ rust_nightly_date }}{% else %}stable{% endif %}{% raw %} - name: Cache cross binary id: cache-cross uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae @@ -74,13 +74,11 @@ 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. + # Select the generated project's supported compiler explicitly and + # omit repository-local linker and Cranelift configuration. env: - # Build release artifacts without repository-local linker flags, - # including mold rustflags from .cargo/config.toml. - RUSTFLAGS: "" - run: cross +stable build --release --target ${{ matrix.target }} + RUSTFLAGS: {% endraw %}{% if enable_polonius %}-Zpolonius=next{% else %}""{% endif %}{% raw %} + run: cross +{% endraw %}{% if enable_polonius %}nightly-{{ rust_nightly_date }}{% else %}stable{% endif %}{% raw %} build --release --target ${{ matrix.target }} - name: Prepare artifact run: | mkdir -p artifacts/${{ matrix.os }}-${{ matrix.arch }} diff --git a/template/AGENTS.md.jinja b/template/AGENTS.md.jinja index 857e247..e1c195f 100644 --- a/template/AGENTS.md.jinja +++ b/template/AGENTS.md.jinja @@ -133,6 +133,25 @@ This repository is written in Rust and uses Cargo for building and dependency management. Contributors should follow these best practices when working on the project: +{% if enable_polonius -%} +### Borrow checker: Polonius, not NLL + +This repository compiles with the Polonius alpha analysis +(`-Zpolonius=next`) on the dated nightly pinned in `rust-toolchain.toml`. +Internal APIs should be borrow-centric: lookups and get-or-create accessors +return references, keys are cloned only on insertion, and error context is +built lazily. See `docs/polonius.md` for the full contract. + +- Never rewrite a site tagged `POLONIUS(...)` into a double lookup, an + `entry(key.clone())` call, id or index indirection, or eager error context. +- Do not add defensive clones merely to end a borrow without first compiling + the natural borrow-returning form under the project toolchain. +- Respect `POLONIUS-REFUSED(...)` tags. Polonius does not relax aliasing, + suspension-point, lock-boundary, or thread-boundary constraints. +- Verify new borrow-sensitive code with the project toolchain and classify it + with and without `-Zpolonius=next` as described in `docs/polonius.md`. + +{% endif -%} - Run `make check-fmt`, `make lint`, and `make test` before committing. These targets wrap the following commands, so contributors understand the exact behaviour and policy enforced: diff --git a/template/Makefile.jinja b/template/Makefile.jinja index 81e0a90..1011128 100644 --- a/template/Makefile.jinja +++ b/template/Makefile.jinja @@ -11,8 +11,21 @@ USER_WHITAKER := $(HOME)/.local/bin/whitaker USER_BIN_PATH := $(HOME)/.cargo/bin:$(HOME)/.local/bin:$(HOME)/.bun/bin CARGO ?= cargo BUILD_JOBS ?= +{% if enable_polonius -%} +# RUSTFLAGS overrides .cargo/config.toml, so recipes that set it must re-state +# the Polonius flag explicitly. +POLONIUS_FLAGS ?= -Zpolonius=next +{% else -%} +POLONIUS_FLAGS ?= +{% endif -%} RUST_FLAGS ?= RUST_FLAGS := -D warnings $(RUST_FLAGS) +{% if dev_target and 'linux' in dev_target -%} +DEV_LINKER_FLAGS ?= $(if $(filter Linux,$(shell uname -s)),-C link-arg=-fuse-ld=mold) +{% else -%} +DEV_LINKER_FLAGS ?= +{% endif -%} +DEV_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) $(DEV_LINKER_FLAGS) RUSTDOC_FLAGS ?= RUSTDOC_FLAGS := -D warnings $(RUSTDOC_FLAGS) CARGO_FLAGS ?= --all-targets --all-features @@ -20,7 +33,7 @@ CLIPPY_FLAGS ?= $(CARGO_FLAGS) -- $(RUST_FLAGS) TEST_FLAGS ?= $(CARGO_FLAGS) TEST_CMD := $(if $(shell $(CARGO) nextest --version 2>/dev/null),nextest run,test) COVERAGE_LINKER_FLAGS ?= -fuse-ld=lld -COVERAGE_RUST_FLAGS ?= $(RUST_FLAGS) -C link-arg=$(COVERAGE_LINKER_FLAGS) +COVERAGE_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) -C link-arg=$(COVERAGE_LINKER_FLAGS) MDLINT ?= markdownlint-cli2 NIXIE ?= nixie TYPOS_VERSION ?= 1.48.0 @@ -38,11 +51,11 @@ clean: ## Remove build artifacts rm -f .typos-oxendict-base.json .typos-oxendict-base.toml 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="$(DEV_RUST_FLAGS)" $(CARGO) $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) + RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) test --doc --workspace --all-features target/%/$(TARGET): ## Build binary in debug or release mode - $(CARGO) build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release){% if flavour == 'app' %} --bin $(TARGET){% endif %} + RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release){% if flavour == 'app' %} --bin $(TARGET){% endif %} coverage: ## Generate lcov coverage with lld for llvm-tools compatibility @echo "coverage linker flags: $(COVERAGE_LINKER_FLAGS)" @@ -53,13 +66,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)" RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) doc --no-deps + RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) clippy $(CLIPPY_FLAGS) @echo "Whitaker binary: $(WHITAKER)" - PATH="$(USER_BIN_PATH):$(PATH)" RUSTFLAGS="$(RUST_FLAGS)" $(WHITAKER) --all -- $(CARGO_FLAGS) + PATH="$(USER_BIN_PATH):$(PATH)" RUSTFLAGS="$(DEV_RUST_FLAGS)" $(WHITAKER) --all -- $(CARGO_FLAGS) typecheck: ## Type-check without building - RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) check $(CARGO_FLAGS) + RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) check $(CARGO_FLAGS) fmt: ## Format Rust and Markdown sources $(CARGO) +nightly fmt --all diff --git a/template/README.md.jinja b/template/README.md.jinja index d3b637a..384d596 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -2,6 +2,12 @@ This is a generated project using [Copier](https://copier.readthedocs.io/). +{% if enable_polonius -%} +This project uses the Polonius alpha borrow checker (`-Zpolonius=next`) on the +dated nightly pinned in `rust-toolchain.toml`. See +[the Polonius policy](docs/polonius.md) for the compiler and API contract. + +{% endif -%} ## Documentation - [Documentation contents](docs/contents.md) diff --git a/template/docs/contents.md.jinja b/template/docs/contents.md.jinja index 6e036bc..220b916 100644 --- a/template/docs/contents.md.jinja +++ b/template/docs/contents.md.jinja @@ -14,7 +14,10 @@ documentation set. - [Documentation style guide](documentation-style-guide.md) defines the spelling, structure, Markdown, Architecture Decision Record (ADR), Request for Comments (RFC), and roadmap conventions used by this documentation set. - +{% if enable_polonius -%} +- [Polonius borrow-checker policy](polonius.md) records the nightly compiler + contract, borrow-centric design rules, and audit tags used by this project. +{% endif %} ## Rust reference material - [Reliable testing in Rust via dependency injection](reliable-testing-in-rust-via-dependency-injection.md) diff --git a/template/docs/developers-guide.md.jinja b/template/docs/developers-guide.md.jinja index 2056c55..ebb9743 100644 --- a/template/docs/developers-guide.md.jinja +++ b/template/docs/developers-guide.md.jinja @@ -34,6 +34,17 @@ mutants into new tests. ## Tooling +{% if enable_polonius -%} +### Polonius borrow checker + +This project compiles with the Polonius alpha analysis +(`-Zpolonius=next`) on the dated nightly pinned in `rust-toolchain.toml`. +`.cargo/config.toml` supplies the flag by default; Makefile recipes and +workflows that set `RUSTFLAGS` must re-state it because the environment value +overrides Cargo configuration. See [the Polonius policy](polonius.md) for the +borrow-centric API and audit-tag conventions. + +{% endif -%} 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 diff --git a/template/docs/repository-layout.md.jinja b/template/docs/repository-layout.md.jinja index 2cc430b..3bccf54 100644 --- a/template/docs/repository-layout.md.jinja +++ b/template/docs/repository-layout.md.jinja @@ -25,6 +25,9 @@ compact and omits build output such as `target/`. ├── docs/ │ ├── contents.md │ ├── developers-guide.md +{% if enable_polonius %} +│ ├── polonius.md +{% endif %} │ ├── repository-layout.md │ ├── users-guide.md │ └── ... @@ -70,6 +73,10 @@ compact and omits build output such as `target/`. public build and test commands. - `docs/developers-guide.md`: Explains the contributor workflow and local tooling used to work on the generated project. +{% if enable_polonius %} +- `docs/polonius.md`: Records the Polonius toolchain contract, borrow-centric + design guidance, and audited borrow-checker classifications. +{% endif %} - `docs/repository-layout.md`: Documents the repository tree and path responsibilities. {% if flavour == 'app' %} diff --git a/template/docs/users-guide.md.jinja b/template/docs/users-guide.md.jinja index 5e06f5f..f976763 100644 --- a/template/docs/users-guide.md.jinja +++ b/template/docs/users-guide.md.jinja @@ -10,6 +10,15 @@ 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. +{% if enable_polonius -%} +This project enables the Polonius alpha borrow checker +(`-Zpolonius=next`) on its dated nightly toolchain. The checked-in Cargo +configuration supplies the flag to normal builds and editor tooling, while the +Makefile and workflows re-state it wherever they override `RUSTFLAGS`. See the +[Polonius policy](polonius.md) before changing the toolchain or borrow-centric +APIs. + +{% endif -%} 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 diff --git a/template/docs/{% if enable_polonius %}polonius.md{% endif %}.jinja b/template/docs/{% if enable_polonius %}polonius.md{% endif %}.jinja new file mode 100644 index 0000000..51cdea8 --- /dev/null +++ b/template/docs/{% if enable_polonius %}polonius.md{% endif %}.jinja @@ -0,0 +1,68 @@ +# Polonius borrow-checker policy + +This project enables the Polonius alpha borrow-checking analysis +(`-Zpolonius=next`) on the dated nightly pinned in `rust-toolchain.toml`. +Polonius accepts borrow-returning control-flow patterns that non-lexical +lifetimes (NLL) reject, allowing internal APIs to model borrowing directly. + +## Compiler contract + +`.cargo/config.toml` enables Polonius for normal Cargo commands and +rust-analyzer. An inherited `RUSTFLAGS` value overrides that configuration, so +the Makefile and GitHub Actions workflows re-state `-Zpolonius=next` whenever +they set `RUSTFLAGS`. Release builds use the same pinned nightly and flag. + +Source builds performed outside this repository, including registry installs, +do not inherit its toolchain or Cargo configuration. If this crate later ships +Polonius-only source through a registry, its installation instructions must +name both the pinned nightly and `RUSTFLAGS=-Zpolonius=next`, or direct users to +pre-built artefacts. + +## Borrow-centric design + +- Prefer lookup and get-or-create APIs that return references. +- Clone keys only on insertion, not on successful lookup. +- Build owned error context only in the failure branch where it escapes. +- Reserve ids and indexes for persistent or cross-boundary identity, not as + substitutes for references. +- Remember that Polonius changes lifetime analysis, not aliasing: simultaneous + mutable borrows, borrows across suspension points, lock-guard lifetimes, and + thread boundaries remain real constraints. + +## Audit tags + +Use one of these greppable tags when a borrow-sensitive design needs its +classification preserved: + +- `POLONIUS(case-3)` marks code verified to require the Polonius analysis. +- `POLONIUS-CANDIDATE(pattern)` marks an NLL workaround awaiting a verified + borrow-centric rewrite. +- `POLONIUS-REFUSED(constraint)` records why an owned form remains necessary, + such as `aliasing`, `suspension-point`, `lock-boundary`, or `id-is-data`. + +Record verified sites below so later reviews start from evidence rather than +re-running the same analysis. + +| Site | Classification | Verified nightly | Notes | +| --- | --- | --- | --- | +| None yet | Initial generated project | `nightly-{{ rust_nightly_date }}` | Add rows as borrow-sensitive APIs are introduced. | + +## Verification + +For each borrow-sensitive change, first run the supported configuration: + +```sh +make typecheck +make test +``` + +Then classify the changed form under plain NLL by overriding the checked-in +flag deliberately: + +```sh +RUSTFLAGS= cargo +nightly-{{ rust_nightly_date }} check --all-targets --all-features +``` + +A no-flag failure establishes that the design genuinely requires Polonius; it +does not make the supported build invalid. Run the complete behavioural suite +under the normal Polonius-enabled configuration. diff --git a/template/rust-toolchain.toml.jinja b/template/rust-toolchain.toml.jinja index 67d29a9..47f2701 100644 --- a/template/rust-toolchain.toml.jinja +++ b/template/rust-toolchain.toml.jinja @@ -1,3 +1,7 @@ +{% if enable_polonius -%} +# This project enables the nightly-only Polonius alpha borrow checker. Keep the +# dated pin reproducible and review docs/polonius.md before changing it. +{% endif -%} [toolchain] channel = "nightly-{{ rust_nightly_date }}" components = [ diff --git a/template/typos.local.toml b/template/typos.local.toml index 2a71820..4c30bfa 100644 --- a/template/typos.local.toml +++ b/template/typos.local.toml @@ -6,7 +6,7 @@ schema = 1 stems = [] [words] -accepted = ["Flavored", "mold"] +accepted = ["Flavored", "mold", "Polonius"] [words.corrections] diff --git a/template/typos.toml b/template/typos.toml index 0d8fdd7..8ea254d 100644 --- a/template/typos.toml +++ b/template/typos.toml @@ -6,6 +6,7 @@ extend-exclude = [ ".git", ".hypothesis", ".pytest_cache", + ".terraform", ".tox", ".typos-oxendict-base.json", ".typos-oxendict-base.toml", @@ -31,11 +32,14 @@ locale = "en-gb" extend-ignore-re = [ "(?s)```.*?```", "Center \\| Microsoft Learn", + "\\brust-analyzer\\b", "`[^`\\n]+`", ] [default.extend-words] +"ASO" = "ASO" "Flavored" = "Flavored" +"Polonius" = "Polonius" "absolutisable" = "absolutizable" "absolutisation" = "absolutization" "absolutisations" = "absolutizations" @@ -594,6 +598,8 @@ extend-ignore-re = [ "desynchronizers" = "desynchronizers" "desynchronizes" = "desynchronizes" "desynchronizing" = "desynchronizing" +"dialog" = "dialog" +"dialogs" = "dialogs" "dockerisable" = "dockerizable" "dockerisation" = "dockerization" "dockerisations" = "dockerizations" @@ -828,6 +834,7 @@ extend-ignore-re = [ "globalizers" = "globalizers" "globalizes" = "globalizes" "globalizing" = "globalizing" +"handwritten" = "handwritten" "harmonisable" = "harmonizable" "harmonisation" = "harmonization" "harmonisations" = "harmonizations" @@ -1008,6 +1015,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" @@ -1497,6 +1522,7 @@ extend-ignore-re = [ "ordinalizing" = "ordinalizing" "organisable" = "organizable" "organisation" = "organization" +"organisational" = "organizational" "organisations" = "organizations" "organise" = "organize" "organised" = "organized" @@ -1506,6 +1532,7 @@ extend-ignore-re = [ "organising" = "organizing" "organizable" = "organizable" "organization" = "organization" +"organizational" = "organizational" "organizations" = "organizations" "organize" = "organize" "organized" = "organized" @@ -1514,6 +1541,24 @@ extend-ignore-re = [ "organizes" = "organizes" "organizing" = "organizing" "oxendict" = "oxendict" +"oxidisable" = "oxidizable" +"oxidisation" = "oxidization" +"oxidisations" = "oxidizations" +"oxidise" = "oxidize" +"oxidised" = "oxidized" +"oxidiser" = "oxidizer" +"oxidisers" = "oxidizers" +"oxidises" = "oxidizes" +"oxidising" = "oxidizing" +"oxidizable" = "oxidizable" +"oxidization" = "oxidization" +"oxidizations" = "oxidizations" +"oxidize" = "oxidize" +"oxidized" = "oxidized" +"oxidizer" = "oxidizer" +"oxidizers" = "oxidizers" +"oxidizes" = "oxidizes" +"oxidizing" = "oxidizing" "palettisable" = "palettizable" "palettisation" = "palettization" "palettisations" = "palettizations" @@ -1658,6 +1703,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" @@ -2414,6 +2477,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" diff --git a/tests/helpers/rendering.py b/tests/helpers/rendering.py index 5f6f6b2..d339c10 100644 --- a/tests/helpers/rendering.py +++ b/tests/helpers/rendering.py @@ -19,11 +19,12 @@ def render_project( project_name: str, package_name: str, flavour: str = LIB, + enable_polonius: bool | None = None, license_year: int | None = 2026, dev_target: str = "x86_64-unknown-linux-gnu", ) -> CopierProject: """Render a generated Rust project with publishable metadata.""" - answers: dict[str, str | int] = { + answers: dict[str, str | int | bool] = { "project_name": project_name, "package_name": package_name, "package_description": f"{project_name} package used by template tests.", @@ -36,6 +37,8 @@ def render_project( "flavour": flavour, "dev_target": dev_target, } + if enable_polonius is not None: + answers["enable_polonius"] = enable_polonius if license_year is not None: answers["license_year"] = license_year diff --git a/tests/helpers/tooling_contracts/__init__.py b/tests/helpers/tooling_contracts/__init__.py index 9a86e71..cac977d 100644 --- a/tests/helpers/tooling_contracts/__init__.py +++ b/tests/helpers/tooling_contracts/__init__.py @@ -8,6 +8,9 @@ from tests.helpers.tooling_contracts.orchestration import ( assert_generated_tooling_contracts, ) +from tests.helpers.tooling_contracts.polonius import ( + assert_polonius_toolchain_contracts, +) from tests.helpers.tooling_contracts.workflows import ( assert_ci_coverage_action_contract, assert_coverage_main_workflow_contract, @@ -19,5 +22,6 @@ "assert_coverage_main_workflow_contract", "assert_documentation_navigation_contracts", "assert_generated_tooling_contracts", + "assert_polonius_toolchain_contracts", "extract_checkout_steps", ] diff --git a/tests/helpers/tooling_contracts/polonius.py b/tests/helpers/tooling_contracts/polonius.py new file mode 100644 index 0000000..2dcd15a --- /dev/null +++ b/tests/helpers/tooling_contracts/polonius.py @@ -0,0 +1,177 @@ +"""Assert rendered Polonius toolchain and documentation contracts.""" + +from __future__ import annotations + +import tomllib +from typing import Any + +from tests.helpers.generated_files import ( + parse_yaml_mapping, + require_mapping, + require_sequence, +) + +POLONIUS_FLAG = "-Zpolonius=next" + + +def _named_step(workflow: str, job_name: str, step_name: str) -> dict[str, Any]: + """Return one named step from a rendered workflow.""" + parsed = parse_yaml_mapping(workflow, f"{job_name} workflow") + jobs = require_mapping(parsed, "jobs", f"{job_name} workflow") + job = require_mapping(jobs, job_name, f"{job_name} workflow jobs") + steps = require_sequence(job, "steps", f"{job_name} workflow job") + matches = [ + step + for step in steps + if isinstance(step, dict) and step.get("name") == step_name + ] + assert len(matches) == 1, f"expected one {step_name!r} step in {job_name}" + return matches[0] + + +def _setup_rust_step(release_workflow: str) -> dict[str, Any]: + """Return the setup-rust step from the release workflow.""" + parsed = parse_yaml_mapping(release_workflow, "release workflow") + jobs = require_mapping(parsed, "jobs", "release workflow") + build = require_mapping(jobs, "build", "release workflow jobs") + steps = require_sequence(build, "steps", "release build job") + matches = [ + step + for step in steps + if isinstance(step, dict) and "actions/setup-rust@" in str(step.get("uses")) + ] + assert len(matches) == 1, "expected one setup-rust step in release workflow" + return matches[0] + + +def _assert_cargo_config(cargo_config: str, *, enabled: bool, dev_target: str) -> None: + """Assert Cargo's build and target rustflags retain Polonius as required.""" + config = tomllib.loads(cargo_config) + build_flags = config.get("build", {}).get("rustflags", []) + assert (POLONIUS_FLAG in build_flags) is enabled, ( + "expected build.rustflags Polonius state to match the Copier answer" + ) + if "linux" not in dev_target: + return + target_flags = config["target"][dev_target]["rustflags"] + assert (POLONIUS_FLAG in target_flags) is enabled, ( + "target rustflags override build.rustflags and must preserve the " + "selected Polonius state" + ) + + +def _assert_makefile(makefile: str, *, enabled: bool, dev_target: str) -> None: + """Assert every generated compile path preserves the selected flag.""" + expected_default = ( + f"POLONIUS_FLAGS ?= {POLONIUS_FLAG}" if enabled else "POLONIUS_FLAGS ?=" + ) + assert expected_default in makefile + assert ( + "COVERAGE_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) " + "-C link-arg=$(COVERAGE_LINKER_FLAGS)" + ) in makefile + assert ( + "DEV_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) $(DEV_LINKER_FLAGS)" + in makefile + ) + linker_default = next( + line for line in makefile.splitlines() if line.startswith("DEV_LINKER_FLAGS ?=") + ) + if "linux" in dev_target: + assert "$(filter Linux,$(shell uname -s))" in linker_default + assert "-fuse-ld=mold" in linker_default + else: + assert linker_default == "DEV_LINKER_FLAGS ?=" + compile_lines = [ + line + for line in makefile.splitlines() + if "RUSTFLAGS=" in line and ("$(CARGO)" in line or "$(WHITAKER)" in line) + ] + assert compile_lines, "expected generated compile recipes to set RUSTFLAGS" + assert all( + "$(DEV_RUST_FLAGS)" in line or "$(COVERAGE_RUST_FLAGS)" in line + for line in compile_lines + ), f"compile recipes must use composed Rust flags: {compile_lines!r}" + + +def _assert_coverage_workflow(workflow: str, job_name: str, *, enabled: bool) -> None: + """Assert coverage's explicit RUSTFLAGS do not shadow Polonius.""" + coverage = _named_step(workflow, job_name, "Test and Measure Coverage") + env = require_mapping(coverage, "env", "coverage step") + rustflags = str(env.get("RUSTFLAGS", "")) + assert "-C link-arg=-fuse-ld=lld" in rustflags + assert (POLONIUS_FLAG in rustflags) is enabled + + +def _assert_release_workflow(release_workflow: str, *, enabled: bool) -> None: + """Assert release artefacts use a compiler compatible with the source.""" + setup = _setup_rust_step(release_workflow) + setup_inputs = require_mapping(setup, "with", "release setup-rust step") + toolchain = str(setup_inputs.get("toolchain", "")) + build = _named_step(release_workflow, "build", "Build release binary") + env = require_mapping(build, "env", "release build step") + rustflags = str(env.get("RUSTFLAGS", "")) + command = str(build.get("run", "")) + if enabled: + assert toolchain.startswith("nightly-20") + assert rustflags == POLONIUS_FLAG + assert f"cross +{toolchain} build" in command + else: + assert toolchain == "stable" + assert rustflags == "" + assert "cross +stable build" in command + + +def assert_polonius_toolchain_contracts( + *, + enabled: bool, + dev_target: str, + cargo_config: str, + makefile: str, + rust_toolchain: str, + ci_workflow: str, + coverage_main_workflow: str, + release_workflow: str | None, + agents: str, + readme: str, + docs_contents: str, + repository_layout: str, + developers_guide: str, + users_guide: str, + polonius_doc: str | None, +) -> None: + """Assert the selected Polonius state across generated project surfaces.""" + _assert_cargo_config(cargo_config, enabled=enabled, dev_target=dev_target) + _assert_makefile(makefile, enabled=enabled, dev_target=dev_target) + _assert_coverage_workflow(ci_workflow, "build-test", enabled=enabled) + _assert_coverage_workflow( + coverage_main_workflow, "coverage-upload", enabled=enabled + ) + if release_workflow is not None: + _assert_release_workflow(release_workflow, enabled=enabled) + + if enabled: + assert "Polonius alpha" in rust_toolchain + assert polonius_doc is not None + assert POLONIUS_FLAG in polonius_doc + for surface in ( + agents, + readme, + docs_contents, + repository_layout, + developers_guide, + users_guide, + ): + assert "Polonius" in surface + else: + assert "Polonius" not in rust_toolchain + assert polonius_doc is None + for surface in ( + agents, + readme, + docs_contents, + repository_layout, + developers_guide, + users_guide, + ): + assert "Polonius" not in surface diff --git a/tests/test_template/__snapshots__/test_snapshots.ambr b/tests/test_template/__snapshots__/test_snapshots.ambr index 098b2aa..009f8fc 100644 --- a/tests/test_template/__snapshots__/test_snapshots.ambr +++ b/tests/test_template/__snapshots__/test_snapshots.ambr @@ -108,6 +108,12 @@ }), }), 'cargo_config': ''' + # Enable the Polonius alpha borrow-checking analysis by default so Cargo, + # rust-analyzer, and verification tools agree about what borrows are legal. + # An inherited RUSTFLAGS value overrides this table; the generated Makefile and + # workflows therefore re-state -Zpolonius=next whenever they set RUSTFLAGS. + [build] + rustflags = ["-Zpolonius=next"] [unstable] codegen-backend = true @@ -116,7 +122,9 @@ [target.x86_64-unknown-linux-gnu] linker = "clang" - rustflags = ["-C", "link-arg=-fuse-ld=mold"] + # Cargo selects target rustflags instead of merging them with build.rustflags, + # so this target-specific mold configuration must repeat the Polonius flag. + rustflags = ["-Zpolonius=next", "-C", "link-arg=-fuse-ld=mold"] ''', 'ci_workflow': dict({ @@ -249,7 +257,7 @@ 'name': 'Log coverage linker configuration', 'run': ''' echo "Coverage linker: clang" - echo "Coverage RUSTFLAGS: -C link-arg=-fuse-ld=lld" + echo "Coverage RUSTFLAGS: -Zpolonius=next -C link-arg=-fuse-ld=lld" echo "Coverage CFLAGS: -fuse-ld=lld" echo "Coverage LDFLAGS: -fuse-ld=lld" @@ -260,7 +268,7 @@ 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER': 'clang', 'CFLAGS': '-fuse-ld=lld', 'LDFLAGS': '-fuse-ld=lld', - 'RUSTFLAGS': '-C link-arg=-fuse-ld=lld', + 'RUSTFLAGS': '-Zpolonius=next -C link-arg=-fuse-ld=lld', }), 'name': 'Test and Measure Coverage', 'uses': 'leynos/shared-actions/.github/actions/generate-coverage@', @@ -330,7 +338,7 @@ 'name': 'Log coverage linker configuration', 'run': ''' echo "Coverage linker: clang" - echo "Coverage RUSTFLAGS: -C link-arg=-fuse-ld=lld" + echo "Coverage RUSTFLAGS: -Zpolonius=next -C link-arg=-fuse-ld=lld" echo "Coverage CFLAGS: -fuse-ld=lld" echo "Coverage LDFLAGS: -fuse-ld=lld" @@ -341,7 +349,7 @@ 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER': 'clang', 'CFLAGS': '-fuse-ld=lld', 'LDFLAGS': '-fuse-ld=lld', - 'RUSTFLAGS': '-C link-arg=-fuse-ld=lld', + 'RUSTFLAGS': '-Zpolonius=next -C link-arg=-fuse-ld=lld', }), 'name': 'Test and Measure Coverage', 'uses': 'leynos/shared-actions/.github/actions/generate-coverage@', @@ -389,8 +397,13 @@ USER_BIN_PATH := $(HOME)/.cargo/bin:$(HOME)/.local/bin:$(HOME)/.bun/bin CARGO ?= cargo BUILD_JOBS ?= + # RUSTFLAGS overrides .cargo/config.toml, so recipes that set it must re-state + # the Polonius flag explicitly. + POLONIUS_FLAGS ?= -Zpolonius=next RUST_FLAGS ?= RUST_FLAGS := -D warnings $(RUST_FLAGS) + DEV_LINKER_FLAGS ?= $(if $(filter Linux,$(shell uname -s)),-C link-arg=-fuse-ld=mold) + DEV_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) $(DEV_LINKER_FLAGS) RUSTDOC_FLAGS ?= RUSTDOC_FLAGS := -D warnings $(RUSTDOC_FLAGS) CARGO_FLAGS ?= --all-targets --all-features @@ -398,7 +411,7 @@ TEST_FLAGS ?= $(CARGO_FLAGS) TEST_CMD := $(if $(shell $(CARGO) nextest --version 2>/dev/null),nextest run,test) COVERAGE_LINKER_FLAGS ?= -fuse-ld=lld - COVERAGE_RUST_FLAGS ?= $(RUST_FLAGS) -C link-arg=$(COVERAGE_LINKER_FLAGS) + COVERAGE_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) -C link-arg=$(COVERAGE_LINKER_FLAGS) MDLINT ?= markdownlint-cli2 NIXIE ?= nixie TYPOS_VERSION ?= 1.48.0 @@ -416,11 +429,11 @@ \trm -f .typos-oxendict-base.json .typos-oxendict-base.toml test: ## Run tests with warnings treated as errors - \tRUSTFLAGS="$(RUST_FLAGS)" $(CARGO) $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) - \tRUSTFLAGS="$(RUST_FLAGS)" $(CARGO) test --doc --workspace --all-features + \tRUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) + \tRUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) test --doc --workspace --all-features target/%/$(TARGET): ## Build binary in debug or release mode - \t$(CARGO) build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release) --bin $(TARGET) + \tRUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release) --bin $(TARGET) coverage: ## Generate lcov coverage with lld for llvm-tools compatibility \t@echo "coverage linker flags: $(COVERAGE_LINKER_FLAGS)" @@ -431,13 +444,13 @@ \t\t$(CARGO) llvm-cov --lcov --output-path lcov.info $(TEST_FLAGS) lint: ## Run Clippy with warnings denied - \tRUSTDOCFLAGS="$(RUSTDOC_FLAGS)" $(CARGO) doc --no-deps - \t$(CARGO) clippy $(CLIPPY_FLAGS) + \tRUSTDOCFLAGS="$(RUSTDOC_FLAGS)" RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) doc --no-deps + \tRUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) clippy $(CLIPPY_FLAGS) \t@echo "Whitaker binary: $(WHITAKER)" - \tPATH="$(USER_BIN_PATH):$(PATH)" RUSTFLAGS="$(RUST_FLAGS)" $(WHITAKER) --all -- $(CARGO_FLAGS) + \tPATH="$(USER_BIN_PATH):$(PATH)" RUSTFLAGS="$(DEV_RUST_FLAGS)" $(WHITAKER) --all -- $(CARGO_FLAGS) typecheck: ## Type-check without building - \tRUSTFLAGS="$(RUST_FLAGS)" $(CARGO) check $(CARGO_FLAGS) + \tRUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) check $(CARGO_FLAGS) fmt: ## Format Rust and Markdown sources \t$(CARGO) +nightly fmt --all @@ -538,7 +551,7 @@ dict({ 'uses': 'leynos/shared-actions/.github/actions/setup-rust@', 'with': dict({ - 'toolchain': 'stable', + 'toolchain': 'nightly-2025-06-10', }), }), dict({ @@ -581,10 +594,10 @@ }), dict({ 'env': dict({ - 'RUSTFLAGS': '', + 'RUSTFLAGS': '-Zpolonius=next', }), 'name': 'Build release binary', - 'run': 'cross +stable build --release --target ${{ matrix.target }}', + 'run': 'cross +nightly-2025-06-10 build --release --target ${{ matrix.target }}', }), dict({ 'name': 'Prepare artifact', diff --git a/tests/test_template/test_basic_rendering.py b/tests/test_template/test_basic_rendering.py index b53f24d..8ff884c 100644 --- a/tests/test_template/test_basic_rendering.py +++ b/tests/test_template/test_basic_rendering.py @@ -41,6 +41,12 @@ def test_template_renders_app_flavour(tmp_path: Path, copier: CopierFixture) -> assert (project / ".github" / "workflows" / "release.yml").exists(), ( "expected release workflow to exist for app flavour" ) + assert "-Zpolonius=next" in (project / ".cargo/config.toml").read_text(), ( + "expected app flavour to enable recommended Polonius support by default" + ) + assert (project / "docs/polonius.md").exists(), ( + "expected app flavour to document default Polonius support" + ) project.run("make all") @@ -59,4 +65,10 @@ def test_template_renders_lib_flavour(tmp_path: Path, copier: CopierFixture) -> assert not (project / ".github" / "workflows" / "release.yml").exists(), ( "expected release workflow to be omitted for lib flavour" ) + assert "-Zpolonius=next" not in (project / ".cargo/config.toml").read_text(), ( + "expected lib flavour to retain wider compiler compatibility by default" + ) + assert not (project / "docs/polonius.md").exists(), ( + "expected lib flavour to omit Polonius policy by default" + ) project.run("make all") diff --git a/tests/test_template/test_compilation.py b/tests/test_template/test_compilation.py index 20f4f29..7e8a540 100644 --- a/tests/test_template/test_compilation.py +++ b/tests/test_template/test_compilation.py @@ -21,3 +21,37 @@ def test_template_compiles(tmp_path: Path, copier: CopierFixture, flavour: str) flavour=flavour, ) project.run("cargo check --all-targets --all-features") + + +def test_polonius_project_accepts_single_lookup_get_or_insert( + tmp_path: Path, copier: CopierFixture +) -> None: + """Polonius-enabled public typecheck accepts a borrow-returning accessor.""" + project = render_project( + tmp_path, + copier, + project_name="PoloniusExample", + package_name="polonius_example", + flavour=LIB, + enable_polonius=True, + ) + (project / "src/lib.rs").write_text( + """//! Borrow-centric Polonius compilation fixture. + +use std::collections::HashMap; + +/// Return the existing value or insert its default with one hit-path lookup. +pub fn get_or_insert<'values>( + values: &'values mut HashMap, + key: &str, +) -> &'values mut u8 { + if let Some(value) = values.get_mut(key) { + return value; + } + values.entry(key.to_owned()).or_default() +} +""", + encoding="utf-8", + ) + + project.run("make typecheck") diff --git a/tests/test_template/test_tooling_contracts.py b/tests/test_template/test_tooling_contracts.py index 7bb4066..66abe2f 100644 --- a/tests/test_template/test_tooling_contracts.py +++ b/tests/test_template/test_tooling_contracts.py @@ -19,20 +19,26 @@ from tests.helpers.tooling_contracts import ( assert_coverage_main_workflow_contract, assert_generated_tooling_contracts, + assert_polonius_toolchain_contracts, ) from tests.helpers.tooling_contracts.workflows import _assert_ci_workflow_contracts @pytest.mark.parametrize( - ("flavour", "dev_target"), + ("flavour", "dev_target", "enable_polonius"), [ - (LIB, "x86_64-unknown-linux-gnu"), - (APP, "x86_64-unknown-linux-gnu"), - (LIB, "aarch64-apple-darwin"), + (LIB, "x86_64-unknown-linux-gnu", False), + (APP, "x86_64-unknown-linux-gnu", True), + (LIB, "aarch64-apple-darwin", True), + (APP, "x86_64-unknown-linux-gnu", False), ], ) def test_generated_tooling_contracts( - tmp_path: Path, copier: CopierFixture, flavour: str, dev_target: str + tmp_path: Path, + copier: CopierFixture, + flavour: str, + dev_target: str, + enable_polonius: bool, ) -> None: """Generated projects include the requested Rust tooling contracts.""" project = render_project( @@ -41,6 +47,7 @@ def test_generated_tooling_contracts( project_name="ToolingExample", package_name="tooling_example", flavour=flavour, + enable_polonius=enable_polonius, dev_target=dev_target, ) @@ -63,8 +70,11 @@ def test_generated_tooling_contracts( project / ".github/workflows/mutation-testing.yml" ) docs_contents = read_generated_text(project / "docs/contents.md") + developers_guide = read_generated_text(project / "docs/developers-guide.md") repository_layout = read_generated_text(project / "docs/repository-layout.md") readme = read_generated_text(project / "README.md") + users_guide = read_generated_text(project / "docs/users-guide.md") + agents = read_generated_text(project / "AGENTS.md") rust_toolchain = read_generated_text(project / "rust-toolchain.toml") test_stub = read_generated_text(project / "tests/stub.rs") typos_config = read_generated_text(project / "typos.toml") @@ -80,6 +90,10 @@ def test_generated_tooling_contracts( if flavour == APP else None ) + polonius_path = project / "docs/polonius.md" + polonius_doc = ( + read_generated_text(polonius_path) if polonius_path.exists() else None + ) assert_generated_tooling_contracts( package=package, metadata=metadata, @@ -100,8 +114,25 @@ def test_generated_tooling_contracts( release_workflow=release_workflow, ) assert_coverage_main_workflow_contract(coverage_main_workflow) + assert_polonius_toolchain_contracts( + enabled=enable_polonius, + dev_target=dev_target, + cargo_config=cargo_config, + makefile=makefile, + rust_toolchain=rust_toolchain, + ci_workflow=ci_workflow, + coverage_main_workflow=coverage_main_workflow, + release_workflow=release_workflow, + agents=agents, + readme=readme, + docs_contents=docs_contents, + repository_layout=repository_layout, + developers_guide=developers_guide, + users_guide=users_guide, + polonius_doc=polonius_doc, + ) assert '[default]\nlocale = "en-gb"' in typos_config - assert 'accepted = ["Flavored", "mold"]' in typos_overlay + assert 'accepted = ["Flavored", "mold", "Polonius"]' in typos_overlay assert "DEFAULT_BASE_URL" in spelling_generator assert "_local_cache_is_current" in spelling_core diff --git a/typos.local.toml b/typos.local.toml index 2c785f7..3196d69 100644 --- a/typos.local.toml +++ b/typos.local.toml @@ -6,7 +6,7 @@ schema = 1 stems = [] [words] -accepted = ["Flavored", "mold"] +accepted = ["Flavored", "mold", "Polonius"] [words.corrections] diff --git a/typos.toml b/typos.toml index de5cdf2..c231e6c 100644 --- a/typos.toml +++ b/typos.toml @@ -41,6 +41,7 @@ extend-ignore-re = [ [default.extend-words] "ASO" = "ASO" "Flavored" = "Flavored" +"Polonius" = "Polonius" "absolutisable" = "absolutizable" "absolutisation" = "absolutization" "absolutisations" = "absolutizations" From d17cb5f79366d1fbaf4cf1ce5efaa5f09eff3272 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 2 Aug 2026 12:30:19 +0200 Subject: [PATCH 2/5] Strengthen Polonius review contracts Bind release workflows to the exact generated toolchain channel and add a render-only property test over the complete flavour, option, and target class matrix. Document the helper contracts and record the optional Polonius decision, risks, progress, and outcome in ADR-004 and the completed tooling plan. --- docs/adr-004-optional-polonius-support.md | 55 ++++++++++++++++ docs/execplans/rust-project-enhancements.md | 23 +++++++ tests/helpers/rendering.py | 28 ++++++++- tests/helpers/tooling_contracts/polonius.py | 53 ++++++++++++++-- tests/test_template/test_tooling_contracts.py | 62 +++++++++++++++++++ 5 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 docs/adr-004-optional-polonius-support.md diff --git a/docs/adr-004-optional-polonius-support.md b/docs/adr-004-optional-polonius-support.md new file mode 100644 index 0000000..93040f8 --- /dev/null +++ b/docs/adr-004-optional-polonius-support.md @@ -0,0 +1,55 @@ +# ADR-004: Make Polonius support optional + +## Status + +Accepted. + +## Context + +Polonius permits borrow-centric internal APIs that the stable non-lexical +lifetime analysis rejects, but its alpha analysis still requires a dated +nightly toolchain and `-Zpolonius=next`. Applications can usually accept that +binding, while reusable libraries often need wider compiler compatibility. +Cargo build flags are also replaced whenever tooling supplies `RUSTFLAGS`, so +an enabled project must preserve the Polonius flag across every such build +surface. + +## Decision + +In the context of generating Rust applications and libraries with different +compiler-compatibility needs, facing the tension between borrow-centric APIs +and Polonius's nightly-only status, we decided for an `enable_polonius` Copier +choice that defaults on for applications and off for libraries and propagates +the selected flag through Cargo, Make, coverage, Continuous Integration (CI), +and release builds, and against enabling Polonius universally, disabling it +universally, or waiting for stabilization before exposing it, to achieve an +explicit and consistently enforced per-project toolchain contract, accepting +that opted-in projects depend on a dated nightly and must keep every +`RUSTFLAGS` override synchronized. + +## Progress and outcome + +The template now renders the option, pinned toolchain, compiler flags, policy +guidance, CI and release configuration for the selected state. Parent-template +contract and compilation tests cover enabled and disabled applications and +libraries. The implementation and its validation are complete; future +generated projects retain an explicit opt-out or opt-in as their compatibility +requirements change. + +## Risks + +- A pinned nightly can become unavailable or acquire regressions and therefore + requires deliberate upgrades. +- A new build path that overrides `RUSTFLAGS` can silently omit Polonius unless + its rendered contract is extended and tested. +- Enabling borrow-centric APIs can make source builds fail under stable Rust or + plain nightly without `-Zpolonius=next`. + +## Consequences + +- Generated applications adopt Polonius by default; generated libraries keep + wider compiler compatibility by default. +- Opted-in projects document their nightly requirement and preserve the flag + across supported build paths. +- Maintainers must treat the Copier answer, dated channel, and explicit flag + propagation as one toolchain policy when updating generated projects. diff --git a/docs/execplans/rust-project-enhancements.md b/docs/execplans/rust-project-enhancements.md index d7f8f1a..8c0327c 100644 --- a/docs/execplans/rust-project-enhancements.md +++ b/docs/execplans/rust-project-enhancements.md @@ -66,6 +66,12 @@ Pinned GitHub Action SHAs drift over time. The mitigation is to resolve the current default-branch commit for each action repository during implementation and record the resulting pins in the plan and pull request validation notes. +Polonius remains a nightly-only alpha analysis, and explicit `RUSTFLAGS` +replace Cargo's build-level flags. Generated projects that opt in therefore +depend on a dated nightly, and every build path that overrides `RUSTFLAGS` must +re-state `-Zpolonius=next`. Contract tests cover the supported override paths; +new paths must extend those tests. + ## Progress - [x] 2026-05-23: Confirmed branch `rust-project-enhancements` and clean @@ -107,6 +113,11 @@ and record the resulting pins in the plan and pull request validation notes. was 9 passed in 18.54 seconds. - [x] 2026-05-23: Pushed `rust-project-enhancements` and created draft pull request . +- [x] 2026-08-02: Extended the completed tooling import with an optional + `enable_polonius` choice, application-recommended and library-compatible + defaults, flag propagation across Cargo, Make, coverage, CI, and release + builds, generated policy guidance, and enabled/disabled contract and + compilation coverage. Recorded the extension in ADR-004. ## Surprises & Discoveries @@ -140,6 +151,12 @@ Validate generated projects through pytest-copier by invoking the generated public `make all` target rather than stitching together private commands from the parent repository. +Expose Polonius as a Copier choice that defaults on for applications and off +for libraries. When enabled, use the dated channel from `rust-toolchain.toml` +and preserve `-Zpolonius=next` wherever `RUSTFLAGS` are replaced; this permits +borrow-centric application internals without imposing nightly compatibility on +generated libraries. ADR-004 records the alternatives and accepted risks. + ## Outcomes & Retrospective The template now renders projects with Cranelift debug codegen, Linux mold @@ -148,3 +165,9 @@ cargo-binstall metadata for app projects, Whitaker linting with CI caching, SHA-pinned CI actions, and pytest-copier coverage that runs generated `make all` gates. The branch was pushed and draft pull request was opened for review. + +The later Polonius extension is also complete. Generated applications now opt +in by default, libraries retain wider compiler compatibility by default, and +all supported flag-override paths preserve the selected compiler contract. +Rendered-project tests exercise both choices, and generated documentation +explains the nightly dependency and borrow-centric design policy. diff --git a/tests/helpers/rendering.py b/tests/helpers/rendering.py index d339c10..8fa56d6 100644 --- a/tests/helpers/rendering.py +++ b/tests/helpers/rendering.py @@ -23,7 +23,33 @@ def render_project( license_year: int | None = 2026, dev_target: str = "x86_64-unknown-linux-gnu", ) -> CopierProject: - """Render a generated Rust project with publishable metadata.""" + """Render a generated Rust project with publishable metadata. + + Parameters + ---------- + tmp_path : Path + Destination directory for the rendered project. + copier : CopierFixture + Copier test fixture used to render the template. + project_name : str + Human-readable project name supplied to Copier. + package_name : str + Rust package name supplied to Copier. + flavour : str + Generated project flavour, either an application or a library. + enable_polonius : bool | None + Explicit Polonius selection. ``None`` omits the answer so Copier uses + the flavour-based default; a Boolean overrides that default. + license_year : int | None + Copyright year. ``None`` omits the answer so Copier uses its default. + dev_target : str + Rust target triple used for generated development tooling. + + Returns + ------- + CopierProject + The rendered project fixture. + """ answers: dict[str, str | int | bool] = { "project_name": project_name, "package_name": package_name, diff --git a/tests/helpers/tooling_contracts/polonius.py b/tests/helpers/tooling_contracts/polonius.py index 2dcd15a..39db707 100644 --- a/tests/helpers/tooling_contracts/polonius.py +++ b/tests/helpers/tooling_contracts/polonius.py @@ -103,8 +103,12 @@ def _assert_coverage_workflow(workflow: str, job_name: str, *, enabled: bool) -> assert (POLONIUS_FLAG in rustflags) is enabled -def _assert_release_workflow(release_workflow: str, *, enabled: bool) -> None: +def _assert_release_workflow( + release_workflow: str, rust_toolchain: str, *, enabled: bool +) -> None: """Assert release artefacts use a compiler compatible with the source.""" + toolchain_config = tomllib.loads(rust_toolchain) + expected_toolchain = str(toolchain_config["toolchain"]["channel"]) setup = _setup_rust_step(release_workflow) setup_inputs = require_mapping(setup, "with", "release setup-rust step") toolchain = str(setup_inputs.get("toolchain", "")) @@ -113,9 +117,9 @@ def _assert_release_workflow(release_workflow: str, *, enabled: bool) -> None: rustflags = str(env.get("RUSTFLAGS", "")) command = str(build.get("run", "")) if enabled: - assert toolchain.startswith("nightly-20") + assert toolchain == expected_toolchain assert rustflags == POLONIUS_FLAG - assert f"cross +{toolchain} build" in command + assert f"cross +{expected_toolchain} build" in command else: assert toolchain == "stable" assert rustflags == "" @@ -140,7 +144,46 @@ def assert_polonius_toolchain_contracts( users_guide: str, polonius_doc: str | None, ) -> None: - """Assert the selected Polonius state across generated project surfaces.""" + """Assert the selected Polonius state across generated project surfaces. + + Parameters + ---------- + enabled : bool + Expected Polonius selection for the rendered project. + dev_target : str + Development target represented in the rendered Cargo and Make files. + cargo_config : str + Rendered Cargo configuration. + makefile : str + Rendered Makefile. + rust_toolchain : str + Rendered Rust toolchain configuration. + ci_workflow : str + Rendered Continuous Integration workflow. + coverage_main_workflow : str + Rendered main-branch coverage workflow. + release_workflow : str | None + Rendered release workflow, or ``None`` when the project has none. + agents : str + Rendered agent guidance. + readme : str + Rendered project README. + docs_contents : str + Rendered documentation contents page. + repository_layout : str + Rendered repository layout documentation. + developers_guide : str + Rendered developers guide. + users_guide : str + Rendered users guide. + polonius_doc : str | None + Rendered Polonius policy, or ``None`` when Polonius is disabled. + + Raises + ------ + AssertionError + If any rendered surface does not match the selected Polonius state. + """ _assert_cargo_config(cargo_config, enabled=enabled, dev_target=dev_target) _assert_makefile(makefile, enabled=enabled, dev_target=dev_target) _assert_coverage_workflow(ci_workflow, "build-test", enabled=enabled) @@ -148,7 +191,7 @@ def assert_polonius_toolchain_contracts( coverage_main_workflow, "coverage-upload", enabled=enabled ) if release_workflow is not None: - _assert_release_workflow(release_workflow, enabled=enabled) + _assert_release_workflow(release_workflow, rust_toolchain, enabled=enabled) if enabled: assert "Polonius alpha" in rust_toolchain diff --git a/tests/test_template/test_tooling_contracts.py b/tests/test_template/test_tooling_contracts.py index 66abe2f..30d5c57 100644 --- a/tests/test_template/test_tooling_contracts.py +++ b/tests/test_template/test_tooling_contracts.py @@ -5,6 +5,8 @@ from pathlib import Path import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st from pytest_copier.plugin import CopierFixture from tests.helpers.generated_files import ( @@ -24,6 +26,18 @@ from tests.helpers.tooling_contracts.workflows import _assert_ci_workflow_contracts +POLONIUS_RENDER_CASES = tuple( + (flavour, enable_polonius, dev_target) + for flavour in (LIB, APP) + for enable_polonius in (None, False, True) + for dev_target in ( + "x86_64-unknown-linux-gnu", + "aarch64-apple-darwin", + "", + ) +) + + @pytest.mark.parametrize( ("flavour", "dev_target", "enable_polonius"), [ @@ -136,7 +150,55 @@ def test_generated_tooling_contracts( assert "DEFAULT_BASE_URL" in spelling_generator assert "_local_cache_is_current" in spelling_core +@given(case=st.sampled_from(POLONIUS_RENDER_CASES)) +@settings( + deadline=None, + max_examples=len(POLONIUS_RENDER_CASES), + suppress_health_check=[HealthCheck.function_scoped_fixture], +) +def test_polonius_flag_invariant_across_rendered_configuration_space( + tmp_path_factory: pytest.TempPathFactory, + copier: CopierFixture, + case: tuple[str, bool | None, str], +) -> None: + """Every rendered RUSTFLAGS override preserves the selected Polonius state.""" + flavour, enable_polonius, dev_target = case + project = render_project( + tmp_path_factory.mktemp("polonius-contract"), + copier, + project_name="InvariantProperty", + package_name="invariant_property", + flavour=flavour, + enable_polonius=enable_polonius, + dev_target=dev_target, + ) + expected_enabled = flavour == APP if enable_polonius is None else enable_polonius + release_path = project / ".github/workflows/release.yml" + polonius_path = project / "docs/polonius.md" + assert_polonius_toolchain_contracts( + enabled=expected_enabled, + dev_target=dev_target, + cargo_config=read_generated_text(project / ".cargo/config.toml"), + makefile=read_generated_text(project / "Makefile"), + rust_toolchain=read_generated_text(project / "rust-toolchain.toml"), + ci_workflow=read_generated_text(project / ".github/workflows/ci.yml"), + coverage_main_workflow=read_generated_text( + project / ".github/workflows/coverage-main.yml" + ), + release_workflow=( + read_generated_text(release_path) if release_path.exists() else None + ), + agents=read_generated_text(project / "AGENTS.md"), + readme=read_generated_text(project / "README.md"), + docs_contents=read_generated_text(project / "docs/contents.md"), + repository_layout=read_generated_text(project / "docs/repository-layout.md"), + developers_guide=read_generated_text(project / "docs/developers-guide.md"), + users_guide=read_generated_text(project / "docs/users-guide.md"), + polonius_doc=( + read_generated_text(polonius_path) if polonius_path.exists() else None + ), + ) def test_ci_contract_rejects_unguarded_duplicate_audit_step( tmp_path: Path, copier: CopierFixture ) -> None: From e7db62509cb2ab9f17f1a02ce07519e58b8a858f Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 2 Aug 2026 13:35:39 +0200 Subject: [PATCH 3/5] Complete Polonius tooling propagation Carry the Polonius flag through rustdoc and directly verify the coverage and documentation recipes that override compiler flags. Reject unsafe nightly-date answers before rendering release shell commands, and document the 0.2.0 migration path and refined architectural decision. --- README.md | 5 ++-- copier.yaml | 4 +++ docs/adr-004-optional-polonius-support.md | 17 +++++------- docs/migrations/0.2.0.md | 27 +++++++++++++++++++ docs/users-guide.md | 3 ++- template/.cargo/config.toml.jinja | 1 + template/Makefile.jinja | 4 +-- tests/helpers/tooling_contracts/polonius.py | 14 ++++++++++ .../__snapshots__/test_snapshots.ambr | 5 ++-- tests/test_template/test_basic_rendering.py | 14 ++++++++++ 10 files changed, 77 insertions(+), 17 deletions(-) create mode 100644 docs/migrations/0.2.0.md diff --git a/README.md b/README.md index c4de0b7..186bfc6 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ flowchart LR Additional details are in [`docs/testing.md`](docs/testing.md). User-facing generated-project behaviour is documented in -[`docs/users-guide.md`](docs/users-guide.md). Parent-template development -requirements are documented in +[`docs/users-guide.md`](docs/users-guide.md), with upgrade guidance in the +[`0.2.0 migration guide`](docs/migrations/0.2.0.md). Parent-template +development requirements are documented in [`docs/developers-guide.md`](docs/developers-guide.md). diff --git a/copier.yaml b/copier.yaml index 4724fb9..e38c78e 100644 --- a/copier.yaml +++ b/copier.yaml @@ -113,6 +113,10 @@ rust_nightly_date: default: '2025-06-10' help: 'Rust nightly toolchain date (YYYY-MM-DD)' placeholder: 'e.g. 2025-06-10' + validator: >- + {% if not (rust_nightly_date | regex_search('^[0-9]{4}-[0-9]{2}-[0-9]{2}$')) %} + Rust nightly date must use YYYY-MM-DD. + {% endif %} dev_target: type: str diff --git a/docs/adr-004-optional-polonius-support.md b/docs/adr-004-optional-polonius-support.md index 93040f8..b59297b 100644 --- a/docs/adr-004-optional-polonius-support.md +++ b/docs/adr-004-optional-polonius-support.md @@ -16,16 +16,13 @@ surface. ## Decision -In the context of generating Rust applications and libraries with different -compiler-compatibility needs, facing the tension between borrow-centric APIs -and Polonius's nightly-only status, we decided for an `enable_polonius` Copier -choice that defaults on for applications and off for libraries and propagates -the selected flag through Cargo, Make, coverage, Continuous Integration (CI), -and release builds, and against enabling Polonius universally, disabling it -universally, or waiting for stabilization before exposing it, to achieve an -explicit and consistently enforced per-project toolchain contract, accepting -that opted-in projects depend on a dated nightly and must keep every -`RUSTFLAGS` override synchronized. +An `enable_polonius` Copier choice defaults to enabled for applications and +disabled for libraries, with either default available for explicit override. +Enabled projects propagate `-Zpolonius=next` through Cargo, Make, coverage, +Continuous Integration (CI) and release builds. This consistent propagation +establishes a single, explicit toolchain contract across every supported build +surface. Enabled projects require a dated nightly toolchain, and every explicit +`RUSTFLAGS` value must remain synchronized with the configured Polonius flag. ## Progress and outcome diff --git a/docs/migrations/0.2.0.md b/docs/migrations/0.2.0.md new file mode 100644 index 0000000..710e72d --- /dev/null +++ b/docs/migrations/0.2.0.md @@ -0,0 +1,27 @@ +# Migrating to 0.2.0 + +Version 0.2.0 adds the `enable_polonius` Copier prompt. The prompt controls +whether generated projects use the nightly Polonius borrow-checking analysis +through `-Zpolonius=next`. + +## Update an existing project + +1. Commit or otherwise preserve local changes before updating. +2. Run `copier update` using the same answers file and template source as the + existing project. +3. Review the new `enable_polonius` answer before accepting the rendered + changes. Applications default to enabled; libraries default to disabled. + Either value can be overridden explicitly. +4. Review changes to the Rust toolchain, Cargo configuration, Makefile, + coverage and release workflows as one toolchain-policy update. +5. Run the generated project's full quality gates after resolving Copier + conflicts. + +Enabling Polonius binds the generated project to its configured dated nightly +toolchain. Every build path that sets `RUSTFLAGS` explicitly must include +`-Zpolonius=next`, because an explicit value replaces Cargo's build-level +flags. Disabling Polonius retains the wider compiler compatibility expected by +libraries, although the template still uses its configured nightly toolchain. + +The rationale and accepted constraints are recorded in +[ADR-004](../adr-004-optional-polonius-support.md). diff --git a/docs/users-guide.md b/docs/users-guide.md index 4f4c7d9..0f051d7 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -15,7 +15,8 @@ metadata used in the generated `Cargo.toml`: (`-Zpolonius=next`). It defaults to enabled for applications, where borrow-centric internal APIs can evolve with the project, and disabled for libraries, which commonly need wider compiler compatibility. Either default - can be overridden. + can be overridden. Existing projects should follow the + [0.2.0 migration guide](migrations/0.2.0.md) when adopting this prompt. - `package_description` becomes `[package].description`. - `repository_url` becomes `[package].repository` and is used by generated app projects for cargo-binstall release URLs. diff --git a/template/.cargo/config.toml.jinja b/template/.cargo/config.toml.jinja index 46493cf..c94bc83 100644 --- a/template/.cargo/config.toml.jinja +++ b/template/.cargo/config.toml.jinja @@ -5,6 +5,7 @@ # workflows therefore re-state -Zpolonius=next whenever they set RUSTFLAGS. [build] rustflags = ["-Zpolonius=next"] +rustdocflags = ["-Zpolonius=next"] {% endif -%} [unstable] codegen-backend = true diff --git a/template/Makefile.jinja b/template/Makefile.jinja index 1011128..df229c1 100644 --- a/template/Makefile.jinja +++ b/template/Makefile.jinja @@ -26,7 +26,7 @@ DEV_LINKER_FLAGS ?= $(if $(filter Linux,$(shell uname -s)),-C link-arg=-fuse-ld= DEV_LINKER_FLAGS ?= {% endif -%} DEV_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) $(DEV_LINKER_FLAGS) -RUSTDOC_FLAGS ?= +RUSTDOC_FLAGS ?= $(POLONIUS_FLAGS) RUSTDOC_FLAGS := -D warnings $(RUSTDOC_FLAGS) CARGO_FLAGS ?= --all-targets --all-features CLIPPY_FLAGS ?= $(CARGO_FLAGS) -- $(RUST_FLAGS) @@ -52,7 +52,7 @@ clean: ## Remove build artifacts test: ## Run tests with warnings treated as errors RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) - RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) test --doc --workspace --all-features + RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) test --doc --workspace --all-features target/%/$(TARGET): ## Build binary in debug or release mode RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release){% if flavour == 'app' %} --bin $(TARGET){% endif %} diff --git a/tests/helpers/tooling_contracts/polonius.py b/tests/helpers/tooling_contracts/polonius.py index 39db707..ede2ba7 100644 --- a/tests/helpers/tooling_contracts/polonius.py +++ b/tests/helpers/tooling_contracts/polonius.py @@ -48,9 +48,13 @@ def _assert_cargo_config(cargo_config: str, *, enabled: bool, dev_target: str) - """Assert Cargo's build and target rustflags retain Polonius as required.""" config = tomllib.loads(cargo_config) build_flags = config.get("build", {}).get("rustflags", []) + rustdoc_flags = config.get("build", {}).get("rustdocflags", []) assert (POLONIUS_FLAG in build_flags) is enabled, ( "expected build.rustflags Polonius state to match the Copier answer" ) + assert (POLONIUS_FLAG in rustdoc_flags) is enabled, ( + "expected build.rustdocflags Polonius state to match the Copier answer" + ) if "linux" not in dev_target: return target_flags = config["target"][dev_target]["rustflags"] @@ -74,6 +78,7 @@ def _assert_makefile(makefile: str, *, enabled: bool, dev_target: str) -> None: "DEV_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) $(DEV_LINKER_FLAGS)" in makefile ) + assert "RUSTDOC_FLAGS ?= $(POLONIUS_FLAGS)" in makefile linker_default = next( line for line in makefile.splitlines() if line.startswith("DEV_LINKER_FLAGS ?=") ) @@ -92,6 +97,15 @@ def _assert_makefile(makefile: str, *, enabled: bool, dev_target: str) -> None: "$(DEV_RUST_FLAGS)" in line or "$(COVERAGE_RUST_FLAGS)" in line for line in compile_lines ), f"compile recipes must use composed Rust flags: {compile_lines!r}" + coverage_recipe = makefile.partition("coverage:")[2].partition("\n\n")[0] + assert 'RUSTFLAGS="$(COVERAGE_RUST_FLAGS)"' in coverage_recipe + rustdoc_lines = [ + line + for line in makefile.splitlines() + if "$(CARGO) test --doc" in line or "$(CARGO) doc" in line + ] + assert len(rustdoc_lines) == 2 + assert all('RUSTDOCFLAGS="$(RUSTDOC_FLAGS)"' in line for line in rustdoc_lines) def _assert_coverage_workflow(workflow: str, job_name: str, *, enabled: bool) -> None: diff --git a/tests/test_template/__snapshots__/test_snapshots.ambr b/tests/test_template/__snapshots__/test_snapshots.ambr index 009f8fc..44cffa3 100644 --- a/tests/test_template/__snapshots__/test_snapshots.ambr +++ b/tests/test_template/__snapshots__/test_snapshots.ambr @@ -114,6 +114,7 @@ # workflows therefore re-state -Zpolonius=next whenever they set RUSTFLAGS. [build] rustflags = ["-Zpolonius=next"] + rustdocflags = ["-Zpolonius=next"] [unstable] codegen-backend = true @@ -404,7 +405,7 @@ RUST_FLAGS := -D warnings $(RUST_FLAGS) DEV_LINKER_FLAGS ?= $(if $(filter Linux,$(shell uname -s)),-C link-arg=-fuse-ld=mold) DEV_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) $(DEV_LINKER_FLAGS) - RUSTDOC_FLAGS ?= + RUSTDOC_FLAGS ?= $(POLONIUS_FLAGS) RUSTDOC_FLAGS := -D warnings $(RUSTDOC_FLAGS) CARGO_FLAGS ?= --all-targets --all-features CLIPPY_FLAGS ?= $(CARGO_FLAGS) -- $(RUST_FLAGS) @@ -430,7 +431,7 @@ test: ## Run tests with warnings treated as errors \tRUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) - \tRUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) test --doc --workspace --all-features + \tRUSTDOCFLAGS="$(RUSTDOC_FLAGS)" RUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) test --doc --workspace --all-features target/%/$(TARGET): ## Build binary in debug or release mode \tRUSTFLAGS="$(DEV_RUST_FLAGS)" $(CARGO) build $(BUILD_JOBS) $(if $(findstring release,$(@)),--release) --bin $(TARGET) diff --git a/tests/test_template/test_basic_rendering.py b/tests/test_template/test_basic_rendering.py index 8ff884c..387b7ed 100644 --- a/tests/test_template/test_basic_rendering.py +++ b/tests/test_template/test_basic_rendering.py @@ -4,11 +4,25 @@ from pathlib import Path +import pytest from pytest_copier.plugin import CopierFixture from tests.helpers.rendering import APP, LIB, render_project +def test_template_rejects_unsafe_nightly_date( + tmp_path: Path, copier: CopierFixture +) -> None: + """Reject a nightly date that could inject shell into release builds.""" + with pytest.raises(ValueError, match="must use YYYY-MM-DD"): + copier.copy( + tmp_path, + project_name="UnsafeNightly", + package_name="unsafe_nightly", + rust_nightly_date="2025-06-10; echo INJECTED", + ) + + def test_template_renders(tmp_path: Path, copier: CopierFixture) -> None: """Template renders with default values and passes public gates.""" project = render_project( From 2cda90e74f6fc51946447b67813db2c4f1f0fd43 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 2 Aug 2026 15:02:15 +0200 Subject: [PATCH 4/5] Tighten Polonius contract diagnostics Reject nightly dates with trailing newlines using a strict end-of-string match and retain regression coverage for the validation boundary. Add actionable messages to rustdoc and coverage contract assertions, and correct the migration guide's essential `because` clause. --- copier.yaml | 2 +- docs/migrations/0.2.0.md | 2 +- tests/helpers/tooling_contracts/polonius.py | 16 ++++++++++++---- tests/test_template/test_basic_rendering.py | 13 +++++++++++++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/copier.yaml b/copier.yaml index e38c78e..0cc9926 100644 --- a/copier.yaml +++ b/copier.yaml @@ -114,7 +114,7 @@ rust_nightly_date: help: 'Rust nightly toolchain date (YYYY-MM-DD)' placeholder: 'e.g. 2025-06-10' validator: >- - {% if not (rust_nightly_date | regex_search('^[0-9]{4}-[0-9]{2}-[0-9]{2}$')) %} + {% if not (rust_nightly_date | regex_search('^[0-9]{4}-[0-9]{2}-[0-9]{2}\\Z')) %} Rust nightly date must use YYYY-MM-DD. {% endif %} diff --git a/docs/migrations/0.2.0.md b/docs/migrations/0.2.0.md index 710e72d..9809460 100644 --- a/docs/migrations/0.2.0.md +++ b/docs/migrations/0.2.0.md @@ -19,7 +19,7 @@ through `-Zpolonius=next`. Enabling Polonius binds the generated project to its configured dated nightly toolchain. Every build path that sets `RUSTFLAGS` explicitly must include -`-Zpolonius=next`, because an explicit value replaces Cargo's build-level +`-Zpolonius=next` because an explicit value replaces Cargo's build-level flags. Disabling Polonius retains the wider compiler compatibility expected by libraries, although the template still uses its configured nightly toolchain. diff --git a/tests/helpers/tooling_contracts/polonius.py b/tests/helpers/tooling_contracts/polonius.py index ede2ba7..115796e 100644 --- a/tests/helpers/tooling_contracts/polonius.py +++ b/tests/helpers/tooling_contracts/polonius.py @@ -78,7 +78,9 @@ def _assert_makefile(makefile: str, *, enabled: bool, dev_target: str) -> None: "DEV_RUST_FLAGS ?= $(RUST_FLAGS) $(POLONIUS_FLAGS) $(DEV_LINKER_FLAGS)" in makefile ) - assert "RUSTDOC_FLAGS ?= $(POLONIUS_FLAGS)" in makefile + assert "RUSTDOC_FLAGS ?= $(POLONIUS_FLAGS)" in makefile, ( + "RUSTDOC_FLAGS must inherit POLONIUS_FLAGS" + ) linker_default = next( line for line in makefile.splitlines() if line.startswith("DEV_LINKER_FLAGS ?=") ) @@ -98,14 +100,20 @@ def _assert_makefile(makefile: str, *, enabled: bool, dev_target: str) -> None: for line in compile_lines ), f"compile recipes must use composed Rust flags: {compile_lines!r}" coverage_recipe = makefile.partition("coverage:")[2].partition("\n\n")[0] - assert 'RUSTFLAGS="$(COVERAGE_RUST_FLAGS)"' in coverage_recipe + assert 'RUSTFLAGS="$(COVERAGE_RUST_FLAGS)"' in coverage_recipe, ( + "coverage recipe must use composed coverage Rust flags" + ) rustdoc_lines = [ line for line in makefile.splitlines() if "$(CARGO) test --doc" in line or "$(CARGO) doc" in line ] - assert len(rustdoc_lines) == 2 - assert all('RUSTDOCFLAGS="$(RUSTDOC_FLAGS)"' in line for line in rustdoc_lines) + assert len(rustdoc_lines) == 2, ( + f"expected exactly two rustdoc recipes, got: {rustdoc_lines!r}" + ) + assert all('RUSTDOCFLAGS="$(RUSTDOC_FLAGS)"' in line for line in rustdoc_lines), ( + f"rustdoc recipes must use composed rustdoc flags: {rustdoc_lines!r}" + ) def _assert_coverage_workflow(workflow: str, job_name: str, *, enabled: bool) -> None: diff --git a/tests/test_template/test_basic_rendering.py b/tests/test_template/test_basic_rendering.py index 387b7ed..21e96df 100644 --- a/tests/test_template/test_basic_rendering.py +++ b/tests/test_template/test_basic_rendering.py @@ -23,6 +23,19 @@ def test_template_rejects_unsafe_nightly_date( ) +def test_template_rejects_nightly_date_with_trailing_newline( + tmp_path: Path, copier: CopierFixture +) -> None: + """Reject a nightly date with content after the expected date.""" + with pytest.raises(ValueError, match="must use YYYY-MM-DD"): + copier.copy( + tmp_path, + project_name="InvalidNightly", + package_name="invalid_nightly", + rust_nightly_date="2025-06-10\n", + ) + + def test_template_renders(tmp_path: Path, copier: CopierFixture) -> None: """Template renders with default values and passes public gates.""" project = render_project( From 40108c62b7b50ecdaeb92f350fe49a28c38ca53f Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 2 Aug 2026 15:32:33 +0200 Subject: [PATCH 5/5] Format replayed tooling contract tests Restore Ruff's required top-level spacing after Weave combines the scheduled mutation-testing changes with the Polonius property contract. --- tests/test_template/test_tooling_contracts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_template/test_tooling_contracts.py b/tests/test_template/test_tooling_contracts.py index 30d5c57..9da5bb9 100644 --- a/tests/test_template/test_tooling_contracts.py +++ b/tests/test_template/test_tooling_contracts.py @@ -150,6 +150,7 @@ def test_generated_tooling_contracts( assert "DEFAULT_BASE_URL" in spelling_generator assert "_local_cache_is_current" in spelling_core + @given(case=st.sampled_from(POLONIUS_RENDER_CASES)) @settings( deadline=None, @@ -199,6 +200,8 @@ def test_polonius_flag_invariant_across_rendered_configuration_space( read_generated_text(polonius_path) if polonius_path.exists() else None ), ) + + def test_ci_contract_rejects_unguarded_duplicate_audit_step( tmp_path: Path, copier: CopierFixture ) -> None: