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},
};
struct DetectedTools {
llvm_config: PathBuf,
llvm_version: String,
clang: PathBuf,
clangxx: PathBuf,
llvm_ar: PathBuf,
llvm_link: PathBuf,
llvm_objcopy: Option<PathBuf>,
}
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> {
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());
let llvm_version = execute_llvm_config(&llvm_config, &["--version"])?;
eprintln!(" LLVM version: {}", llvm_version);
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");
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),
}
}
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(", ")
)));
}
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(());
}
let output_path = args
.output
.as_deref()
.map_or_else(config_filepath, expand_tilde);
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());
}
#[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());
}
}