autoconf-rs-cli 0.1.34

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);
                }
            }

            // Emit `#undef HAVE_X` templates for AC_CHECK_HEADERS/FUNCS/LIB. configure's real probes
            // append `#define HAVE_X 1` to confdefs.h on success, and config.status converts these
            // `#undef` lines accordingly. Without the templates here there is nothing to convert, so
            // detected features never reach config.h (-> `optind undeclared`, missing struct members).
            let mut have_seen = std::collections::BTreeSet::new();
            for e in &trace_log.events {
                let macro_name = match e {
                    AutoconfEvent::CheckHeader { header, .. } => Some(have_macro(header)),
                    AutoconfEvent::CheckFunc { function, .. } => Some(have_macro(function)),
                    AutoconfEvent::CheckLib { library, .. } => {
                        Some(format!("HAVE_LIB{}", library.to_ascii_uppercase()))
                    }
                    _ => None,
                };
                if let Some(m) = macro_name {
                    if have_seen.insert(m.clone()) {
                        println!("#undef {}", m);
                    }
                }
            }

            // 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);
            // The PACKAGE_* forms come from AC_INIT and are always safe.
            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 \"\"");
            // The bare PACKAGE / VERSION macros are defined ONLY by AM_INIT_AUTOMAKE, and ONLY when
            // its `no-define` option is absent. Emitting them unconditionally breaks projects that
            // use `VERSION`/`PACKAGE` as their own identifiers under `no-define` (e.g. pgpdump's
            // `private int VERSION;`). Honor that here.
            let amopts = init_automake_options(&input);
            let emit_bare = amopts.is_some() && !amopts.as_deref().unwrap_or("").contains("no-define");
            if emit_bare {
                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 = sanitize_token(&parts.first().map(|s| strip(s)).unwrap_or_default(), "config");
                let version = sanitize_token(&parts.get(1).map(|s| strip(s)).unwrap_or_default(), "0");
                return (name, version);
            }
        }
    }
    (String::new(), String::new())
}

/// Sanitize an AC_INIT token (name/version) so it's safe to embed in config.h as a C string.
/// AC_INIT args can be unevaluated m4 (e.g. version = `m4_esyscmd_s([git describe...])` with `dnl`
/// comments) which autoconf-rs cannot run (esyscmd blocked). Strip `dnl` comments and reject any
/// value that still looks like m4/multi-word garbage, falling back to a safe default.
fn sanitize_token(v: &str, fallback: &str) -> String {
    // drop `dnl ...` to end of line, join lines
    let no_dnl: String = v
        .lines()
        .map(|l| l.split("dnl").next().unwrap_or(""))
        .collect::<Vec<_>>()
        .join(" ");
    let t = no_dnl.trim().trim_matches('"').trim();
    if t.is_empty()
        || t.contains("m4_")
        || t.contains("esyscmd")
        || t.contains('[')
        || t.contains(']')
        || t.contains('(')
        || t.chars().any(|c| c.is_whitespace())
    {
        return fallback.to_string();
    }
    t.to_string()
}

/// Return the AM_INIT_AUTOMAKE option string (the macro's args), or None if the project doesn't
/// use AM_INIT_AUTOMAKE. Used to honor `no-define` (whether bare PACKAGE/VERSION are defined).
fn init_automake_options(input: &str) -> Option<String> {
    let pos = input.find("AM_INIT_AUTOMAKE")?;
    let after = &input[pos + "AM_INIT_AUTOMAKE".len()..];
    let open = after.find('(')?;
    let mut depth = 0i32;
    for (i, c) in after[open..].char_indices() {
        match c {
            '(' => depth += 1,
            ')' => {
                depth -= 1;
                if depth == 0 {
                    return Some(after[open + 1..open + i].to_string());
                }
            }
            _ => {}
        }
    }
    Some(String::new())
}

/// The C preprocessor macro a successful AC_CHECK_HEADER/FUNC defines (mirrors
/// autoconf_rs_core::configure_body::have_macro): `sys/time.h` -> `HAVE_SYS_TIME_H`.
fn have_macro(name: &str) -> String {
    let up: String = name
        .trim()
        .chars()
        .map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_uppercase() } else { '_' })
        .collect();
    format!("HAVE_{}", up)
}