pub mod logger;
#[cfg(target_has_atomic = "ptr")]
use alloc::sync::Arc;
#[cfg(not(target_has_atomic = "ptr"))]
use portable_atomic_util::Arc;
use serde::Serialize;
use serde::de::DeserializeOwned;
#[cfg(std_io)]
fn report_malformed(message: &str) {
log::warn!("{message}");
std::eprintln!("cubecl config: {message}");
}
pub trait RuntimeConfig:
Default + Clone + Serialize + DeserializeOwned + Send + Sync + 'static
{
fn storage() -> &'static crate::sync::Mutex<Option<Arc<Self>>>;
fn file_names() -> &'static [&'static str];
fn section_file_names() -> &'static [(&'static str, &'static str)] {
&[]
}
#[cfg(std_io)]
fn override_from_env(self) -> Self {
self
}
fn on_loaded(&self) {}
fn get() -> Arc<Self> {
let mut state = Self::storage().lock();
if state.as_ref().is_none() {
cfg_if::cfg_if! {
if #[cfg(std_io)] {
let config = Self::from_current_dir();
let config = config.override_from_env();
} else {
let config = Self::default();
}
}
let config = Arc::new(config);
*state = Some(config.clone());
config.on_loaded();
return config;
}
state.as_ref().cloned().unwrap()
}
fn set(config: Self) {
if !Self::try_set(config) {
panic!("Cannot set the configuration multiple times.");
}
}
fn try_set(config: Self) -> bool {
let mut state = Self::storage().lock();
if state.is_some() {
return false;
}
let config = Arc::new(config);
*state = Some(config.clone());
config.on_loaded();
true
}
#[cfg(std_io)]
fn save_default<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<()> {
use std::io::Write;
let config = Self::get();
let content =
toml::to_string_pretty(config.as_ref()).expect("Default config should be serializable");
let mut file = std::fs::File::create(path)?;
file.write_all(content.as_bytes())?;
Ok(())
}
#[cfg(std_io)]
fn from_current_dir() -> Self {
let Ok(mut dir) = std::env::current_dir() else {
return Self::default();
};
loop {
for name in Self::file_names() {
if let Ok(content) = Self::from_file_path(dir.join(name)) {
return content;
}
}
for (name, section) in Self::section_file_names() {
if let Ok(content) = Self::from_section_file_path(dir.join(name), section) {
return content;
}
}
if !dir.pop() {
break;
}
}
Self::default()
}
#[cfg(std_io)]
fn from_file_path<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<Self> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)?;
match toml::from_str(&content) {
Ok(config) => Ok(config),
Err(err) => {
report_malformed(&alloc::format!(
"Ignoring {path:?}, which doesn't have the right format => {err}"
));
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err))
}
}
}
#[cfg(std_io)]
fn from_section_file_path<P: AsRef<std::path::Path>>(
path: P,
section: &str,
) -> std::io::Result<Self> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)?;
let mut table: toml::Table = match toml::from_str(&content) {
Ok(val) => val,
Err(err) => {
report_malformed(&alloc::format!(
"Ignoring {path:?}, which doesn't have the right format => {err}"
));
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err));
}
};
let value = match table.remove(section) {
Some(val) => val,
None => {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
alloc::format!("Section '{section}' not found"),
));
}
};
match value.try_into() {
Ok(config) => Ok(config),
Err(err) => {
report_malformed(&alloc::format!(
"Ignoring section '{section}' of {path:?}, which doesn't have the right \
format => {err}"
));
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err))
}
}
}
}
#[cfg(all(test, std_io))]
mod tests {
use super::*;
#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
struct Probe {
#[serde(default)]
cache: bool,
}
static PROBE: crate::sync::Mutex<Option<Arc<Probe>>> = crate::sync::Mutex::new(None);
impl RuntimeConfig for Probe {
fn storage() -> &'static crate::sync::Mutex<Option<Arc<Self>>> {
&PROBE
}
fn file_names() -> &'static [&'static str] {
&["probe.toml"]
}
fn section_file_names() -> &'static [(&'static str, &'static str)] {
&[("host.toml", "probe")]
}
}
fn scratch(file: &str, content: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join(file), content).unwrap();
dir
}
#[test]
#[cfg_attr(miri, ignore)]
fn a_wrongly_typed_field_fails_the_whole_file() {
let dir = scratch("probe.toml", "cache = \"target\"\n");
let err = Probe::from_file_path(dir.path().join("probe.toml")).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
#[cfg_attr(miri, ignore)]
fn a_wrongly_typed_field_fails_the_whole_section() {
let dir = scratch("host.toml", "[probe]\ncache = \"target\"\n");
let err = Probe::from_section_file_path(dir.path().join("host.toml"), "probe").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
#[cfg_attr(miri, ignore)]
fn a_missing_section_is_quiet() {
let dir = scratch("host.toml", "[other]\nvalue = 1\n");
let err = Probe::from_section_file_path(dir.path().join("host.toml"), "probe").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[test]
#[cfg_attr(miri, ignore)]
fn a_well_formed_section_parses() {
let dir = scratch("host.toml", "[probe]\ncache = true\n");
let config = Probe::from_section_file_path(dir.path().join("host.toml"), "probe").unwrap();
assert!(config.cache);
}
}