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;
let mut output_arg: Option<String> = None;
while i < args.len() {
match args[i].as_str() {
"-f" | "--force" => force = true,
"--allow-syscmd" => allow_syscmd = true,
"--pure-m4" => pure_m4 = true,
"-o" | "--output" => {
i += 1;
if i < args.len() {
output_arg = Some(args[i].clone());
}
}
a if a.starts_with("--output=") => {
output_arg = Some(a["--output=".len()..].to_string());
}
a if a.starts_with("-o") && a.len() > 2 => {
output_arg = Some(a[2..].to_string());
}
"-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!(" -o, --output FILE Write configure to FILE (chmod +x); '-' = stdout");
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") {
return emit_output(&output_arg, &cached_output);
}
}
let configure_ac = match read_input(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("autoconf: {}", e);
return ExitCode::from(2);
}
};
let configure_ac = protect_hash_comments(&configure_ac);
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{}", strip_toplevel_hash_comments(&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
};
let s = expand_lang_constants(&s);
let s = convert_quadrigraphs(&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,
);
emit_output(&output_arg, &configure_script)
}
fn emit_output(output_arg: &Option<String>, content: &str) -> ExitCode {
match output_arg {
Some(o) if o != "-" => {
if let Err(e) = std::fs::write(o, content) {
eprintln!("autoconf: cannot write {}: {}", o, e);
return ExitCode::from(2);
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(o) {
let mut perm = meta.permissions();
perm.set_mode(0o755);
let _ = std::fs::set_permissions(o, perm);
}
}
ExitCode::SUCCESS
}
_ => {
print!("{}", content);
ExitCode::SUCCESS
}
}
}
fn line_has_macro_call(s: &str) -> bool {
const PREFIXES: &[&str] = &[
"AC_", "AS_", "AM_", "AX_", "LT_", "PKG_", "AH_", "m4_", "_AC_", "_AS_", "_AM_", "_LT_", "gl_",
];
let b = s.as_bytes();
for (i, &c) in b.iter().enumerate() {
if c != b'(' || i == 0 {
continue;
}
let mut j = i;
while j > 0 && (b[j - 1].is_ascii_alphanumeric() || b[j - 1] == b'_') {
j -= 1;
}
if j == i {
continue; }
let id = &s[j..i];
if PREFIXES.iter().any(|p| id.starts_with(p) && id.len() > p.len()) {
return true;
}
}
false
}
fn protect_hash_comments(input: &str) -> String {
let mut out = String::with_capacity(input.len() + 64);
for (i, line) in input.split('\n').enumerate() {
if i > 0 {
out.push('\n');
}
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix('#') {
let indent = &line[..line.len() - trimmed.len()];
if rest.is_empty() {
out.push_str(line);
continue;
}
if line_has_macro_call(rest) {
out.push_str(indent);
out.push('#');
continue;
}
let opens = rest.matches('[').count();
let closes = rest.matches(']').count();
if opens == closes {
out.push_str(indent);
out.push('#');
out.push('[');
out.push_str(rest);
out.push(']');
continue;
}
if !is_cpp_directive(rest) {
out.push_str(indent);
out.push('#');
continue;
}
}
out.push_str(line);
}
out
}
fn is_cpp_directive(rest: &str) -> bool {
let w = rest.trim_start();
const DIRECTIVES: &[&str] = &[
"ifdef", "ifndef", "if", "elif", "else", "endif", "define", "undef", "include_next",
"include", "import", "error", "warning", "pragma", "line",
];
DIRECTIVES.iter().any(|d| {
w.strip_prefix(d)
.is_some_and(|after| after.is_empty() || after.starts_with(|c: char| !c.is_ascii_alphanumeric() && c != '_'))
})
}
fn convert_quadrigraphs(input: &str) -> String {
if !input.contains('@') {
return input.to_string();
}
input
.replace("@<:@", "[")
.replace("@:>@", "]")
.replace("@%:@", "#")
.replace("@{:@", "(")
.replace("@:}@", ")")
.replace("@&t@", "")
}
fn expand_lang_constants(input: &str) -> String {
input
.replace("_AC_LANG_ABBREV", "c")
.replace("_AC_LANG_PREFIX", "C")
.replace("_AC_LANG", "C")
}
fn strip_toplevel_hash_comments(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut depth: i32 = 0;
for line in input.split_inclusive('\n') {
let had_nl = line.ends_with('\n');
let content = line.strip_suffix('\n').unwrap_or(line);
let bytes = content.as_bytes();
let mut d = depth;
let mut cut = content.len();
let mut prev = 0u8;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'[' => d += 1,
b']' => {
if d > 0 {
d -= 1;
}
}
b'#' if d == 0 && prev != b'$' => {
cut = i;
break;
}
_ => {}
}
prev = b;
}
let code = &content[..cut];
for b in code.bytes() {
match b {
b'[' => depth += 1,
b']' => depth -= 1,
_ => {}
}
}
if cut < content.len() {
if code.trim().is_empty() {
continue;
}
out.push_str(code.trim_end());
} else {
out.push_str(content);
}
if had_nl {
out.push('\n');
}
}
out
}
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", ];
const M4_BUILTINS: &[&str] = &[
"pushdef", "popdef", "translit", "ifelse", "ifdef", "undefine", "defn",
"changequote", "changecom", "m4_pattern_allow", "m4_pattern_forbid",
];
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];
if M4_BUILTINS.contains(&id) {
return true;
}
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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_quadrigraphs() {
assert_eq!(convert_quadrigraphs("@<:@0-9@:>@+"), "[0-9]+");
assert_eq!(convert_quadrigraphs("a@%:@b@{:@c@:}@d@&t@e"), "a#b(c)de");
assert_eq!(convert_quadrigraphs("no quads here"), "no quads here");
}
#[test]
fn test_protect_hash_comments_neutralizes_commented_macro() {
assert!(line_has_macro_call(" AS_IF([cond],[act])"));
assert!(!line_has_macro_call("error \"not C11\""));
let got = protect_hash_comments("# AS_IF([cond &&\n# (test x)],\n# [act])\n# plain note\n");
assert!(!got.contains("AS_IF(["), "commented AS_IF must be neutralized: {got:?}");
assert!(got.contains("plain note"), "plain comment kept: {got:?}");
}
#[test]
fn test_strip_toplevel_hash_comments_removes_docblocks() {
let input = "# _AM_PROG_CC_C_O\n# like AC_PROG_CC_C_O, but changed.\nAC_DEFUN([_AM_PROG_CC_C_O], [body])\n";
let out = strip_toplevel_hash_comments(input);
assert!(!out.contains("like AC_PROG_CC_C_O"), "top-level doc comment must be stripped");
assert!(out.contains("AC_DEFUN([_AM_PROG_CC_C_O], [body])"), "the definition must survive");
}
#[test]
fn test_strip_preserves_hash_inside_macro_body() {
let input = "AC_DEFUN([X], [cat > c <<EOF\n#include <stdio.h>\n#define FOO 1\nEOF\n])\n";
let out = strip_toplevel_hash_comments(input);
assert!(out.contains("#include <stdio.h>"), "heredoc #include inside a body must survive");
assert!(out.contains("#define FOO 1"), "heredoc #define inside a body must survive");
}
}