Skip to main content

cubecl_common/config/
mod.rs

1/// Reusable logger configuration and sink management.
2pub mod logger;
3
4#[cfg(target_has_atomic = "ptr")]
5use alloc::sync::Arc;
6
7#[cfg(not(target_has_atomic = "ptr"))]
8use portable_atomic_util::Arc;
9
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12
13/// Trait for runtime configurations potentially loaded from a TOML file.
14///
15/// Implementors provide a global storage slot and the set of file names to search for;
16/// the trait supplies the lookup, lazy-initialization, and serialization logic.
17///
18/// The singleton stored in [`Config::storage`] is initialized on the first call to
19/// [`Config::get`] by walking up the current working directory looking for any of the
20/// names returned by [`Config::file_names`]. If none is found, [`Default`] is used.
21pub trait RuntimeConfig:
22    Default + Clone + Serialize + DeserializeOwned + Send + Sync + 'static
23{
24    /// Global storage for the configuration singleton.
25    ///
26    /// Each implementor must declare its own `static` slot, because Rust traits
27    /// cannot own statics directly.
28    fn storage() -> &'static spin::Mutex<Option<Arc<Self>>>;
29
30    /// File names searched in each directory during [`Config::from_current_dir`].
31    ///
32    /// The first existing file wins.
33    fn file_names() -> &'static [&'static str];
34
35    /// File names searched in each directory, where only a specific TOML section is loaded
36    /// instead of the whole file.
37    ///
38    /// Each entry is `(file_name, section_name)` and the section must deserialize to `Self`.
39    /// Checked after [`Config::file_names`] at each directory level.
40    fn section_file_names() -> &'static [(&'static str, &'static str)] {
41        &[]
42    }
43
44    /// Hook to override fields from environment variables after loading from disk.
45    ///
46    /// The default implementation returns `self` unchanged.
47    #[cfg(std_io)]
48    fn override_from_env(self) -> Self {
49        self
50    }
51
52    /// Retrieves the current configuration, loading it from the current directory if not set.
53    ///
54    /// If no configuration is set, it attempts to load one from any of [`Config::file_names`] in
55    /// the current directory or its parents. If no file is found, a default configuration is used.
56    ///
57    /// # Notes
58    ///
59    /// Calling this function is somewhat expensive, because of a global static lock. The config
60    /// format is optimized for parsing, not for consumption. A good practice is to use a local
61    /// static atomic value that you can populate with the appropriate value from the config
62    /// during initialization.
63    fn get() -> Arc<Self> {
64        let mut state = Self::storage().lock();
65        if state.as_ref().is_none() {
66            cfg_if::cfg_if! {
67                if #[cfg(std_io)] {
68                    let config = Self::from_current_dir();
69                    let config = config.override_from_env();
70                } else {
71                    let config = Self::default();
72                }
73            }
74
75            *state = Some(Arc::new(config));
76        }
77
78        state.as_ref().cloned().unwrap()
79    }
80
81    /// Sets the configuration to the provided value.
82    ///
83    /// # Panics
84    /// Panics if the configuration has already been set or read, as it cannot be overridden.
85    ///
86    /// # Warning
87    /// This method must be called at the start of the program, before any calls to
88    /// [`Config::get`]. Attempting to set the configuration after it has been initialized will
89    /// cause a panic.
90    fn set(config: Self) {
91        if !Self::try_set(config) {
92            panic!("Cannot set the configuration multiple times.");
93        }
94    }
95
96    /// Sets the configuration to the provided value, unless it has already been
97    /// set or read — in which case the existing configuration is kept and
98    /// `false` is returned.
99    ///
100    /// Use this from libraries that want to provide a computed default without
101    /// overriding a configuration the application set first.
102    fn try_set(config: Self) -> bool {
103        let mut state = Self::storage().lock();
104        if state.is_some() {
105            return false;
106        }
107        *state = Some(Arc::new(config));
108        true
109    }
110
111    /// Save the default configuration to the provided file path.
112    #[cfg(std_io)]
113    fn save_default<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<()> {
114        use std::io::Write;
115
116        let config = Self::get();
117        let content =
118            toml::to_string_pretty(config.as_ref()).expect("Default config should be serializable");
119        let mut file = std::fs::File::create(path)?;
120        file.write_all(content.as_bytes())?;
121
122        Ok(())
123    }
124
125    /// Loads configuration from any of [`Config::file_names`] in the current directory or its
126    /// parents.
127    ///
128    /// Traverses up the directory tree until a valid configuration file is found or the root
129    /// is reached. Returns a default configuration if no file is found.
130    #[cfg(std_io)]
131    fn from_current_dir() -> Self {
132        let mut dir = std::env::current_dir().unwrap();
133
134        loop {
135            for name in Self::file_names() {
136                if let Ok(content) = Self::from_file_path(dir.join(name)) {
137                    return content;
138                }
139            }
140
141            for (name, section) in Self::section_file_names() {
142                if let Ok(content) = Self::from_section_file_path(dir.join(name), section) {
143                    return content;
144                }
145            }
146
147            if !dir.pop() {
148                break;
149            }
150        }
151
152        Self::default()
153    }
154
155    /// Loads configuration from a specified file path.
156    #[cfg(std_io)]
157    fn from_file_path<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<Self> {
158        let content = std::fs::read_to_string(path)?;
159        let config: Self = match toml::from_str(&content) {
160            Ok(val) => val,
161            Err(err) => panic!("The file provided doesn't have the right format => {err}"),
162        };
163
164        Ok(config)
165    }
166
167    /// Loads configuration from a specific TOML section of the file at the given path.
168    #[cfg(std_io)]
169    fn from_section_file_path<P: AsRef<std::path::Path>>(
170        path: P,
171        section: &str,
172    ) -> std::io::Result<Self> {
173        let content = std::fs::read_to_string(path)?;
174        let mut table: toml::Table = match toml::from_str(&content) {
175            Ok(val) => val,
176            Err(err) => panic!("The file provided doesn't have the right format => {err}"),
177        };
178
179        let value = match table.remove(section) {
180            Some(val) => val,
181            None => {
182                return Err(std::io::Error::new(
183                    std::io::ErrorKind::NotFound,
184                    alloc::format!("Section '{section}' not found"),
185                ));
186            }
187        };
188
189        let config: Self = match value.try_into() {
190            Ok(val) => val,
191            Err(err) => {
192                panic!("The section '{section}' doesn't have the right format => {err}")
193            }
194        };
195
196        Ok(config)
197    }
198}