use std::future::Future;
use std::io;
use std::path::{Path, PathBuf};
use cbh_git::{capture, parse_git_info};
use cbh_model::GitInfo;
use crate::host::{RustcInfo, parse_rustc_verbose};
use crate::machine::{HardwareProfile, system_profile};
pub trait EnvironmentProbe {
fn git(&self) -> impl Future<Output = io::Result<GitInfo>>;
fn toolchain(&self) -> impl Future<Output = io::Result<RustcInfo>>;
fn hardware(&self) -> impl Future<Output = HardwareProfile>;
}
#[derive(Clone, Debug, Default)]
pub struct SystemProbe {
repo: Option<PathBuf>,
}
impl SystemProbe {
#[must_use]
pub fn in_dir(repo: impl Into<PathBuf>) -> Self {
Self {
repo: Some(repo.into()),
}
}
#[cfg_attr(test, mutants::skip)] async fn git_field(&self, args: &[&str]) -> String {
let repo = self
.repo
.as_deref()
.map(Path::to_string_lossy)
.map(std::borrow::Cow::into_owned);
let mut full: Vec<&str> = Vec::new();
if let Some(repo) = repo.as_deref() {
full.push("-C");
full.push(repo);
}
full.extend_from_slice(args);
match capture("git", &full).await {
Ok(output) if output.status.success() => output.stdout,
_ => String::new(),
}
}
}
impl EnvironmentProbe for SystemProbe {
#[cfg_attr(test, mutants::skip)] async fn git(&self) -> io::Result<GitInfo> {
let commit = self.git_field(&["rev-parse", "HEAD"]).await;
let branch = self.git_field(&["rev-parse", "--abbrev-ref", "HEAD"]).await;
let status = self.git_field(&["status", "--porcelain"]).await;
Ok(parse_git_info(&commit, &branch, &status))
}
#[cfg_attr(test, mutants::skip)] async fn toolchain(&self) -> io::Result<RustcInfo> {
let output = capture("rustc", &["-vV"]).await.ok();
let rustc_output = output
.as_ref()
.filter(|output| output.status.success())
.map(|output| output.stdout.as_str());
Ok(resolve_toolchain(rustc_output))
}
#[cfg_attr(test, mutants::skip)] async fn hardware(&self) -> HardwareProfile {
system_profile().await
}
}
fn resolve_toolchain(rustc_output: Option<&str>) -> RustcInfo {
let mut info = rustc_output.map_or_else(RustcInfo::default, parse_rustc_verbose);
if info.host.is_none() {
info.host = Some(fallback_host_triple());
}
info
}
fn fallback_host_triple() -> String {
use std::env::consts;
host_triple_for(consts::ARCH, consts::OS)
}
fn host_triple_for(arch: &str, os: &str) -> String {
match os {
"windows" => format!("{arch}-pc-windows-msvc"),
"macos" => format!("{arch}-apple-darwin"),
"linux" => format!("{arch}-unknown-linux-gnu"),
other => format!("{arch}-unknown-{other}"),
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::env::consts;
use std::path::PathBuf;
use super::{SystemProbe, fallback_host_triple, host_triple_for, resolve_toolchain};
#[test]
fn in_dir_targets_the_repository_directory() {
let probe = SystemProbe::in_dir("some/repo");
assert_eq!(probe.repo, Some(PathBuf::from("some/repo")));
assert_eq!(SystemProbe::default().repo, None);
}
#[test]
fn fallback_host_triple_describes_the_running_platform() {
let triple = fallback_host_triple();
assert!(triple.starts_with(consts::ARCH), "{triple}");
if cfg!(target_os = "windows") {
assert!(triple.ends_with("-pc-windows-msvc"), "{triple}");
} else if cfg!(target_os = "macos") {
assert!(triple.ends_with("-apple-darwin"), "{triple}");
} else if cfg!(target_os = "linux") {
assert!(triple.ends_with("-unknown-linux-gnu"), "{triple}");
}
}
#[test]
fn host_triple_for_covers_each_platform() {
assert_eq!(
host_triple_for("x86_64", "windows"),
"x86_64-pc-windows-msvc"
);
assert_eq!(host_triple_for("aarch64", "macos"), "aarch64-apple-darwin");
assert_eq!(
host_triple_for("x86_64", "linux"),
"x86_64-unknown-linux-gnu"
);
assert_eq!(host_triple_for("riscv64", "redox"), "riscv64-unknown-redox");
}
#[test]
fn resolve_toolchain_parses_rustc_output() {
let info = resolve_toolchain(Some("release: 1.91.0\nhost: x86_64-unknown-linux-gnu\n"));
assert_eq!(info.version.as_deref(), Some("1.91.0"));
assert_eq!(info.host.as_deref(), Some("x86_64-unknown-linux-gnu"));
}
#[test]
fn resolve_toolchain_without_output_falls_back_to_platform_host() {
let info = resolve_toolchain(None);
assert_eq!(info.version, None);
assert_eq!(info.host, Some(fallback_host_triple()));
}
#[test]
fn resolve_toolchain_fills_missing_host_from_platform() {
let info = resolve_toolchain(Some("release: 1.91.0\n"));
assert_eq!(info.version.as_deref(), Some("1.91.0"));
assert_eq!(info.host, Some(fallback_host_triple()));
}
}