Skip to content
Open
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
24 changes: 13 additions & 11 deletions cuda-oxide/crates/cargo-oxide/src/commands/scaffold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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!(
Expand All @@ -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"] }}
"#
)
Expand All @@ -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}" }}
"#
)
}
Expand Down
57 changes: 42 additions & 15 deletions cuda-oxide/crates/cargo-oxide/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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!(
Expand Down
27 changes: 16 additions & 11 deletions cuda-oxide/scripts/check-shared-crate-pin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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")/.."
Expand Down Expand Up @@ -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}"