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