#[cfg(target_os = "linux")]
use std::time::Duration;
#[cfg(target_os = "linux")]
use crate::doctor::exec::try_exec;
use crate::doctor::types::{Check, CheckCtx, CheckResult, Severity};
static CHECKS: &[&Check] = &[&ROCM_VERSION, &LIBAMDGPU_TOP_ABI, &DRI_PERMS, &MUSL_GATE];
pub fn checks() -> &'static [&'static Check] {
CHECKS
}
static ROCM_VERSION: Check = Check {
id: "amd.rocm.version",
title: "ROCm version",
severity_on_fail: Severity::Warn,
run: check_rocm,
};
static LIBAMDGPU_TOP_ABI: Check = Check {
id: "amd.libamdgpu_top.abi",
title: "libamdgpu_top ABI",
severity_on_fail: Severity::Warn,
run: check_libamdgpu_top,
};
static DRI_PERMS: Check = Check {
id: "amd.dri.perms",
title: "/dev/dri permissions",
severity_on_fail: Severity::Warn,
run: check_dri_perms,
};
static MUSL_GATE: Check = Check {
id: "amd.build.target_env",
title: "AMD build-time availability",
severity_on_fail: Severity::Warn,
run: check_musl_gate,
};
fn check_rocm(_ctx: &CheckCtx) -> CheckResult {
#[cfg(target_os = "linux")]
{
for path in &[
"/opt/rocm/.info/version",
"/opt/rocm/.info/version-dev",
"/opt/rocm/lib/rocm-release-info/version",
] {
if let Ok(s) = std::fs::read_to_string(path) {
let v = s.trim();
if !v.is_empty() {
return CheckResult::Pass(format!("ROCm {v} ({path})"));
}
}
}
if let Some(out) = try_exec("rocminfo", &[], Duration::from_millis(2_500))
&& out.success()
&& let Some(line) = out.stdout.lines().find(|l| l.contains("ROCm"))
{
return CheckResult::Pass(line.trim().to_string());
}
CheckResult::Skip("ROCm not detected (neither /opt/rocm nor rocminfo)".to_string())
}
#[cfg(not(target_os = "linux"))]
{
CheckResult::Skip("ROCm is Linux-only".to_string())
}
}
fn check_libamdgpu_top(_ctx: &CheckCtx) -> CheckResult {
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
{
CheckResult::Pass(format!(
"linked libamdgpu_top {}",
env!("CARGO_PKG_VERSION")
))
}
#[cfg(all(target_os = "linux", target_env = "musl"))]
{
CheckResult::Skip(
"libamdgpu_top not linked in musl builds (see amd.build.target_env)".to_string(),
)
}
#[cfg(not(target_os = "linux"))]
{
CheckResult::Skip("libamdgpu_top is Linux-only".to_string())
}
}
fn check_dri_perms(_ctx: &CheckCtx) -> CheckResult {
#[cfg(target_os = "linux")]
{
let p = std::path::Path::new("/dev/dri");
if !p.exists() {
return CheckResult::Skip("/dev/dri missing".to_string());
}
match std::fs::read_dir(p) {
Ok(iter) => {
let nodes: Vec<String> = iter
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.filter(|n| n.starts_with("render") || n.starts_with("card"))
.collect();
if nodes.is_empty() {
CheckResult::Skip("no card* or render* nodes".to_string())
} else {
CheckResult::Pass(format!("{} node(s): {}", nodes.len(), nodes.join(", ")))
}
}
Err(e) => CheckResult::Fail(
format!("/dev/dri unreadable: {e}"),
Some("ensure the caller is in the render group".to_string()),
),
}
}
#[cfg(not(target_os = "linux"))]
{
CheckResult::Skip("/dev/dri is Linux-only".to_string())
}
}
fn check_musl_gate(_ctx: &CheckCtx) -> CheckResult {
#[cfg(target_env = "musl")]
{
CheckResult::Warn(
"musl build — AMD support compiled out".to_string(),
Some("use a glibc build (x86_64-unknown-linux-gnu) for AMD GPU monitoring".to_string()),
)
}
#[cfg(not(target_env = "musl"))]
{
CheckResult::Pass("glibc or non-Linux target — AMD support available".to_string())
}
}