rllvm 0.4.4

A tool to build whole-program LLVM bitcode files
Documentation
use std::{
    env, fs,
    path::{Path, PathBuf},
};

use clap::Parser;
use rllvm::{
    cli::InitArgs,
    config::config_filepath,
    error::Error,
    utils::{execute_llvm_config, find_llvm_config},
};

/// Detected LLVM tool paths
struct DetectedTools {
    llvm_config: PathBuf,
    llvm_version: String,
    clang: PathBuf,
    clangxx: PathBuf,
    llvm_ar: PathBuf,
    llvm_link: PathBuf,
    /// Optional: used to embed bitcode paths when present
    llvm_objcopy: Option<PathBuf>,
}

/// Find `llvm-config` under `prefix`, and report the directory holding it.
///
/// The path is not canonicalized. A prefix given on the command line is often
/// a stable symlink to a versioned directory -- Homebrew's
/// `/opt/homebrew/opt/llvm` points at `../Cellar/llvm/<version>` -- and
/// resolving it writes the version into the config, so the next upgrade of the
/// toolchain leaves every recorded path dangling. Honouring the prefix as given
/// is what keeps the configuration valid across one.
fn find_llvm_config_with_prefix(prefix: &Path) -> Result<(PathBuf, PathBuf), Error> {
    for bindir in [prefix.join("bin"), prefix.to_path_buf()] {
        let candidate = bindir.join("llvm-config");
        if candidate.exists() {
            return Ok((candidate, bindir));
        }
    }
    Err(Error::MissingFile(format!(
        "llvm-config not found under {:?}",
        prefix
    )))
}

fn detect_tools(llvm_prefix: Option<&Path>) -> Result<DetectedTools, Error> {
    // Step 1: Find llvm-config
    let (llvm_config, prefix_bindir) = if let Some(prefix) = llvm_prefix {
        eprintln!(
            "Searching for LLVM in user-specified prefix: {}",
            prefix.display()
        );
        let (llvm_config, bindir) = find_llvm_config_with_prefix(prefix)?;
        (llvm_config, Some(bindir))
    } else {
        eprintln!("Auto-detecting LLVM installation...");
        (find_llvm_config()?, None)
    };
    eprintln!("  Found llvm-config: {}", llvm_config.display());

    // Step 2: Get LLVM version
    let llvm_version = execute_llvm_config(&llvm_config, &["--version"])?;
    eprintln!("  LLVM version: {}", llvm_version);

    // Step 3: Get bin directory and derive tool paths
    //
    // `llvm-config --bindir` resolves its own real location, so it answers with
    // the versioned directory even when invoked through a symlink. Where the
    // caller named a prefix, that prefix decides instead; otherwise there is
    // nothing better to go on than what the tool reports.
    let bindir = match prefix_bindir {
        Some(bindir) => bindir,
        None => PathBuf::from(execute_llvm_config(&llvm_config, &["--bindir"])?),
    };
    eprintln!("  LLVM bindir: {}", bindir.display());

    let clang = bindir.join("clang");
    let clangxx = bindir.join("clang++");
    let llvm_ar = bindir.join("llvm-ar");
    let llvm_link = bindir.join("llvm-link");
    let llvm_objcopy = bindir.join("llvm-objcopy");

    // Step 4: Check version consistency by querying clang --version
    if clang.exists() {
        match std::process::Command::new(&clang).arg("--version").output() {
            Ok(output) => {
                let clang_version_output = String::from_utf8_lossy(&output.stdout);
                if let Some(first_line) = clang_version_output.lines().next() {
                    eprintln!("  clang: {}", first_line);
                }
            }
            Err(e) => eprintln!("  Warning: could not query clang version: {}", e),
        }
    }

    // Step 5: Report which tools are found / missing
    let tools: &[(&str, &Path)] = &[
        ("clang", &clang),
        ("clang++", &clangxx),
        ("llvm-ar", &llvm_ar),
        ("llvm-link", &llvm_link),
    ];

    let mut missing = Vec::new();
    for (name, path) in tools {
        if path.exists() {
            eprintln!("  {}: OK", name);
        } else {
            eprintln!("  {}: MISSING ({})", name, path.display());
            missing.push(*name);
        }
    }

    if !missing.is_empty() {
        return Err(Error::MissingFile(format!(
            "Missing LLVM tools: {}",
            missing.join(", ")
        )));
    }

    // `llvm-objcopy` is optional: it is the preferred way to embed the bitcode
    // path, but there is an internal fallback, so its absence is not a failure.
    let llvm_objcopy = if llvm_objcopy.exists() {
        eprintln!("  llvm-objcopy: OK");
        Some(llvm_objcopy)
    } else {
        eprintln!("  llvm-objcopy: not found (optional)");
        None
    };

    Ok(DetectedTools {
        llvm_config,
        llvm_version,
        clang,
        clangxx,
        llvm_ar,
        llvm_link,
        llvm_objcopy,
    })
}

fn generate_toml(tools: &DetectedTools) -> String {
    let mut toml_content = format!(
        r#"llvm_config_filepath = "{}"
clang_filepath = "{}"
clangxx_filepath = "{}"
llvm_ar_filepath = "{}"
llvm_link_filepath = "{}"
"#,
        tools.llvm_config.display(),
        tools.clang.display(),
        tools.clangxx.display(),
        tools.llvm_ar.display(),
        tools.llvm_link.display(),
    );

    if let Some(llvm_objcopy) = &tools.llvm_objcopy {
        toml_content.push_str(&format!(
            "llvm_objcopy_filepath = \"{}\"\n",
            llvm_objcopy.display()
        ));
    }

    toml_content
}

fn expand_tilde(path: &str) -> PathBuf {
    if let Some(rest) = path.strip_prefix("~/")
        && let Ok(home) = env::var("HOME")
    {
        return PathBuf::from(home).join(rest);
    }
    PathBuf::from(path)
}

fn main() -> Result<(), Error> {
    let args = InitArgs::parse();

    let tools = detect_tools(args.llvm_prefix.as_deref())?;
    let toml_content = generate_toml(&tools);

    eprintln!();
    eprintln!("=== Configuration Summary ===");
    eprintln!("LLVM version : {}", tools.llvm_version);
    eprintln!("llvm-config  : {}", tools.llvm_config.display());
    eprintln!("clang        : {}", tools.clang.display());
    eprintln!("clang++      : {}", tools.clangxx.display());
    eprintln!("llvm-ar      : {}", tools.llvm_ar.display());
    eprintln!("llvm-link    : {}", tools.llvm_link.display());
    match &tools.llvm_objcopy {
        Some(llvm_objcopy) => eprintln!("llvm-objcopy : {}", llvm_objcopy.display()),
        None => eprintln!("llvm-objcopy : (not found, optional)"),
    }

    if args.dry_run {
        eprintln!();
        eprintln!("=== Generated config.toml (dry run) ===");
        print!("{}", toml_content);
        return Ok(());
    }

    // Resolved through the same helper the loader uses, so init cannot write a
    // configuration that the wrappers will not read. An explicit -o still wins.
    let output_path = args
        .output
        .as_deref()
        .map_or_else(config_filepath, expand_tilde);

    // Create parent directory if needed
    if let Some(parent) = output_path.parent()
        && !parent.exists()
    {
        fs::create_dir_all(parent).map_err(|err| {
            Error::ConfigError(format!(
                "Failed to create config directory {:?}: {}",
                parent, err
            ))
        })?;
    }

    fs::write(&output_path, &toml_content).map_err(|err| {
        Error::ConfigError(format!(
            "Failed to write config to {:?}: {}",
            output_path, err
        ))
    })?;

    eprintln!();
    eprintln!("Config written to: {}", output_path.display());
    eprintln!();
    eprintln!("You can now use rllvm-cc, rllvm-cxx, and rllvm-get-bc.");
    eprintln!("To customize, edit: {}", output_path.display());

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::find_llvm_config_with_prefix;
    use std::fs;

    fn stub_llvm_config(dir: &std::path::Path) {
        fs::create_dir_all(dir).unwrap();
        fs::write(dir.join("llvm-config"), "").unwrap();
    }

    #[test]
    fn finds_llvm_config_under_a_prefix_and_reports_its_directory() {
        let tmp = tempfile::tempdir().unwrap();
        let bindir = tmp.path().join("bin");
        stub_llvm_config(&bindir);

        let (llvm_config, found_bindir) = find_llvm_config_with_prefix(tmp.path()).unwrap();
        assert_eq!(llvm_config, bindir.join("llvm-config"));
        assert_eq!(found_bindir, bindir);
    }

    #[test]
    fn accepts_a_prefix_that_is_itself_the_bin_directory() {
        let tmp = tempfile::tempdir().unwrap();
        stub_llvm_config(tmp.path());

        let (llvm_config, bindir) = find_llvm_config_with_prefix(tmp.path()).unwrap();
        assert_eq!(llvm_config, tmp.path().join("llvm-config"));
        assert_eq!(bindir, tmp.path());
    }

    /// The reason the lookup does not canonicalize.
    ///
    /// A package manager points a stable prefix at a versioned directory --
    /// Homebrew's `opt/llvm` at `Cellar/llvm/<version>` -- so resolving the
    /// symlink writes the version into the config and the next upgrade of the
    /// toolchain leaves every recorded path dangling.
    #[test]
    #[cfg(unix)]
    fn keeps_a_symlinked_prefix_instead_of_resolving_it() {
        let tmp = tempfile::tempdir().unwrap();
        let versioned = tmp.path().join("Cellar").join("llvm").join("1.2.3");
        stub_llvm_config(&versioned.join("bin"));

        let stable = tmp.path().join("opt-llvm");
        std::os::unix::fs::symlink(&versioned, &stable).unwrap();

        let (llvm_config, bindir) = find_llvm_config_with_prefix(&stable).unwrap();
        assert_eq!(llvm_config, stable.join("bin").join("llvm-config"));
        assert_eq!(bindir, stable.join("bin"));
        assert!(
            !llvm_config.to_string_lossy().contains("1.2.3"),
            "the version must not reach the recorded path: {llvm_config:?}"
        );
    }

    #[test]
    fn reports_a_prefix_that_holds_no_llvm_config() {
        let tmp = tempfile::tempdir().unwrap();
        assert!(find_llvm_config_with_prefix(tmp.path()).is_err());
    }
}