1pub mod bootstrap;
10
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::sync::atomic::{AtomicBool, Ordering};
14
15static INTERRUPTED: AtomicBool = AtomicBool::new(false);
16
17#[allow(dead_code)]
21fn setup_signal_handlers() {
22 std::panic::set_hook(Box::new(|_| {
26 INTERRUPTED.store(true, Ordering::SeqCst);
27 }));
28}
29
30#[allow(dead_code)]
32pub fn was_interrupted() -> bool {
33 INTERRUPTED.load(Ordering::SeqCst)
34}
35
36pub fn run_automake() {
38 automake_rs_core::i18n::init_i18n();
39 let args: Vec<String> = std::env::args().collect();
40 let parsed = match automake_rs_core::cli::AutomakeArgs::parse(&args) {
41 Ok(a) => a,
42 Err(e) => {
43 eprintln!("automake-rs: {}", e);
44 std::process::exit(1);
45 }
46 };
47
48 if let Some(ref host) = parsed.host {
50 if parsed.verbose {
51 eprintln!("automake-rs: cross-compilation host: {}", host);
52 }
53 }
54
55 if parsed.help {
56 print_automake_help();
57 return;
58 }
59
60 if parsed.version {
61 print_automake_version();
62 return;
63 }
64
65 if parsed.print_libdir {
67 print_native_libdir();
68 return;
69 }
70
71 let input_files = if parsed.input_files.is_empty() {
73 vec![PathBuf::from("Makefile.am")]
74 } else {
75 parsed.input_files.clone()
76 };
77
78 let configure_ac = find_configure_ac();
80
81 let mut exit_code = 0;
83 for input in &input_files {
84 match process_makefile(&parsed, input, &configure_ac) {
85 Ok(output_path) => {
86 if parsed.verbose {
87 eprintln!("automake-rs: generated {}", output_path.display());
88 }
89 }
90 Err(e) => {
91 eprintln!("automake-rs: {}: {}", input.display(), e);
92 exit_code = 1;
93 }
94 }
95 }
96
97 std::process::exit(exit_code);
98}
99
100fn find_configure_ac() -> PathBuf {
104 let mut dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
105 loop {
106 for name in &["configure.ac", "configure.in"] {
107 let path = dir.join(name);
108 if path.exists() {
109 return path;
110 }
111 }
112 if !dir.pop() {
113 break;
114 }
115 }
116 PathBuf::from("configure.ac")
117}
118
119fn process_makefile(
121 parsed: &automake_rs_core::cli::AutomakeArgs,
122 makefile_path: &Path,
123 configure_ac: &Path,
124) -> Result<PathBuf, String> {
125 let parent = makefile_path.parent().unwrap_or(Path::new("."));
127 let stem = makefile_path
128 .file_stem()
129 .map(|s| s.to_string_lossy().to_string())
130 .unwrap_or_else(|| "Makefile".to_string());
131 let output_path = parent.join(format!("{}.in", stem));
132
133 if parsed.no_force && output_path.exists() {
135 if let (Ok(am_meta), Ok(in_meta)) =
136 (fs::metadata(makefile_path), fs::metadata(&output_path))
137 {
138 if let (Ok(am_time), Ok(in_time)) = (am_meta.modified(), in_meta.modified()) {
139 if in_time >= am_time {
140 if parsed.verbose {
141 eprintln!(
142 "automake-rs: {} is up to date, skipping",
143 output_path.display()
144 );
145 }
146 return Ok(output_path);
147 }
148 }
149 }
150 }
151
152 if parsed.verbose {
154 eprintln!(
155 "automake-rs: extracting traces from {}",
156 configure_ac.display()
157 );
158 }
159
160 let bridge = automake_rs_core::autoconf_bridge::AutoconfBridge::new();
161 let traces = bridge
162 .extract_traces(configure_ac)
163 .map_err(|e| format!("trace extraction failed: {}", e))?;
164
165 let mut config = automake_rs_core::automake_macros::AutomakeConfig::from_options(&format!(
167 "{} {} {}",
168 if parsed.foreign {
169 "foreign"
170 } else if parsed.gnits {
171 "gnits"
172 } else if parsed.gnu {
173 "gnu"
174 } else {
175 traces.strictness.as_deref().unwrap_or("gnu")
176 },
177 parsed
178 .warnings
179 .iter()
180 .map(|w| w.as_str())
181 .collect::<Vec<_>>()
182 .join(" "),
183 ""
184 ));
185
186 if let Ok(ac) = std::fs::read_to_string(configure_ac) {
191 if let Some(start) = ac.find("AM_INIT_AUTOMAKE") {
192 let tail = &ac[start..];
193 if let (Some(o), Some(c)) = (tail.find('('), tail.find(')')) {
194 if c > o {
195 let opts = &tail[o + 1..c];
196 if opts.contains("no-dependencies") {
197 config.dependency_tracking = false;
198 }
199 if opts.contains("subdir-objects") {
200 config.subdir_objects = true;
201 }
202 }
203 }
204 }
205 }
206
207 if let Some(enable) = parsed.dependency_tracking_enabled() {
209 config.dependency_tracking = enable;
210 }
211
212 if parsed.verbose {
214 eprintln!("automake-rs: parsing {}", makefile_path.display());
215 }
216
217 let am = automake_rs_core::makefile_am::MakefileAm::from_file(makefile_path)
218 .map_err(|e| format!("parse error: {}", e))?;
219
220 let strictness = if parsed.foreign {
222 "foreign"
223 } else if parsed.gnits {
224 "gnits"
225 } else if parsed.gnu {
226 "gnu"
227 } else {
228 traces.strictness.as_deref().unwrap_or("gnu")
229 };
230
231 let mut diag =
232 automake_rs_core::diagnostics::DiagnosticManager::from_config(strictness, &parsed.warnings);
233
234 automake_rs_core::diagnostics::run_makefile_diagnostics(&am, &mut diag);
235 automake_rs_core::diagnostics::check_missing_standard_files(&mut diag, strictness);
236
237 diag.print_all();
239
240 if diag.has_errors() {
241 return Err("errors encountered — stopping".to_string());
242 }
243
244 if parsed.verbose {
246 eprintln!("automake-rs: generating {}", output_path.display());
247 }
248
249 let gen = automake_rs_core::makefile_in::MakefileInGenerator::new(am, config, traces);
250 let output = gen.generate();
251
252 fs::write(&output_path, output).map_err(|e| format!("write error: {}", e))?;
254
255 if parsed.add_missing {
257 if parsed.verbose {
258 eprintln!("automake-rs: delegating --add-missing to oracle");
259 }
260 add_missing_files(parsed, makefile_path)?;
261 }
262
263 Ok(output_path)
264}
265
266fn add_missing_files(
270 parsed: &automake_rs_core::cli::AutomakeArgs,
271 makefile_path: &Path,
272) -> Result<(), String> {
273 use automake_rs_core::aux_files;
274 use automake_rs_core::makefile_am::MakefileAm;
275
276 let dir = makefile_path.parent().unwrap_or(Path::new("."));
277
278 let am = MakefileAm::from_file(makefile_path).map_err(|e| format!("parse error: {}", e))?;
280 let src_text = std::fs::read_to_string(makefile_path).unwrap_or_default();
281 let has_compiled = src_text.contains(".c")
282 || src_text.contains("_SOURCES")
283 || src_text.contains("PROGRAMS")
284 || src_text.contains("LIBRARIES");
285 let has_tests = src_text.contains("TESTS");
286 let has_yacc_lex = [".y\n", ".y ", ".l\n", ".l ", ".yy", ".ll"]
287 .iter()
288 .any(|p| src_text.contains(p));
289 let has_static_lib = src_text.contains("_LIBRARIES") && !src_text.contains("_LTLIBRARIES");
290 let has_python = src_text.contains("_PYTHON");
291 let _ = &am;
292
293 let dep_tracking = std::fs::read_to_string(find_configure_ac())
295 .map(|s| {
296 !s.split("AM_INIT_AUTOMAKE")
297 .nth(1)
298 .map(|t| t.split(')').next().unwrap_or("").contains("no-dependencies"))
299 .unwrap_or(false)
300 })
301 .unwrap_or(true);
302
303 let needed = aux_files::needed_aux(
304 dep_tracking,
305 has_compiled,
306 has_tests,
307 has_yacc_lex,
308 has_static_lib,
309 has_python,
310 );
311
312 match aux_files::install_with_receipt(dir, &needed, parsed.force_missing) {
313 Ok(receipt) => {
314 let _ = std::fs::write(dir.join("aux-receipt.json"), &receipt);
315 if parsed.verbose {
316 for f in &needed {
317 eprintln!("automake-rs: installed auxiliary file '{}'", f.filename());
318 }
319 }
320 Ok(())
321 }
322 Err(e) => Err(format!("aux file generation failed: {}", e)),
323 }
324}
325
326fn print_native_libdir() {
328 let dir = native_libdir();
329 println!("{}", dir);
330}
331
332fn native_libdir() -> String {
334 let candidates = &[
335 "/usr/share/automake-1.18",
336 "/usr/share/automake-1.17",
337 "/usr/share/automake-1.16",
338 "/usr/share/automake",
339 ];
340 for path in candidates {
341 if Path::new(path).exists() {
342 return path.to_string();
343 }
344 }
345 if let Ok(out) = std::process::Command::new("automake")
347 .arg("--print-libdir")
348 .output()
349 {
350 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
351 if !s.is_empty() {
352 return s;
353 }
354 }
355 "/usr/share/automake-1.18".to_string()
356}
357
358pub fn effective_libdir(parsed: &automake_rs_core::cli::AutomakeArgs) -> String {
360 if let Some(ref dir) = parsed.libdir {
361 dir.clone()
362 } else {
363 native_libdir()
364 }
365}
366
367pub fn detect_platform() -> String {
371 let arch = std::env::consts::ARCH;
372 let os = std::env::consts::OS;
373 let vendor = "unknown";
374 match (arch, os) {
375 ("x86_64", "linux") => "x86_64-unknown-linux-gnu".into(),
376 ("aarch64", "linux") => "aarch64-unknown-linux-gnu".into(),
377 ("x86_64", "macos") => "x86_64-apple-darwin".into(),
378 ("aarch64", "macos") => "aarch64-apple-darwin".into(),
379 _ => format!("{}-{}-{}", arch, vendor, os),
380 }
381}
382
383pub fn run_aclocal() {
385 automake_rs_core::i18n::init_i18n();
386 let args: Vec<String> = std::env::args().collect();
387 let parsed = match automake_rs_core::cli::AclocalArgs::parse(&args) {
388 Ok(a) => a,
389 Err(e) => {
390 eprintln!("aclocal-rs: {}", e);
391 std::process::exit(1);
392 }
393 };
394
395 if parsed.help {
396 print_aclocal_help();
397 return;
398 }
399
400 if parsed.version {
401 print_aclocal_version();
402 return;
403 }
404
405 if parsed.print_ac_dir {
407 match std::process::Command::new("aclocal")
408 .arg("--print-ac-dir")
409 .output()
410 {
411 Ok(out) => {
412 std::io::Write::write_all(&mut std::io::stdout(), &out.stdout).ok();
413 return;
414 }
415 Err(e) => {
416 eprintln!("aclocal-rs: cannot query oracle: {}", e);
417 std::process::exit(1);
418 }
419 }
420 }
421
422 if parsed.verbose {
423 eprintln!("aclocal-rs: using native engine");
424 }
425
426 let engine = automake_rs_core::aclocal::Aclocal::from_args(&parsed);
427 if let Err(e) = engine.run() {
428 eprintln!("aclocal-rs: {}", e);
429 std::process::exit(1);
430 }
431}
432
433fn print_automake_version() {
434 let version = automake_rs_core::cli::oracle_version();
435 print!("{}", version);
436}
437
438fn print_aclocal_version() {
439 let version = automake_rs_core::cli::oracle_version_aclocal();
440 print!("{}", version);
441}
442
443fn print_automake_help() {
444 println!("Usage: /usr/bin/automake [OPTION]... [Makefile]...");
445 println!();
446 println!("Generate Makefile.in for configure from Makefile.am.");
447 println!();
448 println!("Operation modes:");
449 println!(" --help print this help, then exit");
450 println!(" --version print version number, then exit");
451 println!(" -v, --verbose verbosely list files processed");
452 println!(" --no-force only update Makefile.in's that are out of date");
453 println!(" -W, --warnings=CATEGORY report the warnings falling in CATEGORY");
454 println!();
455 println!("Dependency tracking:");
456 println!(" -i, --ignore-deps disable dependency tracking code");
457 println!(" --include-deps enable dependency tracking code");
458 println!();
459 println!("Flavors:");
460 println!(" --foreign set strictness to foreign");
461 println!(" --gnits set strictness to gnits");
462 println!(" --gnu set strictness to gnu");
463 println!();
464 println!("Library files:");
465 println!(" -a, --add-missing add missing standard files to package");
466 println!(" --libdir=DIR set directory storing library files");
467 println!(" --print-libdir print directory storing library files");
468 println!(" -c, --copy with -a, copy missing files (default is symlink)");
469 println!(" -f, --force-missing force update of standard files");
470 println!();
471 println!(" --host=TRIPLE cross-compilation host triple");
472 println!(" --build=TRIPLE cross-compilation build triple");
473 println!();
474 println!("automake-rs: native Rust reimplementation. Clean-room forensic parity.");
475}
476
477fn print_aclocal_help() {
478 println!("Usage: aclocal [OPTION]...");
479 println!();
480 println!("Generate 'aclocal.m4' by scanning 'configure.ac' or 'configure.in'");
481 println!();
482 println!("Options:");
483 println!(" --automake-acdir=DIR directory holding automake-provided m4 files");
484 println!(" --aclocal-path=PATH colon-separated list of directories to");
485 println!(" search for third-party local files");
486 println!(" --system-acdir=DIR directory holding third-party system-wide files");
487 println!(" --diff[=COMMAND] run COMMAND [diff -u] on M4 files that would be");
488 println!(" changed (implies --install and --dry-run)");
489 println!(" --dry-run pretend to, but do not actually update any file");
490 println!(" --force always update output file");
491 println!(" --help print this help, then exit");
492 println!(" -I DIR add directory to search list for .m4 files");
493 println!(" --install copy third-party files to the first -I directory");
494 println!(" --output=FILE put output in FILE (default aclocal.m4)");
495 println!(" --print-ac-dir print name of directory holding system-wide");
496 println!(" third-party m4 files, then exit");
497 println!(" --verbose don't be silent");
498 println!(" --version print version number, then exit");
499 println!(" -W, --warnings=CATEGORY report the warnings falling in CATEGORY");
500 println!();
501 println!("aclocal-rs: native Rust reimplementation. Clean-room forensic parity.");
502}
503
504pub fn run_autoreconf() {
511 let args: Vec<String> = std::env::args().skip(1).collect();
512 let mut forbid_gnu = true;
513 let mut verbose = false;
514 let mut dir = ".".to_string();
515 for a in &args {
516 match a.as_str() {
517 "--allow-gnu" => forbid_gnu = false,
518 "--forbid-gnu" => forbid_gnu = true,
519 "-v" | "--verbose" => verbose = true,
520 "-f" | "-i" | "-fi" | "-if" | "--force" | "--install" => {}
521 "-h" | "--help" => {
522 println!("autoreconf-rs - native GNU-free Autotools bootstrap driver");
523 println!("Usage: autoreconf-rs [-fi] [--allow-gnu] [-v] [DIR]");
524 println!(" Runs aclocal-rs/autoconf-rs/autoheader-rs (configure, config.h.in) +");
525 println!(" automake-rs (aux files, Makefile.in). Default: GNU-free (fail closed).");
526 return;
527 }
528 s if !s.starts_with('-') => dir = s.to_string(),
529 _ => {}
530 }
531 }
532 let dir = Path::new(&dir);
533
534 let report = bootstrap::run_bootstrap(dir, forbid_gnu, verbose);
536
537 if let Some(automake) = resolve_automake() {
539 let _ = std::process::Command::new(&automake)
540 .current_dir(dir)
541 .args(["--add-missing", "--copy", "--force-missing", "Makefile.am"])
542 .status();
543 if let Ok(entries) = find_makefile_ams(dir) {
544 for mf in entries {
545 let parent = mf.parent().unwrap_or(dir);
546 let _ = std::process::Command::new(&automake)
547 .current_dir(parent)
548 .arg("Makefile.am")
549 .status();
550 }
551 }
552 } else {
553 eprintln!("autoreconf-rs: native automake binary not found (set AUTOMAKE_RS)");
554 }
555
556 print!("{}", report.receipt_json);
557 if !report.ok {
558 eprintln!("autoreconf-rs: bootstrap incomplete (see bootstrap-receipt.json; configure stage is the autoconf-rs boundary)");
559 std::process::exit(1);
560 }
561}
562
563fn resolve_automake() -> Option<PathBuf> {
565 if let Ok(p) = std::env::var("AUTOMAKE_RS") {
566 let pb = PathBuf::from(p);
567 if pb.exists() {
568 return Some(pb);
569 }
570 }
571 if let Ok(exe) = std::env::current_exe() {
572 if let Some(parent) = exe.parent() {
573 let sib = parent.join("automake");
574 if sib.exists() {
575 return Some(sib);
576 }
577 }
578 }
579 for name in ["automake", "automake-rs"] {
580 let path = std::env::var("PATH").unwrap_or_default();
581 for d in path.split(':') {
582 let c = Path::new(d).join(name);
583 if c.exists() {
584 return Some(c);
585 }
586 }
587 }
588 None
589}
590
591fn find_makefile_ams(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
593 let mut out = Vec::new();
594 let mut stack = vec![dir.to_path_buf()];
595 while let Some(d) = stack.pop() {
596 for entry in std::fs::read_dir(&d)?.flatten() {
597 let p = entry.path();
598 if p.is_dir() {
599 let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
600 if name != ".git" && name != "autom4te.cache" {
601 stack.push(p);
602 }
603 } else if p.file_name().and_then(|n| n.to_str()) == Some("Makefile.am") {
604 out.push(p);
605 }
606 }
607 }
608 Ok(out)
609}