Skip to content
Draft
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
12 changes: 12 additions & 0 deletions cuda-oxide/crates/cargo-oxide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,18 @@ links against. It builds the crate in NVVM IR mode, then runs the emitted
`compute_XX`, or a bare `XX`. The default output path is `<crate-dir>/<crate>.ltoir`;
`-o/--output` overrides it.

For a standalone project, omit `<crate>` to use its Cargo target name. The
directory name may differ from the package name, and hyphens in the target name
become underscores in artifact filenames. A package-named target is preferred;
otherwise the package must have one binary or library target, or its target name
must be supplied explicitly. The positional `<crate>` is this Cargo target name;
`default-run` does not select artifacts. This also applies when invoked from a
member of a user Cargo workspace: that member supplies the target, and the IR
and default LTO-IR output are placed in the invocation directory. An explicit
`CUDA_OXIDE_PTX_DIR` selects the intermediate IR directory for both writing and
reading; relative values are resolved from the Cargo workspace root. The process
environment takes precedence over project configuration.

```bash
cargo oxide emit-ltoir standalone_device_fn --arch sm_100
cargo oxide emit-ltoir my_simt_crate --arch sm_120 -o build/simt.ltoir
Expand Down
1 change: 1 addition & 0 deletions cuda-oxide/crates/cargo-oxide/src/commands/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct OxideConfig {
///
/// Built once at startup by [`resolve_context`] and passed by reference to
/// every command handler. Avoids repeated filesystem walks and backend builds.
#[derive(Clone)]
pub struct Context {
/// Absolute path to the workspace root (contains top-level `Cargo.toml`).
pub workspace_root: PathBuf,
Expand Down
58 changes: 57 additions & 1 deletion cuda-oxide/crates/cargo-oxide/src/commands/host_cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,62 @@ pub(super) struct CargoExecutableArtifact {
pub(super) path: PathBuf,
}

/// Infer the artifact stem for a standalone project from Cargo's targets.
/// Keep the package-named target when present, as for workspace examples;
/// otherwise a sole binary or library target supplies its own crate name.
pub fn standalone_crate_name(manifest_dir: &Path) -> Result<String, String> {
let metadata = cargo_metadata(manifest_dir)?;
let manifest_path = manifest_dir.join("Cargo.toml");
let manifest_path = manifest_path
.canonicalize()
.map_err(|error| format!("could not resolve {}: {error}", manifest_path.display()))?;
let packages = metadata
.get("packages")
.and_then(|packages| packages.as_array())
.ok_or_else(|| "Cargo metadata did not include packages".to_string())?;
let selected = cargo_selected_packages(&metadata, packages, &manifest_path)?;
let [package] = selected.as_slice() else {
return Err("run from a single Cargo package to select its device artifact".to_string());
};
let package_name = cargo_package_name(package)?;
let targets = package
.get("targets")
.and_then(|targets| targets.as_array())
.ok_or_else(|| "Cargo metadata did not include package targets".to_string())?;
let mut names: Vec<_> = targets
.iter()
.filter(|target| {
target
.get("kind")
.and_then(|kind| kind.as_array())
.is_some_and(|kinds| {
kinds.iter().any(|kind| {
matches!(
kind.as_str(),
Some("bin" | "lib" | "rlib" | "staticlib" | "cdylib" | "dylib")
)
})
})
})
.filter_map(|target| target.get("name").and_then(|name| name.as_str()))
.map(artifact_stem)
.collect();
names.sort();
names.dedup();
let package_stem = artifact_stem(package_name);
if names.contains(&package_stem) {
return Ok(package_stem);
}
if let [name] = names.as_slice() {
return Ok(name.clone());
}
Err(format!(
"could not choose a device artifact for package `{package_name}`; \
pass the binary or library target name explicitly (available: {})",
names.join(", ")
))
}

pub(super) fn cargo_executable_selection(
manifest_dir: &Path,
explicit_bin: Option<&str>,
Expand Down Expand Up @@ -365,7 +421,7 @@ fn cargo_package_manifest_matches(package: &serde_json::Value, manifest_path: &P
.is_some_and(|path| path == manifest_path)
}

fn cargo_metadata(manifest_dir: &Path) -> Result<serde_json::Value, String> {
pub(super) fn cargo_metadata(manifest_dir: &Path) -> Result<serde_json::Value, String> {
let output = Command::new("cargo")
.args(["metadata", "--format-version=1", "--no-deps"])
.current_dir(manifest_dir)
Expand Down
62 changes: 59 additions & 3 deletions cuda-oxide/crates/cargo-oxide/src/commands/ltoir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

use std::path::Path;
use std::path::{Path, PathBuf};

use super::*;

Expand Down Expand Up @@ -56,12 +56,23 @@ pub fn emit_ltoir(
});
let sm_arch = parsed_arch.sm();

// Cargo invokes rustc from its workspace root even when the selected
// package is a member. Pin this command's IR directory explicitly instead
// of deriving it from the compiler's working directory.
let (build_ctx, ir_dir) =
ltoir_build_context(ctx, &example_dir, std::env::var_os("CUDA_OXIDE_PTX_DIR"))
.unwrap_or_else(|error| {
eprintln!("Error: {error}");
std::process::exit(1);
});
clean_generated_files(&ir_dir, example);

// Step 1: build in NVVM IR mode so the backend writes `<crate>.ll` as
// libNVVM-ready NVVM IR. codegen_build exits on build failure. Pass
// quiet=true so the intermediate "✓ Build succeeded" line is suppressed;
// emit_ltoir prints its own unified summary at the end.
codegen_build(
ctx,
&build_ctx,
example,
verbose,
true,
Expand All @@ -76,7 +87,7 @@ pub fn emit_ltoir(
);

// Step 2: compile that NVVM IR to LTOIR via libNVVM -gen-lto.
let ll_path = emitted_ll_path(&example_dir, example);
let ll_path = emitted_ll_path(&ir_dir, example);
let ir = std::fs::read(&ll_path).unwrap_or_else(|e| {
eprintln!(
"Error: could not read emitted NVVM IR at {}: {e}",
Expand Down Expand Up @@ -165,6 +176,51 @@ pub fn emit_ltoir(
);
}

/// Select the same IR directory for the build and subsequent reader. Keep an
/// inherited override strongest, followed by project config. Relative explicit
/// paths are relative to rustc's Cargo workspace cwd, matching existing backend
/// semantics. Without an override, emit-ltoir owns its invocation directory.
pub(super) fn ltoir_build_context(
ctx: &Context,
example_dir: &Path,
inherited_dir: Option<std::ffi::OsString>,
) -> Result<(Context, PathBuf), String> {
let configured = inherited_dir
.or_else(|| project_config_env(ctx, "CUDA_OXIDE_PTX_DIR").map(std::ffi::OsString::from));
let ir_dir = match configured {
None => example_dir.to_path_buf(),
Some(path) => {
let path = PathBuf::from(path);
if path.is_absolute() {
path
} else {
let metadata = cargo_metadata(example_dir)?;
let root = metadata
.get("workspace_root")
.and_then(|path| path.as_str())
.ok_or_else(|| "Cargo metadata did not include workspace_root".to_string())?;
Path::new(root).join(path)
}
}
};
let value = ir_dir.to_str().ok_or_else(|| {
format!(
"NVVM IR output directory is not Unicode: {}",
ir_dir.display()
)
})?;
let mut build_ctx = ctx.clone();
build_ctx
.config
.env
.retain(|(key, _)| key != "CUDA_OXIDE_PTX_DIR");
build_ctx
.config
.env
.push(("CUDA_OXIDE_PTX_DIR".into(), value.into()));
Ok((build_ctx, ir_dir))
}

/// Normalize a target architecture to libNVVM's `compute_XX` form.
///
/// Accepts `sm_XX` (the form `--arch` and the rest of cargo-oxide use),
Expand Down
1 change: 1 addition & 0 deletions cuda-oxide/crates/cargo-oxide/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub use doctor::*;
pub use examples_list::*;
use fingerprint::*;
pub use fmt::*;
pub use host_cargo::standalone_crate_name;
use host_cargo::*;
use interop::*;
pub use ltoir::*;
Expand Down
121 changes: 121 additions & 0 deletions cuda-oxide/crates/cargo-oxide/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,127 @@ fn emit_ltoir_paths_use_normalized_crate_stem() {
assert_eq!(default_ltoir_path(dir, "vecadd"), dir.join("vecadd.ltoir"));
}

#[test]
fn standalone_artifact_name_comes_from_cargo_targets() {
for (target, source, expected) in [
("", "src/main.rs", "device_export"),
(
"[[bin]]\nname = \"custom-bin\"\npath = \"src/main.rs\"\n",
"src/main.rs",
"custom_bin",
),
(
"[lib]\nname = \"custom_library\"\n",
"src/lib.rs",
"custom_library",
),
] {
let root = unique_temp_dir("directory_unrelated_to_package");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(
root.join("Cargo.toml"),
format!(
"[package]\nname = \"device-export\"\nversion = \"0.1.0\"\n\
edition = \"2024\"\n[workspace]\n{target}"
),
)
.unwrap();
std::fs::write(root.join(source), "fn main() {}\n").unwrap();

let name = standalone_crate_name(&root).unwrap();
assert_eq!(name, expected);
assert_eq!(
emitted_ll_path(&root, &name),
root.join(format!("{expected}.ll"))
);
assert_eq!(
default_ltoir_path(&root, &name),
root.join(format!("{expected}.ltoir"))
);
std::fs::remove_dir_all(root).unwrap();
}
}

#[test]
fn standalone_artifact_name_preserves_package_target_and_rejects_ambiguity() {
let root = unique_temp_dir("cargo_oxide_artifact_multi_target");
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/main.rs"), "fn main() {}\n").unwrap();
std::fs::write(root.join("src/other.rs"), "fn main() {}\n").unwrap();
for name in ["device-export", "other-package"] {
std::fs::write(
root.join("Cargo.toml"),
format!(
"[package]\nname = \"{name}\"\nversion = \"0.1.0\"\n\
edition = \"2024\"\ndefault-run = \"other\"\n[workspace]\n\
[[bin]]\nname = \"device-export\"\npath = \"src/main.rs\"\n\
[[bin]]\nname = \"other\"\npath = \"src/other.rs\"\n"
),
)
.unwrap();
let result = standalone_crate_name(&root);
if name == "device-export" {
// `cargo build` builds both bins; default-run does not select the IR.
assert_eq!(result.unwrap(), "device_export");
} else {
let error = result.unwrap_err();
assert!(
error.contains("pass the binary or library target name explicitly"),
"{error}"
);
}
}
std::fs::remove_dir_all(root).unwrap();
}

#[test]
fn standalone_artifact_name_uses_workspace_package_selection() {
let root = unique_temp_dir("cargo_oxide_artifact_workspace");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = [\"first\", \"second\"]\ndefault-members = [\"first\"]\nresolver = \"3\"\n",
)
.unwrap();
for member in ["first", "second"] {
let dir = root.join(member);
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(
dir.join("Cargo.toml"),
format!(
"[package]\nname = \"{member}-package\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"
),
)
.unwrap();
std::fs::write(dir.join("src/main.rs"), "fn main() {}\n").unwrap();
}
assert_eq!(standalone_crate_name(&root).unwrap(), "first_package");
let member = root.join("second");
assert_eq!(standalone_crate_name(&member).unwrap(), "second_package");
let ctx = test_context(OxideConfig::default());
let (build_ctx, ir_dir) = ltoir_build_context(&ctx, &member, None).unwrap();
assert_eq!(ir_dir, member);
assert_eq!(
project_config_env(&build_ctx, "CUDA_OXIDE_PTX_DIR"),
member.to_str()
);

// An explicit relative backend override retains Cargo workspace semantics,
// even though this command was invoked from a different selected member.
let mut ctx = ctx;
ctx.config
.env
.push(("CUDA_OXIDE_PTX_DIR".into(), "configured-ir".into()));
let (_, configured) = ltoir_build_context(&ctx, &member, None).unwrap();
assert_eq!(
configured,
root.canonicalize().unwrap().join("configured-ir")
);
let (_, inherited) = ltoir_build_context(&ctx, &member, Some("inherited-ir".into())).unwrap();
assert_eq!(inherited, root.canonicalize().unwrap().join("inherited-ir"));
std::fs::remove_dir_all(root).unwrap();
}

#[test]
fn generated_file_cleanup_preserves_ltoir_cubin_cache() {
let unique = std::time::SystemTime::now()
Expand Down
6 changes: 6 additions & 0 deletions cuda-oxide/crates/cargo-oxide/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,12 @@ fn resolve_example_name(name: Option<String>, ctx: &commands::Context, subcomman
return n;
}
if !ctx.is_workspace {
if subcommand == "emit-ltoir" {
return commands::standalone_crate_name(&ctx.workspace_root).unwrap_or_else(|error| {
eprintln!("Error: {error}");
std::process::exit(1);
});
}
return std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
Expand Down
Loading