autoconf-rs-cli 0.1.8

CLI harness for autoconf-rs tools: autoconf, autoheader, autom4te, autoreconf, aclocal, autoscan, autoupdate, ifnames
Documentation
//! autoheader binary — generate config.h.in from configure.ac.
//!
//! Panel mandate: consume trace events (not prescan) for AC_DEFINE detection.
//! Trace events are the data bus between autoconf, autoheader, and automake.
//!
//! Receipt family: AC.CLI.AUTOHEADER.*
//! Status: Phase 4 — trace-driven, autom4te --trace integrated.

use autoconf_rs_cli::read_input;
use autoconf_rs_core::trace::AutoconfEvent;
use autoconf_rs_core::M4Engine;
use std::env;
use std::process::ExitCode;

fn main() -> ExitCode {
    let args: Vec<String> = env::args().collect();
    let input_path = args.get(1).map(|s| s.as_str()).unwrap_or("configure.ac");

    // Handle --help and --version
    if input_path == "--help" || input_path == "-h" {
        println!("autoheader-rs {}", env!("CARGO_PKG_VERSION"));
        println!("Generate config.h.in from configure.ac");
        println!("Usage: autoheader [configure.ac]");
        println!("  -h, --help    Show this help");
        println!("  --version     Show version");
        return ExitCode::SUCCESS;
    }
    if input_path == "--version" {
        println!("autoheader-rs {}", env!("CARGO_PKG_VERSION"));
        return ExitCode::SUCCESS;
    }

    let input = match read_input(input_path) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("autoheader: {}", e);
            return ExitCode::from(2);
        }
    };

    let mut engine = M4Engine::new();
    match engine.process(&input) {
        Ok(_output) => {
            // Panel architecture: consume trace events (source of truth)
            let trace_log = &engine.trace_log;

            // Extract AC_CONFIG_HEADERS from trace events
            let headers: Vec<&str> = trace_log
                .events
                .iter()
                .filter_map(|e| match e {
                    AutoconfEvent::ConfigHeader { output, .. } => Some(output.as_str()),
                    _ => None,
                })
                .collect();

            if headers.is_empty() {
                eprintln!("autoheader: no AC_CONFIG_HEADERS in configure.ac");
                eprintln!(
                    "  (checked {} trace events, 0 ConfigHeader found)",
                    trace_log.events.len()
                );
                return ExitCode::from(1);
            }

            // Extract AC_DEFINE from trace events
            let defines: Vec<(&str, Option<&str>)> = trace_log
                .events
                .iter()
                .filter_map(|e| match e {
                    AutoconfEvent::Define { name, value, .. } => {
                        Some((name.as_str(), value.as_deref()))
                    }
                    _ => None,
                })
                .collect();

            let header_file = headers.first().unwrap_or(&"config.h");

            println!(
                "/* {} — Generated by autoconf-rs autoheader (trace-driven). */",
                header_file
            );
            println!(
                "/* Source: {}{} trace events, {} AC_DEFINE, {} AC_CONFIG_HEADERS */",
                input_path,
                trace_log.events.len(),
                defines.len(),
                headers.len()
            );
            println!();

            if defines.is_empty() {
                println!("/* No AC_DEFINE calls found in trace events. */");
                println!("/* Try: autom4te --trace=AC_DEFINE {} */", input_path);
            } else {
                for (var, _value) in &defines {
                    println!("#undef {}", var);
                }
            }

            // Standard AC_INIT defines. We emit them already as `#define` with the values parsed
            // from AC_INIT (rather than `#undef` + config.status substitution): config.status's
            // header sed only converts the AC_DEFINE `#undef`s, and emitting these resolved here
            // makes config.h correct regardless of which header-generation path runs. Packages
            // routinely `#include config.h` and use PACKAGE_NAME/VERSION.
            let (pname, pver) = parse_ac_init(&input);
            println!("#define PACKAGE_NAME \"{}\"", pname);
            println!("#define PACKAGE_TARNAME \"{}\"", pname);
            println!("#define PACKAGE_VERSION \"{}\"", pver);
            println!("#define PACKAGE_STRING \"{} {}\"", pname, pver);
            println!("#define PACKAGE_BUGREPORT \"\"");
            println!("#define PACKAGE_URL \"\"");
            println!("#define PACKAGE \"{}\"", pname);
            println!("#define VERSION \"{}\"", pver);

            // NB: do NOT emit the `@%:@undef` "template" lines here. `@%:@` is the m4 quadrigraph
            // for `#`, but config.status's `#undef X -> #define X` substitution does not process it,
            // so those lines reach config.h literally as `@%:@undef ...` -> "stray '@' in program".
            // The plain `#undef X` lines above are the correct, substitutable template entries.

            eprintln!(
                "autoheader: generated {} with {} #undef entries (trace-driven)",
                header_file,
                defines.len()
            );
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("autoheader: {}", e);
            ExitCode::from(2)
        }
    }
}

/// Parse AC_INIT([name],[version],...) from configure.ac text. Returns (name, version),
/// stripping m4 `[]` quotes and whitespace; empty strings if not found.
fn parse_ac_init(input: &str) -> (String, String) {
    if let Some(pos) = input.find("AC_INIT") {
        let after = &input[pos + "AC_INIT".len()..];
        if let Some(open) = after.find('(') {
            // collect to matching close paren
            let mut depth = 0i32;
            let mut end = None;
            for (i, c) in after[open..].char_indices() {
                match c {
                    '(' => depth += 1,
                    ')' => { depth -= 1; if depth == 0 { end = Some(open + i); break; } }
                    _ => {}
                }
            }
            if let Some(e) = end {
                let args_str = &after[open + 1..e];
                let strip = |s: &str| s.trim().trim_start_matches('[').trim_end_matches(']').trim().to_string();
                let parts: Vec<&str> = args_str.splitn(3, ',').collect();
                let name = parts.first().map(|s| strip(s)).unwrap_or_default();
                let version = parts.get(1).map(|s| strip(s)).unwrap_or_default();
                return (name, version);
            }
        }
    }
    (String::new(), String::new())
}