Skip to main content

autoconf_rs_cli/
main_autoheader.rs

1//! autoheader binary — generate config.h.in from configure.ac.
2//!
3//! Panel mandate: consume trace events (not prescan) for AC_DEFINE detection.
4//! Trace events are the data bus between autoconf, autoheader, and automake.
5//!
6//! Receipt family: AC.CLI.AUTOHEADER.*
7//! Status: Phase 4 — trace-driven, autom4te --trace integrated.
8
9use crate::read_input;
10use autoconf_rs_core::trace::AutoconfEvent;
11use autoconf_rs_core::M4Engine;
12use std::env;
13use std::process::ExitCode;
14
15pub fn run_autoheader() -> ExitCode {
16    let args: Vec<String> = env::args().collect();
17    let input_path = args.get(1).map(|s| s.as_str()).unwrap_or("configure.ac");
18
19    // Handle --help and --version
20    if input_path == "--help" || input_path == "-h" {
21        println!("autoheader-rs {}", env!("CARGO_PKG_VERSION"));
22        println!("Generate config.h.in from configure.ac");
23        println!("Usage: autoheader [configure.ac]");
24        println!("  -h, --help    Show this help");
25        println!("  --version     Show version");
26        return ExitCode::SUCCESS;
27    }
28    if input_path == "--version" {
29        println!("autoheader-rs {}", env!("CARGO_PKG_VERSION"));
30        return ExitCode::SUCCESS;
31    }
32
33    let input = match read_input(input_path) {
34        Ok(s) => s,
35        Err(e) => {
36            eprintln!("autoheader: {}", e);
37            return ExitCode::from(2);
38        }
39    };
40
41    let mut engine = M4Engine::new();
42    match engine.process(&input) {
43        Ok(_output) => {
44            // Panel architecture: consume trace events (source of truth)
45            let trace_log = &engine.trace_log;
46
47            // Extract AC_CONFIG_HEADERS from trace events
48            let headers: Vec<&str> = trace_log
49                .events
50                .iter()
51                .filter_map(|e| match e {
52                    AutoconfEvent::ConfigHeader { output, .. } => Some(output.as_str()),
53                    _ => None,
54                })
55                .collect();
56
57            if headers.is_empty() {
58                eprintln!("autoheader: no AC_CONFIG_HEADERS in configure.ac");
59                eprintln!(
60                    "  (checked {} trace events, 0 ConfigHeader found)",
61                    trace_log.events.len()
62                );
63                return ExitCode::from(1);
64            }
65
66            // Extract AC_DEFINE from trace events
67            let defines: Vec<(&str, Option<&str>)> = trace_log
68                .events
69                .iter()
70                .filter_map(|e| match e {
71                    AutoconfEvent::Define { name, value, .. } => {
72                        Some((name.as_str(), value.as_deref()))
73                    }
74                    _ => None,
75                })
76                .collect();
77
78            let header_file = headers.first().unwrap_or(&"config.h");
79            // Build the template into a string and WRITE it to `<header>.in` (like GNU autoheader).
80            // Previously every line went to stdout via println!, and autoreconf runs autoheader with
81            // inherited stdout (discarded) -> `config.h.in` was never created -> `make` died with
82            // `config.h: No such file or directory` (any AC_CONFIG_HEADER/HEADERS project without a
83            // committed config.h.in, e.g. redir/rmark).
84            let mut out = String::new();
85            macro_rules! oline {
86                () => {{ out.push('\n'); }};
87                ($($a:tt)*) => {{ out.push_str(&format!($($a)*)); out.push('\n'); }};
88            }
89
90            oline!(
91                "/* {} — Generated by autoconf-rs autoheader (trace-driven). */",
92                header_file
93            );
94            oline!(
95                "/* Source: {} — {} trace events, {} AC_DEFINE, {} AC_CONFIG_HEADERS */",
96                input_path,
97                trace_log.events.len(),
98                defines.len(),
99                headers.len()
100            );
101            oline!();
102
103            if defines.is_empty() {
104                oline!("/* No AC_DEFINE calls found in trace events. */");
105                oline!("/* Try: autom4te --trace=AC_DEFINE {} */", input_path);
106            } else {
107                for (var, _value) in &defines {
108                    oline!("#undef {}", var);
109                }
110            }
111
112            // Emit `#undef HAVE_X` templates for AC_CHECK_HEADERS/FUNCS/LIB. configure's real probes
113            // append `#define HAVE_X 1` to confdefs.h on success, and config.status converts these
114            // `#undef` lines accordingly. Without the templates here there is nothing to convert, so
115            // detected features never reach config.h (-> `optind undeclared`, missing struct members).
116            let mut have_seen = std::collections::BTreeSet::new();
117            for e in &trace_log.events {
118                let macro_name = match e {
119                    AutoconfEvent::CheckHeader { header, .. } => Some(have_macro(header)),
120                    AutoconfEvent::CheckFunc { function, .. } => Some(have_macro(function)),
121                    AutoconfEvent::CheckLib { library, .. } => {
122                        Some(format!("HAVE_LIB{}", library.to_ascii_uppercase()))
123                    }
124                    _ => None,
125                };
126                if let Some(m) = macro_name {
127                    if have_seen.insert(m.clone()) {
128                        oline!("#undef {}", m);
129                    }
130                }
131            }
132
133            // Standard AC_INIT defines. We emit them already as `#define` with the values parsed
134            // from AC_INIT (rather than `#undef` + config.status substitution): config.status's
135            // header sed only converts the AC_DEFINE `#undef`s, and emitting these resolved here
136            // makes config.h correct regardless of which header-generation path runs. Packages
137            // routinely `#include config.h` and use PACKAGE_NAME/VERSION.
138            let (pname, pver) = parse_ac_init(&input);
139            // The PACKAGE_* forms come from AC_INIT and are always safe.
140            oline!("#define PACKAGE_NAME \"{}\"", pname);
141            oline!("#define PACKAGE_TARNAME \"{}\"", pname);
142            oline!("#define PACKAGE_VERSION \"{}\"", pver);
143            oline!("#define PACKAGE_STRING \"{} {}\"", pname, pver);
144            oline!("#define PACKAGE_BUGREPORT \"\"");
145            oline!("#define PACKAGE_URL \"\"");
146            // The bare PACKAGE / VERSION macros are defined ONLY by AM_INIT_AUTOMAKE, and ONLY when
147            // its `no-define` option is absent. Emitting them unconditionally breaks projects that
148            // use `VERSION`/`PACKAGE` as their own identifiers under `no-define` (e.g. pgpdump's
149            // `private int VERSION;`). Honor that here.
150            let amopts = init_automake_options(&input);
151            let emit_bare = amopts.is_some() && !amopts.as_deref().unwrap_or("").contains("no-define");
152            if emit_bare {
153                oline!("#define PACKAGE \"{}\"", pname);
154                oline!("#define VERSION \"{}\"", pver);
155            }
156
157            // NB: do NOT emit the `@%:@undef` "template" lines here. `@%:@` is the m4 quadrigraph
158            // for `#`, but config.status's `#undef X -> #define X` substitution does not process it,
159            // so those lines reach config.h literally as `@%:@undef ...` -> "stray '@' in program".
160            // The plain `#undef X` lines above are the correct, substitutable template entries.
161
162            // Write to `<header>.in` next to configure.ac. A bare `config.h` name -> `config.h.in`.
163            let out_path = {
164                let dir = std::path::Path::new(input_path)
165                    .parent()
166                    .filter(|p| !p.as_os_str().is_empty())
167                    .map(|p| p.to_path_buf())
168                    .unwrap_or_else(|| std::path::PathBuf::from("."));
169                dir.join(format!("{}.in", header_file))
170            };
171            if let Err(e) = std::fs::write(&out_path, &out) {
172                eprintln!("autoheader: cannot write {}: {}", out_path.display(), e);
173                return ExitCode::from(2);
174            }
175            eprintln!(
176                "autoheader: generated {} with {} #undef entries (trace-driven)",
177                out_path.display(),
178                defines.len()
179            );
180            ExitCode::SUCCESS
181        }
182        Err(e) => {
183            eprintln!("autoheader: {}", e);
184            ExitCode::from(2)
185        }
186    }
187}
188
189/// Parse AC_INIT([name],[version],...) from configure.ac text. Returns (name, version),
190/// stripping m4 `[]` quotes and whitespace; empty strings if not found.
191fn parse_ac_init(input: &str) -> (String, String) {
192    if let Some(pos) = input.find("AC_INIT") {
193        let after = &input[pos + "AC_INIT".len()..];
194        if let Some(open) = after.find('(') {
195            // collect to matching close paren
196            let mut depth = 0i32;
197            let mut end = None;
198            for (i, c) in after[open..].char_indices() {
199                match c {
200                    '(' => depth += 1,
201                    ')' => { depth -= 1; if depth == 0 { end = Some(open + i); break; } }
202                    _ => {}
203                }
204            }
205            if let Some(e) = end {
206                let args_str = &after[open + 1..e];
207                let strip = |s: &str| s.trim().trim_start_matches('[').trim_end_matches(']').trim().to_string();
208                let parts: Vec<&str> = args_str.splitn(3, ',').collect();
209                let name = sanitize_token(&parts.first().map(|s| strip(s)).unwrap_or_default(), "config");
210                let version = sanitize_token(&parts.get(1).map(|s| strip(s)).unwrap_or_default(), "0");
211                return (name, version);
212            }
213        }
214    }
215    (String::new(), String::new())
216}
217
218/// Sanitize an AC_INIT token (name/version) so it's safe to embed in config.h as a C string.
219/// AC_INIT args can be unevaluated m4 (e.g. version = `m4_esyscmd_s([git describe...])` with `dnl`
220/// comments) which autoconf-rs cannot run (esyscmd blocked). Strip `dnl` comments and reject any
221/// value that still looks like m4/multi-word garbage, falling back to a safe default.
222fn sanitize_token(v: &str, fallback: &str) -> String {
223    // drop `dnl ...` to end of line, join lines
224    let no_dnl: String = v
225        .lines()
226        .map(|l| l.split("dnl").next().unwrap_or(""))
227        .collect::<Vec<_>>()
228        .join(" ");
229    let t = no_dnl.trim().trim_matches('"').trim();
230    if t.is_empty()
231        || t.contains("m4_")
232        || t.contains("esyscmd")
233        || t.contains('[')
234        || t.contains(']')
235        || t.contains('(')
236        || t.chars().any(|c| c.is_whitespace())
237    {
238        return fallback.to_string();
239    }
240    t.to_string()
241}
242
243/// Return the AM_INIT_AUTOMAKE option string (the macro's args), or None if the project doesn't
244/// use AM_INIT_AUTOMAKE. Used to honor `no-define` (whether bare PACKAGE/VERSION are defined).
245fn init_automake_options(input: &str) -> Option<String> {
246    let pos = input.find("AM_INIT_AUTOMAKE")?;
247    let after = &input[pos + "AM_INIT_AUTOMAKE".len()..];
248    let open = after.find('(')?;
249    let mut depth = 0i32;
250    for (i, c) in after[open..].char_indices() {
251        match c {
252            '(' => depth += 1,
253            ')' => {
254                depth -= 1;
255                if depth == 0 {
256                    return Some(after[open + 1..open + i].to_string());
257                }
258            }
259            _ => {}
260        }
261    }
262    Some(String::new())
263}
264
265/// The C preprocessor macro a successful AC_CHECK_HEADER/FUNC defines (mirrors
266/// autoconf_rs_core::configure_body::have_macro): `sys/time.h` -> `HAVE_SYS_TIME_H`.
267fn have_macro(name: &str) -> String {
268    let up: String = name
269        .trim()
270        .chars()
271        .map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_uppercase() } else { '_' })
272        .collect();
273    format!("HAVE_{}", up)
274}