use autoconf_rs_cli::read_input;
use autoconf_rs_core::autom4te::Autom4teCache;
use autoconf_rs_core::{ConfigureAc, M4Engine};
use std::env;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
fn main() -> ExitCode {
std::thread::Builder::new()
.stack_size(1024 * 1024 * 1024)
.spawn(run)
.ok()
.and_then(|h| h.join().ok())
.unwrap_or(ExitCode::from(2))
}
fn run() -> ExitCode {
let args: Vec<String> = env::args().collect();
let mut input_arg: Option<&str> = None;
let mut force = false;
let mut include_dirs: Vec<PathBuf> = Vec::new();
let mut cache_dir = PathBuf::from("autom4te.cache");
let mut warnings: Vec<String> = Vec::new();
let mut i = 1;
let mut allow_syscmd = false;
let mut pure_m4 = false;
while i < args.len() {
match args[i].as_str() {
"-f" | "--force" => force = true,
"--allow-syscmd" => allow_syscmd = true,
"--pure-m4" => pure_m4 = true,
"-I" | "--include" => {
i += 1;
if i < args.len() {
include_dirs.push(PathBuf::from(&args[i]));
}
}
"-B" | "--prepend-include" => {
i += 1;
if i < args.len() {
include_dirs.insert(0, PathBuf::from(&args[i]));
}
}
"-W" | "--warnings" => {
i += 1;
if i < args.len() {
warnings.push(args[i].clone());
}
}
"--cache" => {
i += 1;
if i < args.len() {
cache_dir = PathBuf::from(&args[i]);
}
}
a if !a.starts_with('-') => input_arg = Some(a),
"-h" | "--help" => {
println!("autoconf-rs {}", env!("CARGO_PKG_VERSION"));
println!("Generate configure scripts from configure.ac");
println!("Usage: autoconf [OPTIONS] [configure.ac]");
println!(" -f, --force Force regeneration");
println!(" -I, --include DIR Add include directory");
println!(" -W, --warnings CAT Enable warning category");
println!(" -h, --help Show this help");
println!(" --version Show version");
println!(" --pure-m4 Use raw M4 expansion (skip prescan+template)");
return ExitCode::SUCCESS;
}
"--version" => {
println!("autoconf-rs {}", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS;
}
_ => {}
}
i += 1;
}
let path = input_arg.unwrap_or("configure.ac").to_string();
let input_path = Path::new(&path);
if include_dirs.is_empty() {
include_dirs.push(PathBuf::from("."));
}
let mut cache = Autom4teCache::new(&cache_dir);
cache.set_force(force);
if !force {
if let Some(cached_output) = cache.lookup(input_path, &include_dirs, "Autoconf") {
print!("{}", cached_output);
return ExitCode::SUCCESS;
}
}
let configure_ac = match read_input(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("autoconf: {}", e);
return ExitCode::from(2);
}
};
let aclocal_path = Path::new(&path)
.parent()
.unwrap_or_else(|| Path::new("."))
.join("aclocal.m4");
let overrides = autoconf_rs_core::macro_overrides();
let configure_ac = match configure_ac.find("AC_INIT") {
Some(pos) => {
let line_start = configure_ac[..pos].rfind('\n').map(|i| i + 1).unwrap_or(0);
format!(
"{}{}\n{}",
&configure_ac[..line_start],
overrides,
&configure_ac[line_start..]
)
}
None => format!("{}\n{}", overrides, configure_ac),
};
let input = match std::fs::read_to_string(&aclocal_path) {
Ok(acm4) => format!("{}\n{}", acm4, configure_ac),
Err(_) => configure_ac,
};
let _ac = ConfigureAc::parse(&input);
let mut engine = M4Engine::new();
engine.allow_syscmd = allow_syscmd;
engine.pure_m4 = pure_m4;
let configure_script = match engine.process(&input) {
Ok(s) => {
let s = if std::env::var("AUTOCONF_RS_NO_NEUTRALIZE").is_err() {
neutralize_leaked_macros(&s)
} else {
s
};
guard_empty_shell_blocks(&s)
}
Err(e) => {
eprintln!("autoconf: M4 error: {}", e);
return ExitCode::from(2);
}
};
let trace_lines: Vec<String> = engine
.trace_log
.emit_autom4te_traces()
.iter()
.map(|t| t.lines().next().unwrap_or(t).to_string())
.collect();
cache.store(
input_path,
&include_dirs,
"Autoconf",
&configure_script,
&trace_lines,
);
print!("{}", configure_script);
ExitCode::SUCCESS
}
fn neutralize_leaked_macros(input: &str) -> String {
const PREFIXES: &[&str] = &[
"AC_", "AX_", "AM_", "LT_", "AS_", "PKG_", "AH_", "_AC_", "_AM_", "_LT_",
"m4_", "_m4_", "gl_", "IT_", "GLIB_", "GTK_", "BOOST_", "AC", "AM", ];
let leaked_macro_at = |s: &str| -> bool {
let id_len = s.bytes().take_while(|b| b.is_ascii_alphanumeric() || *b == b'_').count();
if id_len == 0 || s.as_bytes().get(id_len) != Some(&b'(') {
return false;
}
let id = &s[..id_len];
let has_prefix = PREFIXES.iter().any(|p| id.starts_with(p) && id.len() > p.len());
has_prefix && (id.contains('_') || id.chars().any(|c| c.is_ascii_uppercase()))
};
let lines: Vec<&str> = input.lines().collect();
let mut out: Vec<String> = Vec::with_capacity(lines.len());
let mut i = 0;
while i < lines.len() {
let trimmed = lines[i].trim_start();
if leaked_macro_at(trimmed) {
let mut depth: i32 = 0;
let mut started = false;
let mut j = i;
while j < lines.len() {
for c in lines[j].chars() {
if c == '(' { depth += 1; started = true; }
else if c == ')' { depth -= 1; }
}
if started && depth <= 0 { break; }
j += 1;
}
out.push(":".to_string()); i = j + 1;
continue;
}
out.push(lines[i].to_string());
i += 1;
}
let mut result = out.join("\n");
if input.ends_with('\n') {
result.push('\n');
}
result
}
fn guard_empty_shell_blocks(input: &str) -> String {
let lines: Vec<&str> = input.lines().collect();
let mut out: Vec<String> = Vec::with_capacity(lines.len() + 8);
let opens_block = |t: &str| -> bool {
let tt = t.trim();
if tt == "else" {
return true;
}
match tt.rsplit(|c| c == ' ' || c == ';' || c == '\t').next() {
Some("then") | Some("do") => true,
_ => false,
}
};
let mut i = 0;
while i < lines.len() {
out.push(lines[i].to_string());
if opens_block(lines[i]) {
let mut j = i + 1;
while j < lines.len() && lines[j].trim().is_empty() {
j += 1;
}
if j < lines.len() {
let nt = lines[j].trim_start();
if nt.starts_with("fi") || nt == "else" || nt.starts_with("else ")
|| nt.starts_with("elif") || nt.starts_with("done")
{
out.push(":".to_string());
}
}
}
i += 1;
}
let mut result = out.join("\n");
if input.ends_with('\n') {
result.push('\n');
}
result
}