ic_build/cargo/environment/
mod.rs1#[cfg(test)]
4mod tests;
5
6use sha2::{Digest as _, Sha256};
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(
136 roots: &[PathBuf],
137) -> Result<Vec<ConfigurationFile>, ConfigurationError> {
138 let mut files = Vec::new();
139 let mut active = BTreeSet::new();
140 for root in roots {
141 append_configuration(&mut files, root, &mut active, false)?;
142 }
143 Ok(files)
144}
145
146#[must_use]
149pub fn is_compiler_override(name: &str) -> bool {
150 matches!(
151 name,
152 "RUSTC"
153 | "RUSTC_WRAPPER"
154 | "RUSTC_WORKSPACE_WRAPPER"
155 | "RUSTDOC"
156 | "CARGO_BUILD_RUSTC"
157 | "CARGO_BUILD_RUSTC_WRAPPER"
158 | "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"
159 | "CARGO_BUILD_RUSTDOC"
160 )
161}
162
163#[must_use]
166pub fn is_build_variable(name: &str) -> bool {
167 matches!(
168 name,
169 "RUSTFLAGS" | "RUSTDOCFLAGS" | "CARGO_ENCODED_RUSTFLAGS" | "CARGO_ENCODED_RUSTDOCFLAGS"
170 ) || name.starts_with("CARGO_BUILD_")
171 || name.starts_with("CARGO_PROFILE_")
172 || name.starts_with("CARGO_TARGET_") && name != "CARGO_TARGET_DIR"
173}
174
175fn append_configuration(
176 files: &mut Vec<ConfigurationFile>,
177 config: &Path,
178 active: &mut BTreeSet<PathBuf>,
179 optional: bool,
180) -> Result<(), ConfigurationError> {
181 if !config.is_absolute() {
182 return Err(invalid_configuration(
183 config,
184 "configuration path must be absolute",
185 ));
186 }
187 let contents = match fs::read_to_string(config) {
188 Ok(contents) => contents,
189 Err(error) if optional && error.kind() == io::ErrorKind::NotFound => return Ok(()),
190 Err(source) => {
191 return Err(ConfigurationError::Io {
192 operation: "read Cargo configuration",
193 path: config.to_owned(),
194 source,
195 });
196 }
197 };
198 let parent = config.parent().ok_or_else(|| {
199 invalid_configuration(config, "configuration path has no parent directory")
200 })?;
201 let location =
202 parent
203 .canonicalize()
204 .map_err(|source| ConfigurationError::Io {
205 operation: "resolve Cargo configuration directory",
206 path: parent.to_owned(),
207 source,
208 })?
209 .join(config.file_name().ok_or_else(|| {
210 invalid_configuration(config, "configuration path has no file name")
211 })?);
212 if !active.insert(location.clone()) {
213 return Ok(());
214 }
215 let configuration = toml::from_str::<Value>(&contents)
216 .map_err(|_| invalid_configuration(config, "invalid TOML"))?;
217 if !files.iter().any(|file| file.path == config) {
218 let compiler_overrides = [
219 "rustc",
220 "rustc-wrapper",
221 "rustc-workspace-wrapper",
222 "rustdoc",
223 ]
224 .into_iter()
225 .filter(|field| {
226 configuration
227 .get("build")
228 .and_then(|build| build.get(field))
229 .is_some()
230 })
231 .collect();
232 files.push(ConfigurationFile {
233 path: config.to_owned(),
234 location: location.clone(),
235 sha256: Sha256::digest(contents.as_bytes()).into(),
236 compiler_overrides,
237 });
238 }
239 if let Some(include) = configuration.get("include") {
240 for (included, optional) in configuration_includes(include, config)? {
241 let included = if included.is_absolute() {
242 included
243 } else {
244 parent.join(included)
245 };
246 append_configuration(files, &included, active, optional)?;
247 }
248 }
249 active.remove(&location);
250 Ok(())
251}
252
253fn configuration_includes(
254 include: &Value,
255 config: &Path,
256) -> Result<Vec<(PathBuf, bool)>, ConfigurationError> {
257 let values = match include {
258 Value::Array(values) => values.as_slice(),
259 value => std::slice::from_ref(value),
260 };
261 values
262 .iter()
263 .map(|value| match value {
264 Value::String(path) => Ok((PathBuf::from(path), false)),
265 Value::Table(table) => {
266 let path = table.get("path").and_then(Value::as_str).ok_or_else(|| {
267 invalid_configuration(
268 config,
269 "Cargo configuration include table requires a string `path`",
270 )
271 })?;
272 let optional = table
273 .get("optional")
274 .map(|value| {
275 value.as_bool().ok_or_else(|| {
276 invalid_configuration(
277 config,
278 "Cargo configuration include `optional` must be a boolean",
279 )
280 })
281 })
282 .transpose()?
283 .unwrap_or(false);
284 Ok((PathBuf::from(path), optional))
285 }
286 _ => Err(invalid_configuration(
287 config,
288 "Cargo configuration `include` must contain paths or include tables",
289 )),
290 })
291 .collect()
292}
293
294fn invalid_configuration(path: &Path, message: &str) -> ConfigurationError {
295 ConfigurationError::Invalid {
296 path: path.to_owned(),
297 message: message.to_owned(),
298 }
299}