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");
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) => {
let trace_log = &engine.trace_log;
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);
}
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");
let mut out = String::new();
macro_rules! oline {
() => {{ out.push('\n'); }};
($($a:tt)*) => {{ out.push_str(&format!($($a)*)); out.push('\n'); }};
}
oline!(
"/* {} — Generated by autoconf-rs autoheader (trace-driven). */",
header_file
);
oline!(
"/* Source: {} — {} trace events, {} AC_DEFINE, {} AC_CONFIG_HEADERS */",
input_path,
trace_log.events.len(),
defines.len(),
headers.len()
);
oline!();
if defines.is_empty() {
oline!("/* No AC_DEFINE calls found in trace events. */");
oline!("/* Try: autom4te --trace=AC_DEFINE {} */", input_path);
} else {
for (var, _value) in &defines {
oline!("#undef {}", var);
}
}
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()) {
oline!("#undef {}", m);
}
}
}
let (pname, pver) = parse_ac_init(&input);
oline!("#define PACKAGE_NAME \"{}\"", pname);
oline!("#define PACKAGE_TARNAME \"{}\"", pname);
oline!("#define PACKAGE_VERSION \"{}\"", pver);
oline!("#define PACKAGE_STRING \"{} {}\"", pname, pver);
oline!("#define PACKAGE_BUGREPORT \"\"");
oline!("#define PACKAGE_URL \"\"");
let amopts = init_automake_options(&input);
let emit_bare = amopts.is_some() && !amopts.as_deref().unwrap_or("").contains("no-define");
if emit_bare {
oline!("#define PACKAGE \"{}\"", pname);
oline!("#define VERSION \"{}\"", pver);
}
let out_path = {
let dir = std::path::Path::new(input_path)
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| std::path::PathBuf::from("."));
dir.join(format!("{}.in", header_file))
};
if let Err(e) = std::fs::write(&out_path, &out) {
eprintln!("autoheader: cannot write {}: {}", out_path.display(), e);
return ExitCode::from(2);
}
eprintln!(
"autoheader: generated {} with {} #undef entries (trace-driven)",
out_path.display(),
defines.len()
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("autoheader: {}", e);
ExitCode::from(2)
}
}
}
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('(') {
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())
}
fn sanitize_token(v: &str, fallback: &str) -> String {
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()
}
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())
}
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)
}