rcpufetch 0.0.5

[ALPHA] A rusty crossplatform, but simple CLI binutil for reading CPU information.
//! Shared FreeBSD/OpenBSD CPU info module. Upstream cpufetch doesn't build ARM or
//! PowerPC on FreeBSD at all (x86-only there) and has no OpenBSD support whatsoever;
//! this module fixes both gaps by leaning on `sysctl` (present on every BSD, same
//! shell-out pattern already used for macOS) for the OS-level facts and `crate::cpu`
//! for arch-specific vendor/microarchitecture detection, so it works the same way on
//! any architecture rcpufetch supports rather than being x86-only.

use crate::cpu::ArchInfo;
use crate::report::CpuReport;
use std::process::Command;

pub struct BsdCpuInfo {
    model: String,
    vendor: String,
    logical_cores: u32,
    max_mhz: Option<f32>,
    arch_info: ArchInfo,
}

fn sysctl(key: &str) -> Option<String> {
    let output = Command::new("sysctl").arg("-n").arg(key).output().ok()?;
    if !output.status.success() {
        return None;
    }
    let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if s.is_empty() { None } else { Some(s) }
}

impl BsdCpuInfo {
    pub fn new() -> Result<Self, String> {
        let model = sysctl("hw.model").ok_or_else(|| "sysctl hw.model failed".to_string())?;
        let logical_cores = sysctl("hw.ncpu").and_then(|s| s.parse().ok()).unwrap_or(1);

        // OpenBSD reports current speed as `hw.cpuspeed` (MHz); FreeBSD exposes the
        // available frequency steps as `dev.cpu.0.freq_levels` ("maxMHz/maxmW ...") or,
        // failing that, the boot-time `hw.clockrate`.
        let max_mhz = sysctl("hw.cpuspeed")
            .or_else(|| {
                sysctl("dev.cpu.0.freq_levels")
                    .and_then(|s| s.split('/').next().map(|s| s.to_string()))
            })
            .or_else(|| sysctl("hw.clockrate"))
            .and_then(|s| s.parse::<f32>().ok());

        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
        let arch_info = crate::cpu::x86::detect();
        #[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
        let arch_info = ArchInfo {
            vendor: None,
            uarch: None,
            soc: crate::cpu::arm::soc_from_hint(&model).map(|s| s.to_string()),
            process_nm: None,
            features: Vec::new(),
        };
        #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
        let arch_info = crate::cpu::ppc::detect_from_name_str(&model);
        #[cfg(not(any(
            target_arch = "x86",
            target_arch = "x86_64",
            target_arch = "arm",
            target_arch = "aarch64",
            target_arch = "powerpc",
            target_arch = "powerpc64"
        )))]
        let arch_info = ArchInfo::default();

        let vendor = arch_info.vendor.clone().unwrap_or_else(|| {
            let lower = model.to_lowercase();
            if lower.contains("intel") {
                "Intel".to_string()
            } else if lower.contains("amd") {
                "AMD".to_string()
            } else if lower.contains("arm") || lower.contains("aarch64") {
                "ARM".to_string()
            } else {
                "Unknown".to_string()
            }
        });

        Ok(Self {
            model,
            vendor,
            logical_cores,
            max_mhz,
            arch_info,
        })
    }

    pub fn to_report(&self) -> CpuReport {
        let info = &self.arch_info;
        CpuReport {
            model: self.model.clone(),
            vendor: info.vendor.clone().unwrap_or_else(|| self.vendor.clone()),
            uarch: info.uarch.clone(),
            soc: info.soc.clone(),
            technology_nm: info.process_nm,
            architecture: std::env::consts::ARCH.to_string(),
            // BSD sysctl doesn't cleanly separate physical/logical without extra
            // per-arch parsing; report logical for both rather than guess.
            physical_cores: self.logical_cores,
            logical_cores: self.logical_cores,
            max_ghz: self.max_mhz.map(|mhz| mhz / 1000.0),
            base_ghz: None,
            l1i_kb: None,
            l1d_kb: None,
            l2_kb: None,
            l3_kb: None,
            flags: info.features.clone(),
        }
    }
}