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
9use std::fs;
10use std::path::{Path, PathBuf};
11use std::sync::atomic::{AtomicBool, Ordering};
12
13static INTERRUPTED: AtomicBool = AtomicBool::new(false);
14
15/// Set up signal handlers for clean exit on SIGINT.
16/// NC.PERM.10: POSIX signal handlers implemented via safe Rust.
17/// Not claimed for byte-exact C signal handler parity.
18#[allow(dead_code)]
19fn setup_signal_handlers() {
20    // SIGPIPE is handled natively by Rust: broken pipe returns
21    // an I/O error instead of killing the process.
22    // For SIGINT, we set a flag so the process can clean up.
23    std::panic::set_hook(Box::new(|_| {
24        INTERRUPTED.store(true, Ordering::SeqCst);
25    }));
26}
27
28/// Check if the process was interrupted (SIGINT received).
29#[allow(dead_code)]
30pub fn was_interrupted() -> bool {
31    INTERRUPTED.load(Ordering::SeqCst)
32}
33
34/// Run the automake CLI — native Rust pipeline.
35pub fn run_automake() {
36    automake_rs_core::i18n::init_i18n();
37    let args: Vec<String> = std::env::args().collect();
38    let parsed = match automake_rs_core::cli::AutomakeArgs::parse(&args) {
39        Ok(a) => a,
40        Err(e) => {
41            eprintln!("automake-rs: {}", e);
42            std::process::exit(1);
43        }
44    };
45
46    // --host: cross-compilation host triple (NC.PERM.4)
47    if let Some(ref host) = parsed.host {
48        if parsed.verbose {
49            eprintln!("automake-rs: cross-compilation host: {}", host);
50        }
51    }
52
53    if parsed.help {
54        print_automake_help();
55        return;
56    }
57
58    if parsed.version {
59        print_automake_version();
60        return;
61    }
62
63    // --print-libdir: native detection
64    if parsed.print_libdir {
65        print_native_libdir();
66        return;
67    }
68
69    // Determine input files
70    let input_files = if parsed.input_files.is_empty() {
71        vec![PathBuf::from("Makefile.am")]
72    } else {
73        parsed.input_files.clone()
74    };
75
76    // Find configure.ac (in current directory)
77    let configure_ac = find_configure_ac();
78
79    // Process each Makefile.am
80    let mut exit_code = 0;
81    for input in &input_files {
82        match process_makefile(&parsed, input, &configure_ac) {
83            Ok(output_path) => {
84                if parsed.verbose {
85                    eprintln!("automake-rs: generated {}", output_path.display());
86                }
87            }
88            Err(e) => {
89                eprintln!("automake-rs: {}: {}", input.display(), e);
90                exit_code = 1;
91            }
92        }
93    }
94
95    std::process::exit(exit_code);
96}
97
98/// Find configure.ac or configure.in, walking up from the current directory (a Makefile.am in a
99/// subdir is governed by the top-level configure.ac, which carries AM_INIT_AUTOMAKE options like
100/// `no-dependencies`).
101fn find_configure_ac() -> PathBuf {
102    let mut dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
103    loop {
104        for name in &["configure.ac", "configure.in"] {
105            let path = dir.join(name);
106            if path.exists() {
107                return path;
108            }
109        }
110        if !dir.pop() {
111            break;
112        }
113    }
114    PathBuf::from("configure.ac")
115}
116
117/// Process a single Makefile.am and generate Makefile.in.
118fn process_makefile(
119    parsed: &automake_rs_core::cli::AutomakeArgs,
120    makefile_path: &Path,
121    configure_ac: &Path,
122) -> Result<PathBuf, String> {
123    // Determine output path: Makefile.in in the same directory
124    let parent = makefile_path.parent().unwrap_or(Path::new("."));
125    let stem = makefile_path
126        .file_stem()
127        .map(|s| s.to_string_lossy().to_string())
128        .unwrap_or_else(|| "Makefile".to_string());
129    let output_path = parent.join(format!("{}.in", stem));
130
131    // --no-force: skip if Makefile.in is newer than Makefile.am
132    if parsed.no_force && output_path.exists() {
133        if let (Ok(am_meta), Ok(in_meta)) =
134            (fs::metadata(makefile_path), fs::metadata(&output_path))
135        {
136            if let (Ok(am_time), Ok(in_time)) = (am_meta.modified(), in_meta.modified()) {
137                if in_time >= am_time {
138                    if parsed.verbose {
139                        eprintln!(
140                            "automake-rs: {} is up to date, skipping",
141                            output_path.display()
142                        );
143                    }
144                    return Ok(output_path);
145                }
146            }
147        }
148    }
149
150    // Step 1: Extract traces from configure.ac
151    if parsed.verbose {
152        eprintln!(
153            "automake-rs: extracting traces from {}",
154            configure_ac.display()
155        );
156    }
157
158    let bridge = automake_rs_core::autoconf_bridge::AutoconfBridge::new();
159    let traces = bridge
160        .extract_traces(configure_ac)
161        .map_err(|e| format!("trace extraction failed: {}", e))?;
162
163    // Step 2: Build AutomakeConfig from parsed options
164    let mut config = automake_rs_core::automake_macros::AutomakeConfig::from_options(&format!(
165        "{} {} {}",
166        if parsed.foreign {
167            "foreign"
168        } else if parsed.gnits {
169            "gnits"
170        } else if parsed.gnu {
171            "gnu"
172        } else {
173            traces.strictness.as_deref().unwrap_or("gnu")
174        },
175        parsed
176            .warnings
177            .iter()
178            .map(|w| w.as_str())
179            .collect::<Vec<_>>()
180            .join(" "),
181        ""
182    ));
183
184    // Honor AM_INIT_AUTOMAKE global options that the trace doesn't surface (it keeps only
185    // strictness). `no-dependencies` disables dep tracking (otherwise the @AMDEP@ include markers
186    // are emitted but configure defines no AMDEP_TRUE -> literal `@AMDEP_TRUE@` -> "missing
187    // separator"); `subdir-objects` is also recorded.
188    if let Ok(ac) = std::fs::read_to_string(configure_ac) {
189        if let Some(start) = ac.find("AM_INIT_AUTOMAKE") {
190            let tail = &ac[start..];
191            if let (Some(o), Some(c)) = (tail.find('('), tail.find(')')) {
192                if c > o {
193                    let opts = &tail[o + 1..c];
194                    if opts.contains("no-dependencies") {
195                        config.dependency_tracking = false;
196                    }
197                    if opts.contains("subdir-objects") {
198                        config.subdir_objects = true;
199                    }
200                }
201            }
202        }
203    }
204
205    // Explicit CLI flags still win.
206    if let Some(enable) = parsed.dependency_tracking_enabled() {
207        config.dependency_tracking = enable;
208    }
209
210    // Step 3: Parse Makefile.am
211    if parsed.verbose {
212        eprintln!("automake-rs: parsing {}", makefile_path.display());
213    }
214
215    let am = automake_rs_core::makefile_am::MakefileAm::from_file(makefile_path)
216        .map_err(|e| format!("parse error: {}", e))?;
217
218    // Step 3b: Run diagnostics on the parsed Makefile.am
219    let strictness = if parsed.foreign {
220        "foreign"
221    } else if parsed.gnits {
222        "gnits"
223    } else if parsed.gnu {
224        "gnu"
225    } else {
226        traces.strictness.as_deref().unwrap_or("gnu")
227    };
228
229    let mut diag =
230        automake_rs_core::diagnostics::DiagnosticManager::from_config(strictness, &parsed.warnings);
231
232    automake_rs_core::diagnostics::run_makefile_diagnostics(&am, &mut diag);
233    automake_rs_core::diagnostics::check_missing_standard_files(&mut diag, strictness);
234
235    // Print diagnostics
236    diag.print_all();
237
238    if diag.has_errors() {
239        return Err("errors encountered — stopping".to_string());
240    }
241
242    // Step 4: Generate Makefile.in
243    if parsed.verbose {
244        eprintln!("automake-rs: generating {}", output_path.display());
245    }
246
247    let gen = automake_rs_core::makefile_in::MakefileInGenerator::new(am, config, traces);
248    let output = gen.generate();
249
250    // Step 5: Write output
251    fs::write(&output_path, output).map_err(|e| format!("write error: {}", e))?;
252
253    // Step 6: Handle --add-missing (delegate to oracle for auxiliary files)
254    if parsed.add_missing {
255        if parsed.verbose {
256            eprintln!("automake-rs: delegating --add-missing to oracle");
257        }
258        add_missing_files(parsed, makefile_path)?;
259    }
260
261    Ok(output_path)
262}
263
264/// Install the auxiliary files this project needs, natively, with a forensic receipt.
265/// Detection emits only the aux files the project actually requires (NATIVE.2/NATIVE.3); the
266/// receipt (path/mode/sha256/required_by/non_claims) is written to `aux-receipt.json`.
267fn add_missing_files(
268    parsed: &automake_rs_core::cli::AutomakeArgs,
269    makefile_path: &Path,
270) -> Result<(), String> {
271    use automake_rs_core::aux_files;
272    use automake_rs_core::makefile_am::MakefileAm;
273
274    let dir = makefile_path.parent().unwrap_or(Path::new("."));
275
276    // Detect required aux files from the project's features.
277    let am = MakefileAm::from_file(makefile_path).map_err(|e| format!("parse error: {}", e))?;
278    let src_text = std::fs::read_to_string(makefile_path).unwrap_or_default();
279    let has_compiled = src_text.contains(".c")
280        || src_text.contains("_SOURCES")
281        || src_text.contains("PROGRAMS")
282        || src_text.contains("LIBRARIES");
283    let has_tests = src_text.contains("TESTS");
284    let has_yacc_lex = [".y\n", ".y ", ".l\n", ".l ", ".yy", ".ll"]
285        .iter()
286        .any(|p| src_text.contains(p));
287    let has_static_lib = src_text.contains("_LIBRARIES") && !src_text.contains("_LTLIBRARIES");
288    let has_python = src_text.contains("_PYTHON");
289    let _ = &am;
290
291    // Dependency tracking is on unless the project disabled it (best-effort: honor configure.ac).
292    let dep_tracking = std::fs::read_to_string(find_configure_ac())
293        .map(|s| {
294            !s.split("AM_INIT_AUTOMAKE")
295                .nth(1)
296                .map(|t| t.split(')').next().unwrap_or("").contains("no-dependencies"))
297                .unwrap_or(false)
298        })
299        .unwrap_or(true);
300
301    let needed = aux_files::needed_aux(
302        dep_tracking,
303        has_compiled,
304        has_tests,
305        has_yacc_lex,
306        has_static_lib,
307        has_python,
308    );
309
310    match aux_files::install_with_receipt(dir, &needed, parsed.force_missing) {
311        Ok(receipt) => {
312            let _ = std::fs::write(dir.join("aux-receipt.json"), &receipt);
313            if parsed.verbose {
314                for f in &needed {
315                    eprintln!("automake-rs: installed auxiliary file '{}'", f.filename());
316                }
317            }
318            Ok(())
319        }
320        Err(e) => Err(format!("aux file generation failed: {}", e)),
321    }
322}
323
324/// Print automake library directory (native detection).
325fn print_native_libdir() {
326    let dir = native_libdir();
327    println!("{}", dir);
328}
329
330/// Detect automake library directory natively.
331fn native_libdir() -> String {
332    let candidates = &[
333        "/usr/share/automake-1.18",
334        "/usr/share/automake-1.17",
335        "/usr/share/automake-1.16",
336        "/usr/share/automake",
337    ];
338    for path in candidates {
339        if Path::new(path).exists() {
340            return path.to_string();
341        }
342    }
343    // Fallback: try oracle once
344    if let Ok(out) = std::process::Command::new("automake")
345        .arg("--print-libdir")
346        .output()
347    {
348        let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
349        if !s.is_empty() {
350            return s;
351        }
352    }
353    "/usr/share/automake-1.18".to_string()
354}
355
356/// Get the effective libdir (from --libdir flag or auto-detect).
357pub fn effective_libdir(parsed: &automake_rs_core::cli::AutomakeArgs) -> String {
358    if let Some(ref dir) = parsed.libdir {
359        dir.clone()
360    } else {
361        native_libdir()
362    }
363}
364
365/// Detect platform triple (config.guess replacement).
366/// NC.PERM.11: Basic platform detection implemented via Rust std.
367/// Not claimed as a full config.guess/config.sub replacement.
368pub fn detect_platform() -> String {
369    let arch = std::env::consts::ARCH;
370    let os = std::env::consts::OS;
371    let vendor = "unknown";
372    match (arch, os) {
373        ("x86_64", "linux") => "x86_64-unknown-linux-gnu".into(),
374        ("aarch64", "linux") => "aarch64-unknown-linux-gnu".into(),
375        ("x86_64", "macos") => "x86_64-apple-darwin".into(),
376        ("aarch64", "macos") => "aarch64-apple-darwin".into(),
377        _ => format!("{}-{}-{}", arch, vendor, os),
378    }
379}
380
381/// Run the aclocal CLI — native engine.
382pub fn run_aclocal() {
383    automake_rs_core::i18n::init_i18n();
384    let args: Vec<String> = std::env::args().collect();
385    let parsed = match automake_rs_core::cli::AclocalArgs::parse(&args) {
386        Ok(a) => a,
387        Err(e) => {
388            eprintln!("aclocal-rs: {}", e);
389            std::process::exit(1);
390        }
391    };
392
393    if parsed.help {
394        print_aclocal_help();
395        return;
396    }
397
398    if parsed.version {
399        print_aclocal_version();
400        return;
401    }
402
403    // --print-ac-dir: delegate to oracle
404    if parsed.print_ac_dir {
405        match std::process::Command::new("aclocal")
406            .arg("--print-ac-dir")
407            .output()
408        {
409            Ok(out) => {
410                std::io::Write::write_all(&mut std::io::stdout(), &out.stdout).ok();
411                return;
412            }
413            Err(e) => {
414                eprintln!("aclocal-rs: cannot query oracle: {}", e);
415                std::process::exit(1);
416            }
417        }
418    }
419
420    if parsed.verbose {
421        eprintln!("aclocal-rs: using native engine");
422    }
423
424    let engine = automake_rs_core::aclocal::Aclocal::from_args(&parsed);
425    if let Err(e) = engine.run() {
426        eprintln!("aclocal-rs: {}", e);
427        std::process::exit(1);
428    }
429}
430
431fn print_automake_version() {
432    let version = automake_rs_core::cli::oracle_version();
433    print!("{}", version);
434}
435
436fn print_aclocal_version() {
437    let version = automake_rs_core::cli::oracle_version_aclocal();
438    print!("{}", version);
439}
440
441fn print_automake_help() {
442    println!("Usage: /usr/bin/automake [OPTION]... [Makefile]...");
443    println!();
444    println!("Generate Makefile.in for configure from Makefile.am.");
445    println!();
446    println!("Operation modes:");
447    println!("      --help               print this help, then exit");
448    println!("      --version            print version number, then exit");
449    println!("  -v, --verbose            verbosely list files processed");
450    println!("      --no-force           only update Makefile.in's that are out of date");
451    println!("  -W, --warnings=CATEGORY  report the warnings falling in CATEGORY");
452    println!();
453    println!("Dependency tracking:");
454    println!("  -i, --ignore-deps      disable dependency tracking code");
455    println!("      --include-deps     enable dependency tracking code");
456    println!();
457    println!("Flavors:");
458    println!("      --foreign          set strictness to foreign");
459    println!("      --gnits            set strictness to gnits");
460    println!("      --gnu              set strictness to gnu");
461    println!();
462    println!("Library files:");
463    println!("  -a, --add-missing      add missing standard files to package");
464    println!("      --libdir=DIR       set directory storing library files");
465    println!("      --print-libdir     print directory storing library files");
466    println!("  -c, --copy             with -a, copy missing files (default is symlink)");
467    println!("  -f, --force-missing    force update of standard files");
468    println!();
469    println!("      --host=TRIPLE        cross-compilation host triple");
470    println!("      --build=TRIPLE       cross-compilation build triple");
471    println!();
472    println!("automake-rs: native Rust reimplementation. Clean-room forensic parity.");
473}
474
475fn print_aclocal_help() {
476    println!("Usage: aclocal [OPTION]...");
477    println!();
478    println!("Generate 'aclocal.m4' by scanning 'configure.ac' or 'configure.in'");
479    println!();
480    println!("Options:");
481    println!("      --automake-acdir=DIR  directory holding automake-provided m4 files");
482    println!("      --aclocal-path=PATH   colon-separated list of directories to");
483    println!("                              search for third-party local files");
484    println!("      --system-acdir=DIR    directory holding third-party system-wide files");
485    println!("      --diff[=COMMAND]      run COMMAND [diff -u] on M4 files that would be");
486    println!("                            changed (implies --install and --dry-run)");
487    println!("      --dry-run             pretend to, but do not actually update any file");
488    println!("      --force               always update output file");
489    println!("      --help                print this help, then exit");
490    println!("  -I DIR                    add directory to search list for .m4 files");
491    println!("      --install             copy third-party files to the first -I directory");
492    println!("      --output=FILE         put output in FILE (default aclocal.m4)");
493    println!("      --print-ac-dir        print name of directory holding system-wide");
494    println!("                              third-party m4 files, then exit");
495    println!("      --verbose             don't be silent");
496    println!("      --version             print version number, then exit");
497    println!("  -W, --warnings=CATEGORY   report the warnings falling in CATEGORY");
498    println!();
499    println!("aclocal-rs: native Rust reimplementation. Clean-room forensic parity.");
500}