use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use super::{Compiler, CompilerAdapter, CompilerId, RefuseReason};
pub const NVCC_ID: CompilerId = CompilerId::new("nvcc");
pub const ADAPTER: CompilerAdapter =
CompilerAdapter::new(NVCC_ID, "nvcc", NvccCompiler::recognizes);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NvccMode {
Compile,
DeviceLink,
Link,
Lib,
Preprocess,
EmitPtx,
EmitCubin,
EmitFatbin,
EmitOptixIr,
Query,
}
#[derive(Debug, Clone)]
pub struct NvccArgs {
pub program: String,
pub rest: Vec<String>,
pub mode: NvccMode,
pub sources: Vec<PathBuf>,
pub output: Option<PathBuf>,
pub depfile: Option<PathBuf>,
pub depgen_with_compile: bool,
pub implicit_depfile: bool,
pub separate_device_code: bool,
pub device_debug: bool,
pub keep_temps: bool,
pub response_file: bool,
pub deferred_flags: Vec<String>,
pub unknown_flags: Vec<String>,
pub xcompiler_values: Vec<String>,
}
const VALUE_FLAGS: &[&str] = &[
"-D",
"-U",
"-I",
"-isystem",
"-iquote",
"-include",
"-O",
"-std",
"--std",
"-x",
"-L",
"-l",
"-MT",
"-MQ",
"-Xlinker",
"-Xptxas",
"-Xnvlink",
"-gencode",
"-arch",
"-code",
"--gpu-architecture",
"--gpu-code",
"-march",
"-mcpu",
"-mtune",
];
const JOINED_PREFIXES: &[&str] = &[
"-D",
"-U",
"-I",
"-O",
"-std=",
"--std=",
"-x",
"-g",
"-m",
"--expt-",
"-Xfatbin",
"-MT",
"-MQ",
"-Xlinker",
"-Xptxas",
"-Xnvlink",
"-gencode",
"-arch=",
"-code=",
"--gpu-architecture=",
"--gpu-code=",
"--W",
"-W",
];
const BARE_DEFERRED_FLAGS: &[&str] = &[
"-shared",
"--shared",
"-static",
"-v",
"--verbose",
"-w",
"-Wall",
"-W",
];
fn is_nvcc_source(name: &str) -> bool {
let ext = std::path::Path::new(name)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
matches!(
ext,
"cu" | "cuh" | "c" | "cc" | "cpp" | "cxx" | "C" | "h" | "hpp" | "hxx"
)
}
fn parse_rdc_value(value: &str) -> Result<bool> {
match value {
"true" => Ok(true),
"false" => Ok(false),
_ => anyhow::bail!("nvcc: invalid relocatable device code value {value:?}"),
}
}
impl NvccArgs {
pub fn parse(args: &[String]) -> Result<Self> {
let Some(program) = args.first().cloned() else {
anyhow::bail!("nvcc: empty argv");
};
let rest = args[1..].to_vec();
let mut parsed = NvccArgs {
program,
rest,
mode: NvccMode::Link,
sources: Vec::new(),
output: None,
depfile: None,
depgen_with_compile: false,
implicit_depfile: false,
separate_device_code: false,
device_debug: false,
keep_temps: false,
response_file: false,
deferred_flags: Vec::new(),
unknown_flags: Vec::new(),
xcompiler_values: Vec::new(),
};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Vote {
Link,
Compile,
Lib,
DeviceLink,
Emit,
Preprocess,
Query,
}
let mut vote = Vote::Link;
let mut set_vote = |v: Vote, mode: NvccMode, parsed: &mut NvccArgs| {
if v >= vote {
vote = v;
parsed.mode = mode;
}
};
let mut argv = parsed.rest.clone().into_iter();
while let Some(arg) = argv.next() {
let arg = arg.as_str();
let take_value =
|argv: &mut std::vec::IntoIter<String>, flag: &str| -> Result<String> {
argv.next()
.with_context(|| format!("nvcc: {flag} missing value"))
};
match arg {
"-c" | "--compile" => set_vote(Vote::Compile, NvccMode::Compile, &mut parsed),
"-dc" | "--device-c" => {
parsed.separate_device_code = true;
parsed.deferred_flags.push(arg.to_string());
set_vote(Vote::Compile, NvccMode::Compile, &mut parsed);
}
"-dlink" | "--device-link" => {
set_vote(Vote::DeviceLink, NvccMode::DeviceLink, &mut parsed);
}
"--lib" | "-lib" => set_vote(Vote::Lib, NvccMode::Lib, &mut parsed),
"-E" | "--preprocess" => {
set_vote(Vote::Preprocess, NvccMode::Preprocess, &mut parsed);
}
"-ptx" | "--ptx" => set_vote(Vote::Emit, NvccMode::EmitPtx, &mut parsed),
"-cubin" | "--cubin" => {
set_vote(Vote::Emit, NvccMode::EmitCubin, &mut parsed);
}
"-fatbin" | "--fatbin" => {
set_vote(Vote::Emit, NvccMode::EmitFatbin, &mut parsed);
}
"--optix-ir" => set_vote(Vote::Emit, NvccMode::EmitOptixIr, &mut parsed),
"-G" | "--device-debug" => parsed.device_debug = true,
"-keep" | "--keep" | "--save-temps" | "-save-temps" => parsed.keep_temps = true,
"--version" | "-V" | "--help" | "-h" | "--dryrun" => {
set_vote(Vote::Query, NvccMode::Query, &mut parsed);
}
"-o" => {
let value = take_value(&mut argv, "-o")?;
parsed.output = Some(PathBuf::from(value));
}
_ if arg.starts_with("-o") && !arg.starts_with("-O") => {
parsed.output = Some(PathBuf::from(&arg[2..]));
}
"-MF" => {
let value = take_value(&mut argv, "-MF")?;
parsed.depfile = Some(PathBuf::from(&value));
parsed.deferred_flags.push(format!("-MF {value}"));
}
_ if arg.starts_with("-MF") => {
parsed.depfile = Some(PathBuf::from(&arg["-MF".len()..]));
parsed.deferred_flags.push(arg.to_string());
}
"-Xcompiler" | "--compiler-options" => {
let value = take_value(&mut argv, arg)?;
parsed.xcompiler_values.push(value.clone());
parsed.deferred_flags.push(format!("{arg} {value}"));
}
_ if arg.starts_with("-Xcompiler") || arg.starts_with("--compiler-options") => {
let flag = if arg.starts_with("-Xcompiler") {
"-Xcompiler"
} else {
"--compiler-options"
};
let mut value = &arg[flag.len()..];
value = value.strip_prefix('=').unwrap_or(value);
parsed.xcompiler_values.push(value.to_string());
parsed.deferred_flags.push(arg.to_string());
}
"-M" | "-MM" | "-MG" | "-MP" | "--generate-dependencies" => {
parsed.implicit_depfile = true;
}
"-MD" | "-MMD" => {
parsed.depgen_with_compile = true;
parsed.deferred_flags.push(arg.to_string());
}
"-rdc" | "--relocatable-device-code" => {
let value = take_value(&mut argv, arg)?;
parsed.separate_device_code = parse_rdc_value(&value)?;
parsed.deferred_flags.push(format!("{arg} {value}"));
}
_ if arg.starts_with("-rdc=") || arg.starts_with("--relocatable-device-code=") => {
let value = arg.rsplit('=').next().unwrap_or("");
parsed.separate_device_code = parse_rdc_value(value)?;
parsed.deferred_flags.push(arg.to_string());
}
_ if BARE_DEFERRED_FLAGS.contains(&arg) => {
parsed.deferred_flags.push(arg.to_string());
}
_ if VALUE_FLAGS.contains(&arg) => {
let value = take_value(&mut argv, arg)?;
parsed.deferred_flags.push(format!("{arg} {value}"));
}
_ if JOINED_PREFIXES.iter().any(|p| arg.starts_with(p)) => {
parsed.deferred_flags.push(arg.to_string());
}
_ if arg.starts_with('@') => parsed.response_file = true,
_ if arg.starts_with('-') => parsed.unknown_flags.push(arg.to_string()),
_ => {
if is_nvcc_source(arg) {
parsed.sources.push(PathBuf::from(arg));
} else {
parsed.unknown_flags.push(arg.to_string());
}
}
}
}
Ok(parsed)
}
pub fn refuse_reasons(&self, extra_allowlist_flags: &[String]) -> Vec<RefuseReason> {
match self.mode {
NvccMode::Query => return vec![RefuseReason::NotPrimary],
NvccMode::Preprocess => {
return vec![RefuseReason::Unsupported(
"nvcc preprocessor mode (-E) — not yet supported",
)];
}
NvccMode::EmitPtx => {
return vec![RefuseReason::Unsupported(
"nvcc standalone -ptx emission — not yet supported",
)];
}
NvccMode::EmitCubin => {
return vec![RefuseReason::Unsupported(
"nvcc standalone -cubin emission — not yet supported",
)];
}
NvccMode::EmitFatbin => {
return vec![RefuseReason::Unsupported(
"nvcc standalone -fatbin emission — not yet supported",
)];
}
NvccMode::EmitOptixIr => {
return vec![RefuseReason::Unsupported(
"nvcc standalone --optix-ir emission — not yet supported",
)];
}
NvccMode::Lib => {
return vec![RefuseReason::Unsupported(
"nvcc library mode (--lib) — not yet supported",
)];
}
NvccMode::DeviceLink => {
return vec![RefuseReason::Unsupported(
"nvcc device-link (-dlink) mode — not yet supported",
)];
}
NvccMode::Link => {
return vec![RefuseReason::Unsupported(
"nvcc link mode — not yet supported",
)];
}
NvccMode::Compile => {}
}
let mut reasons = Vec::new();
if self.sources.len() != 1 {
reasons.push(RefuseReason::Unsupported(
"nvcc multi-source or source-less compile — not yet supported",
));
}
if self.device_debug {
reasons.push(RefuseReason::Unsupported(
"nvcc device debug (-G) — not yet supported",
));
}
if self.keep_temps {
reasons.push(RefuseReason::Unsupported(
"nvcc kept intermediates (-keep/--save-temps) — not yet supported",
));
}
if self.response_file {
reasons.push(RefuseReason::Unsupported(
"nvcc response file (@file) — not yet supported",
));
}
if self.output.is_none() {
reasons.push(RefuseReason::Unsupported(
"nvcc default output naming (missing -o) — not yet supported",
));
}
if self.implicit_depfile {
reasons.push(RefuseReason::Unsupported(
"nvcc dependency-only mode (-M/-MM) — not yet supported",
));
}
if self.depgen_with_compile && self.depfile.is_none() {
reasons.push(RefuseReason::Unsupported(
"nvcc implicit depfile (-MD/-MMD without -MF) — pass -MF <file> (not yet supported)",
));
}
if nvcc_xcompiler_smuggles_pp(&self.xcompiler_values) {
reasons.push(RefuseReason::Unsupported(
"nvcc -Xcompiler hides preprocessor flags (-I/-D/…) from dependency tracking — not yet supported",
));
}
if nvcc_has_native_resolution(&self.deferred_flags, &self.unknown_flags) {
reasons.push(RefuseReason::Unsupported(
"nvcc host- or device-resolved value (native) — not yet supported",
));
}
let unmatched: Vec<&String> = self
.unknown_flags
.iter()
.filter(|f| !extra_allowlist_flags.iter().any(|a| flag_matches(a, f)))
.collect();
if !unmatched.is_empty() {
let detail = format!(
"unrecognized nvcc flag(s) {} — not yet supported",
unmatched
.iter()
.map(|f| f.as_str())
.collect::<Vec<_>>()
.join(", ")
);
tracing::debug!("nvcc unrecognized flags refused: {detail}");
reasons.push(RefuseReason::Unsupported(
"unrecognized nvcc flag(s) — not yet supported",
));
}
reasons
}
pub fn object_output_path(&self) -> Option<PathBuf> {
self.output.clone()
}
pub fn depinfo_output_path(&self) -> Option<PathBuf> {
self.depfile.clone()
}
}
fn flag_matches(allow: &str, flag: &str) -> bool {
flag == allow || flag.starts_with(allow) && flag[allow.len()..].starts_with([' ', '='])
}
const NVCC_NATIVE_MARKERS: &[&str] = &[
"-march=native",
"-mcpu=native",
"-mtune=native",
"-march native",
"-mcpu native",
"-mtune native",
"-arch=native",
"-arch native",
"--gpu-architecture=native",
"--gpu-architecture native",
"-code=native",
"--gpu-code=native",
"arch=native",
];
fn nvcc_has_native_resolution(deferred: &[String], unknown: &[String]) -> bool {
deferred
.iter()
.chain(unknown.iter())
.any(|entry| NVCC_NATIVE_MARKERS.iter().any(|m| entry.contains(m)))
}
const NVCC_PP_TOKENS: &[&str] = &["-I", "-D", "-U", "-isystem", "-iquote", "-include"];
fn nvcc_pp_token(token: &str) -> bool {
NVCC_PP_TOKENS
.iter()
.any(|f| token == *f || token.starts_with(f))
}
fn nvcc_xcompiler_smuggles_pp(values: &[String]) -> bool {
values
.iter()
.any(|value| value.split([',', ' ', '\t']).any(nvcc_pp_token))
}
const NVCC_ENV_BLOCKED_EXACT: &[&str] = &[
"-G",
"--device-debug",
"-dc",
"--device-c",
"-rdc",
"-dlink",
"--device-link",
"--lib",
"-ptx",
"--ptx",
"-cubin",
"--cubin",
"-fatbin",
"--fatbin",
"--optix-ir",
"-E",
"--preprocess",
"-keep",
"--save-temps",
"--time",
];
const NVCC_ENV_BLOCKED_PREFIX: &[&str] = &["-rdc=", "--relocatable-device-code="];
fn nvcc_env_value_blocked(value: &str) -> bool {
value.split([',', ' ', '\t']).any(|token| {
NVCC_ENV_BLOCKED_EXACT.contains(&token)
|| NVCC_ENV_BLOCKED_PREFIX.iter().any(|p| token.starts_with(p))
|| nvcc_pp_token(token)
})
}
fn nvcc_check_env_value(var: &str, value: &str) -> Result<()> {
if nvcc_env_value_blocked(value) {
anyhow::bail!(
"nvcc: {var} alters the compilation mode or hides preprocessor inputs; only codegen tuning is supported (not yet supported)"
);
}
if NVCC_NATIVE_MARKERS.iter().any(|m| value.contains(m)) {
anyhow::bail!("nvcc: {var} carries a machine-resolved (native) value — not yet supported");
}
Ok(())
}
fn nvcc_driver_env_flags() -> Result<Vec<(String, String)>> {
let mut out = Vec::new();
for var in ["NVCC_PREPEND_FLAGS", "NVCC_APPEND_FLAGS"] {
let Some(os) = std::env::var_os(var) else {
continue;
};
let value = os
.into_string()
.map_err(|_| anyhow::anyhow!("nvcc: {var} is not valid UTF-8"))?;
if value.trim().is_empty() {
continue;
}
nvcc_check_env_value(var, &value)?;
out.push((var.to_string(), value));
}
Ok(out)
}
const NVCC_DEP_FORWARD_FLAGS: &[&str] = &[
"-I", "-isystem", "-iquote", "-D", "-U", "-std", "--std", "-x", "-include", "-m", "-O",
];
fn nvcc_dep_forward_args(deferred: &[String]) -> Vec<String> {
let mut out = Vec::new();
for entry in deferred {
let mut words = entry.splitn(2, ' ');
let head = words.next().unwrap_or("");
if NVCC_DEP_FORWARD_FLAGS.contains(&head) {
out.push(head.to_string());
if let Some(value) = words.next() {
out.push(value.to_string());
}
continue;
}
if NVCC_DEP_FORWARD_FLAGS.iter().any(|f| {
entry.starts_with(f) && (f.len() == 2 || entry[f.len()..].starts_with(['/', '=']))
}) {
out.push(entry.clone());
}
}
out
}
fn leading_rule_offset(text: &str) -> usize {
let mut offset = 0;
for line in text.split_inclusive('\n') {
if line_has_rule_target(line) {
break;
}
offset += line.len();
}
offset
}
fn line_has_rule_target(line: &str) -> bool {
let mut unescaped = String::with_capacity(line.len());
let mut chars = line.chars();
while let Some(c) = chars.next() {
if c != '\\' {
unescaped.push(c);
} else {
chars.next();
}
}
match unescaped.find(':') {
Some(colon) => {
let target = unescaped[..colon].trim();
!target.is_empty() && !target.contains([' ', '\t'])
}
None => false,
}
}
fn parse_nvcc_make_deps(text: &str) -> Result<Vec<PathBuf>> {
let text = &text[leading_rule_offset(text)..];
const ESCAPED_SPACE: char = '\u{E000}';
let mut joined = String::with_capacity(text.len());
let mut chars = text.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('\n') => continue,
Some(' ') => joined.push(ESCAPED_SPACE),
Some(other) => {
joined.push('\\');
joined.push(other);
}
None => joined.push('\\'),
}
} else {
joined.push(c);
}
}
let separator = joined
.match_indices(':')
.map(|(index, _)| index)
.find(|&index| {
!(index == 1
&& joined
.as_bytes()
.first()
.is_some_and(|b| b.is_ascii_alphabetic()))
})
.with_context(|| "nvcc -M output has no target separator")?;
let deps = joined[separator + 1..]
.split_whitespace()
.map(|dep| PathBuf::from(dep.replace(ESCAPED_SPACE, " ")))
.collect();
Ok(deps)
}
pub(crate) fn nvcc_dependency_closure(parsed: &NvccArgs) -> Result<Vec<PathBuf>> {
let cmd_args = nvcc_dependency_query_args(parsed)?;
let output = std::process::Command::new(&parsed.program)
.env("LC_ALL", "C")
.args(&cmd_args)
.output()
.with_context(|| format!("running `{} -M`", parsed.program))?;
if !output.status.success() {
anyhow::bail!("`{} -M` exited {}", parsed.program, output.status);
}
let text = String::from_utf8_lossy(&output.stdout);
let cwd = std::env::current_dir().context("nvcc -M: no current directory")?;
let mut deps: Vec<PathBuf> = parse_nvcc_make_deps(&text)?
.into_iter()
.map(|dep| {
if dep.is_absolute() {
dep
} else {
cwd.join(dep)
}
})
.collect();
deps.sort();
deps.dedup();
Ok(deps)
}
fn nvcc_dependency_query_args(parsed: &NvccArgs) -> Result<Vec<String>> {
let source = parsed
.sources
.first()
.context("nvcc -M with no source file")?;
let mut args = vec!["-M".to_string(), source.to_string_lossy().into_owned()];
args.extend(nvcc_dep_forward_args(&parsed.deferred_flags));
if parsed.separate_device_code {
args.push("-rdc=true".to_string());
}
Ok(args)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NvccPrefixMap {
pub from: String,
pub to: String,
}
pub(crate) const NVCC_ROOT_SENTINEL: &str = "/kache/nvcc-root";
fn nvcc_path_normalize_enabled() -> bool {
parse_nvcc_normalize_toggle(std::env::var("KACHE_NVCC_PATH_NORMALIZE").ok().as_deref())
}
fn parse_nvcc_normalize_toggle(value: Option<&str>) -> bool {
value.map(|v| v != "0").unwrap_or(true)
}
fn nvcc_absolutize(path: &Path, cwd: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
}
}
fn nvcc_canonicalize_or_self(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
pub(crate) fn nvcc_prefix_maps(
source: &Path,
output: Option<&Path>,
configured_base_dirs: &[String],
) -> Vec<NvccPrefixMap> {
let mut maps: Vec<NvccPrefixMap> = Vec::new();
if !nvcc_path_normalize_enabled() {
return maps;
}
let Ok(cwd) = std::env::current_dir() else {
return maps;
};
for (from, to) in crate::path_normalizer::configured_base_dir_prefix_maps(configured_base_dirs)
{
nvcc_push_unique_map(&mut maps, from, to);
}
let mut roots = vec![cwd.clone()];
if let Some(parent) = nvcc_absolutize(source, &cwd).parent() {
roots.push(parent.to_path_buf());
}
if let Some(output) = output
&& let Some(parent) = nvcc_absolutize(output, &cwd).parent()
{
roots.push(parent.to_path_buf());
}
for root in roots {
for candidate in [root.clone(), nvcc_canonicalize_or_self(&root)] {
nvcc_push_unique_map(
&mut maps,
candidate.to_string_lossy().into_owned(),
NVCC_ROOT_SENTINEL.to_string(),
);
}
}
maps.sort_by(|a, b| {
b.from
.len()
.cmp(&a.from.len())
.then_with(|| a.from.cmp(&b.from))
});
maps
}
fn nvcc_push_unique_map(maps: &mut Vec<NvccPrefixMap>, from: String, to: String) {
if !from.is_empty() && !maps.iter().any(|m| m.from == from) {
maps.push(NvccPrefixMap { from, to });
}
}
pub(crate) fn nvcc_normalize_path(path: &str, maps: &[NvccPrefixMap]) -> String {
for map in maps {
if let Some(rest) = path.strip_prefix(map.from.as_str())
&& (rest.is_empty() || rest.starts_with('/') || rest.starts_with('\\'))
{
return format!("{}{}", map.to, rest);
}
}
path.to_string()
}
pub(crate) fn nvcc_prefix_map_args(maps: &[NvccPrefixMap]) -> Result<Vec<String>> {
let mut args = Vec::new();
for map in maps {
if map.from.is_empty()
|| map.to.is_empty()
|| map.from.contains([',', ' ', '\t', '"', '\''])
|| map.to.contains([',', ' ', '\t', '"', '\''])
{
anyhow::bail!("nvcc: prefix map {:?} is not -Xcompiler-safe", map.from);
}
args.push("-Xcompiler".to_string());
args.push(format!("-ffile-prefix-map={}={}", map.from, map.to));
}
Ok(args)
}
fn nvcc_effective_source_date_epoch() -> Option<std::ffi::OsString> {
resolve_nvcc_source_date_epoch(
std::env::var_os("SOURCE_DATE_EPOCH"),
nvcc_source_date_epoch_passthrough(),
)
}
fn resolve_nvcc_source_date_epoch(
build_value: Option<std::ffi::OsString>,
passthrough: bool,
) -> Option<std::ffi::OsString> {
match build_value {
Some(v) => Some(v),
None if passthrough => None,
None => Some(std::ffi::OsString::from("0")),
}
}
fn nvcc_source_date_epoch_passthrough() -> bool {
std::env::var("KACHE_NVCC_SOURCE_DATE_EPOCH")
.ok()
.map(|v| nvcc_epoch_opt_out(&v))
.unwrap_or(false)
}
fn nvcc_epoch_opt_out(value: &str) -> bool {
let v = value.trim().to_ascii_lowercase();
v == "passthrough" || v == "wallclock" || v == "off"
}
fn nvcc_object_embeds_mapped_root(path: &Path, maps: &[NvccPrefixMap]) -> std::io::Result<bool> {
let bytes = std::fs::read(path)?;
Ok(maps
.iter()
.map(|map| map.from.as_bytes())
.filter(|from| !from.is_empty())
.any(|from| {
bytes
.iter()
.enumerate()
.filter(|(_, byte)| **byte == from[0])
.any(|(start, _)| bytes[start..].starts_with(from))
}))
}
pub(crate) fn nvcc_target_label(deferred: &[String]) -> String {
let mut archs: Vec<&str> = deferred
.iter()
.filter(|f| {
f.starts_with("-arch")
|| f.starts_with("-gencode")
|| f.starts_with("--gpu-architecture")
|| f.starts_with("-code")
|| f.starts_with("--gpu-code")
})
.map(String::as_str)
.collect();
archs.sort_unstable();
archs.dedup();
if archs.is_empty() {
"generic".to_string()
} else {
archs.join("+")
}
}
pub struct NvccCompiler {
extra_allowlist_flags: Vec<String>,
base_dirs: Vec<String>,
}
impl NvccCompiler {
pub fn with_extra_allowlist_flags(extra_allowlist_flags: Vec<String>) -> Self {
Self {
extra_allowlist_flags,
base_dirs: Vec::new(),
}
}
pub fn with_base_dirs(mut self, base_dirs: Vec<String>) -> Self {
self.base_dirs = base_dirs;
self.base_dirs.sort();
self.base_dirs.dedup();
self
}
pub fn recognizes(args: &[String]) -> bool {
let Some(program) = args.first() else {
return false;
};
super::command_basename(program)
.map(super::strip_windows_exe_suffix)
.is_some_and(|name| name.eq_ignore_ascii_case("nvcc"))
}
}
impl Compiler for NvccCompiler {
type Parsed = NvccArgs;
fn id(&self) -> CompilerId {
NVCC_ID
}
fn parse(&self, args: &[String]) -> Result<NvccArgs> {
NvccArgs::parse(args)
}
fn refuse_reasons(&self, parsed: &NvccArgs) -> Vec<RefuseReason> {
parsed.refuse_reasons(&self.extra_allowlist_flags)
}
fn cache_key(&self, parsed: &NvccArgs, ctx: &super::KeyCtx<'_, '_>) -> Result<String> {
let source = parsed
.sources
.first()
.context("nvcc cache key with no source file")?;
let mut hasher = blake3::Hasher::new();
hasher.update(b"nvcc_key_version:");
hasher.update(crate::cache_key::CACHE_KEY_VERSION.to_string().as_bytes());
hasher.update(b"\n");
let maps = nvcc_prefix_maps(source, parsed.output.as_deref(), &self.base_dirs);
nvcc_prefix_map_args(&maps)?;
let mut sentinels: Vec<&str> = maps.iter().map(|m| m.to.as_str()).collect();
sentinels.sort_unstable();
sentinels.dedup();
hasher.update(b"prefix_maps:");
for sentinel in sentinels {
hasher.update(sentinel.as_bytes());
hasher.update(b"\x1f");
}
hasher.update(b"\n");
let program_name = std::path::Path::new(&parsed.program)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(parsed.program.as_str());
hasher.update(b"compiler:");
hasher.update(program_name.as_bytes());
hasher.update(b"\n");
let mut config_args = vec!["-c".to_string()];
config_args.extend(parsed.deferred_flags.iter().cloned());
let env_flags = nvcc_driver_env_flags()?;
config_args.extend(
env_flags
.iter()
.map(|(var, value)| format!("{var}={value}")),
);
let resolved = crate::probe::probe(
ctx.cache_dir,
&crate::probe::NvccProber,
&crate::probe::ProbeRequest {
compiler: &parsed.program,
args: &parsed.rest,
key_args: &config_args,
per_tu_paths: &[],
windows_aware: false,
},
)?;
hasher.update(b"prober:nvcc\n");
hasher.update(b"compiler_version:");
hasher.update(resolved.version_line.as_bytes());
hasher.update(b"\n");
hasher.update(b"host_version:");
hasher.update(
resolved
.host_version_line
.as_deref()
.context("nvcc probe has no host version")?
.as_bytes(),
);
hasher.update(b"\n");
hasher.update(b"flags:");
for flag in parsed
.deferred_flags
.iter()
.chain(parsed.unknown_flags.iter().filter(|f| {
self.extra_allowlist_flags
.iter()
.any(|a| flag_matches(a, f))
}))
{
hasher.update(flag.as_bytes());
hasher.update(b"\x1f");
}
for (var, value) in &env_flags {
hasher.update(b"env:");
hasher.update(var.as_bytes());
hasher.update(b"=");
hasher.update(value.as_bytes());
hasher.update(b"\n");
}
hasher.update(b"\n");
let closure = nvcc_dependency_closure(parsed)?;
hasher.update(b"inputs:");
let mut inputs: Vec<(&Path, String)> = Vec::with_capacity(closure.len() + 1);
inputs.push((source, ctx.file_hasher.hash(source)?));
for dep in &closure {
inputs.push((dep, ctx.file_hasher.hash(dep)?));
}
inputs.sort_by(|a, b| a.0.cmp(b.0));
for (path, content_hash) in &inputs {
let display = nvcc_normalize_path(&path.to_string_lossy(), &maps);
hasher.update(display.as_bytes());
hasher.update(b"=");
hasher.update(content_hash.as_bytes());
hasher.update(b"\x1f");
}
hasher.update(b"\n");
let key = hasher.finalize().to_hex().to_string();
let key = crate::cache_key::apply_key_salt(key, ctx.key_salt, "nvcc");
Ok(crate::cache_key::apply_key_env_vars(
key,
ctx.key_env_vars,
"nvcc",
))
}
fn execute(&self, parsed: &NvccArgs) -> Result<super::CompileResult> {
crate::opcounts::record_compiler_run();
let source = parsed
.sources
.first()
.context("nvcc execute with no source file")?;
let maps = nvcc_prefix_maps(source, parsed.output.as_deref(), &self.base_dirs);
let injection = nvcc_prefix_map_args(&maps)?;
let mut command = std::process::Command::new(&parsed.program);
command.args(&parsed.rest);
command.args(&injection);
if let Some(epoch) = nvcc_effective_source_date_epoch() {
command.env("SOURCE_DATE_EPOCH", epoch);
}
let output = command
.output()
.with_context(|| format!("executing {}", parsed.program))?;
let exit_code = output.status.code().unwrap_or(1);
let mut artifacts = super::ArtifactSet::empty();
if exit_code == 0 {
let object = parsed.object_output_path().filter(|p| p.is_file());
if let Some(object) = object {
let store_name = object
.file_name()
.and_then(|n| n.to_str())
.context("nvcc object has no file name")?
.to_string();
let mut found = vec![super::Artifact {
path: object.clone(),
store_name,
kind: super::ArtifactKind::Object,
required: true,
}];
if let Some(depfile) = parsed.depinfo_output_path()
&& depfile.is_file()
{
let store_name = depfile
.file_name()
.and_then(|n| n.to_str())
.context("nvcc dep-info has no file name")?
.to_string();
found.push(super::Artifact {
path: depfile,
store_name,
kind: super::ArtifactKind::DepInfo,
required: true,
});
}
artifacts = super::ArtifactSet::new(found);
}
if !artifacts.is_empty()
&& let Some(object) = parsed.object_output_path()
&& nvcc_object_embeds_mapped_root(&object, &maps).unwrap_or(true)
{
tracing::warn!("nvcc: object embeds a raw mapped root; not caching it");
artifacts = super::ArtifactSet::empty();
}
}
Ok(super::CompileResult {
exit_code,
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
artifacts,
keepalive: Vec::new(),
})
}
fn classify_output(&self, _parsed: &NvccArgs, name: &str) -> super::ArtifactKind {
super::classify_by_filename(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(args: &[&str]) -> Vec<String> {
args.iter().map(|a| a.to_string()).collect()
}
fn parse_ok(args: &[&str]) -> NvccArgs {
NvccArgs::parse(&s(args)).expect("parse must succeed")
}
#[test]
fn recognizes_nvcc_spellings() {
assert!(NvccCompiler::recognizes(&s(&["nvcc", "-c", "a.cu"])));
assert!(NvccCompiler::recognizes(&s(&[
"/usr/local/cuda/bin/nvcc",
"-c",
"a.cu"
])));
assert!(NvccCompiler::recognizes(&s(&[
r"C:\CUDA\bin\nvcc.exe",
"-c",
"kernel.cu"
])));
assert!(NvccCompiler::recognizes(&s(&["NVCC", "--version"])));
assert!(!NvccCompiler::recognizes(&s(&["cc", "-c", "a.c"])));
assert!(!NvccCompiler::recognizes(&s(&["gcc", "-c", "a.c"])));
assert!(!NvccCompiler::recognizes(&s(&[
"nvcc-wrapper",
"-c",
"a.cu"
])));
assert!(!NvccCompiler::recognizes(&[]));
}
#[test]
fn parses_single_source_compile() {
let parsed = parse_ok(&["nvcc", "-c", "src/kernel.cu", "-o", "build/kernel.o"]);
assert_eq!(parsed.mode, NvccMode::Compile);
assert_eq!(parsed.sources, vec![PathBuf::from("src/kernel.cu")]);
assert_eq!(
parsed.object_output_path(),
Some(PathBuf::from("build/kernel.o"))
);
assert!(!parsed.separate_device_code);
assert!(!parsed.device_debug);
}
#[test]
fn parses_joined_output_and_arch_flags() {
let parsed = parse_ok(&[
"nvcc",
"-c",
"k.cu",
"-obuild/k.o",
"-gencode",
"arch=compute_80,code=sm_80",
"-Xcompiler",
"-fPIC",
"-DUSE_CUDA",
"-O2",
]);
assert_eq!(parsed.mode, NvccMode::Compile);
assert_eq!(parsed.output, Some(PathBuf::from("build/k.o")));
assert!(parsed.unknown_flags.is_empty());
assert_eq!(parsed.deferred_flags.len(), 4);
}
#[test]
fn query_is_not_primary() {
for query in [["nvcc", "--version"], ["nvcc", "-V"], ["nvcc", "--dryrun"]] {
let parsed = parse_ok(&query);
assert_eq!(parsed.mode, NvccMode::Query);
let reasons = parsed.refuse_reasons(&[]);
assert_eq!(reasons.len(), 1);
assert!(matches!(reasons[0], RefuseReason::NotPrimary));
}
}
#[test]
fn link_and_device_link_refuse() {
let parsed = parse_ok(&["nvcc", "a.o", "b.o", "-o", "app"]);
assert_eq!(parsed.mode, NvccMode::Link);
let reasons = parsed.refuse_reasons(&[]);
assert!(
reasons
.iter()
.any(|r| { matches!(r, RefuseReason::Unsupported(d) if d.contains("link mode")) })
);
let parsed = parse_ok(&["nvcc", "-dlink", "a.o", "-o", "dlink.o"]);
assert_eq!(parsed.mode, NvccMode::DeviceLink);
assert!(!parsed.refuse_reasons(&[]).is_empty());
}
#[test]
fn separable_modes_are_cacheable_and_keyed() {
let parsed = parse_ok(&["nvcc", "-c", "-dc", "k.cu", "-o", "k.o"]);
assert!(parsed.separate_device_code);
assert!(parsed.refuse_reasons(&[]).is_empty());
assert_eq!(parsed.deferred_flags, ["-dc"]);
let parsed = parse_ok(&["nvcc", "-c", "-rdc=true", "k.cu", "-o", "k.o"]);
assert!(parsed.separate_device_code);
assert!(parsed.refuse_reasons(&[]).is_empty());
assert_eq!(parsed.deferred_flags, ["-rdc=true"]);
let parsed = parse_ok(&["nvcc", "-c", "-rdc=false", "k.cu", "-o", "k.o"]);
assert!(!parsed.separate_device_code);
assert!(parsed.refuse_reasons(&[]).is_empty());
assert_eq!(parsed.deferred_flags, ["-rdc=false"]);
let parsed = parse_ok(&[
"nvcc",
"-c",
"--relocatable-device-code=true",
"k.cu",
"-o",
"k.o",
]);
assert!(parsed.separate_device_code);
assert!(parsed.refuse_reasons(&[]).is_empty());
assert_eq!(parsed.deferred_flags, ["--relocatable-device-code=true"]);
let parsed = parse_ok(&["nvcc", "--device-c", "k.cu", "-o", "k.o"]);
assert!(parsed.separate_device_code);
assert!(parsed.refuse_reasons(&[]).is_empty());
assert_eq!(parsed.deferred_flags, ["--device-c"]);
}
#[test]
fn device_debug_still_refuses() {
let parsed = parse_ok(&["nvcc", "-c", "-G", "k.cu", "-o", "k.o"]);
assert!(parsed.device_debug);
let reasons = parsed.refuse_reasons(&[]);
assert!(
reasons
.iter()
.any(|r| { matches!(r, RefuseReason::Unsupported(d) if d.contains("-G")) })
);
}
#[test]
fn multi_source_refuses() {
let parsed = parse_ok(&["nvcc", "-c", "a.cu", "b.cu", "-o", "out.o"]);
assert!(
parsed.refuse_reasons(&[]).iter().any(|r| {
matches!(r, RefuseReason::Unsupported(d) if d.contains("multi-source"))
})
);
}
#[test]
fn unknown_flags_refuse_unless_allowlisted() {
let parsed = parse_ok(&["nvcc", "-c", "k.cu", "--fancy-new-flag", "-o", "k.o"]);
assert_eq!(parsed.unknown_flags, vec!["--fancy-new-flag".to_string()]);
assert!(!parsed.refuse_reasons(&[]).is_empty());
let reasons = parsed.refuse_reasons(&["--fancy-new-flag".to_string()]);
assert!(reasons.is_empty(), "unexpected refusals: {reasons:?}");
}
#[test]
fn response_file_and_keep_refuse() {
let parsed = parse_ok(&["nvcc", "-c", "@args.rsp", "-o", "k.o"]);
assert!(parsed.response_file);
let parsed = parse_ok(&["nvcc", "-c", "-keep", "k.cu", "-o", "k.o"]);
assert!(parsed.keep_temps);
assert!(!parsed.refuse_reasons(&[]).is_empty());
}
#[test]
fn missing_value_is_parse_error() {
assert!(NvccArgs::parse(&s(&["nvcc", "-c", "k.cu", "-o"])).is_err());
assert!(NvccArgs::parse(&s(&["nvcc", "-c", "k.cu", "-gencode"])).is_err());
assert!(NvccArgs::parse(&[]).is_err());
}
#[test]
fn every_emit_mode_names_its_refusal() {
let cases: &[(&[&str], NvccMode, &str)] = &[
(
&["nvcc", "-E", "k.cu"],
NvccMode::Preprocess,
"preprocessor",
),
(
&["nvcc", "--lib", "a.o", "-o", "lib.a"],
NvccMode::Lib,
"--lib",
),
(&["nvcc", "-ptx", "k.cu"], NvccMode::EmitPtx, "-ptx"),
(&["nvcc", "-cubin", "k.cu"], NvccMode::EmitCubin, "-cubin"),
(
&["nvcc", "-fatbin", "k.cu"],
NvccMode::EmitFatbin,
"-fatbin",
),
(
&["nvcc", "--optix-ir", "k.cu"],
NvccMode::EmitOptixIr,
"--optix-ir",
),
];
for (argv, mode, reason_part) in cases {
let parsed = parse_ok(argv);
assert_eq!(parsed.mode, *mode, "wrong mode for {argv:?}");
assert!(
parsed.refuse_reasons(&[]).iter().any(|r| {
matches!(r, RefuseReason::Unsupported(d) if d.contains(reason_part))
}),
"missing {reason_part:?} refusal for {argv:?}"
);
}
}
#[test]
fn every_flag_table_lands_deferred() {
let parsed = parse_ok(&[
"nvcc",
"-c",
"k.cu",
"-o",
"k.o",
"-MF",
"deps.d",
"-MFdeps2.d",
"-shared",
"-v",
"-D",
"FOO=1",
"-I/usr/include",
"-arch=sm_80",
"--gpu-architecture=compute_80",
"-Werror",
"-m64",
]);
assert_eq!(parsed.mode, NvccMode::Compile);
assert!(
parsed.unknown_flags.is_empty(),
"unexpected unknowns: {:?}",
parsed.unknown_flags
);
for expected in [
"-MF deps.d",
"-MFdeps2.d",
"-shared",
"-v",
"-D FOO=1",
"-I/usr/include",
"-arch=sm_80",
"--gpu-architecture=compute_80",
"-Werror",
"-m64",
] {
assert!(
parsed.deferred_flags.iter().any(|f| f == expected),
"missing deferred {expected:?} in {:?}",
parsed.deferred_flags
);
}
assert_eq!(
parsed.depinfo_output_path(),
Some(PathBuf::from("deps2.d")),
"last -MF wins, like the driver"
);
}
#[test]
fn bare_depinfo_flags_set_implicit_depfile() {
for flag in ["-M", "-MM", "-MG", "-MP", "--generate-dependencies"] {
let parsed = parse_ok(&["nvcc", "-c", "k.cu", "-o", "k.o", flag]);
assert!(parsed.implicit_depfile, "{flag} must set implicit_depfile");
assert!(
parsed.refuse_reasons(&[]).iter().any(|r| {
matches!(r, RefuseReason::Unsupported(d) if d.contains("dependency-only"))
}),
"{flag} must refuse with the depfile reason"
);
}
}
#[test]
fn depgen_with_compile_needs_explicit_mf() {
for flag in ["-MD", "-MMD"] {
let parsed = parse_ok(&["nvcc", "-c", "k.cu", "-o", "k.o", flag, "-MF", "k.d"]);
assert!(
parsed.depgen_with_compile,
"{flag} must set depgen_with_compile"
);
assert!(!parsed.implicit_depfile);
assert_eq!(parsed.depinfo_output_path(), Some(PathBuf::from("k.d")));
assert!(
parsed.deferred_flags.iter().any(|f| f == flag),
"{flag} must be keyed, got {:?}",
parsed.deferred_flags
);
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"{flag} with -MF must be cacheable, got {:?}",
parsed.refuse_reasons(&[])
);
}
for flag in ["-MD", "-MMD"] {
let parsed = parse_ok(&["nvcc", "-c", "k.cu", "-o", "k.o", flag]);
assert!(
parsed.refuse_reasons(&[]).iter().any(|r| {
matches!(r, RefuseReason::Unsupported(d) if d.contains("-MF <file>"))
}),
"{flag} without -MF must refuse with the -MF reason"
);
}
}
#[test]
fn missing_output_refuses() {
let parsed = parse_ok(&["nvcc", "-c", "k.cu"]);
assert!(parsed.output.is_none());
assert!(
parsed
.refuse_reasons(&[])
.iter()
.any(|r| { matches!(r, RefuseReason::Unsupported(d) if d.contains("missing -o")) })
);
}
#[cfg(unix)]
#[test]
fn dependency_closure_lists_and_bails() {
use std::os::unix::fs::PermissionsExt;
let _lock = crate::test_support::process_state_test_lock();
let dir = tempfile::tempdir().unwrap();
let shell =
crate::compiler::resolve_program_on_path("sh").expect("sh must be available on PATH");
let fake = dir.path().join("nvcc");
std::fs::write(
&fake,
format!(
"#!{}\nprintf '%s' \"$NVCC_FAKE_M_OUT\"\nexit \"${{NVCC_FAKE_M_EXIT:-0}}\"\n",
shell.display()
),
)
.unwrap();
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
let previous_out = std::env::var_os("NVCC_FAKE_M_OUT");
let previous_exit = std::env::var_os("NVCC_FAKE_M_EXIT");
unsafe {
std::env::set_var("NVCC_FAKE_M_OUT", "k.o: /a/k.cu /a/h.h /a/k.cu\n");
std::env::remove_var("NVCC_FAKE_M_EXIT");
}
let parsed = NvccCompiler::with_extra_allowlist_flags(Vec::new())
.parse(&[
fake.to_string_lossy().into_owned(),
"-c".to_string(),
"/a/k.cu".to_string(),
"-o".to_string(),
"/a/k.o".to_string(),
"-I/a".to_string(),
])
.unwrap();
let closure = nvcc_dependency_closure(&parsed).unwrap();
assert_eq!(
closure,
vec![PathBuf::from("/a/h.h"), PathBuf::from("/a/k.cu")]
);
unsafe {
std::env::set_var("NVCC_FAKE_M_EXIT", "1");
}
assert!(nvcc_dependency_closure(&parsed).is_err());
unsafe {
std::env::set_var("NVCC_FAKE_M_OUT", "garbage without separator");
std::env::remove_var("NVCC_FAKE_M_EXIT");
}
assert!(nvcc_dependency_closure(&parsed).is_err());
match previous_out {
Some(value) => unsafe {
std::env::set_var("NVCC_FAKE_M_OUT", value);
},
None => unsafe {
std::env::remove_var("NVCC_FAKE_M_OUT");
},
}
match previous_exit {
Some(value) => unsafe {
std::env::set_var("NVCC_FAKE_M_EXIT", value);
},
None => unsafe {
std::env::remove_var("NVCC_FAKE_M_EXIT");
},
}
}
#[test]
fn separate_rdc_value_is_keyed() {
let parsed = parse_ok(&["nvcc", "-c", "-rdc", "true", "k.cu", "-o", "k.o"]);
assert!(parsed.separate_device_code);
assert!(parsed.refuse_reasons(&[]).is_empty());
assert_eq!(parsed.deferred_flags, ["-rdc true"]);
let parsed = parse_ok(&[
"nvcc",
"-c",
"--relocatable-device-code",
"false",
"k.cu",
"-o",
"k.o",
]);
assert!(!parsed.separate_device_code);
assert_eq!(parsed.deferred_flags, ["--relocatable-device-code false"]);
assert!(NvccArgs::parse(&s(&["nvcc", "-c", "-rdc=maybe", "k.cu"])).is_err());
assert!(NvccArgs::parse(&s(&["nvcc", "-c", "-rdc"])).is_err());
}
#[test]
fn dependency_query_uses_effective_rdc_mode() {
let parsed = parse_ok(&["nvcc", "-dc", "k.cu", "-o", "k.o"]);
assert_eq!(
nvcc_dependency_query_args(&parsed).unwrap(),
["-M", "k.cu", "-rdc=true"]
);
let parsed = parse_ok(&["nvcc", "-c", "-rdc=true", "k.cu", "-o", "k.o"]);
assert_eq!(
nvcc_dependency_query_args(&parsed).unwrap(),
["-M", "k.cu", "-rdc=true"]
);
let parsed = parse_ok(&["nvcc", "-dc", "-rdc=false", "k.cu", "-o", "k.o"]);
assert_eq!(nvcc_dependency_query_args(&parsed).unwrap(), ["-M", "k.cu"]);
}
#[test]
fn linker_inputs_are_not_sources() {
let parsed = parse_ok(&["nvcc", "-c", "a.o", "-o", "app"]);
assert!(parsed.sources.is_empty());
assert_eq!(parsed.unknown_flags, vec!["a.o".to_string()]);
assert!(
parsed.refuse_reasons(&[]).iter().any(|r| {
matches!(r, RefuseReason::Unsupported(d) if d.contains("source-less"))
})
);
}
#[test]
fn dash_prefixed_positionals_are_never_sources() {
let parsed = parse_ok(&["nvcc", "-c", "-q.cu", "-o", "k.o"]);
assert!(parsed.sources.is_empty());
assert_eq!(parsed.unknown_flags, vec!["-q.cu".to_string()]);
}
#[test]
fn time_falls_through_to_unknown() {
let parsed = parse_ok(&["nvcc", "-c", "k.cu", "--time", "-o", "k.o"]);
assert_eq!(parsed.unknown_flags, vec!["--time".to_string()]);
assert!(!parsed.refuse_reasons(&[]).is_empty());
}
#[test]
fn flag_matches_boundary_rules() {
assert!(flag_matches("--fancy-new-flag", "--fancy-new-flag"));
assert!(flag_matches("-O", "-O 2"));
assert!(flag_matches("-gencode", "-gencode=arch"));
assert!(!flag_matches("-O", "-O2"));
assert!(!flag_matches("-O", "--fancy"));
assert!(!flag_matches("--long-flag", "-D"));
}
#[test]
fn deferred_flags_need_no_allowlist() {
let parsed = parse_ok(&["nvcc", "-c", "k.cu", "-O2", "-o", "k.o"]);
assert!(parsed.refuse_reasons(&[]).is_empty());
assert!(parsed.refuse_reasons(&["-O".to_string()]).is_empty());
let parsed = parse_ok(&["nvcc", "-c", "k.cu", "--fancy-9", "-o", "k.o"]);
assert!(!parsed.refuse_reasons(&["-O".to_string()]).is_empty());
assert!(parsed.refuse_reasons(&["--fancy-9".to_string()]).is_empty());
}
#[test]
fn compiler_trait_refuse_delegates() {
let compiler = NvccCompiler::with_extra_allowlist_flags(Vec::new());
let link = parse_ok(&["nvcc", "a.o", "b.o", "-o", "app"]);
assert!(!compiler.refuse_reasons(&link).is_empty());
let compile = parse_ok(&["nvcc", "-c", "k.cu", "-O2", "-o", "k.o"]);
assert!(compiler.refuse_reasons(&compile).is_empty());
}
#[test]
fn xcompiler_values_are_structured() {
let parsed = parse_ok(&[
"nvcc",
"-c",
"k.cu",
"-o",
"k.o",
"-Xcompiler",
"-fPIC",
"--compiler-options=-Wall",
"-Xptxas",
"-O3",
]);
assert_eq!(parsed.xcompiler_values, vec!["-fPIC", "-Wall"]);
assert!(parsed.deferred_flags.iter().any(|f| f == "-Xptxas -O3"));
}
#[test]
fn xcompiler_smuggling_refuses() {
for smuggled in [
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-Xcompiler", "-I/opt/x"],
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-Xcompiler", "-DFOO"],
vec![
"nvcc",
"-c",
"k.cu",
"-o",
"k.o",
"-Xcompiler",
"-O2,-isystem/x",
],
vec![
"nvcc",
"-c",
"k.cu",
"-o",
"k.o",
"--compiler-options",
"-UFOO",
],
] {
let parsed = parse_ok(&smuggled);
assert!(
parsed.refuse_reasons(&[]).iter().any(|r| {
matches!(r, RefuseReason::Unsupported(d) if d.contains("-Xcompiler"))
}),
"{smuggled:?} must refuse"
);
}
let parsed = parse_ok(&[
"nvcc",
"-c",
"k.cu",
"-o",
"k.o",
"-Xcompiler",
"-O2",
"-Xcompiler",
"-DFOO",
]);
assert_eq!(parsed.xcompiler_values, vec!["-O2", "-DFOO"]);
assert!(
parsed
.refuse_reasons(&[])
.iter()
.any(|r| { matches!(r, RefuseReason::Unsupported(d) if d.contains("-Xcompiler")) })
);
let parsed = parse_ok(&[
"nvcc",
"-c",
"k.cu",
"-o",
"k.o",
"-Xcompiler",
"-O2",
"-Xcompiler",
"-fPIC",
]);
assert!(parsed.refuse_reasons(&[]).is_empty());
}
#[test]
fn native_resolution_refuses() {
for native in [
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-march=native"],
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-march", "native"],
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-mcpu=native"],
vec![
"nvcc",
"-c",
"k.cu",
"-o",
"k.o",
"-Xcompiler",
"-march=native",
],
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-arch=native"],
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-arch", "native"],
vec![
"nvcc",
"-c",
"k.cu",
"-o",
"k.o",
"-gencode",
"arch=native,code=sm_90",
],
vec![
"nvcc",
"-c",
"k.cu",
"-o",
"k.o",
"--gpu-architecture=native",
],
] {
let parsed = parse_ok(&native);
assert!(
parsed
.refuse_reasons(&[])
.iter()
.any(|r| { matches!(r, RefuseReason::Unsupported(d) if d.contains("native")) }),
"{native:?} must refuse"
);
}
let parsed = parse_ok(&["nvcc", "-c", "k.cu", "-o", "k.o", "--march=native"]);
assert!(
parsed
.refuse_reasons(&["--march=native".to_string()])
.iter()
.any(|r| { matches!(r, RefuseReason::Unsupported(d) if d.contains("native")) })
);
let parsed = parse_ok(&["nvcc", "-c", "k.cu", "-o", "k.o", "-O2", "-march=native"]);
assert!(
parsed
.refuse_reasons(&[])
.iter()
.any(|r| { matches!(r, RefuseReason::Unsupported(d) if d.contains("native")) })
);
for fine in [
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-arch=sm_80"],
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-DARCH=native"],
vec!["nvcc", "-c", "k.cu", "-o", "k.o", "-I/opt/native/include"],
] {
let parsed = parse_ok(&fine);
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"{fine:?} must stay cacheable"
);
}
}
#[test]
fn env_value_blocked_classification() {
for blocked in [
"-G",
"--device-debug -O2",
"-dc",
"-rdc=true",
"--relocatable-device-code=true",
"-dlink",
"--lib",
"-ptx",
"-E",
"-DFOO",
"-I/opt/x",
"-O2,-isystem/x",
] {
assert!(
nvcc_env_value_blocked(blocked),
"{blocked:?} must be blocked"
);
}
for clean in [
"",
"-O2",
"-gencode arch=sm_80,code=sm_80",
"-fPIC",
"--expt-relaxed-constexpr",
] {
assert!(
!nvcc_env_value_blocked(clean),
"{clean:?} must pass to keying"
);
}
}
#[test]
fn env_value_native_veto() {
assert!(nvcc_check_env_value("NVCC_PREPEND_FLAGS", "-march=native").is_err());
assert!(nvcc_check_env_value("NVCC_APPEND_FLAGS", "-O2").is_ok());
assert!(nvcc_check_env_value("NVCC_PREPEND_FLAGS", "").is_ok());
}
#[test]
fn dep_forward_args_cover_resolution_inputs() {
let forwarded = nvcc_dep_forward_args(
&[
"-D FOO=1",
"-DUSE_CUDA",
"-I/usr/include",
"-I /opt/include",
"-isystem/sys",
"-std=c++17",
"-x cu",
"-O2",
"-gencode",
"arch=compute_80,code=sm_80",
"-Xcompiler",
"-fPIC",
"-Werror",
"-m64",
]
.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>(),
);
for expected in [
"-D",
"FOO=1",
"-DUSE_CUDA",
"-I/usr/include",
"-I",
"/opt/include",
"-isystem/sys",
"-std=c++17",
"-x",
"cu",
"-O2",
"-m64",
] {
assert!(
forwarded.iter().any(|f| f == expected),
"missing forwarded {expected:?} in {forwarded:?}"
);
}
for excluded in ["-gencode", "-Xcompiler", "-fPIC", "-Werror"] {
assert!(
!forwarded.iter().any(|f| f == excluded),
"must not forward {excluded:?}: {forwarded:?}"
);
}
}
#[test]
fn line_rule_target_shapes() {
for rule in [
"kernel.o: kernel.cu\n",
"kernel.o : kernel.cu\n",
"C:\\b\\k.obj: C:\\s\\k.cu\n",
"C:/b/k.obj: C:/s/k.cu\n",
"my\\ dir/k.o: k.cu\n",
" spaced.o: k.cu\n",
] {
assert!(line_has_rule_target(rule), "{rule:?} must qualify");
}
for prose in [
"nvcc warning : Support for offline compilation\n",
"\n",
"no colon here\n",
": leading colon\n",
] {
assert!(!line_has_rule_target(prose), "{prose:?} must not qualify");
}
}
#[test]
fn make_deps_skips_driver_prologue() {
let parsed = parse_nvcc_make_deps(
"nvcc warning : Support for offline compilation for architectures prior to '<compute/sm/lto>_75' will be removed\nkernel.o : kernel.cu \\\n inc/k.h \\\n /usr/include/stdc-predef.h\n",
)
.unwrap();
assert_eq!(
parsed,
vec![
PathBuf::from("kernel.cu"),
PathBuf::from("inc/k.h"),
PathBuf::from("/usr/include/stdc-predef.h"),
]
);
}
#[test]
fn make_deps_parse_handles_continuations_and_drives() {
let parsed = parse_nvcc_make_deps(
"build/kernel.o: src/kernel.cu \\\n src/kernel.h /usr/local/cuda/include/cuda_runtime.h \\\n",
)
.unwrap();
assert_eq!(
parsed,
vec![
PathBuf::from("src/kernel.cu"),
PathBuf::from("src/kernel.h"),
PathBuf::from("/usr/local/cuda/include/cuda_runtime.h"),
]
);
let parsed = parse_nvcc_make_deps("k.o: dir/my\\ header.h dir/ok.h\n").unwrap();
assert_eq!(
parsed,
vec![PathBuf::from("dir/my header.h"), PathBuf::from("dir/ok.h"),]
);
let parsed = parse_nvcc_make_deps("k.o: C:\\temp\\a.h\n").unwrap();
assert_eq!(parsed, vec![PathBuf::from("C:\\temp\\a.h")]);
assert!(parse_nvcc_make_deps("no separator here\n").is_err());
let parsed = parse_nvcc_make_deps("C:\\b\\k.obj: C:\\s\\k.cu C:\\s\\k.h\n").unwrap();
assert_eq!(
parsed,
vec![PathBuf::from("C:\\s\\k.cu"), PathBuf::from("C:\\s\\k.h")]
);
}
#[test]
fn absolutize_joins_relative_and_passes_absolute() {
let cwd = Path::new("/work/tree");
assert_eq!(
nvcc_absolutize(Path::new("src/k.cu"), cwd),
PathBuf::from("/work/tree/src/k.cu")
);
assert_eq!(
nvcc_absolutize(Path::new("/elsewhere/k.cu"), cwd),
PathBuf::from("/elsewhere/k.cu")
);
}
#[test]
fn canonicalize_falls_back_to_self() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
nvcc_canonicalize_or_self(dir.path()),
std::fs::canonicalize(dir.path()).unwrap()
);
let missing = dir.path().join("nope");
assert_eq!(nvcc_canonicalize_or_self(&missing), missing);
}
#[test]
fn normalize_disabled_by_knob() {
let _lock = crate::test_support::process_state_test_lock();
let previous = std::env::var_os("KACHE_NVCC_PATH_NORMALIZE");
unsafe {
std::env::set_var("KACHE_NVCC_PATH_NORMALIZE", "0");
}
let maps = nvcc_prefix_maps(Path::new("k.cu"), None, &[]);
match previous {
Some(value) => unsafe {
std::env::set_var("KACHE_NVCC_PATH_NORMALIZE", value);
},
None => unsafe {
std::env::remove_var("KACHE_NVCC_PATH_NORMALIZE");
},
}
assert!(maps.is_empty());
}
#[test]
fn epoch_passthrough_knob_disables_the_pin() {
let _lock = crate::test_support::process_state_test_lock();
let previous_epoch = std::env::var_os("SOURCE_DATE_EPOCH");
let previous_knob = std::env::var_os("KACHE_NVCC_SOURCE_DATE_EPOCH");
unsafe {
std::env::remove_var("SOURCE_DATE_EPOCH");
std::env::set_var("KACHE_NVCC_SOURCE_DATE_EPOCH", "wallclock");
}
let epoch = nvcc_effective_source_date_epoch();
match previous_epoch {
Some(value) => unsafe {
std::env::set_var("SOURCE_DATE_EPOCH", value);
},
None => unsafe {
std::env::remove_var("SOURCE_DATE_EPOCH");
},
}
match previous_knob {
Some(value) => unsafe {
std::env::set_var("KACHE_NVCC_SOURCE_DATE_EPOCH", value);
},
None => unsafe {
std::env::remove_var("KACHE_NVCC_SOURCE_DATE_EPOCH");
},
}
assert_eq!(epoch, None);
}
#[test]
fn prefix_maps_cover_cwd_and_source() {
let _lock = crate::test_support::process_state_test_lock();
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("src").join("k.cu");
let maps = nvcc_prefix_maps(&source, None, &[]);
assert!(
maps.iter().any(|m| m.to == NVCC_ROOT_SENTINEL),
"derived roots must map: {maps:?}"
);
let positions: Vec<usize> = maps.iter().map(|m| m.from.len()).collect();
let mut sorted = positions.clone();
sorted.sort_unstable_by(|a, b| b.cmp(a));
assert_eq!(positions, sorted);
}
#[test]
fn prefix_map_push_skips_empty_and_duplicate_from() {
let mut maps = Vec::new();
nvcc_push_unique_map(&mut maps, String::new(), NVCC_ROOT_SENTINEL.to_string());
assert!(maps.is_empty(), "empty from must not become a prefix map");
nvcc_push_unique_map(
&mut maps,
"/a/proj".to_string(),
NVCC_ROOT_SENTINEL.to_string(),
);
nvcc_push_unique_map(
&mut maps,
"/a/proj".to_string(),
"/kache/base-dir-0".to_string(),
);
assert_eq!(maps.len(), 1);
assert_eq!(maps[0].to, NVCC_ROOT_SENTINEL);
}
#[cfg(unix)]
#[test]
fn execute_drops_artifacts_when_object_embeds_mapped_root() {
let dir = tempfile::tempdir().unwrap();
let nvcc = dir.path().join("nvcc");
let src = dir.path().join("k.cu");
let obj = dir.path().join("k.o");
std::fs::write(&src, "void k(void) {}\n").unwrap();
let payload = dir.path().join("payload");
std::fs::write(&payload, format!("ELF{}", dir.path().display())).unwrap();
std::fs::write(
&nvcc,
format!(
"#!/bin/sh\nout=\nprev=\nfor a in \"$@\"; do\n if [ \"$prev\" = \"-o\" ]; then out=$a; fi\n prev=$a\ndone\ncp '{}' \"$out\"\n",
payload.display()
),
)
.unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&nvcc, std::fs::Permissions::from_mode(0o755)).unwrap();
let parsed = parse_ok(&[
nvcc.to_str().unwrap(),
"-c",
src.to_str().unwrap(),
"-o",
obj.to_str().unwrap(),
]);
let result = NvccCompiler::with_extra_allowlist_flags(Vec::new())
.execute(&parsed)
.unwrap();
assert_eq!(result.exit_code, 0, "stderr={}", result.stderr);
assert!(obj.is_file(), "the compile must still write the object");
assert!(
result.artifacts.is_empty(),
"an object that embeds a mapped root must not be cached, got {:?}",
result
.artifacts
.outputs()
.iter()
.map(|a| &a.path)
.collect::<Vec<_>>()
);
}
#[test]
fn normalize_toggle_defaults_on_opts_out_on_zero() {
assert!(parse_nvcc_normalize_toggle(None));
assert!(parse_nvcc_normalize_toggle(Some("1")));
assert!(!parse_nvcc_normalize_toggle(Some("0")));
}
#[test]
fn normalize_path_needs_separator_boundary() {
let maps = vec![NvccPrefixMap {
from: "/a/proj".to_string(),
to: NVCC_ROOT_SENTINEL.to_string(),
}];
assert_eq!(
nvcc_normalize_path("/a/proj/src/k.cu", &maps),
"/kache/nvcc-root/src/k.cu"
);
assert_eq!(nvcc_normalize_path("/a/proj", &maps), "/kache/nvcc-root");
assert_eq!(nvcc_normalize_path("/a/proj2/k.cu", &maps), "/a/proj2/k.cu");
assert_eq!(nvcc_normalize_path("/other/k.cu", &maps), "/other/k.cu");
let win_maps = vec![NvccPrefixMap {
from: "C:\\proj".to_string(),
to: NVCC_ROOT_SENTINEL.to_string(),
}];
assert_eq!(
nvcc_normalize_path("C:\\proj\\k.cu", &win_maps),
"/kache/nvcc-root\\k.cu"
);
assert_eq!(
nvcc_normalize_path("C:\\proj2\\k.cu", &win_maps),
"C:\\proj2\\k.cu"
);
}
#[test]
fn prefix_map_args_reject_unsafe_sides() {
let ok = nvcc_prefix_map_args(&[NvccPrefixMap {
from: "/a/proj".to_string(),
to: NVCC_ROOT_SENTINEL.to_string(),
}])
.unwrap();
assert_eq!(
ok,
vec![
"-Xcompiler".to_string(),
"-ffile-prefix-map=/a/proj=/kache/nvcc-root".to_string()
]
);
for bad in ["/a,proj", "/a proj", "", "/a\"proj"] {
assert!(
nvcc_prefix_map_args(&[NvccPrefixMap {
from: bad.to_string(),
to: NVCC_ROOT_SENTINEL.to_string(),
}])
.is_err(),
"{bad:?} must not be forwarded"
);
}
assert!(
nvcc_prefix_map_args(&[NvccPrefixMap {
from: "/a/proj".to_string(),
to: "/kache,nvcc-root".to_string(),
}])
.is_err()
);
}
#[test]
fn epoch_opt_out_names() {
for enabled in ["passthrough", "wallclock", "off", "PASSTHROUGH", " Off "] {
assert!(nvcc_epoch_opt_out(enabled), "{enabled:?} must opt out");
}
for other in ["", "yes", "0", "1", "passthroughx"] {
assert!(!nvcc_epoch_opt_out(other), "{other:?} must pin");
}
}
#[test]
fn epoch_resolution_prefers_build_pin_then_opt_out() {
use std::ffi::OsString;
assert_eq!(
resolve_nvcc_source_date_epoch(Some(OsString::from("12345")), false),
Some(OsString::from("12345"))
);
assert_eq!(
resolve_nvcc_source_date_epoch(Some(OsString::from("12345")), true),
Some(OsString::from("12345"))
);
assert_eq!(resolve_nvcc_source_date_epoch(None, true), None);
assert_eq!(
resolve_nvcc_source_date_epoch(None, false),
Some(OsString::from("0"))
);
}
#[test]
fn object_scan_finds_raw_roots() {
let dir = tempfile::tempdir().unwrap();
let object = dir.path().join("k.o");
std::fs::write(&object, b"\x7fELF__FILE__=/a/proj/src/k.cu\x00rest").unwrap();
let maps = vec![
NvccPrefixMap {
from: "/unrelated".to_string(),
to: NVCC_ROOT_SENTINEL.to_string(),
},
NvccPrefixMap {
from: "/a/proj".to_string(),
to: NVCC_ROOT_SENTINEL.to_string(),
},
];
assert!(nvcc_object_embeds_mapped_root(&object, &maps).unwrap());
std::fs::write(&object, b"\x7fELF__FILE__=/kache/nvcc-root/src/k.cu\x00").unwrap();
assert!(!nvcc_object_embeds_mapped_root(&object, &maps).unwrap());
let empty_maps = vec![NvccPrefixMap {
from: String::new(),
to: NVCC_ROOT_SENTINEL.to_string(),
}];
assert!(!nvcc_object_embeds_mapped_root(&object, &empty_maps).unwrap());
assert!(nvcc_object_embeds_mapped_root(&dir.path().join("missing.o"), &maps).is_err());
}
#[test]
fn target_label_names_arches() {
assert_eq!(nvcc_target_label(&[]), "generic");
assert_eq!(
nvcc_target_label(&[
"-arch=sm_80".to_string(),
"-gencode arch=compute_90,code=sm_90".to_string(),
"--gpu-architecture=compute_90".to_string(),
"-code=sm_90".to_string(),
"--gpu-code=sm_90".to_string(),
"-O2".to_string(),
]),
"--gpu-architecture=compute_90+--gpu-code=sm_90+-arch=sm_80+-code=sm_90+-gencode arch=compute_90,code=sm_90"
);
}
#[test]
fn base_dirs_ordering_is_canonical() {
let _lock = crate::test_support::process_state_test_lock();
let sorted = NvccCompiler::with_extra_allowlist_flags(Vec::new()).with_base_dirs(vec![
"/b".to_string(),
"/a".to_string(),
"/b".to_string(),
]);
let maps_a = nvcc_prefix_maps(Path::new("k.cu"), None, &sorted.base_dirs);
let maps_b = nvcc_prefix_maps(
Path::new("k.cu"),
None,
&["/a".to_string(), "/b".to_string()],
);
assert_eq!(maps_a, maps_b);
}
}