ic_build/cargo/environment/
mod.rs1#[cfg(test)]
4mod tests;
5
6use ic_host_artifacts::artifact::Sha256Digest;
7use std::{
8 collections::BTreeSet,
9 ffi::OsStr,
10 fmt, fs, io,
11 path::{Path, PathBuf},
12};
13use toml::Value;
14
15pub struct ConfigurationFile {
19 pub path: PathBuf,
21 pub location: PathBuf,
23 pub sha256: [u8; 32],
25 pub compiler_overrides: Vec<&'static str>,
27}
28
29#[derive(Debug)]
31pub enum ConfigurationError {
32 Io {
34 operation: &'static str,
36 path: PathBuf,
38 source: io::Error,
40 },
41 Invalid {
43 path: PathBuf,
45 message: String,
47 },
48}
49
50impl fmt::Display for ConfigurationError {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::Io {
54 operation,
55 path,
56 source,
57 } => write!(f, "{operation} {}: {source}", path.display()),
58 Self::Invalid { path, message } => write!(
59 f,
60 "invalid Cargo configuration {}: {message}",
61 path.display()
62 ),
63 }
64 }
65}
66
67impl std::error::Error for ConfigurationError {
68 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69 match self {
70 Self::Io { source, .. } => Some(source),
71 Self::Invalid { .. } => None,
72 }
73 }
74}
75
76pub fn configuration_directories(
84 invocation: &Path,
85 cargo_home: Option<&OsStr>,
86 default_home: Option<&OsStr>,
87) -> io::Result<Vec<PathBuf>> {
88 if !invocation.is_absolute() {
89 return Err(io::Error::new(
90 io::ErrorKind::InvalidInput,
91 "Cargo invocation directory must be absolute",
92 ));
93 }
94 let mut directories: Vec<_> = invocation
95 .ancestors()
96 .map(|path| path.join(".cargo"))
97 .collect();
98 let home = cargo_home
99 .map(PathBuf::from)
100 .or_else(|| default_home.map(|home| PathBuf::from(home).join(".cargo")));
101 if let Some(home) = home {
102 directories.push(if home.is_absolute() {
103 home
104 } else {
105 invocation.join(home)
106 });
107 }
108 Ok(directories)
109}
110
111#[must_use]
114pub fn preferred_configuration(directory: &Path) -> Option<PathBuf> {
115 let extensionless = directory.join("config");
116 if extensionless.exists() {
117 return Some(extensionless);
118 }
119 let toml = directory.join("config.toml");
120 toml.exists().then_some(toml)
121}
122
123pub fn observe_configurations(
137 roots: &[PathBuf],
138) -> Result<Vec<ConfigurationFile>, ConfigurationError> {
139 let mut files = Vec::new();
140 let mut active = BTreeSet::new();
141 for root in roots {
142 append_configuration(&mut files, root, &mut active, false)?;
143 }
144 Ok(files)
145}
146
147#[must_use]
150pub fn is_compiler_override(name: &str) -> bool {
151 matches!(
152 name,
153 "RUSTC"
154 | "RUSTC_WRAPPER"
155 | "RUSTC_WORKSPACE_WRAPPER"
156 | "RUSTDOC"
157 | "CARGO_BUILD_RUSTC"
158 | "CARGO_BUILD_RUSTC_WRAPPER"
159 | "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"
160 | "CARGO_BUILD_RUSTDOC"
161 )
162}
163
164#[must_use]
167pub fn is_build_variable(name: &str) -> bool {
168 matches!(
169 name,
170 "RUSTFLAGS" | "RUSTDOCFLAGS" | "CARGO_ENCODED_RUSTFLAGS" | "CARGO_ENCODED_RUSTDOCFLAGS"
171 ) || name.starts_with("CARGO_BUILD_")
172 || name.starts_with("CARGO_PROFILE_")
173 || name.starts_with("CARGO_TARGET_") && name != "CARGO_TARGET_DIR"
174}
175
176fn append_configuration(
177 files: &mut Vec<ConfigurationFile>,
178 config: &Path,
179 active: &mut BTreeSet<PathBuf>,
180 optional: bool,
181) -> Result<(), ConfigurationError> {
182 if !config.is_absolute() {
183 return Err(invalid_configuration(
184 config,
185 "configuration path must be absolute",
186 ));
187 }
188 if files.iter().any(|file| file.path == config) {
189 return Ok(());
190 }
191 let contents = match fs::read_to_string(config) {
192 Ok(contents) => contents,
193 Err(error) if optional && error.kind() == io::ErrorKind::NotFound => return Ok(()),
194 Err(source) => {
195 return Err(ConfigurationError::Io {
196 operation: "read Cargo configuration",
197 path: config.to_owned(),
198 source,
199 });
200 }
201 };
202 let parent = config.parent().ok_or_else(|| {
203 invalid_configuration(config, "configuration path has no parent directory")
204 })?;
205 let location =
206 parent
207 .canonicalize()
208 .map_err(|source| ConfigurationError::Io {
209 operation: "resolve Cargo configuration directory",
210 path: parent.to_owned(),
211 source,
212 })?
213 .join(config.file_name().ok_or_else(|| {
214 invalid_configuration(config, "configuration path has no file name")
215 })?);
216 if !active.insert(location.clone()) {
217 return Ok(());
218 }
219 let configuration = toml::from_str::<Value>(&contents)
220 .map_err(|_| invalid_configuration(config, "invalid TOML"))?;
221 let compiler_overrides = [
222 "rustc",
223 "rustc-wrapper",
224 "rustc-workspace-wrapper",
225 "rustdoc",
226 ]
227 .into_iter()
228 .filter(|field| {
229 configuration
230 .get("build")
231 .and_then(|build| build.get(field))
232 .is_some()
233 })
234 .collect();
235 files.push(ConfigurationFile {
236 path: config.to_owned(),
237 location: location.clone(),
238 sha256: *Sha256Digest::compute(contents.as_bytes()).as_bytes(),
239 compiler_overrides,
240 });
241 if let Some(include) = configuration.get("include") {
242 for (included, optional) in configuration_includes(include, config)? {
243 let included = if included.is_absolute() {
244 included
245 } else {
246 parent.join(included)
247 };
248 append_configuration(files, &included, active, optional)?;
249 }
250 }
251 active.remove(&location);
252 Ok(())
253}
254
255fn configuration_includes(
256 include: &Value,
257 config: &Path,
258) -> Result<Vec<(PathBuf, bool)>, ConfigurationError> {
259 let values = match include {
260 Value::Array(values) => values.as_slice(),
261 value => std::slice::from_ref(value),
262 };
263 values
264 .iter()
265 .map(|value| match value {
266 Value::String(path) => Ok((PathBuf::from(path), false)),
267 Value::Table(table) => {
268 let path = table.get("path").and_then(Value::as_str).ok_or_else(|| {
269 invalid_configuration(
270 config,
271 "Cargo configuration include table requires a string `path`",
272 )
273 })?;
274 let optional = table
275 .get("optional")
276 .map(|value| {
277 value.as_bool().ok_or_else(|| {
278 invalid_configuration(
279 config,
280 "Cargo configuration include `optional` must be a boolean",
281 )
282 })
283 })
284 .transpose()?
285 .unwrap_or(false);
286 Ok((PathBuf::from(path), optional))
287 }
288 _ => Err(invalid_configuration(
289 config,
290 "Cargo configuration `include` must contain paths or include tables",
291 )),
292 })
293 .collect()
294}
295
296fn invalid_configuration(path: &Path, message: &str) -> ConfigurationError {
297 ConfigurationError::Invalid {
298 path: path.to_owned(),
299 message: message.to_owned(),
300 }
301}