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 configure_ac = find_configure_ac();
73
74 let input_files = if !parsed.input_files.is_empty() {
79 parsed.input_files.clone()
80 } else {
81 let mut files = makefile_ams_from_config_files(&configure_ac);
82 if files.is_empty() {
83 files.push(PathBuf::from("Makefile.am"));
84 }
85 files
86 };
87
88 let mut exit_code = 0;
90 for input in &input_files {
91 match process_makefile(&parsed, input, &configure_ac) {
92 Ok(output_path) => {
93 if parsed.verbose {
94 eprintln!("automake-rs: generated {}", output_path.display());
95 }
96 }
97 Err(e) => {
98 eprintln!("automake-rs: {}: {}", input.display(), e);
99 exit_code = 1;
100 }
101 }
102 }
103
104 std::process::exit(exit_code);
105}
106
107fn makefile_ams_from_config_files(configure_ac: &std::path::Path) -> Vec<PathBuf> {
112 let content = match std::fs::read_to_string(configure_ac) {
113 Ok(c) => c,
114 Err(_) => return Vec::new(),
115 };
116 let base = configure_ac.parent().unwrap_or_else(|| std::path::Path::new("."));
117 let mut out: Vec<PathBuf> = Vec::new();
118 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
119 for macro_name in ["AC_CONFIG_FILES", "AC_OUTPUT"] {
122 let mut from = 0;
123 while let Some(rel) = content[from..].find(macro_name) {
124 let start = from + rel;
125 from = start + macro_name.len();
126 let rest = content[from..].trim_start();
127 let Some(rest) = rest.strip_prefix('(') else { continue };
128 let Some(end) = rest.find(')') else { continue };
130 let arg = &rest[..end];
131 for tok in arg.split(|c: char| c.is_whitespace() || c == ',') {
132 let t = tok.trim().trim_matches(|c| c == '[' || c == ']' || c == '"' || c == '\'');
133 if t.is_empty() {
134 continue;
135 }
136 let is_makefile = t == "Makefile" || t.ends_with("/Makefile");
138 if !is_makefile {
139 continue;
140 }
141 let am_rel = format!("{t}.am");
142 let am_path = base.join(&am_rel);
143 if am_path.exists() && seen.insert(am_rel.clone()) {
144 out.push(PathBuf::from(am_rel));
145 }
146 }
147 }
148 }
149 out
150}
151
152fn find_configure_ac() -> PathBuf {
156 let mut dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
157 loop {
158 for name in &["configure.ac", "configure.in"] {
159 let path = dir.join(name);
160 if path.exists() {
161 return path;
162 }
163 }
164 if !dir.pop() {
165 break;
166 }
167 }
168 PathBuf::from("configure.ac")
169}
170
171fn process_makefile(
173 parsed: &automake_rs_core::cli::AutomakeArgs,
174 makefile_path: &Path,
175 configure_ac: &Path,
176) -> Result<PathBuf, String> {
177 let parent = makefile_path.parent().unwrap_or(Path::new("."));
179 let stem = makefile_path
180 .file_stem()
181 .map(|s| s.to_string_lossy().to_string())
182 .unwrap_or_else(|| "Makefile".to_string());
183 let output_path = parent.join(format!("{}.in", stem));
184
185 if parsed.no_force && output_path.exists() {
187 if let (Ok(am_meta), Ok(in_meta)) =
188 (fs::metadata(makefile_path), fs::metadata(&output_path))
189 {
190 if let (Ok(am_time), Ok(in_time)) = (am_meta.modified(), in_meta.modified()) {
191 if in_time >= am_time {
192 if parsed.verbose {
193 eprintln!(
194 "automake-rs: {} is up to date, skipping",
195 output_path.display()
196 );
197 }
198 return Ok(output_path);
199 }
200 }
201 }
202 }
203
204 if parsed.verbose {
206 eprintln!(
207 "automake-rs: extracting traces from {}",
208 configure_ac.display()
209 );
210 }
211
212 let bridge = automake_rs_core::autoconf_bridge::AutoconfBridge::new();
213 let traces = bridge
214 .extract_traces(configure_ac)
215 .map_err(|e| format!("trace extraction failed: {}", e))?;
216
217 let mut config = automake_rs_core::automake_macros::AutomakeConfig::from_options(&format!(
219 "{} {} {}",
220 if parsed.foreign {
221 "foreign"
222 } else if parsed.gnits {
223 "gnits"
224 } else if parsed.gnu {
225 "gnu"
226 } else {
227 traces.strictness.as_deref().unwrap_or("gnu")
228 },
229 parsed
230 .warnings
231 .iter()
232 .map(|w| w.as_str())
233 .collect::<Vec<_>>()
234 .join(" "),
235 ""
236 ));
237
238 if let Ok(ac) = std::fs::read_to_string(configure_ac) {
243 if let Some(start) = ac.find("AM_INIT_AUTOMAKE") {
244 let tail = &ac[start..];
245 if let (Some(o), Some(c)) = (tail.find('('), tail.find(')')) {
246 if c > o {
247 let opts = &tail[o + 1..c];
248 if opts.contains("no-dependencies") {
249 config.dependency_tracking = false;
250 }
251 if opts.contains("subdir-objects") {
252 config.subdir_objects = true;
253 }
254 }
255 }
256 }
257 }
258
259 if let Some(enable) = parsed.dependency_tracking_enabled() {
261 config.dependency_tracking = enable;
262 }
263
264 if parsed.verbose {
266 eprintln!("automake-rs: parsing {}", makefile_path.display());
267 }
268
269 let am = automake_rs_core::makefile_am::MakefileAm::from_file(makefile_path)
270 .map_err(|e| format!("parse error: {}", e))?;
271
272 let strictness = if parsed.foreign {
274 "foreign"
275 } else if parsed.gnits {
276 "gnits"
277 } else if parsed.gnu {
278 "gnu"
279 } else {
280 traces.strictness.as_deref().unwrap_or("gnu")
281 };
282
283 let mut diag =
284 automake_rs_core::diagnostics::DiagnosticManager::from_config(strictness, &parsed.warnings);
285
286 automake_rs_core::diagnostics::run_makefile_diagnostics(&am, &mut diag);
287 automake_rs_core::diagnostics::check_missing_standard_files(&mut diag, strictness);
288
289 diag.print_all();
291
292 if diag.has_errors() {
293 return Err("errors encountered — stopping".to_string());
294 }
295
296 if parsed.verbose {
298 eprintln!("automake-rs: generating {}", output_path.display());
299 }
300
301 let gen = automake_rs_core::makefile_in::MakefileInGenerator::new(am, config, traces);
302 let output = gen.generate();
303
304 fs::write(&output_path, output).map_err(|e| format!("write error: {}", e))?;
306
307 if parsed.add_missing {
309 if parsed.verbose {
310 eprintln!("automake-rs: delegating --add-missing to oracle");
311 }
312 add_missing_files(parsed, makefile_path)?;
313 }
314
315 Ok(output_path)
316}
317
318fn add_missing_files(
322 parsed: &automake_rs_core::cli::AutomakeArgs,
323 makefile_path: &Path,
324) -> Result<(), String> {
325 use automake_rs_core::aux_files;
326 use automake_rs_core::makefile_am::MakefileAm;
327
328 let dir = makefile_path.parent().unwrap_or(Path::new("."));
329
330 let am = MakefileAm::from_file(makefile_path).map_err(|e| format!("parse error: {}", e))?;
332 let src_text = std::fs::read_to_string(makefile_path).unwrap_or_default();
333 let has_compiled = src_text.contains(".c")
334 || src_text.contains("_SOURCES")
335 || src_text.contains("PROGRAMS")
336 || src_text.contains("LIBRARIES");
337 let has_tests = src_text.contains("TESTS");
338 let has_yacc_lex = [".y\n", ".y ", ".l\n", ".l ", ".yy", ".ll"]
339 .iter()
340 .any(|p| src_text.contains(p));
341 let has_static_lib = src_text.contains("_LIBRARIES") && !src_text.contains("_LTLIBRARIES");
342 let has_python = src_text.contains("_PYTHON");
343 let _ = &am;
344
345 let dep_tracking = std::fs::read_to_string(find_configure_ac())
347 .map(|s| {
348 !s.split("AM_INIT_AUTOMAKE")
349 .nth(1)
350 .map(|t| t.split(')').next().unwrap_or("").contains("no-dependencies"))
351 .unwrap_or(false)
352 })
353 .unwrap_or(true);
354
355 let needed = aux_files::needed_aux(
356 dep_tracking,
357 has_compiled,
358 has_tests,
359 has_yacc_lex,
360 has_static_lib,
361 has_python,
362 );
363
364 match aux_files::install_with_receipt(dir, &needed, parsed.force_missing) {
365 Ok(receipt) => {
366 let _ = std::fs::write(dir.join("aux-receipt.json"), &receipt);
367 if parsed.verbose {
368 for f in &needed {
369 eprintln!("automake-rs: installed auxiliary file '{}'", f.filename());
370 }
371 }
372 Ok(())
373 }
374 Err(e) => Err(format!("aux file generation failed: {}", e)),
375 }
376}
377
378fn print_native_libdir() {
380 let dir = native_libdir();
381 println!("{}", dir);
382}
383
384fn native_libdir() -> String {
386 let candidates = &[
387 "/usr/share/automake-1.18",
388 "/usr/share/automake-1.17",
389 "/usr/share/automake-1.16",
390 "/usr/share/automake",
391 ];
392 for path in candidates {
393 if Path::new(path).exists() {
394 return path.to_string();
395 }
396 }
397 if let Ok(out) = std::process::Command::new("automake")
399 .arg("--print-libdir")
400 .output()
401 {
402 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
403 if !s.is_empty() {
404 return s;
405 }
406 }
407 "/usr/share/automake-1.18".to_string()
408}
409
410pub fn effective_libdir(parsed: &automake_rs_core::cli::AutomakeArgs) -> String {
412 if let Some(ref dir) = parsed.libdir {
413 dir.clone()
414 } else {
415 native_libdir()
416 }
417}
418
419pub fn detect_platform() -> String {
423 let arch = std::env::consts::ARCH;
424 let os = std::env::consts::OS;
425 let vendor = "unknown";
426 match (arch, os) {
427 ("x86_64", "linux") => "x86_64-unknown-linux-gnu".into(),
428 ("aarch64", "linux") => "aarch64-unknown-linux-gnu".into(),
429 ("x86_64", "macos") => "x86_64-apple-darwin".into(),
430 ("aarch64", "macos") => "aarch64-apple-darwin".into(),
431 _ => format!("{}-{}-{}", arch, vendor, os),
432 }
433}
434
435pub fn run_aclocal() {
437 automake_rs_core::i18n::init_i18n();
438 let args: Vec<String> = std::env::args().collect();
439 let parsed = match automake_rs_core::cli::AclocalArgs::parse(&args) {
440 Ok(a) => a,
441 Err(e) => {
442 eprintln!("aclocal-rs: {}", e);
443 std::process::exit(1);
444 }
445 };
446
447 if parsed.help {
448 print_aclocal_help();
449 return;
450 }
451
452 if parsed.version {
453 print_aclocal_version();
454 return;
455 }
456
457 if parsed.print_ac_dir {
459 match std::process::Command::new("aclocal")
460 .arg("--print-ac-dir")
461 .output()
462 {
463 Ok(out) => {
464 std::io::Write::write_all(&mut std::io::stdout(), &out.stdout).ok();
465 return;
466 }
467 Err(e) => {
468 eprintln!("aclocal-rs: cannot query oracle: {}", e);
469 std::process::exit(1);
470 }
471 }
472 }
473
474 if parsed.verbose {
475 eprintln!("aclocal-rs: using native engine");
476 }
477
478 let engine = automake_rs_core::aclocal::Aclocal::from_args(&parsed);
479 if let Err(e) = engine.run() {
480 eprintln!("aclocal-rs: {}", e);
481 std::process::exit(1);
482 }
483}
484
485fn print_automake_version() {
486 let version = automake_rs_core::cli::oracle_version();
487 print!("{}", version);
488}
489
490fn print_aclocal_version() {
491 let version = automake_rs_core::cli::oracle_version_aclocal();
492 print!("{}", version);
493}
494
495fn print_automake_help() {
496 println!("Usage: /usr/bin/automake [OPTION]... [Makefile]...");
497 println!();
498 println!("Generate Makefile.in for configure from Makefile.am.");
499 println!();
500 println!("Operation modes:");
501 println!(" --help print this help, then exit");
502 println!(" --version print version number, then exit");
503 println!(" -v, --verbose verbosely list files processed");
504 println!(" --no-force only update Makefile.in's that are out of date");
505 println!(" -W, --warnings=CATEGORY report the warnings falling in CATEGORY");
506 println!();
507 println!("Dependency tracking:");
508 println!(" -i, --ignore-deps disable dependency tracking code");
509 println!(" --include-deps enable dependency tracking code");
510 println!();
511 println!("Flavors:");
512 println!(" --foreign set strictness to foreign");
513 println!(" --gnits set strictness to gnits");
514 println!(" --gnu set strictness to gnu");
515 println!();
516 println!("Library files:");
517 println!(" -a, --add-missing add missing standard files to package");
518 println!(" --libdir=DIR set directory storing library files");
519 println!(" --print-libdir print directory storing library files");
520 println!(" -c, --copy with -a, copy missing files (default is symlink)");
521 println!(" -f, --force-missing force update of standard files");
522 println!();
523 println!(" --host=TRIPLE cross-compilation host triple");
524 println!(" --build=TRIPLE cross-compilation build triple");
525 println!();
526 println!("automake-rs: native Rust reimplementation. Clean-room forensic parity.");
527}
528
529fn print_aclocal_help() {
530 println!("Usage: aclocal [OPTION]...");
531 println!();
532 println!("Generate 'aclocal.m4' by scanning 'configure.ac' or 'configure.in'");
533 println!();
534 println!("Options:");
535 println!(" --automake-acdir=DIR directory holding automake-provided m4 files");
536 println!(" --aclocal-path=PATH colon-separated list of directories to");
537 println!(" search for third-party local files");
538 println!(" --system-acdir=DIR directory holding third-party system-wide files");
539 println!(" --diff[=COMMAND] run COMMAND [diff -u] on M4 files that would be");
540 println!(" changed (implies --install and --dry-run)");
541 println!(" --dry-run pretend to, but do not actually update any file");
542 println!(" --force always update output file");
543 println!(" --help print this help, then exit");
544 println!(" -I DIR add directory to search list for .m4 files");
545 println!(" --install copy third-party files to the first -I directory");
546 println!(" --output=FILE put output in FILE (default aclocal.m4)");
547 println!(" --print-ac-dir print name of directory holding system-wide");
548 println!(" third-party m4 files, then exit");
549 println!(" --verbose don't be silent");
550 println!(" --version print version number, then exit");
551 println!(" -W, --warnings=CATEGORY report the warnings falling in CATEGORY");
552 println!();
553 println!("aclocal-rs: native Rust reimplementation. Clean-room forensic parity.");
554}
555
556pub fn run_autoreconf() {
563 let args: Vec<String> = std::env::args().skip(1).collect();
564 let mut forbid_gnu = true;
565 let mut verbose = false;
566 let mut dir = ".".to_string();
567 for a in &args {
568 match a.as_str() {
569 "--allow-gnu" => forbid_gnu = false,
570 "--forbid-gnu" => forbid_gnu = true,
571 "-v" | "--verbose" => verbose = true,
572 "-f" | "-i" | "-fi" | "-if" | "--force" | "--install" => {}
573 "-h" | "--help" => {
574 println!("autoreconf-rs - native GNU-free Autotools bootstrap driver");
575 println!("Usage: autoreconf-rs [-fi] [--allow-gnu] [-v] [DIR]");
576 println!(" Runs aclocal-rs/autoconf-rs/autoheader-rs (configure, config.h.in) +");
577 println!(" automake-rs (aux files, Makefile.in). Default: GNU-free (fail closed).");
578 return;
579 }
580 s if !s.starts_with('-') => dir = s.to_string(),
581 _ => {}
582 }
583 }
584 let dir = Path::new(&dir);
585
586 let report = bootstrap::run_bootstrap(dir, forbid_gnu, verbose);
588
589 if let Some(automake) = resolve_automake() {
591 let _ = std::process::Command::new(&automake)
592 .current_dir(dir)
593 .args(["--add-missing", "--copy", "--force-missing", "Makefile.am"])
594 .status();
595 if let Ok(entries) = find_makefile_ams(dir) {
596 for mf in entries {
597 let parent = mf.parent().unwrap_or(dir);
598 let _ = std::process::Command::new(&automake)
599 .current_dir(parent)
600 .arg("Makefile.am")
601 .status();
602 }
603 }
604 } else {
605 eprintln!("autoreconf-rs: native automake binary not found (set AUTOMAKE_RS)");
606 }
607
608 print!("{}", report.receipt_json);
609 if !report.ok {
610 eprintln!("autoreconf-rs: bootstrap incomplete (see bootstrap-receipt.json; configure stage is the autoconf-rs boundary)");
611 std::process::exit(1);
612 }
613}
614
615fn resolve_automake() -> Option<PathBuf> {
617 if let Ok(p) = std::env::var("AUTOMAKE_RS") {
618 let pb = PathBuf::from(p);
619 if pb.exists() {
620 return Some(pb);
621 }
622 }
623 if let Ok(exe) = std::env::current_exe() {
624 if let Some(parent) = exe.parent() {
625 let sib = parent.join("automake");
626 if sib.exists() {
627 return Some(sib);
628 }
629 }
630 }
631 for name in ["automake", "automake-rs"] {
632 let path = std::env::var("PATH").unwrap_or_default();
633 for d in path.split(':') {
634 let c = Path::new(d).join(name);
635 if c.exists() {
636 return Some(c);
637 }
638 }
639 }
640 None
641}
642
643fn find_makefile_ams(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
645 let mut out = Vec::new();
646 let mut stack = vec![dir.to_path_buf()];
647 while let Some(d) = stack.pop() {
648 for entry in std::fs::read_dir(&d)?.flatten() {
649 let p = entry.path();
650 if p.is_dir() {
651 let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
652 if name != ".git" && name != "autom4te.cache" {
653 stack.push(p);
654 }
655 } else if p.file_name().and_then(|n| n.to_str()) == Some("Makefile.am") {
656 out.push(p);
657 }
658 }
659 }
660 Ok(out)
661}