1use std::path::{Path, PathBuf};
17use std::process::Command;
18
19struct Tool {
21 stage: &'static str,
23 path: Option<PathBuf>,
25 is_gnu: bool,
27}
28
29fn resolve_tool(stage: &'static str, env_var: &str, candidates: &[&str]) -> Tool {
32 let mut tried: Vec<String> = Vec::new();
33 if let Ok(p) = std::env::var(env_var) {
34 tried.push(p);
35 }
36 for c in candidates {
37 tried.push(c.to_string());
38 }
39 for cand in &tried {
40 if let Some(path) = which(cand) {
41 let is_gnu = looks_like_gnu(&path);
42 return Tool { stage, path: Some(path), is_gnu };
43 }
44 }
45 Tool { stage, path: None, is_gnu: false }
46}
47
48fn which(name: &str) -> Option<PathBuf> {
50 let p = Path::new(name);
51 if p.is_absolute() || name.contains('/') {
52 return if p.exists() { Some(p.to_path_buf()) } else { None };
53 }
54 let path = std::env::var("PATH").unwrap_or_default();
55 for dir in path.split(':') {
56 let cand = Path::new(dir).join(name);
57 if cand.exists() {
58 return Some(cand);
59 }
60 }
61 None
62}
63
64fn looks_like_gnu(path: &Path) -> bool {
66 let out = Command::new(path).arg("--version").output();
67 if let Ok(o) = out {
68 let v = String::from_utf8_lossy(&o.stdout).to_lowercase();
69 let is_rs = v.contains("-rs") || v.contains("rust") || v.contains("automake-rs");
70 return v.contains("gnu") && !is_rs;
71 }
72 false
73}
74
75pub struct BootstrapReport {
77 pub ok: bool,
78 pub receipt_json: String,
79}
80
81pub fn run_bootstrap(dir: &Path, forbid_gnu: bool, verbose: bool) -> BootstrapReport {
84 let cf = ["configure.ac", "configure.in"]
85 .iter()
86 .map(|n| dir.join(n))
87 .find(|p| p.exists());
88 let cf = match cf {
89 Some(p) => p,
90 None => {
91 return BootstrapReport {
92 ok: false,
93 receipt_json: err_receipt("no configure.ac / configure.in found"),
94 }
95 }
96 };
97 let ac_text = std::fs::read_to_string(&cf).unwrap_or_default();
98 let needs_header = ac_text.contains("AC_CONFIG_HEADERS")
99 || ac_text.contains("AC_CONFIG_HEADER")
100 || ac_text.contains("AM_CONFIG_HEADER");
101
102 let aclocal = resolve_tool("aclocal", "ACLOCAL_RS", &["aclocal-rs", "acrs-aclocal", "aclocal"]);
104 let autoconf = resolve_tool("autoconf", "AUTOCONF_RS", &["autoconf-rs", "acrs-autoconf", "autoconf"]);
105 let autoheader = resolve_tool("autoheader", "AUTOHEADER_RS", &["autoheader-rs", "acrs-autoheader", "autoheader"]);
106
107 let mut steps: Vec<String> = Vec::new();
108 let mut ok = true;
109 let mut run = |tool: &Tool, args: &[&str], stdout_to: Option<&Path>| -> bool {
110 let path = match &tool.path {
112 Some(p) => p,
113 None => {
114 steps.push(step_json(tool.stage, "missing", "no native provider found", None));
115 return false;
116 }
117 };
118 if tool.is_gnu && forbid_gnu {
119 steps.push(step_json(tool.stage, "rejected-gnu", &path.display().to_string(), None));
120 return false;
121 }
122 let mut cmd = Command::new(path);
123 cmd.current_dir(dir).args(args);
124 let output = cmd.output();
125 match output {
126 Ok(o) => {
127 if let Some(dest) = stdout_to {
128 let _ = std::fs::write(dir.join(dest), &o.stdout);
129 }
130 let provider = if tool.is_gnu { "GNU (allowed: --forbid-gnu off)" } else { "native (autoconf-rs)" };
131 let st = if o.status.success() { "ok" } else { "nonzero-exit" };
132 steps.push(step_json(tool.stage, st, &path.display().to_string(), Some(provider)));
133 o.status.success()
134 }
135 Err(e) => {
136 steps.push(step_json(tool.stage, "spawn-failed", &e.to_string(), None));
137 false
138 }
139 }
140 };
141
142 if verbose { eprintln!("autoreconf-rs: aclocal -> aclocal.m4"); }
143 let _ = run(&aclocal, &[cf.file_name().unwrap().to_str().unwrap()], None);
147
148 if verbose { eprintln!("autoreconf-rs: autoconf -> configure"); }
149 let cfg_ok = run(&autoconf, &[cf.file_name().unwrap().to_str().unwrap()], Some(Path::new("configure")));
150 if cfg_ok {
151 #[cfg(unix)]
152 {
153 use std::os::unix::fs::PermissionsExt;
154 let _ = std::fs::set_permissions(dir.join("configure"), std::fs::Permissions::from_mode(0o755));
155 }
156 } else {
157 ok = false;
158 }
159
160 if needs_header {
161 let header = config_header_name(&ac_text);
165 let header_in = format!("{}.in", header);
166 if verbose { eprintln!("autoreconf-rs: autoheader -> {}", header_in); }
167 let _ = run(&autoheader, &[cf.file_name().unwrap().to_str().unwrap()], Some(Path::new(&header_in)));
168 }
169
170 steps.push(step_json("aux-files", "delegated", "automake-rs --add-missing (native)", Some("native (automake-rs)")));
173 steps.push(step_json("makefile.in", "delegated", "automake-rs (native)", Some("native (automake-rs)")));
174
175 let gnu_used: Vec<&str> = [&aclocal, &autoconf, &autoheader]
176 .iter()
177 .filter(|t| t.is_gnu && !forbid_gnu && t.path.is_some())
178 .map(|t| t.stage)
179 .collect();
180
181 let receipt = format!(
182 "{{\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",
183 ok && gnu_used.is_empty(),
184 forbid_gnu,
185 gnu_used.iter().map(|s| format!("{:?}", s)).collect::<Vec<_>>().join(", "),
186 provider_str(&aclocal, forbid_gnu),
187 provider_str(&autoconf, forbid_gnu),
188 provider_str(&autoheader, forbid_gnu),
189 steps.iter().map(|s| format!(" {}", s)).collect::<Vec<_>>().join(",\n"),
190 );
191 let _ = std::fs::write(dir.join("bootstrap-receipt.json"), &receipt);
192 BootstrapReport { ok, receipt_json: receipt }
193}
194
195fn provider_str(t: &Tool, forbid_gnu: bool) -> String {
196 match &t.path {
197 None => "\"unresolved\"".to_string(),
198 Some(p) => {
199 let kind = if t.is_gnu {
200 if forbid_gnu { "rejected-gnu" } else { "gnu" }
201 } else {
202 "native"
203 };
204 format!("{{ \"path\": {:?}, \"kind\": {:?} }}", p.display().to_string(), kind)
205 }
206 }
207}
208
209fn step_json(stage: &str, status: &str, detail: &str, provider: Option<&str>) -> String {
210 match provider {
211 Some(p) => format!(
212 "{{ \"stage\": {:?}, \"status\": {:?}, \"detail\": {:?}, \"provider\": {:?} }}",
213 stage, status, detail, p
214 ),
215 None => format!(
216 "{{ \"stage\": {:?}, \"status\": {:?}, \"detail\": {:?} }}",
217 stage, status, detail
218 ),
219 }
220}
221
222fn err_receipt(msg: &str) -> String {
223 format!("{{ \"native_bootstrap\": false, \"error\": {:?} }}\n", msg)
224}
225
226fn config_header_name(ac_text: &str) -> String {
230 for macro_name in ["AC_CONFIG_HEADERS", "AC_CONFIG_HEADER", "AM_CONFIG_HEADER"] {
231 if let Some(pos) = ac_text.find(macro_name) {
232 let after = &ac_text[pos + macro_name.len()..];
233 if let Some(open) = after.find('(') {
234 if let Some(close) = after[open..].find(')') {
235 let inner = &after[open + 1..open + close];
236 let first = inner
237 .trim()
238 .trim_start_matches('[')
239 .split([']', ',', ' ', '\t', '\n'])
240 .next()
241 .unwrap_or("")
242 .trim();
243 if !first.is_empty() {
244 return first.to_string();
245 }
246 }
247 }
248 }
249 }
250 "config.h".to_string()
251}