use std::path::{Path, PathBuf};
use std::process::Command;
struct Tool {
stage: &'static str,
path: Option<PathBuf>,
is_gnu: bool,
}
fn resolve_tool(stage: &'static str, env_var: &str, candidates: &[&str]) -> Tool {
let mut tried: Vec<String> = Vec::new();
if let Ok(p) = std::env::var(env_var) {
tried.push(p);
}
for c in candidates {
tried.push(c.to_string());
}
for cand in &tried {
if let Some(path) = which(cand) {
let is_gnu = looks_like_gnu(&path);
return Tool { stage, path: Some(path), is_gnu };
}
}
Tool { stage, path: None, is_gnu: false }
}
fn which(name: &str) -> Option<PathBuf> {
let p = Path::new(name);
if p.is_absolute() || name.contains('/') {
return if p.exists() { Some(p.to_path_buf()) } else { None };
}
let path = std::env::var("PATH").unwrap_or_default();
for dir in path.split(':') {
let cand = Path::new(dir).join(name);
if cand.exists() {
return Some(cand);
}
}
None
}
fn looks_like_gnu(path: &Path) -> bool {
let out = Command::new(path).arg("--version").output();
if let Ok(o) = out {
let v = String::from_utf8_lossy(&o.stdout).to_lowercase();
let is_rs = v.contains("-rs") || v.contains("rust") || v.contains("automake-rs");
return v.contains("gnu") && !is_rs;
}
false
}
pub struct BootstrapReport {
pub ok: bool,
pub receipt_json: String,
}
pub fn run_bootstrap(dir: &Path, forbid_gnu: bool, verbose: bool) -> BootstrapReport {
let cf = ["configure.ac", "configure.in"]
.iter()
.map(|n| dir.join(n))
.find(|p| p.exists());
let cf = match cf {
Some(p) => p,
None => {
return BootstrapReport {
ok: false,
receipt_json: err_receipt("no configure.ac / configure.in found"),
}
}
};
let ac_text = std::fs::read_to_string(&cf).unwrap_or_default();
let needs_header = ac_text.contains("AC_CONFIG_HEADERS") || ac_text.contains("AC_CONFIG_HEADER");
let aclocal = resolve_tool("aclocal", "ACLOCAL_RS", &["aclocal-rs", "acrs-aclocal", "aclocal"]);
let autoconf = resolve_tool("autoconf", "AUTOCONF_RS", &["autoconf-rs", "acrs-autoconf", "autoconf"]);
let autoheader = resolve_tool("autoheader", "AUTOHEADER_RS", &["autoheader-rs", "acrs-autoheader", "autoheader"]);
let mut steps: Vec<String> = Vec::new();
let mut ok = true;
let mut run = |tool: &Tool, args: &[&str], stdout_to: Option<&Path>| -> bool {
let path = match &tool.path {
Some(p) => p,
None => {
steps.push(step_json(tool.stage, "missing", "no native provider found", None));
return false;
}
};
if tool.is_gnu && forbid_gnu {
steps.push(step_json(tool.stage, "rejected-gnu", &path.display().to_string(), None));
return false;
}
let mut cmd = Command::new(path);
cmd.current_dir(dir).args(args);
let output = cmd.output();
match output {
Ok(o) => {
if let Some(dest) = stdout_to {
let _ = std::fs::write(dir.join(dest), &o.stdout);
}
let provider = if tool.is_gnu { "GNU (allowed: --forbid-gnu off)" } else { "native (autoconf-rs)" };
let st = if o.status.success() { "ok" } else { "nonzero-exit" };
steps.push(step_json(tool.stage, st, &path.display().to_string(), Some(provider)));
o.status.success()
}
Err(e) => {
steps.push(step_json(tool.stage, "spawn-failed", &e.to_string(), None));
false
}
}
};
if verbose { eprintln!("autoreconf-rs: aclocal -> aclocal.m4"); }
let _ = run(&aclocal, &[cf.file_name().unwrap().to_str().unwrap()], Some(Path::new("aclocal.m4")));
if verbose { eprintln!("autoreconf-rs: autoconf -> configure"); }
let cfg_ok = run(&autoconf, &[cf.file_name().unwrap().to_str().unwrap()], Some(Path::new("configure")));
if cfg_ok {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(dir.join("configure"), std::fs::Permissions::from_mode(0o755));
}
} else {
ok = false;
}
if needs_header {
if verbose { eprintln!("autoreconf-rs: autoheader -> config.h.in"); }
let _ = run(&autoheader, &[cf.file_name().unwrap().to_str().unwrap()], Some(Path::new("config.h.in")));
}
steps.push(step_json("aux-files", "delegated", "automake-rs --add-missing (native)", Some("native (automake-rs)")));
steps.push(step_json("makefile.in", "delegated", "automake-rs (native)", Some("native (automake-rs)")));
let gnu_used: Vec<&str> = [&aclocal, &autoconf, &autoheader]
.iter()
.filter(|t| t.is_gnu && !forbid_gnu && t.path.is_some())
.map(|t| t.stage)
.collect();
let receipt = format!(
"{{\n \"native_bootstrap\": {},\n \"forbid_gnu\": {},\n \"gnu_tools_invoked\": [{}],\n \"providers\": {{\n \"aclocal\": {},\n \"autoconf\": {},\n \"autoheader\": {},\n \"aux\": \"automake-rs\",\n \"makefile_in\": \"automake-rs\"\n }},\n \"steps\": [\n{}\n ]\n}}\n",
ok && gnu_used.is_empty(),
forbid_gnu,
gnu_used.iter().map(|s| format!("{:?}", s)).collect::<Vec<_>>().join(", "),
provider_str(&aclocal, forbid_gnu),
provider_str(&autoconf, forbid_gnu),
provider_str(&autoheader, forbid_gnu),
steps.iter().map(|s| format!(" {}", s)).collect::<Vec<_>>().join(",\n"),
);
let _ = std::fs::write(dir.join("bootstrap-receipt.json"), &receipt);
BootstrapReport { ok, receipt_json: receipt }
}
fn provider_str(t: &Tool, forbid_gnu: bool) -> String {
match &t.path {
None => "\"unresolved\"".to_string(),
Some(p) => {
let kind = if t.is_gnu {
if forbid_gnu { "rejected-gnu" } else { "gnu" }
} else {
"native"
};
format!("{{ \"path\": {:?}, \"kind\": {:?} }}", p.display().to_string(), kind)
}
}
}
fn step_json(stage: &str, status: &str, detail: &str, provider: Option<&str>) -> String {
match provider {
Some(p) => format!(
"{{ \"stage\": {:?}, \"status\": {:?}, \"detail\": {:?}, \"provider\": {:?} }}",
stage, status, detail, p
),
None => format!(
"{{ \"stage\": {:?}, \"status\": {:?}, \"detail\": {:?} }}",
stage, status, detail
),
}
}
fn err_receipt(msg: &str) -> String {
format!("{{ \"native_bootstrap\": false, \"error\": {:?} }}\n", msg)
}