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/// Generate auxiliary files natively (replaces oracle delegation).
265fn add_missing_files(
266    parsed: &automake_rs_core::cli::AutomakeArgs,
267    makefile_path: &Path,
268) -> Result<(), String> {
269    use automake_rs_core::aux_scripts;
270
271    let dir = makefile_path.parent().unwrap_or(Path::new("."));
272
273    match aux_scripts::install_aux_files(dir, parsed.copy, parsed.force_missing) {
274        Ok(installed) => {
275            if parsed.verbose {
276                for name in &installed {
277                    eprintln!("automake-rs: installed auxiliary file '{}'", name);
278                }
279            }
280            Ok(())
281        }
282        Err(e) => Err(format!("aux file generation failed: {}", e)),
283    }
284}
285
286/// Print automake library directory (native detection).
287fn print_native_libdir() {
288    let dir = native_libdir();
289    println!("{}", dir);
290}
291
292/// Detect automake library directory natively.
293fn native_libdir() -> String {
294    let candidates = &[
295        "/usr/share/automake-1.18",
296        "/usr/share/automake-1.17",
297        "/usr/share/automake-1.16",
298        "/usr/share/automake",
299    ];
300    for path in candidates {
301        if Path::new(path).exists() {
302            return path.to_string();
303        }
304    }
305    // Fallback: try oracle once
306    if let Ok(out) = std::process::Command::new("automake")
307        .arg("--print-libdir")
308        .output()
309    {
310        let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
311        if !s.is_empty() {
312            return s;
313        }
314    }
315    "/usr/share/automake-1.18".to_string()
316}
317
318/// Get the effective libdir (from --libdir flag or auto-detect).
319pub fn effective_libdir(parsed: &automake_rs_core::cli::AutomakeArgs) -> String {
320    if let Some(ref dir) = parsed.libdir {
321        dir.clone()
322    } else {
323        native_libdir()
324    }
325}
326
327/// Detect platform triple (config.guess replacement).
328/// NC.PERM.11: Basic platform detection implemented via Rust std.
329/// Not claimed as a full config.guess/config.sub replacement.
330pub fn detect_platform() -> String {
331    let arch = std::env::consts::ARCH;
332    let os = std::env::consts::OS;
333    let vendor = "unknown";
334    match (arch, os) {
335        ("x86_64", "linux") => "x86_64-unknown-linux-gnu".into(),
336        ("aarch64", "linux") => "aarch64-unknown-linux-gnu".into(),
337        ("x86_64", "macos") => "x86_64-apple-darwin".into(),
338        ("aarch64", "macos") => "aarch64-apple-darwin".into(),
339        _ => format!("{}-{}-{}", arch, vendor, os),
340    }
341}
342
343/// Run the aclocal CLI — native engine.
344pub fn run_aclocal() {
345    automake_rs_core::i18n::init_i18n();
346    let args: Vec<String> = std::env::args().collect();
347    let parsed = match automake_rs_core::cli::AclocalArgs::parse(&args) {
348        Ok(a) => a,
349        Err(e) => {
350            eprintln!("aclocal-rs: {}", e);
351            std::process::exit(1);
352        }
353    };
354
355    if parsed.help {
356        print_aclocal_help();
357        return;
358    }
359
360    if parsed.version {
361        print_aclocal_version();
362        return;
363    }
364
365    // --print-ac-dir: delegate to oracle
366    if parsed.print_ac_dir {
367        match std::process::Command::new("aclocal")
368            .arg("--print-ac-dir")
369            .output()
370        {
371            Ok(out) => {
372                std::io::Write::write_all(&mut std::io::stdout(), &out.stdout).ok();
373                return;
374            }
375            Err(e) => {
376                eprintln!("aclocal-rs: cannot query oracle: {}", e);
377                std::process::exit(1);
378            }
379        }
380    }
381
382    if parsed.verbose {
383        eprintln!("aclocal-rs: using native engine");
384    }
385
386    let engine = automake_rs_core::aclocal::Aclocal::from_args(&parsed);
387    if let Err(e) = engine.run() {
388        eprintln!("aclocal-rs: {}", e);
389        std::process::exit(1);
390    }
391}
392
393fn print_automake_version() {
394    let version = automake_rs_core::cli::oracle_version();
395    print!("{}", version);
396}
397
398fn print_aclocal_version() {
399    let version = automake_rs_core::cli::oracle_version_aclocal();
400    print!("{}", version);
401}
402
403fn print_automake_help() {
404    println!("Usage: /usr/bin/automake [OPTION]... [Makefile]...");
405    println!();
406    println!("Generate Makefile.in for configure from Makefile.am.");
407    println!();
408    println!("Operation modes:");
409    println!("      --help               print this help, then exit");
410    println!("      --version            print version number, then exit");
411    println!("  -v, --verbose            verbosely list files processed");
412    println!("      --no-force           only update Makefile.in's that are out of date");
413    println!("  -W, --warnings=CATEGORY  report the warnings falling in CATEGORY");
414    println!();
415    println!("Dependency tracking:");
416    println!("  -i, --ignore-deps      disable dependency tracking code");
417    println!("      --include-deps     enable dependency tracking code");
418    println!();
419    println!("Flavors:");
420    println!("      --foreign          set strictness to foreign");
421    println!("      --gnits            set strictness to gnits");
422    println!("      --gnu              set strictness to gnu");
423    println!();
424    println!("Library files:");
425    println!("  -a, --add-missing      add missing standard files to package");
426    println!("      --libdir=DIR       set directory storing library files");
427    println!("      --print-libdir     print directory storing library files");
428    println!("  -c, --copy             with -a, copy missing files (default is symlink)");
429    println!("  -f, --force-missing    force update of standard files");
430    println!();
431    println!("      --host=TRIPLE        cross-compilation host triple");
432    println!("      --build=TRIPLE       cross-compilation build triple");
433    println!();
434    println!("automake-rs: native Rust reimplementation. Clean-room forensic parity.");
435}
436
437fn print_aclocal_help() {
438    println!("Usage: aclocal [OPTION]...");
439    println!();
440    println!("Generate 'aclocal.m4' by scanning 'configure.ac' or 'configure.in'");
441    println!();
442    println!("Options:");
443    println!("      --automake-acdir=DIR  directory holding automake-provided m4 files");
444    println!("      --aclocal-path=PATH   colon-separated list of directories to");
445    println!("                              search for third-party local files");
446    println!("      --system-acdir=DIR    directory holding third-party system-wide files");
447    println!("      --diff[=COMMAND]      run COMMAND [diff -u] on M4 files that would be");
448    println!("                            changed (implies --install and --dry-run)");
449    println!("      --dry-run             pretend to, but do not actually update any file");
450    println!("      --force               always update output file");
451    println!("      --help                print this help, then exit");
452    println!("  -I DIR                    add directory to search list for .m4 files");
453    println!("      --install             copy third-party files to the first -I directory");
454    println!("      --output=FILE         put output in FILE (default aclocal.m4)");
455    println!("      --print-ac-dir        print name of directory holding system-wide");
456    println!("                              third-party m4 files, then exit");
457    println!("      --verbose             don't be silent");
458    println!("      --version             print version number, then exit");
459    println!("  -W, --warnings=CATEGORY   report the warnings falling in CATEGORY");
460    println!();
461    println!("aclocal-rs: native Rust reimplementation. Clean-room forensic parity.");
462}