autoconf-rs-cli 0.1.13

CLI harness for autoconf-rs tools: autoconf, autoheader, autom4te, autoreconf, aclocal, autoscan, autoupdate, ifnames
Documentation
//! autoscan binary — scan C sources for Autoconf macro suggestions.
//!
//! Scans .c and .h files in the current directory tree for:
//! - Function calls → AC_CHECK_FUNCS
//! - #include directives → AC_CHECK_HEADERS
//! - Library functions → AC_CHECK_LIB
//!
//! Outputs a configure.scan template file.
//!
//! Receipt family: AC.CLI.AUTOSCAN.*
//! Court: AC.AUTOSCAN.1 — functional scanner
//! Status: Phase 5 — scanning C sources for macro suggestions.

use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::Path;
use std::process::ExitCode;

/// Well-known functions that suggest library dependencies.
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();

    // Walk directory tree for .c and .h files
    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);
        // Still output a minimal configure.scan
    }

    // Output configure.scan
    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() {
                // Skip hidden dirs and common non-source dirs
                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();

        // Scan #include directives
        if trimmed.starts_with("#include") {
            if let Some(hdr) = extract_header(trimmed) {
                // Only track standard/system headers
                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());
                    }
                }
            }
        }

        // Scan for function calls: identifier followed by (
        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());
                // Check if this function suggests a library
                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 {
    // Standard C/POSIX headers that are worth checking for
    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 {
    // Skip C keywords and common non-check-worthy identifiers
    !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"
    )
}