use std::path::Path;
use crate::util::system;
const HOST_TOOLS: &[(&str, &str)] = &[("curl", "curl"), ("unzip", "unzip"), ("c++", "g++")];
pub const ROCM_HEADERS: &[(&str, &str)] = &[
("hip/hip_runtime.h", "hip-dev"),
("rccl/rccl.h", "rccl-dev"),
("hipblas/hipblas.h", "hipblas-dev"),
("hipblas-common/hipblas-common.h", "hipblas-common-dev"),
("hipblaslt/hipblaslt.h", "hipblaslt-dev"),
("hipsolver/hipsolver.h", "hipsolver-dev"),
("hipsparse/hipsparse.h", "hipsparse-dev"),
];
pub const CUDA_HEADERS: &[(&str, &str)] = &[
("cuda_runtime.h", "cuda-cudart-dev-<M>-<m>"),
("crt/host_config.h", "cuda-crt-<M>-<m>"),
("cublas_v2.h", "libcublas-dev-<M>-<m>"),
("cusolverDn.h", "libcusolver-dev-<M>-<m>"),
("cusparse.h", "libcusparse-dev-<M>-<m>"),
("nccl.h", "libnccl-dev"),
];
pub fn missing_host_tools() -> Vec<&'static str> {
HOST_TOOLS
.iter()
.filter(|(probe, _)| {
if *probe == "curl" {
return !system::has_command("curl") && !system::has_command("wget");
}
!system::has_command(probe)
})
.map(|(_, pkg)| *pkg)
.collect()
}
const SYSTEM_INCLUDE_DIRS: &[&str] = &["/usr/include", "/usr/local/include"];
pub fn missing_headers<'a>(
root: &Path,
headers: &'a [(&'a str, &'a str)],
) -> Vec<&'a (&'a str, &'a str)> {
let root_include = root.join("include");
headers
.iter()
.filter(|(h, _)| {
if root_include.join(h).exists() {
return false;
}
if SYSTEM_INCLUDE_DIRS
.iter()
.any(|d| Path::new(d).join(h).exists())
{
return false;
}
!matches!(header_reachable(h, &[root_include.as_path()]), Some(true))
})
.collect()
}
pub fn header_reachable(header: &str, include_dirs: &[&Path]) -> Option<bool> {
use std::io::Write;
use std::process::{Command, Stdio};
let cxx = std::env::var("CXX").unwrap_or_else(|_| "c++".to_string());
if !system::has_command(&cxx) {
return None;
}
let mut cmd = Command::new(&cxx);
for dir in include_dirs {
cmd.arg("-I").arg(dir);
}
cmd.args(["-E", "-x", "c++", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null());
let mut child = cmd.spawn().ok()?;
child
.stdin
.as_mut()?
.write_all(format!("#include <{header}>\n").as_bytes())
.ok()?;
Some(child.wait().ok()?.success())
}
pub fn packages_for(missing: &[&(&str, &str)]) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
missing
.iter()
.filter(|(_, p)| seen.insert(*p))
.map(|(_, p)| (*p).to_string())
.collect()
}
#[derive(Debug)]
pub struct ToolkitGap {
pub root: std::path::PathBuf,
pub headers: Vec<String>,
pub install: String,
}
pub fn toolkit_gap(vendor: flodl_hw::GpuVendor) -> Option<ToolkitGap> {
use std::path::PathBuf;
let (root, headers, metapackages): (PathBuf, _, Option<&[&str]>) = match vendor {
flodl_hw::GpuVendor::Amd => (
flodl_hw::rocm_runtime_root()
.or_else(|| std::env::var("ROCM_PATH").ok().map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("/opt/rocm")),
ROCM_HEADERS,
None,
),
flodl_hw::GpuVendor::Nvidia => (
PathBuf::from(
std::env::var("CUDA_HOME").unwrap_or_else(|_| "/usr/local/cuda".to_string()),
),
CUDA_HEADERS,
Some(&["cuda-toolkit", "libnccl-dev"]),
),
_ => return None,
};
let missing = missing_headers(&root, headers);
if missing.is_empty() {
return None;
}
let packages: Vec<String> = match metapackages {
Some(m) => m.iter().map(|p| p.to_string()).collect(),
None => packages_for(&missing),
};
Some(ToolkitGap {
root,
headers: missing.iter().map(|(h, _)| h.to_string()).collect(),
install: install_hint(&packages),
})
}
pub fn rpm_name(deb: &str) -> String {
if deb == "g++" {
return "gcc-c++".to_string();
}
match deb.strip_suffix("-dev") {
Some(stem) => format!("{stem}-devel"),
None => deb.replace("-dev-", "-devel-"),
}
}
pub fn install_hint(packages: &[String]) -> String {
if packages.is_empty() {
return String::new();
}
if cfg!(target_os = "macos") {
format!(
"brew install {} (names may differ on macOS)",
packages.join(" ")
)
} else if cfg!(target_os = "windows") {
"no native Windows build is supported; use WSL2 \
(https://flodl.dev/guide/windows-wsl)"
.to_string()
} else if crate::util::platform::Platform::detect() == crate::util::platform::Platform::Rhel {
let list = packages.iter().map(|p| rpm_name(p)).collect::<Vec<_>>();
format!(
"sudo dnf install {} (or your distribution's equivalent)",
list.join(" ")
)
} else {
format!(
"sudo apt install {} (or your distribution's equivalent)",
packages.join(" ")
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn scratch_root(tag: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!("flodl-req-{tag}-{}", std::process::id()));
std::fs::create_dir_all(root.join("include/sub")).unwrap();
std::fs::write(root.join("include/present.h"), "").unwrap();
std::fs::write(root.join("include/sub/nested.h"), "").unwrap();
root
}
#[test]
fn missing_headers_lists_only_what_is_absent() {
let root = scratch_root("absent");
let table: &[(&str, &str)] = &[
("present.h", "present-pkg"),
("sub/nested.h", "nested-pkg"),
("nope.h", "absent-pkg"),
];
let missing = missing_headers(&root, table);
assert_eq!(missing.len(), 1, "{missing:?}");
assert_eq!(missing[0].1, "absent-pkg");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_system_header_counts_as_present() {
let root = scratch_root("sys");
let sys_header = SYSTEM_INCLUDE_DIRS
.iter()
.map(|d| Path::new(d).join("stdio.h"))
.find(|p| p.exists());
if let Some(h) = sys_header {
let name = h.file_name().unwrap().to_str().unwrap();
let table: &[(&str, &str)] = &[("stdio.h", "libc6-dev")];
assert!(
missing_headers(&root, table).is_empty(),
"{name} is in a default include dir and must not be reported missing"
);
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn packages_are_deduplicated() {
let a = ("h1", "pkg");
let b = ("h2", "other");
let c = ("h3", "pkg");
let missing = vec![&a, &b, &c];
assert_eq!(packages_for(&missing), vec!["pkg", "other"]);
}
#[test]
fn every_header_names_a_package() {
assert_eq!(ROCM_HEADERS.len(), 7);
for (h, p) in ROCM_HEADERS {
assert!(!h.is_empty() && !p.is_empty(), "{h} -> {p}");
}
for (h, p) in CUDA_HEADERS {
assert!(!h.is_empty() && !p.is_empty(), "{h} -> {p}");
}
}
#[test]
fn install_hint_is_empty_when_nothing_is_missing() {
assert!(install_hint(&[]).is_empty());
}
#[test]
fn install_hint_names_the_packages() {
let h = install_hint(&["curl".into(), "g++".into()]);
if cfg!(target_os = "windows") {
assert!(h.contains("WSL2"), "{h}");
} else {
assert!(h.contains("curl"), "{h}");
assert!(h.contains("g++") || h.contains("gcc-c++"), "{h}");
}
}
#[test]
fn the_compiler_answers_for_headers_the_path_scan_cannot_see() {
match header_reachable("cstdio", &[]) {
Some(true) => {}
Some(false) => panic!("the compiler could not resolve <cstdio>"),
None => {}
}
if header_reachable("cstdio", &[]) == Some(true) {
assert_eq!(
header_reachable("flodl_no_such_header_42.h", &[]),
Some(false),
);
}
}
#[test]
fn a_header_outside_the_toolkit_root_is_not_reported_missing() {
let root = scratch_root("reach");
let table: &[(&str, &str)] = &[("cstdio", "libstdc++-dev")];
let missing = missing_headers(&root, table);
if header_reachable("cstdio", &[]) == Some(true) {
assert!(
missing.is_empty(),
"compiler-visible header reported missing"
);
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn rpm_names_match_the_verified_rhel_spellings() {
for (deb, rpm) in [
("hip-dev", "hip-devel"),
("rccl-dev", "rccl-devel"),
("hipblas-common-dev", "hipblas-common-devel"),
("hipblaslt-dev", "hipblaslt-devel"),
("cuda-cudart-dev-<M>-<m>", "cuda-cudart-devel-<M>-<m>"),
("libcublas-dev-<M>-<m>", "libcublas-devel-<M>-<m>"),
("libnccl-dev", "libnccl-devel"),
("cuda-crt-<M>-<m>", "cuda-crt-<M>-<m>"),
("cuda-toolkit", "cuda-toolkit"),
("g++", "gcc-c++"),
("curl", "curl"),
("unzip", "unzip"),
] {
assert_eq!(rpm_name(deb), rpm, "{deb}");
}
}
}