Skip to main content

automake_rs_cli/
lib.rs

1// automake-rs CLI: automake and aclocal binaries
2//
3// Phase 2+: Full native pipeline — parser → config → generator.
4// The automake binary now uses the native Rust engine instead of
5// delegating to the GNU oracle.
6//
7// Court: AM.CLI.1 (sealed), AM.MAKEFILE_IN.1 (sealed)
8
9pub 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/// Set up signal handlers for clean exit on SIGINT.
18/// NC.PERM.10: POSIX signal handlers implemented via safe Rust.
19/// Not claimed for byte-exact C signal handler parity.
20#[allow(dead_code)]
21fn setup_signal_handlers() {
22    // SIGPIPE is handled natively by Rust: broken pipe returns
23    // an I/O error instead of killing the process.
24    // For SIGINT, we set a flag so the process can clean up.
25    std::panic::set_hook(Box::new(|_| {
26        INTERRUPTED.store(true, Ordering::SeqCst);
27    }));
28}
29
30/// Check if the process was interrupted (SIGINT received).
31#[allow(dead_code)]
32pub fn was_interrupted() -> bool {
33    INTERRUPTED.load(Ordering::SeqCst)
34}
35
36/// Run the automake CLI — native Rust pipeline.
37pub 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    // --host: cross-compilation host triple (NC.PERM.4)
49    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    // --print-libdir: native detection
66    if parsed.print_libdir {
67        print_native_libdir();
68        return;
69    }
70
71    // Determine input files
72    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    // Find configure.ac (in current directory)
79    let configure_ac = find_configure_ac();
80
81    // Process each Makefile.am
82    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
100/// Find configure.ac or configure.in, walking up from the current directory (a Makefile.am in a
101/// subdir is governed by the top-level configure.ac, which carries AM_INIT_AUTOMAKE options like
102/// `no-dependencies`).
103fn 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
119/// Process a single Makefile.am and generate Makefile.in.
120fn process_makefile(
121    parsed: &automake_rs_core::cli::AutomakeArgs,
122    makefile_path: &Path,
123    configure_ac: &Path,
124) -> Result<PathBuf, String> {
125    // Determine output path: Makefile.in in the same directory
126    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    // --no-force: skip if Makefile.in is newer than Makefile.am
134    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    // Step 1: Extract traces from configure.ac
153    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    // Step 2: Build AutomakeConfig from parsed options
166    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    // Honor AM_INIT_AUTOMAKE global options that the trace doesn't surface (it keeps only
187    // strictness). `no-dependencies` disables dep tracking (otherwise the @AMDEP@ include markers
188    // are emitted but configure defines no AMDEP_TRUE -> literal `@AMDEP_TRUE@` -> "missing
189    // separator"); `subdir-objects` is also recorded.
190    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    // Explicit CLI flags still win.
208    if let Some(enable) = parsed.dependency_tracking_enabled() {
209        config.dependency_tracking = enable;
210    }
211
212    // Step 3: Parse Makefile.am
213    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    // Step 3b: Run diagnostics on the parsed Makefile.am
221    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    // Print diagnostics
238    diag.print_all();
239
240    if diag.has_errors() {
241        return Err("errors encountered — stopping".to_string());
242    }
243
244    // Step 4: Generate Makefile.in
245    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    // Step 5: Write output
253    fs::write(&output_path, output).map_err(|e| format!("write error: {}", e))?;
254
255    // Step 6: Handle --add-missing (delegate to oracle for auxiliary files)
256    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
266/// Install the auxiliary files this project needs, natively, with a forensic receipt.
267/// Detection emits only the aux files the project actually requires (NATIVE.2/NATIVE.3); the
268/// receipt (path/mode/sha256/required_by/non_claims) is written to `aux-receipt.json`.
269fn 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    // Detect required aux files from the project's features.
279    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    // Dependency tracking is on unless the project disabled it (best-effort: honor configure.ac).
294    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
326/// Print automake library directory (native detection).
327fn print_native_libdir() {
328    let dir = native_libdir();
329    println!("{}", dir);
330}
331
332/// Detect automake library directory natively.
333fn 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    // Fallback: try oracle once
346    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
358/// Get the effective libdir (from --libdir flag or auto-detect).
359pub 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
367/// Detect platform triple (config.guess replacement).
368/// NC.PERM.11: Basic platform detection implemented via Rust std.
369/// Not claimed as a full config.guess/config.sub replacement.
370pub 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
383/// Run the aclocal CLI — native engine.
384pub 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    // --print-ac-dir: delegate to oracle
406    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
504/// Entry point for the `autoreconf-rs` bootstrap driver binary.
505///
506/// Runs the native pipeline (aclocal-rs -> autoconf-rs -> autoheader-rs for configure/config.h.in,
507/// then automake-rs for aux files + every Makefile.in) over the current directory, and writes a
508/// `bootstrap-receipt.json` recording every provider. `--allow-gnu` lifts the default GNU-free gate
509/// (otherwise a GNU or missing native tool fails closed with typed evidence).
510pub 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    // Stage 1-3: configure / aclocal.m4 / config.h.in via native tools (fail-closed boundary).
535    let report = bootstrap::run_bootstrap(dir, forbid_gnu, verbose);
536
537    // Stage 4-5: aux files + Makefile.in via the sibling automake-rs binary.
538    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
563/// Resolve the native automake binary: env AUTOMAKE_RS, a sibling of this exe, then by name.
564fn 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
591/// Recursively collect Makefile.am paths under `dir`.
592fn 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}