diff --git a/.github/workflows/status-guard.yml b/.github/workflows/status-guard.yml index 5704006c2..d7ce00cfa 100644 --- a/.github/workflows/status-guard.yml +++ b/.github/workflows/status-guard.yml @@ -229,11 +229,12 @@ jobs: - name: Verify docs and templates spell the shared crates' simt paths run: bash cuda-oxide/scripts/check-host-api-paths.sh - # The shared host-crate release is named once in the root workspace and - # copied into every example manifest and into the `cargo oxide new` - # templates. A drifted example resolves a second runtime and breaks the - # shared example build cache; a drifted scaffold hands each new project a - # runtime the generated code was not written against. + # The shared host crates are path dependencies in the SIMT and example + # workspaces, and the `cargo oxide new` templates take them from the same + # git source as cuda-host. A drifted example resolves a second runtime and + # breaks the shared example build cache; a scaffold naming a crates.io copy + # hands each new project two cuda_core crates, and the project fails to + # compile. shared-crate-pin-parity: name: shared host-crate pin parity runs-on: ubuntu-latest @@ -243,7 +244,7 @@ jobs: uses: actions/checkout@v4 # Reads sources only: no rustup, no cargo, no CUDA toolkit. - - name: Verify the host crates are path dependencies at tip of tree and the scaffold matches the root version + - name: Verify the host crates are path dependencies at tip of tree and the scaffold takes them from git beside cuda-host run: bash cuda-oxide/scripts/check-shared-crate-pin.sh # cuda-oxide/crates/oxide-artifacts is only the publish source now; the diff --git a/cuda-oxide/Cargo.toml b/cuda-oxide/Cargo.toml index 47a76791f..63f14ff30 100644 --- a/cuda-oxide/Cargo.toml +++ b/cuda-oxide/Cargo.toml @@ -83,9 +83,8 @@ cuda-macros = { path = "crates/cuda-macros", default-features = false } # the crates that depend on them in this tree are not published, so nothing # needs one (#1459). Examples path-depend on the same directories so the graph # has one cuda_core (and one cuda-async / cuda-bindings). `cargo oxide new` -# scaffolds out-of-tree projects on the crates.io release instead; its -# SHARED_HOST_CRATES_VERSION is checked against the root manifests by -# scripts/check-shared-crate-pin.sh. +# names every CUDA crate from one git source for the same reason; +# scripts/check-shared-crate-pin.sh enforces both. cuda-bindings = { path = "../cuda-bindings" } cuda-core = { path = "../cuda-core" } cuda-async = { path = "../cuda-async" } diff --git a/cuda-oxide/crates/cargo-oxide/src/commands/scaffold.rs b/cuda-oxide/crates/cargo-oxide/src/commands/scaffold.rs index 599dd8634..a886b212e 100644 --- a/cuda-oxide/crates/cargo-oxide/src/commands/scaffold.rs +++ b/cuda-oxide/crates/cargo-oxide/src/commands/scaffold.rs @@ -13,12 +13,6 @@ use super::*; const GIT_REPO: &str = "https://github.com/NVlabs/cuda-oxide.git"; -/// crates.io release of the host-side runtime (`cuda-core`, `cuda-async`). -/// In-tree examples and `cuda-host` path-depend on the git-root copies; -/// `cargo oxide new` still uses this version because a standalone project -/// is not inside the monorepo. -pub(super) const SHARED_HOST_CRATES_VERSION: &str = "0.4.0"; - const RUST_TOOLCHAIN_TOML: &str = r#"[toolchain] channel = "nightly-2026-10-11" components = ["rust-src", "rustc-dev", "rust-analyzer", "clippy", "rustfmt", "llvm-tools"] @@ -104,6 +98,12 @@ cargo oxide run ) } +/// Every CUDA crate comes from `GIT_REPO`. `cuda-host` depends on `cuda-core` +/// and `cuda-async` by path at tip of tree, so in a project that takes +/// `cuda-host` from git those crates resolve from the same checkout. Naming +/// them from crates.io instead adds a second, distinct `cuda_core` whose +/// `CudaContext` and `CudaModule` do not unify with the ones `#[cuda_module]` +/// expands against, and the project fails to compile. fn scaffold_cargo_toml(name: &str, async_mode: bool) -> String { if async_mode { format!( @@ -117,9 +117,10 @@ edition = "2024" [dependencies] cuda-device = {{ git = "{GIT_REPO}" }} cuda-host = {{ git = "{GIT_REPO}", features = ["async"] }} -# Shared host-side runtime, published from NVIDIA/cuda-rust. -cuda-core = "{SHARED_HOST_CRATES_VERSION}" -cuda-async = "{SHARED_HOST_CRATES_VERSION}" +# Shared host-side runtime. It has to come from the same repository as +# cuda-host, which depends on it; a crates.io copy is a second cuda-core. +cuda-core = {{ git = "{GIT_REPO}" }} +cuda-async = {{ git = "{GIT_REPO}" }} tokio = {{ version = "1", features = ["rt", "rt-multi-thread", "macros"] }} "# ) @@ -135,8 +136,9 @@ edition = "2024" [dependencies] cuda-device = {{ git = "{GIT_REPO}" }} cuda-host = {{ git = "{GIT_REPO}" }} -# Shared host-side runtime, published from NVIDIA/cuda-rust. -cuda-core = "{SHARED_HOST_CRATES_VERSION}" +# Shared host-side runtime. It has to come from the same repository as +# cuda-host, which depends on it; a crates.io copy is a second cuda-core. +cuda-core = {{ git = "{GIT_REPO}" }} "# ) } diff --git a/cuda-oxide/crates/cargo-oxide/src/commands/tests.rs b/cuda-oxide/crates/cargo-oxide/src/commands/tests.rs index 1af0d7163..876349255 100644 --- a/cuda-oxide/crates/cargo-oxide/src/commands/tests.rs +++ b/cuda-oxide/crates/cargo-oxide/src/commands/tests.rs @@ -4768,15 +4768,45 @@ fn scaffold_sync_template_uses_launch_contract_and_docs() { assert!(files.main_rs.contains("prepare_vecadd")); assert!(files.main_rs.contains("LaunchConfig1D")); assert!(!files.main_rs.contains("LaunchConfig::for_num_elems")); - // The host runtime is the crates.io release of the git-root crates. - // In-tree examples path-depend on those crates; a freshly scaffolded - // project is not inside this monorepo, so it cannot use those paths. - assert!( - files + // cuda-core comes from the same git source as cuda-host, which depends on + // it at tip of tree; a crates.io copy would be a second cuda_core. + assert!(files.cargo_toml.contains("cuda-core = { git = \"")); + assert!(!files.cargo_toml.contains("cuda-core = \"")); +} + +/// Every CUDA crate in either template resolves from the git source +/// `cuda-device` names. A host crate taken from crates.io next to a git +/// `cuda-host` puts two `cuda_core` crates in the graph, and `#[cuda_module]` +/// then fails to type-check. +#[test] +fn scaffold_takes_every_cuda_crate_from_one_git_source() { + for async_mode in [false, true] { + let files = scaffold_files("one_source", async_mode); + let git_source = |line: &str| { + line.split("git = \"") + .nth(1) + .and_then(|rest| rest.split('"').next()) + .map(str::to_owned) + }; + let cuda_lines: Vec<&str> = files .cargo_toml - .contains(&format!("cuda-core = \"{SHARED_HOST_CRATES_VERSION}\"")) - ); - assert!(!files.cargo_toml.contains("cuda-core = {")); + .lines() + .filter(|line| line.starts_with("cuda-")) + .collect(); + assert!(cuda_lines.len() >= 3, "{cuda_lines:?}"); + let device = cuda_lines + .iter() + .find(|line| line.starts_with("cuda-device")) + .and_then(|line| git_source(line)) + .expect("cuda-device comes from git"); + for line in cuda_lines { + assert_eq!( + git_source(line).as_deref(), + Some(device.as_str()), + "async={async_mode}: {line}" + ); + } + } } #[test] @@ -4794,13 +4824,10 @@ fn scaffold_async_template_keeps_async_deps_and_docs() { assert!(files.main_rs.contains("vecadd_async")); assert!(files.main_rs.contains("use cuda_host::cuda_module;")); assert!(!files.main_rs.contains("use cuda_device::{cuda_module")); - // Shared host runtime from crates.io; the SIMT surface lives under simt::. - assert!( - files - .cargo_toml - .contains(&format!("cuda-async = \"{SHARED_HOST_CRATES_VERSION}\"")) - ); - assert!(!files.cargo_toml.contains("cuda-async = {")); + // Shared host runtime from the same git source as cuda-host; the SIMT + // surface lives under simt::. + assert!(files.cargo_toml.contains("cuda-async = { git = \"")); + assert!(!files.cargo_toml.contains("cuda-async = \"")); assert!(!files.cargo_toml.contains("cuda-bindings")); assert!(files.main_rs.contains("use cuda_core::simt::LaunchConfig;")); assert!( diff --git a/cuda-oxide/scripts/check-shared-crate-pin.sh b/cuda-oxide/scripts/check-shared-crate-pin.sh index 0231b2d70..fbf04ae26 100755 --- a/cuda-oxide/scripts/check-shared-crate-pin.sh +++ b/cuda-oxide/scripts/check-shared-crate-pin.sh @@ -15,10 +15,11 @@ # `version` field. Examples are their own [workspace], so they cannot # inherit the SIMT entry, and a crates.io copy there would resolve a # second cuda_core and break type unification with path cuda-host. -# 3. `cargo oxide new` scaffolds out-of-tree projects, which cannot use the -# git-root paths, so its SHARED_HOST_CRATES_VERSION is the one place a -# crates.io version of the host crates lives. It must equal the root -# crates' version until it is derived from the root manifests directly. +# 3. `cargo oxide new` takes cuda-core and cuda-async from the same git +# source as cuda-host. cuda-host depends on them by path, so in a project +# that pulls cuda-host from git they resolve from that checkout; a +# crates.io requirement beside it is a second cuda_core, and the +# scaffolded project fails to type-check. set -euo pipefail export LC_ALL=C cd "$(dirname "$0")/.." @@ -64,13 +65,17 @@ done < <(git -C "${GIT_ROOT}" ls-files \ 'cuda-oxide/crates/rustc-codegen-cuda/examples/*/*/Cargo.toml' \ 'cuda-oxide/crates/rustc-codegen-cuda/examples/*/*/*/Cargo.toml') -# 3. The scaffold's crates.io version. -scaffold_version="$(sed -n -E 's/^pub\(super\) const SHARED_HOST_CRATES_VERSION: &str = "([^"]+)";/\1/p' "${SCAFFOLD}")" -if [ -z "${scaffold_version}" ]; then - echo "error: ${SCAFFOLD}: SHARED_HOST_CRATES_VERSION not found" >&2; status=1 -elif [ "${scaffold_version}" != "${root_version}" ]; then - echo "error: ${SCAFFOLD}: SHARED_HOST_CRATES_VERSION is ${scaffold_version}, root host crates are ${root_version}" >&2; status=1 +# 3. The scaffold names every host crate from GIT_REPO. +scaffold_lines="$(grep -E '^(cuda-bindings|cuda-core|cuda-async)[[:space:]]*=' "${SCAFFOLD}" || true)" +if [ -z "${scaffold_lines}" ]; then + echo "error: ${SCAFFOLD}: no host-crate dependency lines in the templates" >&2; status=1 fi +while IFS= read -r line; do + [ -n "${line}" ] || continue + if [[ ! "${line}" =~ git[[:space:]]*=[[:space:]]*\"\{GIT_REPO\}\" ]]; then + echo "error: ${SCAFFOLD}: '${line}' (want git = \"{GIT_REPO}\", the source cuda-host comes from)" >&2; status=1 + fi +done <<< "${scaffold_lines}" -[ "${status}" -eq 0 ] && echo "OK: host crates are path dependencies at tip of tree; scaffold pins crates.io ${root_version}." +[ "${status}" -eq 0 ] && echo "OK: host crates are path dependencies at tip of tree; the scaffold takes them from git beside cuda-host." exit "${status}"