use std::{
env, fs,
path::{Path, PathBuf},
sync::OnceLock,
};
use serde::{Deserialize, Serialize};
use tracing::Level;
use crate::{
constants::{
BITCODE_ROOT_ENV_NAME, DEFAULT_CONF_FILEPATH_UNDER_HOME,
DEFAULT_RLLVM_CONF_FILEPATH_ENV_NAME, HOME_ENV_NAME, LOG_LEVEL_ENV_NAME, LTO_MODE_ENV_NAME,
RUSTC_ENV_NAME,
},
diagnostics::{check_version_compatibility, print_missing_tool_error},
error::Error,
lto::LtoMode,
utils::{execute_llvm_config, find_llvm_config},
};
type ConfigResult = Result<RLLVMConfig, String>;
fn config_result_to_ref(result: &'static ConfigResult) -> Result<&'static RLLVMConfig, Error> {
match result {
Ok(config) => Ok(config),
Err(message) => Err(Error::ConfigError(message.clone())),
}
}
#[cfg(not(test))]
pub fn try_rllvm_config() -> Result<&'static RLLVMConfig, Error> {
static RLLVM_CONFIG: OnceLock<ConfigResult> = OnceLock::new();
config_result_to_ref(RLLVM_CONFIG.get_or_init(|| {
RLLVMConfig::new().map_err(|err| format!("Failed to load rllvm configuration: {err}"))
}))
}
#[cfg(test)]
pub fn try_rllvm_config() -> Result<&'static RLLVMConfig, Error> {
static RLLVM_CONFIG: OnceLock<ConfigResult> = OnceLock::new();
config_result_to_ref(RLLVM_CONFIG.get_or_init(|| {
RLLVMConfig::try_default()
.map_err(|err| format!("Failed to infer rllvm configuration: {err}"))
}))
}
pub fn config_filepath() -> PathBuf {
env::var(DEFAULT_RLLVM_CONF_FILEPATH_ENV_NAME).map_or_else(
|_| {
PathBuf::from(env::var(HOME_ENV_NAME).unwrap_or("".into()))
.join(DEFAULT_CONF_FILEPATH_UNDER_HOME)
},
PathBuf::from,
)
}
#[derive(Serialize, Deserialize, Debug)]
pub struct RLLVMConfig {
llvm_config_filepath: PathBuf,
clang_filepath: PathBuf,
clangxx_filepath: PathBuf,
llvm_ar_filepath: PathBuf,
llvm_link_filepath: PathBuf,
llvm_objcopy_filepath: Option<PathBuf>,
rustc_filepath: Option<PathBuf>,
bitcode_store_path: Option<PathBuf>,
llvm_link_flags: Option<Vec<String>>,
lto_ldflags: Option<Vec<String>>,
bitcode_generation_flags: Option<Vec<String>>,
is_configure_only: Option<bool>,
log_level: Option<u8>,
cache_enabled: Option<bool>,
bitcode_root: Option<PathBuf>,
lto_mode: Option<LtoMode>,
cache_dir: Option<PathBuf>,
}
impl RLLVMConfig {
pub fn llvm_config_filepath(&self) -> &PathBuf {
&self.llvm_config_filepath
}
pub fn clang_filepath(&self) -> &PathBuf {
&self.clang_filepath
}
pub fn clangxx_filepath(&self) -> &PathBuf {
&self.clangxx_filepath
}
pub fn llvm_ar_filepath(&self) -> &PathBuf {
&self.llvm_ar_filepath
}
pub fn llvm_link_filepath(&self) -> &PathBuf {
&self.llvm_link_filepath
}
pub fn llvm_objcopy_filepath(&self) -> Option<&PathBuf> {
self.llvm_objcopy_filepath.as_ref()
}
pub fn bitcode_store_path(&self) -> Option<&PathBuf> {
self.bitcode_store_path.as_ref()
}
pub fn llvm_link_flags(&self) -> Option<&Vec<String>> {
self.llvm_link_flags.as_ref()
}
pub fn lto_ldflags(&self) -> Option<&Vec<String>> {
self.lto_ldflags.as_ref()
}
pub fn bitcode_generation_flags(&self) -> Option<&Vec<String>> {
self.bitcode_generation_flags.as_ref()
}
pub fn is_configure_only(&self) -> bool {
self.is_configure_only.unwrap_or_default()
}
pub fn log_level(&self) -> Level {
let level = env::var(LOG_LEVEL_ENV_NAME)
.ok()
.and_then(|value| value.parse::<u8>().ok())
.unwrap_or_else(|| self.log_level.unwrap_or_default());
match level {
0 => Level::ERROR,
1 => Level::WARN,
2 => Level::INFO,
3 => Level::DEBUG,
_ => Level::TRACE,
}
}
pub fn cache_enabled(&self) -> bool {
self.cache_enabled.unwrap_or_default()
}
pub fn bitcode_root(&self) -> Option<PathBuf> {
env::var(BITCODE_ROOT_ENV_NAME)
.ok()
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.or_else(|| self.bitcode_root.clone())
}
pub fn rustc_filepath(&self) -> Option<PathBuf> {
env::var(RUSTC_ENV_NAME)
.ok()
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.or_else(|| self.rustc_filepath.clone())
}
pub fn lto_mode(&self) -> Result<LtoMode, Error> {
match env::var(LTO_MODE_ENV_NAME) {
Ok(value) if !value.is_empty() => value.parse(),
_ => Ok(self.lto_mode.unwrap_or_default()),
}
}
pub fn cache_dir(&self) -> Option<&PathBuf> {
self.cache_dir.as_ref()
}
}
impl RLLVMConfig {
pub fn new() -> Result<Self, Error> {
Self::load_path(config_filepath())
}
fn load_path<P>(config_filepath: P) -> Result<Self, Error>
where
P: AsRef<Path> + std::fmt::Debug,
{
let config_filepath = config_filepath.as_ref();
let mut config = if Self::config_file_has_content(config_filepath) {
Self::parse_file(config_filepath)?
} else {
let inferred = Self::try_default()?;
inferred.write_to(config_filepath)?;
inferred
};
config.validate_tool_paths();
if let Some(bitcode_store_path) = &config.bitcode_store_path {
if !bitcode_store_path.is_absolute() {
tracing::warn!(
"Ignore the bitcode store path, as it is not absolute: {:?}",
bitcode_store_path
);
config.bitcode_store_path = None;
} else {
if !bitcode_store_path.exists() {
tracing::info!(
"Create the directory for the bitcode store: {:?}",
bitcode_store_path
);
fs::create_dir_all(bitcode_store_path).map_err(|err| {
tracing::error!(
"Failed to create the bitcode store directory: err={}",
err
);
err
})?;
} else {
if !bitcode_store_path.is_dir() {
tracing::warn!(
"Ignore the bitcode store path, as it is not a directory: {:?}",
bitcode_store_path
);
config.bitcode_store_path = None;
}
}
}
}
Ok(config)
}
}
impl RLLVMConfig {
fn config_file_has_content(config_filepath: &Path) -> bool {
fs::metadata(config_filepath).is_ok_and(|meta| meta.is_file() && meta.len() > 0)
}
fn parse_file(config_filepath: &Path) -> Result<Self, Error> {
let contents = fs::read_to_string(config_filepath).map_err(|err| {
tracing::error!(
"Failed to read configuration: config_filepath={:?}, err={}",
config_filepath,
err
);
Error::ConfigError(format!(
"Failed to read configuration from {config_filepath:?}: {err}"
))
})?;
toml::from_str(&contents).map_err(|err| {
tracing::error!(
"Failed to parse configuration: config_filepath={:?}, err={}",
config_filepath,
err
);
Error::ConfigError(format!(
"Failed to parse configuration from {config_filepath:?}: {err}"
))
})
}
fn write_to(&self, config_filepath: &Path) -> Result<(), Error> {
if let Some(parent_dir) = config_filepath.parent()
&& !parent_dir.as_os_str().is_empty()
{
fs::create_dir_all(parent_dir)?;
}
let contents = toml::to_string_pretty(self).map_err(|err| {
Error::ConfigError(format!("Failed to serialize the configuration: {err}"))
})?;
fs::write(config_filepath, contents).map_err(|err| {
tracing::error!(
"Failed to write configuration: config_filepath={:?}, err={}",
config_filepath,
err
);
err
})?;
tracing::info!("Wrote inferred configuration to {:?}", config_filepath);
Ok(())
}
}
impl RLLVMConfig {
fn validate_tool_paths(&self) {
let tools: &[(&str, &Path)] = &[
("llvm-config", &self.llvm_config_filepath),
("clang", &self.clang_filepath),
("clang++", &self.clangxx_filepath),
("llvm-ar", &self.llvm_ar_filepath),
("llvm-link", &self.llvm_link_filepath),
];
for (name, path) in tools {
if !path.exists() {
print_missing_tool_error(name, Some(path));
}
}
if let Some(llvm_objcopy_filepath) = &self.llvm_objcopy_filepath
&& !llvm_objcopy_filepath.exists()
{
tracing::debug!(
"Configured `llvm-objcopy` does not exist: {:?}",
llvm_objcopy_filepath
);
}
if self.clang_filepath.exists() && self.llvm_config_filepath.exists() {
check_version_compatibility(&self.clang_filepath, &self.llvm_config_filepath);
}
}
pub fn try_default() -> Result<Self, Error> {
tracing::info!("Infer rllvm configurations ...");
let llvm_config_filepath = find_llvm_config().inspect_err(|_| {
print_missing_tool_error("llvm-config", None);
})?;
tracing::info!("- llvm-config: {:?}", llvm_config_filepath);
match execute_llvm_config(&llvm_config_filepath, &["--version"]) {
Ok(llvm_version) => tracing::info!("- LLVM version: {}", llvm_version),
Err(err) => tracing::warn!("- LLVM version: (unknown, err={:?})", err),
}
let llvm_bindir = PathBuf::from(
execute_llvm_config(&llvm_config_filepath, &["--bindir"]).map_err(|err| {
tracing::error!("Failed to execute `llvm-config --bindir`: {:?}", err);
err
})?,
);
let clang_filepath = llvm_bindir.join("clang");
let clangxx_filepath = llvm_bindir.join("clang++");
let llvm_ar_filepath = llvm_bindir.join("llvm-ar");
let llvm_link_filepath = llvm_bindir.join("llvm-link");
let llvm_objcopy_filepath = llvm_bindir.join("llvm-objcopy");
let llvm_objcopy_filepath = if llvm_objcopy_filepath.exists() {
Some(llvm_objcopy_filepath)
} else {
tracing::debug!("- llvm-objcopy: (not found in {:?})", llvm_bindir);
None
};
let llvm_bin_tools: &[(&str, &PathBuf)] = &[
("clang", &clang_filepath),
("clang++", &clangxx_filepath),
("llvm-ar", &llvm_ar_filepath),
("llvm-link", &llvm_link_filepath),
];
for (name, filepath) in llvm_bin_tools {
if !filepath.exists() {
print_missing_tool_error(name, Some(filepath));
return Err(Error::MissingFile(format!("{filepath:?}")));
}
}
check_version_compatibility(&clang_filepath, &llvm_config_filepath);
Ok(Self {
llvm_config_filepath,
clang_filepath,
clangxx_filepath,
llvm_ar_filepath,
llvm_link_filepath,
llvm_objcopy_filepath,
rustc_filepath: None,
bitcode_store_path: None,
llvm_link_flags: None,
lto_ldflags: None,
lto_mode: None,
bitcode_generation_flags: None,
is_configure_only: None,
log_level: None,
bitcode_root: None,
cache_enabled: None,
cache_dir: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lto::LtoMode;
fn write_config(extra: &str) -> (tempfile::TempDir, PathBuf, RLLVMConfig) {
let inferred = RLLVMConfig::try_default().expect("Failed to infer the LLVM tool paths");
let contents = format!(
"llvm_config_filepath = '{}'\n\
clang_filepath = '{}'\n\
clangxx_filepath = '{}'\n\
llvm_ar_filepath = '{}'\n\
llvm_link_filepath = '{}'\n\
{}",
inferred.llvm_config_filepath().display(),
inferred.clang_filepath().display(),
inferred.clangxx_filepath().display(),
inferred.llvm_ar_filepath().display(),
inferred.llvm_link_filepath().display(),
extra,
);
let dir = tempfile::tempdir().expect("Failed to create a temporary directory");
let config_filepath = dir.path().join("config.toml");
fs::write(&config_filepath, contents).expect("Failed to write the test config file");
(dir, config_filepath, inferred)
}
#[test]
fn bitcode_store_path_relative_is_ignored() {
let (_dir, config_filepath, _) = write_config("bitcode_store_path = 'relative/dir'\n");
let config = RLLVMConfig::load_path(&config_filepath).expect("load failed");
assert!(
config.bitcode_store_path().is_none(),
"a relative bitcode store path must be ignored"
);
}
#[test]
fn bitcode_store_path_absolute_is_created_when_missing() {
let dir = tempfile::tempdir().unwrap();
let store = dir.path().join("store").join("nested");
assert!(!store.exists());
let (_cfg_dir, config_filepath, _) =
write_config(&format!("bitcode_store_path = '{}'\n", store.display()));
let config = RLLVMConfig::load_path(&config_filepath).expect("load failed");
assert_eq!(config.bitcode_store_path(), Some(&store));
assert!(store.is_dir(), "the store directory was not created");
}
#[test]
fn bitcode_store_path_pointing_at_a_file_is_ignored() {
let dir = tempfile::tempdir().unwrap();
let not_a_dir = dir.path().join("a_file");
fs::write(¬_a_dir, b"x").unwrap();
let (_cfg_dir, config_filepath, _) =
write_config(&format!("bitcode_store_path = '{}'\n", not_a_dir.display()));
let config = RLLVMConfig::load_path(&config_filepath).expect("load failed");
assert!(
config.bitcode_store_path().is_none(),
"a store path that is not a directory must be ignored"
);
}
#[test]
fn bitcode_store_path_existing_directory_is_kept() {
let dir = tempfile::tempdir().unwrap();
let store = dir.path().join("store");
fs::create_dir_all(&store).unwrap();
let (_cfg_dir, config_filepath, _) =
write_config(&format!("bitcode_store_path = '{}'\n", store.display()));
let config = RLLVMConfig::load_path(&config_filepath).expect("load failed");
assert_eq!(config.bitcode_store_path(), Some(&store));
}
#[test]
fn missing_config_file_is_written_from_inferred_values() {
let dir = tempfile::tempdir().unwrap();
let config_filepath = dir.path().join("nested").join("config.toml");
assert!(!config_filepath.exists());
let config = RLLVMConfig::load_path(&config_filepath).expect("load failed");
assert!(
config_filepath.exists(),
"first run must write the inferred config"
);
assert!(config.clang_filepath().exists());
}
#[test]
fn optional_flag_accessors_round_trip() {
let (_dir, config_filepath, _) = write_config(
"llvm_link_flags = ['-v']\n\
lto_ldflags = ['-flto']\n\
bitcode_generation_flags = ['-g']\n\
is_configure_only = true\n\
cache_enabled = true\n\
log_level = 3\n",
);
let config = RLLVMConfig::load_path(&config_filepath).expect("load failed");
assert_eq!(config.llvm_link_flags(), Some(&vec!["-v".to_string()]));
assert_eq!(config.lto_ldflags(), Some(&vec!["-flto".to_string()]));
assert_eq!(
config.bitcode_generation_flags(),
Some(&vec!["-g".to_string()])
);
assert!(config.is_configure_only());
assert!(config.cache_enabled());
assert_eq!(config.log_level(), Level::DEBUG);
}
#[test]
fn log_level_mapping_covers_every_value() {
for (value, expected) in [
(0u8, Level::ERROR),
(1, Level::WARN),
(2, Level::INFO),
(3, Level::DEBUG),
(4, Level::TRACE),
(9, Level::TRACE),
] {
let (_dir, config_filepath, _) = write_config(&format!("log_level = {value}\n"));
let config = RLLVMConfig::load_path(&config_filepath).expect("load failed");
assert_eq!(config.log_level(), expected, "log_level = {value}");
}
}
#[test]
fn load_config_without_llvm_objcopy_filepath() {
let (_dir, config_filepath, inferred) = write_config("");
let config = RLLVMConfig::load_path(&config_filepath)
.expect("A config without `llvm_objcopy_filepath` should load");
assert!(config.llvm_objcopy_filepath().is_none());
assert_eq!(config.clang_filepath(), inferred.clang_filepath());
assert_eq!(config.llvm_link_filepath(), inferred.llvm_link_filepath());
}
#[test]
fn lto_mode_is_read_from_the_config_file() {
let (_dir, config_filepath, _) = write_config("lto_mode = 'save-temps'\n");
let config = RLLVMConfig::load_path(&config_filepath).expect("load failed");
assert_eq!(config.lto_mode().unwrap(), LtoMode::SaveTemps);
}
#[test]
fn lto_mode_defaults_to_marker_when_absent() {
let (_dir, config_filepath, _) = write_config("log_level = 0\n");
let config = RLLVMConfig::load_path(&config_filepath).expect("load failed");
assert_eq!(config.lto_mode().unwrap(), LtoMode::Marker);
}
#[test]
fn load_config_with_llvm_objcopy_filepath() {
let llvm_objcopy_filepath = RLLVMConfig::try_default()
.expect("Failed to infer the LLVM tool paths")
.llvm_objcopy_filepath()
.cloned()
.unwrap_or_else(|| PathBuf::from("llvm-objcopy"));
let (_dir, config_filepath, _inferred) = write_config(&format!(
"llvm_objcopy_filepath = '{}'\n",
llvm_objcopy_filepath.display()
));
let config = RLLVMConfig::load_path(&config_filepath)
.expect("A config with `llvm_objcopy_filepath` should load");
assert_eq!(config.llvm_objcopy_filepath(), Some(&llvm_objcopy_filepath));
}
}