use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::Path;
use std::process::ExitCode;
const LIB_FUNCTIONS: &[(&str, &str)] = &[
("sin", "m"),
("cos", "m"),
("sqrt", "m"),
("pow", "m"),
("pthread_create", "pthread"),
("dlopen", "dl"),
("crypt", "crypt"),
("zlibVersion", "z"),
("socket", "socket"),
("gethostbyname", "nsl"),
("XOpenDisplay", "X11"),
("curses", "curses"),
("tgetent", "termcap"),
("readline", "readline"),
];
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
let start_dir = args.get(1).map(|s| s.as_str()).unwrap_or(".");
if start_dir == "--help" || start_dir == "-h" {
println!("autoscan-rs {}", env!("CARGO_PKG_VERSION"));
println!("Scan C sources for Autoconf macro suggestions");
println!("Usage: autoscan [directory]");
println!(" -h, --help Show this help");
println!(" --version Show version");
return ExitCode::SUCCESS;
}
if start_dir == "--version" {
println!("autoscan-rs {}", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS;
}
let start_path = Path::new(start_dir);
let mut funcs = HashSet::new();
let mut headers = HashSet::new();
let mut libs: Vec<(&str, &str)> = Vec::new();
scan_dir(start_path, &mut funcs, &mut headers, &mut libs);
if funcs.is_empty() && headers.is_empty() && libs.is_empty() {
eprintln!("autoscan: no C source files found in {}", start_dir);
}
let output_path = format!("{}/configure.scan", start_dir);
let mut output = String::new();
output.push_str("dnl configure.scan — Generated by autoconf-rs autoscan.\n");
output.push_str("dnl Process this file with autoconf to produce a configure script.\n\n");
output.push_str("AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])\n");
output.push_str("AC_CONFIG_SRCDIR([configure.ac])\n");
output.push_str("AC_CONFIG_HEADERS([config.h])\n\n");
if !funcs.is_empty() {
let mut sorted: Vec<&str> = funcs.iter().map(|s| s.as_str()).collect();
sorted.sort();
output.push_str("# Checks for functions.\n");
output.push_str(&format!("AC_CHECK_FUNCS([{}])\n\n", sorted.join(" ")));
}
if !headers.is_empty() {
let mut sorted: Vec<&str> = headers.iter().map(|s| s.as_str()).collect();
sorted.sort();
output.push_str("# Checks for header files.\n");
for hdr in &sorted {
output.push_str(&format!("AC_CHECK_HEADERS([{}])\n", hdr));
}
output.push('\n');
}
if !libs.is_empty() {
output.push_str("# Checks for libraries.\n");
for (func, lib) in &libs {
output.push_str(&format!("AC_CHECK_LIB([{}], [{}])\n", lib, func));
}
output.push('\n');
}
output.push_str("AC_CONFIG_FILES([Makefile])\n");
output.push_str("AC_OUTPUT\n");
match fs::write(&output_path, &output) {
Ok(_) => {
eprintln!(
"autoscan: wrote configure.scan ({} functions, {} headers, {} libs)",
funcs.len(),
headers.len(),
libs.len()
);
print!("{}", output);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("autoscan: cannot write {}: {}", output_path, e);
print!("{}", output);
ExitCode::from(2)
}
}
}
fn scan_dir(
dir: &Path,
funcs: &mut HashSet<String>,
headers: &mut HashSet<String>,
libs: &mut Vec<(&str, &str)>,
) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name.starts_with('.')
|| name == "target"
|| name == "build"
|| name == "autom4te.cache"
{
continue;
}
scan_dir(&path, funcs, headers, libs);
} else if let Some(ext) = path.extension() {
if ext == "c" || ext == "h" || ext == "cpp" || ext == "cxx" || ext == "hpp" {
if let Ok(content) = fs::read_to_string(&path) {
scan_content(&content, funcs, headers, libs);
}
}
}
}
}
}
fn scan_content(
content: &str,
funcs: &mut HashSet<String>,
headers: &mut HashSet<String>,
libs: &mut Vec<(&str, &str)>,
) {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("#include") {
if let Some(hdr) = extract_header(trimmed) {
if !hdr.starts_with('"') || hdr.contains(".h") {
let clean = hdr.trim_matches('"').trim_matches('<').trim_matches('>');
if is_interesting_header(clean) {
headers.insert(clean.to_string());
}
}
}
}
for word in trimmed.split(|c: char| !c.is_alphanumeric() && c != '_') {
let word = word.trim();
if word.len() > 3
&& !word.is_empty()
&& word.chars().next().map_or(false, |c| c.is_alphabetic())
&& is_interesting_function(word)
{
funcs.insert(word.to_string());
for (lib_func, lib_name) in LIB_FUNCTIONS {
if *lib_func == word && !libs.iter().any(|(f, _)| *f == word) {
libs.push((lib_func, lib_name));
}
}
}
}
}
}
fn extract_header(line: &str) -> Option<&str> {
let line = line.trim();
if line.starts_with("#include") {
let rest = &line["#include".len()..].trim();
if rest.starts_with('<') {
rest.find('>').map(|e| &rest[..=e])
} else if rest.starts_with('"') {
rest[1..].find('"').map(|e| &rest[1..=e + 1])
} else {
None
}
} else {
None
}
}
fn is_interesting_header(hdr: &str) -> bool {
matches!(
hdr,
"stdlib.h"
| "stdio.h"
| "string.h"
| "unistd.h"
| "fcntl.h"
| "sys/types.h"
| "sys/stat.h"
| "sys/socket.h"
| "sys/time.h"
| "sys/wait.h"
| "netdb.h"
| "netinet/in.h"
| "arpa/inet.h"
| "signal.h"
| "errno.h"
| "math.h"
| "time.h"
| "dirent.h"
| "pthread.h"
| "dlfcn.h"
| "curses.h"
| "termios.h"
| "limits.h"
| "stdint.h"
| "stddef.h"
| "ctype.h"
| "locale.h"
| "setjmp.h"
)
}
fn is_interesting_function(func: &str) -> bool {
!matches!(
func,
"int"
| "char"
| "void"
| "long"
| "short"
| "float"
| "double"
| "struct"
| "enum"
| "union"
| "const"
| "static"
| "extern"
| "sizeof"
| "return"
| "ifdef"
| "ifndef"
| "endif"
| "define"
| "include"
| "main"
| "printf"
| "fprintf"
| "sprintf"
| "scanf"
| "exit"
| "free"
| "NULL"
| "true"
| "false"
| "FILE"
)
}