Skip to content

cargo-oxide: make cargo oxide new emit rustfmt-clean projects - #1193

Merged
nihalpasham merged 1 commit into
NVIDIA:mainfrom
vyncint:fix/scaffold-templates-not-rustfmt-clean
Aug 30, 2026
Merged

nihalpasham merged 1 commit into
NVIDIA:mainfrom
vyncint:fix/scaffold-templates-not-rustfmt-clean

Conversation

@vyncint

@vyncint vyncint commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1192.

Both cargo oxide new templates emitted use statements in an order rustfmt does not produce, so a freshly scaffolded project failed its own format check on code the user never wrote. CONTRIBUTING points contributors at cargo oxide fmt, so this is the first thing a new project is told to run.

Before / after

Before — sync template, straight after cargo oxide new demo:

$ cargo oxide fmt --check
Diff in src/main.rs:1:
-use cuda_device::{kernel, launch_bounds, launch_contract, thread, DisjointSlice};
-use cuda_host::cuda_module;
 use cuda_core::{CudaContext, DeviceBuffer, LaunchConfig1D};
+use cuda_device::{DisjointSlice, kernel, launch_bounds, launch_contract, thread};
+use cuda_host::cuda_module;

❌ Some files need formatting. Run: cargo oxide fmt

Before — --async, wrong in two places rather than one:

Diff in src/main.rs:1:
-use cuda_device::{kernel, thread, DisjointSlice};
-use cuda_host::cuda_module;
 use cuda_async::device_context::init_device_contexts;
 use cuda_async::device_operation::DeviceOperation;
 use cuda_core::LaunchConfig;
Diff in src/main.rs:6:
+use cuda_device::{DisjointSlice, kernel, thread};
+use cuda_host::cuda_module;

After — both templates:

$ cargo oxide new dsync           && (cd dsync  && cargo fmt --check) && echo CLEAN
CLEAN
$ cargo oxide new dasync --async  && (cd dasync && cargo fmt --check) && echo CLEAN
CLEAN

Two independent orderings were wrong

  1. Crate order — cuda_device / cuda_host preceded cuda_core, and in the async template also cuda_async.
  2. Order inside the braces — DisjointSlice trailed the lowercase items; under the scaffold's edition = "2024" it sorts first.

Why nothing caught it

The templates are string constants, so the repository's own fmt job formats the file that holds them and never their contents, and no gate runs cargo oxide new.

The existing test also pinned the defect in place:

assert!(files.main_rs.starts_with(
    "use cuda_device::{kernel, launch_bounds, launch_contract, thread, DisjointSlice};"
));

It now asserts the canonical line and leaves ordering to the new test.

The test runs real rustfmt

scaffold_templates_are_rustfmt_clean renders each template and runs the real rustfmt over it, taking the edition back out of the scaffolded Cargo.toml so the check cannot drift from the manifest it describes.

I deliberately did not re-derive rustfmt's import ordering in the test: such a check agrees with a stale template exactly as readily as with a correct one, and sees nothing outside the import block. release_depfile_path_matches_real_cargo_uplift_for_hyphenated_bin shells out to real cargo for the same reason, so this follows that precedent. If rustfmt is absent the test fails loudly rather than passing quietly; it is in rust-toolchain.toml's component list.

Mutations (each applied to the fixed tree, each caught)

mutation result
sync template reverted to the old import order caught
async template reverted to the old import order caught
non-import drift — one line of the sync body re-indented caught
trailing whitespace after a sync import caught
control: unmodified fixed tree passes

The third and fourth matter: they are drift a hand-written ordering check would sail past.

The book moves with it

getting-started/hello-gpu.md presents the async listing as "the generated async vecadd template". #548 exists precisely to keep it in step with scaffold_main_rs(true), and nothing automated holds them together, so leaving it would have silently re-opened that issue. Parity is verified by checking the scaffold's rendered import block appears verbatim in the page.

The book's other use cuda_device::{...} blocks are hand-written examples rather than scaffold output, so they are out of scope here — book code blocks are not formatted by anything today, which is a separate gap I am happy to file if wanted.

Scope

Test plan

  • cargo test -p cargo-oxide — 186 passed, 0 failed (185 + the new test).
  • cargo clippy -p cargo-oxide --all-targets -- -D warnings — clean.
  • cargo fmt --check — clean.
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -p cargo-oxide — clean.
  • sphinx-build -W --keep-going -b html cuda-oxide-book — build succeeded.
  • Guard scripts (check-book-api-names, check-cli-doc-coverage, check-crate-inventory, check-spdx-headers, check-toolchain-parity, check-test-matrix-coverage, check-error-example-status, check-example-license-policy, check-reserved-prefixes, check-book-catalog-stamp) — all pass.
  • End to end: scaffolded both templates with the built binary and confirmed cargo fmt --check is clean in each.

Both scaffold templates declared their imports in an order rustfmt does not
produce, so a freshly created project failed `cargo fmt --check` on code the
user never wrote:

    $ cargo oxide new demo && cd demo && cargo oxide fmt --check
    Diff in src/main.rs:1:
    -use cuda_device::{kernel, launch_bounds, launch_contract, thread, DisjointSlice};
    -use cuda_host::cuda_module;
     use cuda_core::{CudaContext, DeviceBuffer, LaunchConfig1D};
    ❌ Some files need formatting. Run: cargo oxide fmt

Two orderings were wrong in both templates: the crates were not sorted
(`cuda_device`/`cuda_host` preceded `cuda_core`, and in the async template
also `cuda_async`), and inside the braces `DisjointSlice` sorted after the
lowercase items under the scaffold's `edition = "2024"`. CONTRIBUTING points
contributors at `cargo oxide fmt`, so this is the first thing a new project
is told to run.

Nothing formats these today: the templates are string constants, so the
repository's own fmt job formats the file holding them and never their
contents, and no gate runs `cargo oxide new`.

Both templates now carry rustfmt's own output, pinned by
`scaffold_templates_are_rustfmt_clean`, which runs the real `rustfmt` over
each rendered template and takes the edition from the scaffolded
`Cargo.toml` so the two cannot drift. A re-derived ordering check would have
agreed with the stale template just as readily, and would not have noticed
drift elsewhere in the file.

`scaffold_sync_template_uses_launch_contract_and_docs` asserted the old
unformatted line, so the defect was an asserted invariant; it now asserts the
canonical one and leaves ordering to the new test.

The book's `getting-started/hello-gpu.md` presents one of these listings as
the generated async template, and NVIDIA#548 exists to keep it in step with
`scaffold_main_rs(true)` with no automated guard between them, so it moves
with the scaffold here.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
@nihalpasham

Copy link
Copy Markdown
Collaborator

Reviewed and validated; nothing to change, this is ready as-is. The root-cause read is right (the templates are string constants, so the repo's own fmt gate formats the file holding the strings and never the text inside them, and the old test asserted the stale line), and the fix follows what cargo new itself does: ship canonical text, enforce it with a test that runs the real formatter.

Validated on today's main + the new nightly pin (your branch predates the toolchain bump, so this was the open question):

  • merge onto main: clean; cargo test -p cargo-oxide 190 passed incl. the new scaffold_templates_are_rustfmt_clean; clippy -D warnings + fmt clean
  • end to end: scaffolded both flavors with the built binary, ran rustfmt 1.10.0-nightly (edition read from the generated manifest) over the output:
$ cargo oxide new demo && cd demo && cargo oxide fmt --check
before: Diff in src/main.rs:1  (a project the user didn't write a line of)
after:  clean, exit 0          (same for --async)
  • book parity: the block captioned as the generated async template matches the rendered template through the whole import block and main() signature; the tail's teaching comments are the pre-existing "minor formatting edits" the caption already discloses, so we left that alone

The test shelling out to the real rustfmt instead of re-deriving its rules is the part that keeps this fixed: next nightly that changes canonical form fails the test instead of the user's first fmt --check. CI approval + landing next. If you want to file the issue for the hand-written book blocks that still use the old import order, that's a fine follow-up.

@nihalpasham nihalpasham added bug Something isn't working cargo-oxide The `cargo oxide` subcommand and build automation QoL-improvement Quality-of-life improvement; not a correctness fix labels Aug 30, 2026
@nihalpasham
nihalpasham merged commit 990fcd0 into NVIDIA:main Aug 30, 2026
52 checks passed
nihalpasham added a commit that referenced this pull request Sep 6, 2026
* docs(book): put the book's Rust blocks on rustfmt's import order

#1193 made `cargo oxide new` emit rustfmt-clean projects, because a scaffolded
project failing its own first `cargo oxide fmt --check` is a bad first five
minutes. The book's hand-written blocks kept the old order, so a reader who
copies one into a project gets the same diff on the same command.

Measured over every tracked book page, taking each ```rust block that contains
a `cuda_*` import and running the pinned rustfmt at the book's edition:

    already rustfmt-clean                            58
    differ in imports only         <- fixed here     15
    differ in other formatting     <- left alone     23

The 15 are the same two orderings #1193 fixed in the templates: crates not
sorted (`cuda_device`/`cuda_host` ahead of `cuda_core`, and `cuda_async` where
present), and `DisjointSlice` and friends after the lowercase items inside the
braces rather than before them under style edition 2024.

The 23 differ for reasons that have nothing to do with imports -- one
parameter per line in a function signature, `{ /* ... */ }` split across two
lines, a wrapped call collapsed. Reformatting deliberately laid-out teaching
code is a different change with a different argument, so those blocks are
untouched.

Only rustfmt's import region is spliced back into each block, not its whole
formatted output, so every non-import line stays byte-identical. Taking the
whole block instead reflows that teaching code, which is what made the
distinction worth drawing rather than assuming.

Nothing catches this today: a markdown code block is compiled by nothing and
formatted by nothing, and `check-book-api-names.sh` verifies that the device-API
names the docs show exist, which is a different property and already passes.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>

* docs(book): finish the import-order sweep on the remaining 30 blocks

The first commit put 17 Rust blocks in 14 book pages on rustfmt's
edition-2024 import order. 30 blocks in 12 pages still carried the old
order (crates unsorted, uppercase names after lowercase ones). This
splices rustfmt's output for the top-level `use` region into each of
them, the same mechanism the first commit used: 16 whole-item blocks
and 14 partial snippets whose `use` lines format on their own. Every
changed line is a `use` statement or a brace-list continuation; no
other line moved, so the one-param-per-line teaching layout stays.

Also adds the `DisjointSlice` import the `reduce_dynamic` block in the
shared-memory chapter used without importing, then formats that region.

With both commits the book has one import order: 47 blocks in 18 pages
changed, 0 blocks differ from `rustfmt --edition 2024` in their imports.

Signed-off-by: nihalpasham <nihalp@nvidia.com>

* Add a status guard for the book's Rust import order

A markdown block is compiled by nothing and formatted by nothing, which
is how 47 blocks drifted from the order `cargo fmt` writes in an
edition-2024 `cargo oxide new` project after the scaffold templates
were fixed with a real-rustfmt test.

scripts/check-book-import-order.sh walks every tracked ```rust block
under cuda-oxide-book/, extracts each contiguous run of top-level `use`
items, and runs `rustfmt --edition 2024 --check` on that region alone.
It prints the page, the fence line and rustfmt's diff, and exits
non-zero. Signatures, bodies and comments are never formatted, so
teaching layout and partial snippets with no enclosing item cannot
trip it. A `use` line rustfmt cannot parse is reported too. Parse
self-test: at least 50 blocks with imports must be found.

rustfmt comes from rust-toolchain.toml through the rustup proxy, as in
fmt.yml, so the pinned nightly's order is the one checked.

On the branch before the sweep the guard listed all 30 stale blocks in
12 pages; after it, 98 import regions in 98 blocks across 42 pages pass.
Wired into status-guard.yml as `book-import-order` and into the
Justfile's `check-guards` recipe, which mirrors that workflow.

Signed-off-by: nihalpasham <nihalp@nvidia.com>

---------

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Signed-off-by: nihalpasham <nihalp@nvidia.com>
Co-authored-by: nihalpasham <nihalp@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cargo-oxide The `cargo oxide` subcommand and build automation QoL-improvement Quality-of-life improvement; not a correctness fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cargo oxide new scaffolds a project that fails cargo oxide fmt --check

2 participants