From 5e8235ab2eb039a924dbc8943718583e13b4d5d8 Mon Sep 17 00:00:00 2001 From: Vyncint Ng <115854244+vyncint@users.noreply.github.com> Date: Sat, 29 Aug 2026 06:48:02 +0700 Subject: [PATCH] fix(cargo-oxide): make `cargo oxide new` emit rustfmt-clean projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #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> --- crates/cargo-oxide/src/commands/scaffold.rs | 10 ++-- crates/cargo-oxide/src/commands/tests.rs | 58 +++++++++++++++++++- cuda-oxide-book/getting-started/hello-gpu.md | 4 +- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/crates/cargo-oxide/src/commands/scaffold.rs b/crates/cargo-oxide/src/commands/scaffold.rs index e02dc36d15..6dd2c33332 100644 --- a/crates/cargo-oxide/src/commands/scaffold.rs +++ b/crates/cargo-oxide/src/commands/scaffold.rs @@ -137,11 +137,11 @@ cuda-core = {{ git = "{GIT_REPO}" }} fn scaffold_main_rs(async_mode: bool) -> String { if async_mode { - r#"use cuda_device::{kernel, thread, DisjointSlice}; -use cuda_host::cuda_module; -use cuda_async::device_context::init_device_contexts; + r#"use cuda_async::device_context::init_device_contexts; use cuda_async::device_operation::DeviceOperation; use cuda_core::LaunchConfig; +use cuda_device::{DisjointSlice, kernel, thread}; +use cuda_host::cuda_module; #[cuda_module] mod kernels { @@ -231,9 +231,9 @@ async fn main() -> Result<(), Box> { "# .to_string() } else { - r#"use cuda_device::{kernel, launch_bounds, launch_contract, thread, DisjointSlice}; + r#"use cuda_core::{CudaContext, DeviceBuffer, LaunchConfig1D}; +use cuda_device::{DisjointSlice, kernel, launch_bounds, launch_contract, thread}; use cuda_host::cuda_module; -use cuda_core::{CudaContext, DeviceBuffer, LaunchConfig1D}; #[cuda_module] mod kernels { diff --git a/crates/cargo-oxide/src/commands/tests.rs b/crates/cargo-oxide/src/commands/tests.rs index 4b0958e311..47753016e0 100644 --- a/crates/cargo-oxide/src/commands/tests.rs +++ b/crates/cargo-oxide/src/commands/tests.rs @@ -4065,9 +4065,10 @@ fn scaffold_sync_template_uses_launch_contract_and_docs() { assert!(files.gitignore.contains("/target/")); // The template uses the launch_bounds / launch_contract attribute // macros, so the cuda_device import must bring them in; a scaffolded - // project fails to compile without this exact line. - assert!(files.main_rs.starts_with( - "use cuda_device::{kernel, launch_bounds, launch_contract, thread, DisjointSlice};" + // project fails to compile without these names. The order is rustfmt's, + // pinned by `scaffold_templates_are_rustfmt_clean`. + assert!(files.main_rs.contains( + "use cuda_device::{DisjointSlice, kernel, launch_bounds, launch_contract, thread};" )); assert!( files @@ -4096,6 +4097,57 @@ fn scaffold_async_template_keeps_async_deps_and_docs() { assert!(!files.main_rs.contains("use cuda_device::{cuda_module")); } +/// A scaffolded project must already be `rustfmt`-clean. +/// +/// CONTRIBUTING points contributors at `cargo oxide fmt`, so a template that is +/// not canonical makes a brand-new project fail its very first format check on +/// code the user never wrote. +/// +/// This runs the real `rustfmt` over the rendered template instead of +/// re-deriving its import ordering here. A hand-written ordering check would +/// agree with a stale template as readily as with a correct one, and would not +/// notice drift anywhere else in the file -- +/// `release_depfile_path_matches_real_cargo_uplift_for_hyphenated_bin` shells +/// out to real `cargo` for the same reason. +/// +/// The edition is read back out of the scaffolded `Cargo.toml` so this cannot +/// drift from the manifest it is meant to describe. +#[test] +fn scaffold_templates_are_rustfmt_clean() { + for (async_mode, label) in [(false, "sync"), (true, "async")] { + let files = scaffold_files("demo_kernel", async_mode); + + let edition = files + .cargo_toml + .lines() + .find_map(|line| line.trim().strip_prefix("edition = ")) + .map(|value| value.trim().trim_matches('"').to_string()) + .unwrap_or_else(|| panic!("{label} scaffold Cargo.toml declares no edition")); + + let root = unique_temp_dir(&format!("cargo_oxide_scaffold_fmt_{label}")); + std::fs::create_dir_all(&root).unwrap(); + let main_rs = root.join("main.rs"); + std::fs::write(&main_rs, &files.main_rs).unwrap(); + + // A scaffolded project carries no rustfmt.toml, so plain defaults for + // its edition are exactly what `cargo fmt` applies there. + let check = Command::new("rustfmt") + .args(["--edition", edition.as_str(), "--check"]) + .arg(&main_rs) + .output() + .expect("failed to run rustfmt; it is in rust-toolchain.toml's component list"); + + let diff = String::from_utf8_lossy(&check.stdout).into_owned(); + std::fs::remove_dir_all(&root).unwrap(); + + assert!( + check.status.success() && diff.is_empty(), + "the {label} scaffold template is not rustfmt-clean, so `cargo oxide fmt \ + --check` fails in a freshly scaffolded project:\n{diff}" + ); + } +} + #[test] fn scaffold_gitignore_covers_every_clean_artifact_suffix() { let gitignore = scaffold_gitignore(); diff --git a/cuda-oxide-book/getting-started/hello-gpu.md b/cuda-oxide-book/getting-started/hello-gpu.md index d6ea1c18a2..1fbbb0e931 100644 --- a/cuda-oxide-book/getting-started/hello-gpu.md +++ b/cuda-oxide-book/getting-started/hello-gpu.md @@ -211,11 +211,11 @@ The `--async` flag generates a project with `tokio` and `cuda-async` dependencie Here's the generated async vecadd template (with minor formatting edits for readability): ```rust -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; +use cuda_device::{DisjointSlice, kernel, thread}; +use cuda_host::cuda_module; #[cuda_module] mod kernels {