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}