use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct RustcOutput {
pub success: bool,
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub command_line: String,
pub primary_output: Option<PathBuf>,
pub metadata_output: Option<PathBuf>,
}
static RESOLVED_RUSTC: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
static RESOLVED_CLIPPY: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
static RESOLVED_RUSTDOC: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
static RESOLVED_CARGO: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
static RESOLVED_HOST_TRIPLE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
pub fn resolve_cargo() -> PathBuf {
RESOLVED_CARGO
.get_or_init(|| {
if let Ok(p) = std::env::var("CARGO") {
let pb = PathBuf::from(p);
if pb.is_file() {
return pb;
}
}
if let Ok(paths) = std::env::var("PATH") {
for dir in std::env::split_paths(&paths) {
let p = dir.join("cargo");
if p.is_file() {
return p;
}
}
}
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".cargo/bin/cargo"))
.filter(|p| p.is_file())
.unwrap_or_else(|| PathBuf::from("cargo"))
})
.clone()
}
pub fn resolve_host_triple() -> &'static str {
RESOLVED_HOST_TRIPLE.get_or_init(|| {
let rustc = resolve_rustc();
if let Ok(out) = std::process::Command::new(&rustc).arg("-vV").output() {
if let Ok(text) = String::from_utf8(out.stdout) {
for line in text.lines() {
if let Some(rest) = line.strip_prefix("host: ") {
return rest.trim().to_string();
}
}
}
}
"unknown".to_string()
})
}
pub fn resolve_rustc() -> PathBuf {
RESOLVED_RUSTC
.get_or_init(|| {
if let Ok(p) = std::env::var("RUSTC") {
return PathBuf::from(p);
}
let home_cargo_rustup = std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".cargo/bin/rustup"));
let candidates = ["rustup".into()]
.into_iter()
.chain(home_cargo_rustup)
.collect::<Vec<PathBuf>>();
for rustup in candidates {
if let Ok(output) = std::process::Command::new(&rustup)
.arg("which")
.arg("rustc")
.output()
{
if output.status.success() {
if let Ok(s) = String::from_utf8(output.stdout) {
let p = PathBuf::from(s.trim());
if p.is_file() {
return p;
}
}
}
}
}
if let Ok(paths) = std::env::var("PATH") {
for dir in std::env::split_paths(&paths) {
let p = dir.join("rustc");
if p.is_file() {
return p;
}
}
}
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".cargo/bin/rustc"))
.filter(|p| p.is_file())
.unwrap_or_else(|| PathBuf::from("rustc"))
})
.clone()
}
pub fn resolve_clippy_driver() -> PathBuf {
RESOLVED_CLIPPY
.get_or_init(|| {
if let Ok(p) = std::env::var("CLIPPY_DRIVER") {
return PathBuf::from(p);
}
let home_cargo_rustup = std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".cargo/bin/rustup"));
let candidates = ["rustup".into()]
.into_iter()
.chain(home_cargo_rustup)
.collect::<Vec<PathBuf>>();
for rustup in candidates {
if let Ok(output) = std::process::Command::new(&rustup)
.arg("which")
.arg("clippy-driver")
.output()
{
if output.status.success() {
if let Ok(s) = String::from_utf8(output.stdout) {
let p = PathBuf::from(s.trim());
if p.is_file() {
return p;
}
}
}
}
}
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".cargo/bin/clippy-driver"))
.filter(|p| p.is_file())
.unwrap_or_else(|| PathBuf::from("clippy-driver"))
})
.clone()
}
pub fn resolve_rustdoc() -> PathBuf {
RESOLVED_RUSTDOC
.get_or_init(|| {
if let Ok(p) = std::env::var("RUSTDOC") {
return PathBuf::from(p);
}
let home_cargo_rustup = std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".cargo/bin/rustup"));
let candidates = ["rustup".into()]
.into_iter()
.chain(home_cargo_rustup)
.collect::<Vec<PathBuf>>();
for rustup in candidates {
if let Ok(output) = std::process::Command::new(&rustup)
.arg("which")
.arg("rustdoc")
.output()
{
if output.status.success() {
if let Ok(s) = String::from_utf8(output.stdout) {
let p = PathBuf::from(s.trim());
if p.is_file() {
return p;
}
}
}
}
}
if let Ok(paths) = std::env::var("PATH") {
for dir in std::env::split_paths(&paths) {
let p = dir.join("rustdoc");
if p.is_file() {
return p;
}
}
}
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".cargo/bin/rustdoc"))
.filter(|p| p.is_file())
.unwrap_or_else(|| PathBuf::from("rustdoc"))
})
.clone()
}
pub fn find_workspace_root(manifest_path: &Path) -> PathBuf {
let start = if manifest_path.is_dir() {
manifest_path
} else {
manifest_path.parent().unwrap_or(Path::new("."))
};
for anc in start.ancestors() {
let toml = anc.join("Cargo.toml");
if toml.is_file()
&& (fs::read_to_string(&toml)
.map(|c| c.contains("[workspace]"))
.unwrap_or(false)
|| anc.join("Cargo.lock").is_file())
{
return anc.to_path_buf();
}
}
start.to_path_buf()
}
pub fn parse_version_parts(version: &str) -> (String, String, String, String) {
if let Ok(sem) = semver::Version::parse(version) {
(
sem.major.to_string(),
sem.minor.to_string(),
sem.patch.to_string(),
sem.pre.as_str().to_string(),
)
} else {
let mut ver_parts = version.split('.');
let maj = ver_parts.next().unwrap_or("0");
let min = ver_parts.next().unwrap_or("0");
let pat_with_pre = ver_parts.next().unwrap_or("0");
let (pat, p) = pat_with_pre.split_once('-').unwrap_or((pat_with_pre, ""));
(
maj.to_string(),
min.to_string(),
pat.to_string(),
p.to_string(),
)
}
}