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 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: Sha256Digest,
26 pub compiler_overrides: Vec<&'static str>,
28}
29
30#[derive(Debug, thiserror::Error)]
32pub enum ConfigurationError {
33 #[error("{operation} {}: {source}", path.display())]
35 Io {
36 operation: &'static str,
38 path: PathBuf,
40 source: io::Error,
42 },
43 #[error("invalid Cargo configuration {}: {message}", path.display())]
45 Invalid {
46 path: PathBuf,
48 message: String,
50 },
51}
52
53pub fn configuration_directories(
61 invocation: &Path,
62 cargo_home: Option<&OsStr>,
63 default_home: Option<&OsStr>,
64) -> io::Result<Vec<PathBuf>> {
65 if !invocation.is_absolute() {
66 return Err(io::Error::new(
67 io::ErrorKind::InvalidInput,
68 "Cargo invocation directory must be absolute",
69 ));
70 }
71 let mut directories: Vec<_> = invocation
72 .ancestors()
73 .map(|path| path.join(".cargo"))
74 .collect();
75 let home = cargo_home
76 .map(PathBuf::from)
77 .or_else(|| default_home.map(|home| PathBuf::from(home).join(".cargo")));
78 if let Some(home) = home {
79 directories.push(if home.is_absolute() {
80 home
81 } else {
82 invocation.join(home)
83 });
84 }
85 Ok(directories)
86}
87
88#[must_use]
91pub fn preferred_configuration(directory: &Path) -> Option<PathBuf> {
92 let extensionless = directory.join("config");
93 if extensionless.exists() {
94 return Some(extensionless);
95 }
96 let toml = directory.join("config.toml");
97 toml.exists().then_some(toml)
98}
99
100pub fn observe_configurations(
114 roots: &[PathBuf],
115) -> Result<Vec<ConfigurationFile>, ConfigurationError> {
116 let mut files = Vec::new();
117 let mut active = BTreeSet::new();
118 for root in roots {
119 append_configuration(&mut files, root, &mut active, false)?;
120 }
121 Ok(files)
122}
123
124#[must_use]
127pub fn is_compiler_override(name: &str) -> bool {
128 matches!(
129 name,
130 "RUSTC"
131 | "RUSTC_WRAPPER"
132 | "RUSTC_WORKSPACE_WRAPPER"
133 | "RUSTDOC"
134 | "CARGO_BUILD_RUSTC"
135 | "CARGO_BUILD_RUSTC_WRAPPER"
136 | "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"
137 | "CARGO_BUILD_RUSTDOC"
138 )
139}
140
141#[must_use]
144pub fn is_build_variable(name: &str) -> bool {
145 matches!(
146 name,
147 "RUSTFLAGS" | "RUSTDOCFLAGS" | "CARGO_ENCODED_RUSTFLAGS" | "CARGO_ENCODED_RUSTDOCFLAGS"
148 ) || name.starts_with("CARGO_BUILD_")
149 || name.starts_with("CARGO_PROFILE_")
150 || name.starts_with("CARGO_TARGET_") && name != "CARGO_TARGET_DIR"
151}
152
153fn append_configuration(
154 files: &mut Vec<ConfigurationFile>,
155 config: &Path,
156 active: &mut BTreeSet<PathBuf>,
157 optional: bool,
158) -> Result<(), ConfigurationError> {
159 if !config.is_absolute() {
160 return Err(invalid_configuration(
161 config,
162 "configuration path must be absolute",
163 ));
164 }
165 if files.iter().any(|file| file.path == config) {
166 return Ok(());
167 }
168 let contents = match fs::read_to_string(config) {
169 Ok(contents) => contents,
170 Err(error) if optional && error.kind() == io::ErrorKind::NotFound => return Ok(()),
171 Err(source) => {
172 return Err(ConfigurationError::Io {
173 operation: "read Cargo configuration",
174 path: config.to_owned(),
175 source,
176 });
177 }
178 };
179 let parent = config.parent().ok_or_else(|| {
180 invalid_configuration(config, "configuration path has no parent directory")
181 })?;
182 let location =
183 parent
184 .canonicalize()
185 .map_err(|source| ConfigurationError::Io {
186 operation: "resolve Cargo configuration directory",
187 path: parent.to_owned(),
188 source,
189 })?
190 .join(config.file_name().ok_or_else(|| {
191 invalid_configuration(config, "configuration path has no file name")
192 })?);
193 if !active.insert(location.clone()) {
194 return Ok(());
195 }
196 let configuration = toml::from_str::<Value>(&contents)
197 .map_err(|_| invalid_configuration(config, "invalid TOML"))?;
198 let compiler_overrides = [
199 "rustc",
200 "rustc-wrapper",
201 "rustc-workspace-wrapper",
202 "rustdoc",
203 ]
204 .into_iter()
205 .filter(|field| {
206 configuration
207 .get("build")
208 .and_then(|build| build.get(field))
209 .is_some()
210 })
211 .collect();
212 files.push(ConfigurationFile {
213 path: config.to_owned(),
214 location: location.clone(),
215 sha256: Sha256Digest::compute(contents.as_bytes()),
216 compiler_overrides,
217 });
218 if let Some(include) = configuration.get("include") {
219 for (included, optional) in configuration_includes(include, config)? {
220 let included = if included.is_absolute() {
221 Cow::Borrowed(included)
222 } else {
223 Cow::Owned(parent.join(included))
224 };
225 append_configuration(files, &included, active, optional)?;
226 }
227 }
228 active.remove(&location);
229 Ok(())
230}
231
232fn configuration_includes<'a>(
233 include: &'a Value,
234 config: &Path,
235) -> Result<Vec<(&'a Path, bool)>, ConfigurationError> {
236 let values = match include {
237 Value::Array(values) => values.as_slice(),
238 value => std::slice::from_ref(value),
239 };
240 values
241 .iter()
242 .map(|value| match value {
243 Value::String(path) => Ok((Path::new(path), false)),
244 Value::Table(table) => {
245 let path = table.get("path").and_then(Value::as_str).ok_or_else(|| {
246 invalid_configuration(
247 config,
248 "Cargo configuration include table requires a string `path`",
249 )
250 })?;
251 let optional = table
252 .get("optional")
253 .map(|value| {
254 value.as_bool().ok_or_else(|| {
255 invalid_configuration(
256 config,
257 "Cargo configuration include `optional` must be a boolean",
258 )
259 })
260 })
261 .transpose()?
262 .unwrap_or(false);
263 Ok((Path::new(path), optional))
264 }
265 _ => Err(invalid_configuration(
266 config,
267 "Cargo configuration `include` must contain paths or include tables",
268 )),
269 })
270 .collect()
271}
272
273fn invalid_configuration(path: &Path, message: &str) -> ConfigurationError {
274 ConfigurationError::Invalid {
275 path: path.to_owned(),
276 message: message.to_owned(),
277 }
278}