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 in the current directory.
99fn find_configure_ac() -> PathBuf {
100    for name in &["configure.ac", "configure.in"] {
101        let path = Path::new(name);
102        if path.exists() {
103            return path.to_path_buf();
104        }
105    }
106    PathBuf::from("configure.ac")
107}
108
109/// Process a single Makefile.am and generate Makefile.in.
110fn process_makefile(
111    parsed: &automake_rs_core::cli::AutomakeArgs,
112    makefile_path: &Path,
113    configure_ac: &Path,
114) -> Result<PathBuf, String> {
115    // Determine output path: Makefile.in in the same directory
116    let parent = makefile_path.parent().unwrap_or(Path::new("."));
117    let stem = makefile_path
118        .file_stem()
119        .map(|s| s.to_string_lossy().to_string())
120        .unwrap_or_else(|| "Makefile".to_string());
121    let output_path = parent.join(format!("{}.in", stem));
122
123    // --no-force: skip if Makefile.in is newer than Makefile.am
124    if parsed.no_force && output_path.exists() {
125        if let (Ok(am_meta), Ok(in_meta)) =
126            (fs::metadata(makefile_path), fs::metadata(&output_path))
127        {
128            if let (Ok(am_time), Ok(in_time)) = (am_meta.modified(), in_meta.modified()) {
129                if in_time >= am_time {
130                    if parsed.verbose {
131                        eprintln!(
132                            "automake-rs: {} is up to date, skipping",
133                            output_path.display()
134                        );
135                    }
136                    return Ok(output_path);
137                }
138            }
139        }
140    }
141
142    // Step 1: Extract traces from configure.ac
143    if parsed.verbose {
144        eprintln!(
145            "automake-rs: extracting traces from {}",
146            configure_ac.display()
147        );
148    }
149
150    let bridge = automake_rs_core::autoconf_bridge::AutoconfBridge::new();
151    let traces = bridge
152        .extract_traces(configure_ac)
153        .map_err(|e| format!("trace extraction failed: {}", e))?;
154
155    // Step 2: Build AutomakeConfig from parsed options
156    let mut config = automake_rs_core::automake_macros::AutomakeConfig::from_options(&format!(
157        "{} {} {}",
158        if parsed.foreign {
159            "foreign"
160        } else if parsed.gnits {
161            "gnits"
162        } else if parsed.gnu {
163            "gnu"
164        } else {
165            traces.strictness.as_deref().unwrap_or("gnu")
166        },
167        parsed
168            .warnings
169            .iter()
170            .map(|w| w.as_str())
171            .collect::<Vec<_>>()
172            .join(" "),
173        ""
174    ));
175
176    // Apply dependency tracking flags
177    if let Some(enable) = parsed.dependency_tracking_enabled() {
178        config.dependency_tracking = enable;
179    }
180
181    // Step 3: Parse Makefile.am
182    if parsed.verbose {
183        eprintln!("automake-rs: parsing {}", makefile_path.display());
184    }
185
186    let am = automake_rs_core::makefile_am::MakefileAm::from_file(makefile_path)
187        .map_err(|e| format!("parse error: {}", e))?;
188
189    // Step 3b: Run diagnostics on the parsed Makefile.am
190    let strictness = if parsed.foreign {
191        "foreign"
192    } else if parsed.gnits {
193        "gnits"
194    } else if parsed.gnu {
195        "gnu"
196    } else {
197        traces.strictness.as_deref().unwrap_or("gnu")
198    };
199
200    let mut diag =
201        automake_rs_core::diagnostics::DiagnosticManager::from_config(strictness, &parsed.warnings);
202
203    automake_rs_core::diagnostics::run_makefile_diagnostics(&am, &mut diag);
204    automake_rs_core::diagnostics::check_missing_standard_files(&mut diag, strictness);
205
206    // Print diagnostics
207    diag.print_all();
208
209    if diag.has_errors() {
210        return Err("errors encountered — stopping".to_string());
211    }
212
213    // Step 4: Generate Makefile.in
214    if parsed.verbose {
215        eprintln!("automake-rs: generating {}", output_path.display());
216    }
217
218    let gen = automake_rs_core::makefile_in::MakefileInGenerator::new(am, config, traces);
219    let output = gen.generate();
220
221    // Step 5: Write output
222    fs::write(&output_path, output).map_err(|e| format!("write error: {}", e))?;
223
224    // Step 6: Handle --add-missing (delegate to oracle for auxiliary files)
225    if parsed.add_missing {
226        if parsed.verbose {
227            eprintln!("automake-rs: delegating --add-missing to oracle");
228        }
229        add_missing_files(parsed, makefile_path)?;
230    }
231
232    Ok(output_path)
233}
234
235/// Generate auxiliary files natively (replaces oracle delegation).
236fn add_missing_files(
237    parsed: &automake_rs_core::cli::AutomakeArgs,
238    makefile_path: &Path,
239) -> Result<(), String> {
240    use automake_rs_core::aux_scripts;
241
242    let dir = makefile_path.parent().unwrap_or(Path::new("."));
243
244    match aux_scripts::install_aux_files(dir, parsed.copy, parsed.force_missing) {
245        Ok(installed) => {
246            if parsed.verbose {
247                for name in &installed {
248                    eprintln!("automake-rs: installed auxiliary file '{}'", name);
249                }
250            }
251            Ok(())
252        }
253        Err(e) => Err(format!("aux file generation failed: {}", e)),
254    }
255}
256
257/// Print automake library directory (native detection).
258fn print_native_libdir() {
259    let dir = native_libdir();
260    println!("{}", dir);
261}
262
263/// Detect automake library directory natively.
264fn native_libdir() -> String {
265    let candidates = &[
266        "/usr/share/automake-1.18",
267        "/usr/share/automake-1.17",
268        "/usr/share/automake-1.16",
269        "/usr/share/automake",
270    ];
271    for path in candidates {
272        if Path::new(path).exists() {
273            return path.to_string();
274        }
275    }
276    // Fallback: try oracle once
277    if let Ok(out) = std::process::Command::new("automake")
278        .arg("--print-libdir")
279        .output()
280    {
281        let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
282        if !s.is_empty() {
283            return s;
284        }
285    }
286    "/usr/share/automake-1.18".to_string()
287}
288
289/// Get the effective libdir (from --libdir flag or auto-detect).
290pub fn effective_libdir(parsed: &automake_rs_core::cli::AutomakeArgs) -> String {
291    if let Some(ref dir) = parsed.libdir {
292        dir.clone()
293    } else {
294        native_libdir()
295    }
296}
297
298/// Detect platform triple (config.guess replacement).
299/// NC.PERM.11: Basic platform detection implemented via Rust std.
300/// Not claimed as a full config.guess/config.sub replacement.
301pub fn detect_platform() -> String {
302    let arch = std::env::consts::ARCH;
303    let os = std::env::consts::OS;
304    let vendor = "unknown";
305    match (arch, os) {
306        ("x86_64", "linux") => "x86_64-unknown-linux-gnu".into(),
307        ("aarch64", "linux") => "aarch64-unknown-linux-gnu".into(),
308        ("x86_64", "macos") => "x86_64-apple-darwin".into(),
309        ("aarch64", "macos") => "aarch64-apple-darwin".into(),
310        _ => format!("{}-{}-{}", arch, vendor, os),
311    }
312}
313
314/// Run the aclocal CLI — native engine.
315pub fn run_aclocal() {
316    automake_rs_core::i18n::init_i18n();
317    let args: Vec<String> = std::env::args().collect();
318    let parsed = match automake_rs_core::cli::AclocalArgs::parse(&args) {
319        Ok(a) => a,
320        Err(e) => {
321            eprintln!("aclocal-rs: {}", e);
322            std::process::exit(1);
323        }
324    };
325
326    if parsed.help {
327        print_aclocal_help();
328        return;
329    }
330
331    if parsed.version {
332        print_aclocal_version();
333        return;
334    }
335
336    // --print-ac-dir: delegate to oracle
337    if parsed.print_ac_dir {
338        match std::process::Command::new("aclocal")
339            .arg("--print-ac-dir")
340            .output()
341        {
342            Ok(out) => {
343                std::io::Write::write_all(&mut std::io::stdout(), &out.stdout).ok();
344                return;
345            }
346            Err(e) => {
347                eprintln!("aclocal-rs: cannot query oracle: {}", e);
348                std::process::exit(1);
349            }
350        }
351    }
352
353    if parsed.verbose {
354        eprintln!("aclocal-rs: using native engine");
355    }
356
357    let engine = automake_rs_core::aclocal::Aclocal::from_args(&parsed);
358    if let Err(e) = engine.run() {
359        eprintln!("aclocal-rs: {}", e);
360        std::process::exit(1);
361    }
362}
363
364fn print_automake_version() {
365    let version = automake_rs_core::cli::oracle_version();
366    print!("{}", version);
367}
368
369fn print_aclocal_version() {
370    let version = automake_rs_core::cli::oracle_version_aclocal();
371    print!("{}", version);
372}
373
374fn print_automake_help() {
375    println!("Usage: /usr/bin/automake [OPTION]... [Makefile]...");
376    println!();
377    println!("Generate Makefile.in for configure from Makefile.am.");
378    println!();
379    println!("Operation modes:");
380    println!("      --help               print this help, then exit");
381    println!("      --version            print version number, then exit");
382    println!("  -v, --verbose            verbosely list files processed");
383    println!("      --no-force           only update Makefile.in's that are out of date");
384    println!("  -W, --warnings=CATEGORY  report the warnings falling in CATEGORY");
385    println!();
386    println!("Dependency tracking:");
387    println!("  -i, --ignore-deps      disable dependency tracking code");
388    println!("      --include-deps     enable dependency tracking code");
389    println!();
390    println!("Flavors:");
391    println!("      --foreign          set strictness to foreign");
392    println!("      --gnits            set strictness to gnits");
393    println!("      --gnu              set strictness to gnu");
394    println!();
395    println!("Library files:");
396    println!("  -a, --add-missing      add missing standard files to package");
397    println!("      --libdir=DIR       set directory storing library files");
398    println!("      --print-libdir     print directory storing library files");
399    println!("  -c, --copy             with -a, copy missing files (default is symlink)");
400    println!("  -f, --force-missing    force update of standard files");
401    println!();
402    println!("      --host=TRIPLE        cross-compilation host triple");
403    println!("      --build=TRIPLE       cross-compilation build triple");
404    println!();
405    println!("automake-rs: native Rust reimplementation. Clean-room forensic parity.");
406}
407
408fn print_aclocal_help() {
409    println!("Usage: aclocal [OPTION]...");
410    println!();
411    println!("Generate 'aclocal.m4' by scanning 'configure.ac' or 'configure.in'");
412    println!();
413    println!("Options:");
414    println!("      --automake-acdir=DIR  directory holding automake-provided m4 files");
415    println!("      --aclocal-path=PATH   colon-separated list of directories to");
416    println!("                              search for third-party local files");
417    println!("      --system-acdir=DIR    directory holding third-party system-wide files");
418    println!("      --diff[=COMMAND]      run COMMAND [diff -u] on M4 files that would be");
419    println!("                            changed (implies --install and --dry-run)");
420    println!("      --dry-run             pretend to, but do not actually update any file");
421    println!("      --force               always update output file");
422    println!("      --help                print this help, then exit");
423    println!("  -I DIR                    add directory to search list for .m4 files");
424    println!("      --install             copy third-party files to the first -I directory");
425    println!("      --output=FILE         put output in FILE (default aclocal.m4)");
426    println!("      --print-ac-dir        print name of directory holding system-wide");
427    println!("                              third-party m4 files, then exit");
428    println!("      --verbose             don't be silent");
429    println!("      --version             print version number, then exit");
430    println!("  -W, --warnings=CATEGORY   report the warnings falling in CATEGORY");
431    println!();
432    println!("aclocal-rs: native Rust reimplementation. Clean-room forensic parity.");
433}