diff --git a/cuda-oxide/crates/cargo-oxide/README.md b/cuda-oxide/crates/cargo-oxide/README.md index 2ed176b4ed..f6aa41e396 100644 --- a/cuda-oxide/crates/cargo-oxide/README.md +++ b/cuda-oxide/crates/cargo-oxide/README.md @@ -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 `/.ltoir`; `-o/--output` overrides it. +For a standalone project, omit `` 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 `` 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 diff --git a/cuda-oxide/crates/cargo-oxide/src/commands/context.rs b/cuda-oxide/crates/cargo-oxide/src/commands/context.rs index 66fa592d43..d226ece019 100644 --- a/cuda-oxide/crates/cargo-oxide/src/commands/context.rs +++ b/cuda-oxide/crates/cargo-oxide/src/commands/context.rs @@ -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, diff --git a/cuda-oxide/crates/cargo-oxide/src/commands/host_cargo.rs b/cuda-oxide/crates/cargo-oxide/src/commands/host_cargo.rs index 36b1f36724..e28ed9e2d7 100644 --- a/cuda-oxide/crates/cargo-oxide/src/commands/host_cargo.rs +++ b/cuda-oxide/crates/cargo-oxide/src/commands/host_cargo.rs @@ -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 { + 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>, @@ -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 { +pub(super) fn cargo_metadata(manifest_dir: &Path) -> Result { let output = Command::new("cargo") .args(["metadata", "--format-version=1", "--no-deps"]) .current_dir(manifest_dir) diff --git a/cuda-oxide/crates/cargo-oxide/src/commands/ltoir.rs b/cuda-oxide/crates/cargo-oxide/src/commands/ltoir.rs index feecbef80e..1e1530f0ef 100644 --- a/cuda-oxide/crates/cargo-oxide/src/commands/ltoir.rs +++ b/cuda-oxide/crates/cargo-oxide/src/commands/ltoir.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use std::path::Path; +use std::path::{Path, PathBuf}; use super::*; @@ -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 `.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, @@ -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}", @@ -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, +) -> 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), diff --git a/cuda-oxide/crates/cargo-oxide/src/commands/mod.rs b/cuda-oxide/crates/cargo-oxide/src/commands/mod.rs index 10bc374ee6..81e29f322a 100644 --- a/cuda-oxide/crates/cargo-oxide/src/commands/mod.rs +++ b/cuda-oxide/crates/cargo-oxide/src/commands/mod.rs @@ -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::*; diff --git a/cuda-oxide/crates/cargo-oxide/src/commands/tests.rs b/cuda-oxide/crates/cargo-oxide/src/commands/tests.rs index bf0551deb2..ea8fd241d3 100644 --- a/cuda-oxide/crates/cargo-oxide/src/commands/tests.rs +++ b/cuda-oxide/crates/cargo-oxide/src/commands/tests.rs @@ -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() diff --git a/cuda-oxide/crates/cargo-oxide/src/main.rs b/cuda-oxide/crates/cargo-oxide/src/main.rs index 9bdef87cfa..685ce9bae1 100644 --- a/cuda-oxide/crates/cargo-oxide/src/main.rs +++ b/cuda-oxide/crates/cargo-oxide/src/main.rs @@ -1203,6 +1203,12 @@ fn resolve_example_name(name: Option, 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()))