Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 28 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,22 +193,31 @@ working on the Rust portions of the project:
- `make check-fmt` executes:

```sh
cargo fmt --all -- --check
cargo +nightly-2026-05-28 fmt --all -- --check
```

validating formatting across the Rust workspace without modifying files.
- `make lint` executes:
- `make lint` executes its Rustdoc and Clippy steps through
`RUST_DEBUG_CARGO`, then runs Whitaker separately:

```sh
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps
cargo clippy --all-targets --all-features -- -D warnings
RUSTFLAGS="-D warnings" whitaker --all -- --all-targets --all-features
cd rust && RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" \
$(RUST_DEBUG_CARGO) doc --no-deps $(DOC_FLAGS) && \
$(RUST_DEBUG_CARGO) clippy $(CLIPPY_FLAGS)
cd rust && $(LOCAL_TOOL_ENV) \
RUSTFLAGS="$(WHITAKER_RUSTFLAGS)" $(WHITAKER) --all -- \
$(WHITAKER_CARGO_FLAGS)
yamllint --config-file .yamllint.yml .github/workflows
actionlint
```

linting every target with all features enabled, denying all Clippy
warnings, and validating GitHub Actions workflows. Keep `.yamllint.yml`
On Linux, `RUST_DEBUG_CARGO` selects `nightly-2026-08-23` with the explicit
`tools/dev-fast/config.toml` fragment; elsewhere it selects the prescribed
Cargo route. Whitaker never receives that fragment. The protected coverage,
release, verification, and MSRV paths also retain their separate toolchain
and linker policy. The target lints every target with all features enabled,
denies all Clippy warnings, and validates GitHub Actions workflows. Keep
`.yamllint.yml`
compatible with GitHub's unquoted `on` trigger key and require each
workflow to begin with `---`. CI must install
yamllint with `uv tool` and use the pinned, checksum-verified actionlint
Expand All @@ -221,20 +230,21 @@ working on the Rust portions of the project:
```

running the Rust test suite with warnings denied. Use `make fmt`
(`cargo fmt --all`) to apply formatting fixes reported by the formatter
check alongside the Python and Markdown formatters.
(`cargo +nightly-2026-05-28 fmt --all`) to apply formatting fixes reported
by the formatter check alongside the Python and Markdown formatters.
- The project toolchain stays pinned to Rust `1.85.0` and declares `rustfmt`,
`clippy`, and `rust-analyzer`. CI also provisions `nightly-2026-05-28` for
the maintenance formatter, then restores the project toolchain. The current
`fmt` and `check-fmt` recipes still use the project toolchain. Linux-only
debug Rust work additionally uses the separately pinned dev-fast nightly
through `tools/dev-fast/config.toml`; run `make dev-fast-check` first, since
it requires both the `rustc-codegen-cranelift` and `clippy` components. The
stable toolchain remains mandatory for release, coverage, Whitaker, the
explicit `make msrv-check` verification target, and the macOS and Windows
platform alternatives. All Rust packages inherit Cargo's
`rust-version = "1.85.0"` contract, which also keeps MSRV-aware Clippy
findings valid when debug linting uses the dev-fast nightly.
`fmt` and `check-fmt` recipes explicitly select the pinned nightly only for
formatting. Linux-only debug Rust work additionally uses the separately
pinned dev-fast nightly through `tools/dev-fast/config.toml`; run
`make dev-fast-check` first, since it requires both the
`rustc-codegen-cranelift` and `clippy` components. The stable toolchain
remains mandatory for release, coverage, Whitaker, the explicit
`make msrv-check` verification target, and the macOS and Windows platform
alternatives. All Rust packages inherit Cargo's `rust-version = "1.85.0"`
contract, which also keeps MSRV-aware Clippy findings valid when debug
linting uses the dev-fast nightly.
- Clippy warnings MUST be disallowed.
- Fix any warnings emitted during tests in code instead of silencing them.
- Where a function is too long, extract meaningfully named helper functions
Expand Down
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ DEVELOP_DEV_FAST_ENV = $(if $(DEVELOP_DEV_FAST_ENABLED),RUSTUP_TOOLCHAIN=$(DEV_F
DEV_FAST_CHECK_COMMAND = test "$(DEV_FAST_HOST_IS_LINUX)" = yes || { printf '%s\n' 'dev-fast is supported only on Linux; use the stable backend on this host' >&2; exit 1; }; test -f "$(DEV_FAST_CONFIG_RELATIVE)" || { printf 'dev-fast configuration is missing: %s\n' "$(DEV_FAST_CONFIG_RELATIVE)" >&2; exit 1; }; command -v mold >/dev/null 2>&1 || { printf 'mold %s is required for Linux dev-fast builds\n' "$(DEV_FAST_MOLD_VERSION)" >&2; exit 1; }; mold --version | grep -q '^mold $(DEV_FAST_MOLD_VERSION_PATTERN)\($$\|[[:space:]]\)' || { printf 'mold %s is required for Linux dev-fast builds\n' "$(DEV_FAST_MOLD_VERSION)" >&2; exit 1; }; components="$$(rustup component list --installed --toolchain "$(DEV_FAST_TOOLCHAIN)")" || { printf 'cannot inspect the components installed for %s\n' "$(DEV_FAST_TOOLCHAIN)" >&2; exit 1; }; for component in $(DEV_FAST_REQUIRED_COMPONENTS); do printf '%s\n' "$$components" | grep -q "^$$component" || { printf 'install %s for %s before using dev-fast\n' "$$component" "$(DEV_FAST_TOOLCHAIN)" >&2; exit 1; }; done
DEV_FAST_TEST_RUSTFLAGS = $(TEST_RUSTFLAGS) $(if $(DEV_FAST_HOST_IS_LINUX),-Clink-arg=-fuse-ld=mold)
DEV_FAST_TEST_COMMAND = if $(LOCAL_TOOL_ENV) command -v cargo-nextest >/dev/null 2>&1; then cd $(RUST_DIR) && CARGO_BUILD_JOBS="$(TEST_CARGO_BUILD_JOBS)" RUSTFLAGS="$(DEV_FAST_TEST_RUSTFLAGS)" $(DEV_FAST_CARGO_COMMAND) nextest run $(TEST_FLAGS) $(BUILD_JOBS); else echo "cargo-nextest not found; falling back to cargo test." >&2; cd $(RUST_DIR) && CARGO_BUILD_JOBS="$(TEST_CARGO_BUILD_JOBS)" RUSTFLAGS="$(DEV_FAST_TEST_RUSTFLAGS)" $(DEV_FAST_CARGO_COMMAND) test $(TEST_FLAGS) $(BUILD_JOBS); fi
RUSTFMT_TOOLCHAIN ?= nightly-2026-05-28
RUSTFMT_CARGO ?= $(CARGO) +$(RUSTFMT_TOOLCHAIN)
WHITAKER ?= whitaker
BUILD_JOBS ?=
RUST_FLAGS ?= -D warnings
Expand Down Expand Up @@ -272,13 +274,13 @@ endif
fmt: ruff ## Format sources
$(RUFF) format
$(RUFF) check --select I --fix
cd $(RUST_DIR) && $(CARGO) fmt --all
cd $(RUST_DIR) && $(RUSTFMT_CARGO) fmt --all
$(LOCAL_TOOL_ENV) $(MDTABLEFIX) --in-place $(MDTABLEFIX_SELECT) $(MDTABLEFIX_EXTENSIONS) $(MDTABLEFIX_RULES)
$(call run_markdownlint_files,$(MDLINT_FIX_COMMAND))

check-fmt: ruff ## Verify formatting
$(RUFF) format --check
cd $(RUST_DIR) && $(CARGO) fmt --all -- --check
cd $(RUST_DIR) && $(RUSTFMT_CARGO) fmt --all -- --check
$(LOCAL_TOOL_ENV) $(MDTABLEFIX) --check $(MDTABLEFIX_SELECT) $(MDTABLEFIX_EXTENSIONS) $(MDTABLEFIX_RULES)

test-markdown-format: ## Validate the Markdown formatting Makefile contract
Expand Down
98 changes: 98 additions & 0 deletions cuprum/unittests/test_rust_formatter_toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from __future__ import annotations

import re
import shutil
import subprocess # ruff: ignore[suspicious-subprocess-import] - expands fixed local Makefile recipes.
import sys
import tomllib

from tests.helpers.docs import repo_root
Expand Down Expand Up @@ -42,6 +45,59 @@
# Only these characters can begin a lexeme: `/` for comments, `r` for raw
# strings, `"` for strings, and `'` for character literals.
_LEXEME_STARTS = frozenset("/r\"'")
# The Makefile gates dev-fast on `uname -s`, which reports Linux for the hosts
# `sys.platform` reports as `linux*`. Keeping the check explicit means a broken
# host probe in the Makefile fails the contract below instead of mirroring it.
_HOST_IS_LINUX = sys.platform.startswith("linux")
_EXPECTED_DEBUG_ROUTE = (
"RUSTUP_TOOLCHAIN=nightly-2026-08-23 probe-cargo "
"--config ../tools/dev-fast/config.toml"
)
_DEV_FAST_FRAGMENTS = (
"RUSTUP_TOOLCHAIN=nightly-2026-08-23",
"--config ../tools/dev-fast/config.toml",
)


def _assert_target_avoids_the_formatter_route(
target: str, cargo_lines: list[str]
) -> None:
"""Assert one lint or test target keeps the formatter's nightly route out.

Parameters
----------
target
Makefile target whose expanded recipe is being checked.
cargo_lines
Cargo command lines from that target's dry-run recipe.
"""
assert all("probe-cargo +nightly-2026-05-28" not in line for line in cargo_lines), (
f"{target} must not select the formatter's nightly Cargo route"
)


def _assert_target_debug_route(target: str, cargo_lines: list[str]) -> None:
"""Assert one lint or test target carries the host-appropriate debug route.

The Makefile routes debug work through dev-fast only on Linux and falls back
to the bare injected Cargo elsewhere, so the fragment is required on Linux
and must be absent on every other host.

Parameters
----------
target
Makefile target whose expanded recipe is being checked.
cargo_lines
Cargo command lines from that target's dry-run recipe.
"""
if _HOST_IS_LINUX:
assert any(_EXPECTED_DEBUG_ROUTE in line for line in cargo_lines), (
f"{target} must retain the Linux dev-fast Cargo route"
)
return
assert all(
fragment not in line for line in cargo_lines for fragment in _DEV_FAST_FRAGMENTS
), f"{target} must omit the Linux-only dev-fast Cargo route off Linux"


def test_formatter_toolchain_precedes_the_project_toolchain(
Expand Down Expand Up @@ -244,3 +300,45 @@ def test_formatter_skips_are_limited_to_known_rstest_fixtures() -> None:
assert observed == _FORMATTER_FIXTURE_SKIPS, (
"add a mutation proof before extending the formatter exception set"
)


def test_make_formatter_targets_select_the_pinned_nightly() -> None:
"""Only formatter recipes select the pinned nightly cargo route."""
make_executable = shutil.which("make")
assert make_executable is not None, "make must be available to expand recipes"
root = repo_root()

def expanded_recipes(*targets: str) -> str:
"""Return dry-run output for the fixed formatter contract targets."""
completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[make_executable, "--dry-run", "CARGO=probe-cargo", *targets],
check=True,
shell=False,
cwd=root,
capture_output=True,
encoding="utf-8",
)
return completed.stdout

formatter_recipes = expanded_recipes("fmt", "check-fmt")
formatter_lines = [
line
for line in formatter_recipes.splitlines()
if line.startswith("cd rust && probe-cargo")
]
expected_lines = [
"cd rust && probe-cargo +nightly-2026-05-28 fmt --all",
"cd rust && probe-cargo +nightly-2026-05-28 fmt --all -- --check",
]
assert formatter_lines == expected_lines, (
"formatter targets must use only the injected pinned nightly cargo route"
)

for target in ("lint", "test"):
target_recipes = expanded_recipes(target)
cargo_lines = [
line for line in target_recipes.splitlines() if "probe-cargo" in line
]
assert cargo_lines, f"{target} must expand at least one Cargo command"
_assert_target_avoids_the_formatter_route(target, cargo_lines)
_assert_target_debug_route(target, cargo_lines)
15 changes: 12 additions & 3 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,18 @@ The lint job installs Nixie and Whitaker through the pinned
`leynos/shared-actions` installers. Nixie's installer also provisions Merman;
the bootstrap uses Rust `1.95.0` because Merman CLI `0.7.0` requires that
compiler. The job then restores Rust `1.85.0`, the project's supported
toolchain, before installing Whitaker and running the project gates. The
Whitaker action receives `WHITAKER_INSTALLER_VERSION` from the job environment
(`0.2.7`, the workflow's configured installer version).
toolchain, before installing Whitaker and running the project gates. On Linux,
standard debug Rust build, test, Rustdoc, and Clippy commands select
`nightly-2026-08-23` with the explicit `tools/dev-fast/config.toml` fragment.
The current `typecheck` target is Python-only and selects no Cargo fragment.
`fmt` and `check-fmt` alone select `nightly-2026-05-28` through `RUSTFMT_CARGO`
in the Makefile. `rust/.rustfmt.toml` configures formatter behaviour only;
change the Makefile when changing the formatter toolchain. Coverage, release,
verification, MSRV, and Whitaker commands, together with macOS and Windows,
retain their prescribed fragment-free or separately pinned toolchains. The
stable pin declares `rustfmt`, `clippy`, and `rust-analyzer` for local
maintenance. The Whitaker action receives `WHITAKER_INSTALLER_VERSION` from the
job environment (`0.2.7`, the workflow's configured installer version).

cargo-nextest is no longer installed here at all. The coverage job is the only
place it runs, and the shared action installs it from checksummed official
Expand Down
16 changes: 16 additions & 0 deletions rust/.rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
unstable_features = true
comment_width = 100
format_code_in_doc_comments = true
imports_granularity = "Crate"
imports_layout = "HorizontalVertical"
wrap_comments = true
group_imports = "StdExternalCrate"
use_try_shorthand = true
hex_literal_case = "Lower"
format_strings = true
format_macro_matchers = true
fn_single_line = true
condense_wildcard_suffixes = true
use_field_init_shorthand = true

ignore = ["cuprum-rust/tests/ui"]
14 changes: 9 additions & 5 deletions rust/cuprum-native-io/src/fd_ownership_kani_proofs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
//!
//! The invariant under proof, over every modelled exit path:
//!
//! - a reader passed through the production retention kernel is **never** closed by
//! Rust — it stays caller-owned on normal completion and error return;
//! - the `pump_stream` writer is **consumed** and closes exactly once on
//! every exit path, which is what signals EOF downstream.
//! - a reader passed through the production retention kernel is **never** closed by Rust — it stays
//! caller-owned on normal completion and error return;
//! - the `pump_stream` writer is **consumed** and closes exactly once on every exit path, which is
//! what signals EOF downstream.
//!
//! Scope. These are bounded-model proofs over the ownership *model* in
//! [`super::fd_ownership_model`], not over real descriptors: Kani does not
Expand All @@ -19,7 +19,11 @@
//! is bounded explicitly.

use super::fd_ownership_model::{
CloseLog, ExitMode, ModelFd, model_consume_stream, model_pump_stream,
CloseLog,
ExitMode,
ModelFd,
model_consume_stream,
model_pump_stream,
model_with_borrowed_reader,
};

Expand Down
22 changes: 8 additions & 14 deletions rust/cuprum-native-io/src/fd_ownership_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
//! real panic-unwind are proved by this model: native regressions exercise
//! those paths, including the historical trailing-`mem::forget` mistake.

use crate::memory::with_retained_owner;
use core::cell::Cell;

use crate::memory::with_retained_owner;

/// Records how many times the modelled descriptor was closed.
///
/// A real close is an unmodellable side effect, so the model counts them.
Expand All @@ -26,13 +27,9 @@ impl CloseLog {
}

/// Number of closes recorded so far.
pub(crate) const fn closes(self) -> u32 {
self.closes.into_inner()
}
pub(crate) const fn closes(self) -> u32 { self.closes.into_inner() }

fn record_close(&self) {
self.closes.set(self.closes.get().saturating_add(1));
}
fn record_close(&self) { self.closes.set(self.closes.get().saturating_add(1)); }
}

/// A modelled *owning* descriptor handle.
Expand All @@ -47,15 +44,11 @@ pub(crate) struct ModelFd<'log> {

impl<'log> ModelFd<'log> {
/// Reconstruct an owning handle that reports closes to `log`.
pub(crate) const fn new(log: &'log CloseLog) -> Self {
Self { log }
}
pub(crate) const fn new(log: &'log CloseLog) -> Self { Self { log } }
}

impl Drop for ModelFd<'_> {
fn drop(&mut self) {
self.log.record_close();
}
fn drop(&mut self) { self.log.record_close(); }
}

/// Represents an ordinary operation error, not panic-unwind.
Expand Down Expand Up @@ -118,9 +111,10 @@ mod tests {
//! Concrete unit tests for the ownership model, mirroring the bounded
//! Kani proofs so the contract is exercised by `make test` as well.

use super::{CloseLog, ExitMode, ModelFd, model_consume_stream, model_pump_stream};
use rstest::rstest;

use super::{CloseLog, ExitMode, ModelFd, model_consume_stream, model_pump_stream};

#[rstest]
#[case(ExitMode::Normal)]
#[case(ExitMode::Error)]
Expand Down
4 changes: 1 addition & 3 deletions rust/cuprum-native-io/src/kani_proofs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ use super::{
struct Resource<'a>(&'a Cell<u8>);

impl Drop for Resource<'_> {
fn drop(&mut self) {
self.0.set(self.0.get() + 1);
}
fn drop(&mut self) { self.0.set(self.0.get() + 1); }
}

#[kani::proof]
Expand Down
9 changes: 5 additions & 4 deletions rust/cuprum-native-io/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ mod fd_ownership_model;

#[cfg(unix)]
pub use std::os::fd::{AsFd as AsStream, BorrowedFd as BorrowedStream, OwnedFd as OwnedStream};
#[cfg(unix)]
use std::os::fd::{AsRawFd, FromRawFd};
#[cfg(windows)]
pub use std::os::windows::io::{
AsHandle as AsStream, BorrowedHandle as BorrowedStream, OwnedHandle as OwnedStream,
AsHandle as AsStream,
BorrowedHandle as BorrowedStream,
OwnedHandle as OwnedStream,
};

#[cfg(unix)]
use std::os::fd::{AsRawFd, FromRawFd};
#[cfg(windows)]
use std::os::windows::io::{FromRawHandle, RawHandle};

Expand Down
7 changes: 3 additions & 4 deletions rust/cuprum-native-io/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,14 @@ pub(crate) fn with_retained_owner<T, R>(value: T, operation: impl FnOnce(&mut T)
#[cfg(test)]
mod tests {
//! Real unwinding through the production retention frame.
use super::with_retained_owner;
use core::cell::Cell;
use std::panic::{AssertUnwindSafe, catch_unwind};

use super::with_retained_owner;

struct Close<'a>(&'a Cell<u32>);
impl Drop for Close<'_> {
fn drop(&mut self) {
self.0.set(self.0.get() + 1);
}
fn drop(&mut self) { self.0.set(self.0.get() + 1); }
}

#[test]
Expand Down
Loading
Loading