ic_build/cargo/environment/
mod.rs1#[cfg(test)]
4mod tests;
5
6use ic_host_artifacts::artifact::{ArtifactError, Sha256Digest};
7use std::{
8 borrow::Cow,
9 collections::BTreeSet,
10 ffi::OsStr,
11 io,
12 path::{Path, PathBuf},
13};
14use toml::Value;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct ConfigurationLimits {
19 pub file_bytes: usize,
21 pub total_bytes: usize,
23 pub files: usize,
25 pub depth: usize,
27}
28
29pub struct ConfigurationFile {
33 pub path: PathBuf,
35 pub location: PathBuf,
37 pub sha256: Sha256Digest,
39 pub recipe_sha256: Option<Sha256Digest>,
43 pub compiler_overrides: Vec<&'static str>,
45}
46
47#[derive(Debug, thiserror::Error)]
49pub enum ConfigurationError {
50 #[error("read Cargo configuration {}: {source}", path.display())]
52 Read {
53 path: PathBuf,
55 source: ArtifactError,
57 },
58 #[error("{operation} {}: {source}", path.display())]
60 Io {
61 operation: &'static str,
63 path: PathBuf,
65 source: io::Error,
67 },
68 #[error("invalid Cargo configuration {}: {message}", path.display())]
70 Invalid {
71 path: PathBuf,
73 message: String,
75 },
76}
77
78pub fn configuration_directories(
86 invocation: &Path,
87 cargo_home: Option<&OsStr>,
88 default_home: Option<&OsStr>,
89) -> io::Result<Vec<PathBuf>> {
90 if !invocation.is_absolute() {
91 return Err(io::Error::new(
92 io::ErrorKind::InvalidInput,
93 "Cargo invocation directory must be absolute",
94 ));
95 }
96 let mut directories: Vec<_> = invocation
97 .ancestors()
98 .map(|path| path.join(".cargo"))
99 .collect();
100 let home = cargo_home
101 .map(PathBuf::from)
102 .or_else(|| default_home.map(|home| PathBuf::from(home).join(".cargo")));
103 if let Some(home) = home {
104 directories.push(if home.is_absolute() {
105 home
106 } else {
107 invocation.join(home)
108 });
109 }
110 Ok(directories)
111}
112
113#[must_use]
116pub fn preferred_configuration(directory: &Path) -> Option<PathBuf> {
117 let extensionless = directory.join("config");
118 if extensionless.exists() {
119 return Some(extensionless);
120 }
121 let toml = directory.join("config.toml");
122 toml.exists().then_some(toml)
123}
124
125pub fn observe_configurations(
140 roots: &[PathBuf],
141 mut limits: ConfigurationLimits,
142) -> Result<Vec<ConfigurationFile>, ConfigurationError> {
143 let mut files = Vec::new();
144 let mut active = BTreeSet::new();
145 for root in roots {
146 append_configuration(&mut files, root, &mut active, false, &mut limits, 0)?;
147 }
148 Ok(files)
149}
150
151#[must_use]
154pub fn is_compiler_override(name: &str) -> bool {
155 matches!(
156 name,
157 "RUSTC"
158 | "RUSTC_WRAPPER"
159 | "RUSTC_WORKSPACE_WRAPPER"
160 | "RUSTDOC"
161 | "CARGO_BUILD_RUSTC"
162 | "CARGO_BUILD_RUSTC_WRAPPER"
163 | "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"
164 | "CARGO_BUILD_RUSTDOC"
165 )
166}
167
168#[must_use]
171pub fn is_build_variable(name: &str) -> bool {
172 matches!(
173 name,
174 "RUSTFLAGS" | "RUSTDOCFLAGS" | "CARGO_ENCODED_RUSTFLAGS" | "CARGO_ENCODED_RUSTDOCFLAGS"
175 ) || name.starts_with("CARGO_BUILD_")
176 || name.starts_with("CARGO_PROFILE_")
177 || name.starts_with("CARGO_TARGET_") && name != "CARGO_TARGET_DIR"
178}
179
180pub(crate) fn is_recipe_variable(name: &str) -> bool {
181 !matches!(name, "CARGO_BUILD_TARGET_DIR" | "CARGO_BUILD_BUILD_DIR")
182 && (is_build_variable(name)
183 || is_compiler_override(name)
184 || matches!(name, "CARGO_INCREMENTAL" | "RUSTC_BOOTSTRAP"))
185}
186
187fn append_configuration(
188 files: &mut Vec<ConfigurationFile>,
189 config: &Path,
190 active: &mut BTreeSet<PathBuf>,
191 optional: bool,
192 limits: &mut ConfigurationLimits,
193 depth: usize,
194) -> Result<(), ConfigurationError> {
195 if !config.is_absolute() {
196 return Err(invalid_configuration(
197 config,
198 "configuration path must be absolute",
199 ));
200 }
201 if files.iter().any(|file| file.path == config) {
202 return Ok(());
203 }
204 if depth > limits.depth {
205 return Err(invalid_configuration(
206 config,
207 "include depth limit exceeded",
208 ));
209 }
210 if limits.files == 0 {
211 return Err(invalid_configuration(
212 config,
213 "configuration file count limit exceeded",
214 ));
215 }
216 let contents =
217 match ic_host_fs::read::read_file(config, limits.file_bytes.min(limits.total_bytes)) {
218 Ok(contents) => contents,
219 Err(ArtifactError::Io(error))
220 if optional && error.kind() == io::ErrorKind::NotFound =>
221 {
222 return Ok(());
223 }
224 Err(ArtifactError::Io(source)) => {
225 return Err(ConfigurationError::Io {
226 operation: "read Cargo configuration",
227 path: config.to_owned(),
228 source,
229 });
230 }
231 Err(source) => {
232 return Err(ConfigurationError::Read {
233 path: config.to_owned(),
234 source,
235 });
236 }
237 };
238 limits.files -= 1;
239 limits.total_bytes -= contents.len();
240 let contents = String::from_utf8(contents)
241 .map_err(|_| invalid_configuration(config, "configuration is not UTF-8"))?;
242 let parent = config.parent().ok_or_else(|| {
243 invalid_configuration(config, "configuration path has no parent directory")
244 })?;
245 let location =
246 parent
247 .canonicalize()
248 .map_err(|source| ConfigurationError::Io {
249 operation: "resolve Cargo configuration directory",
250 path: parent.to_owned(),
251 source,
252 })?
253 .join(config.file_name().ok_or_else(|| {
254 invalid_configuration(config, "configuration path has no file name")
255 })?);
256 if !active.insert(location.clone()) {
257 return Ok(());
258 }
259 let configuration = toml::from_str::<Value>(&contents)
260 .map_err(|_| invalid_configuration(config, "invalid TOML"))?;
261 let compiler_overrides = [
262 "rustc",
263 "rustc-wrapper",
264 "rustc-workspace-wrapper",
265 "rustdoc",
266 ]
267 .into_iter()
268 .filter(|field| {
269 configuration
270 .get("build")
271 .and_then(|build| build.get(field))
272 .is_some()
273 })
274 .collect();
275 files.push(ConfigurationFile {
276 path: config.to_owned(),
277 location: location.clone(),
278 sha256: Sha256Digest::compute(contents.as_bytes()),
279 compiler_overrides,
280 recipe_sha256: configuration_recipe(&configuration),
281 });
282 if let Some(include) = configuration.get("include") {
283 for (included, optional) in configuration_includes(include, config)? {
284 let included = if included.is_absolute() {
285 Cow::Borrowed(included)
286 } else {
287 Cow::Owned(parent.join(included))
288 };
289 append_configuration(files, &included, active, optional, limits, depth + 1)?;
290 }
291 }
292 active.remove(&location);
293 Ok(())
294}
295
296fn configuration_includes<'a>(
297 include: &'a Value,
298 config: &Path,
299) -> Result<Vec<(&'a Path, bool)>, ConfigurationError> {
300 let values = match include {
301 Value::Array(values) => values.as_slice(),
302 value => std::slice::from_ref(value),
303 };
304 values
305 .iter()
306 .map(|value| match value {
307 Value::String(path) => Ok((Path::new(path), false)),
308 Value::Table(table) => {
309 let path = table.get("path").and_then(Value::as_str).ok_or_else(|| {
310 invalid_configuration(
311 config,
312 "Cargo configuration include table requires a string `path`",
313 )
314 })?;
315 let optional = table
316 .get("optional")
317 .map(|value| {
318 value.as_bool().ok_or_else(|| {
319 invalid_configuration(
320 config,
321 "Cargo configuration include `optional` must be a boolean",
322 )
323 })
324 })
325 .transpose()?
326 .unwrap_or(false);
327 Ok((Path::new(path), optional))
328 }
329 _ => Err(invalid_configuration(
330 config,
331 "Cargo configuration `include` must contain paths or include tables",
332 )),
333 })
334 .collect()
335}
336
337fn invalid_configuration(path: &Path, message: &str) -> ConfigurationError {
338 ConfigurationError::Invalid {
339 path: path.to_owned(),
340 message: message.to_owned(),
341 }
342}
343
344fn configuration_recipe(configuration: &Value) -> Option<Sha256Digest> {
345 let mut selected = toml::map::Map::new();
346 for key in ["build", "target", "profile", "unstable", "include"] {
347 if let Some(value) = configuration.get(key) {
348 let mut value = value.clone();
349 if key == "build"
350 && let Some(table) = value.as_table_mut()
351 {
352 table.remove("target-dir");
353 table.remove("build-dir");
354 }
355 if !value.as_table().is_some_and(toml::map::Map::is_empty) {
356 selected.insert(key.to_owned(), value);
357 }
358 }
359 }
360 if let Some(environment) = configuration.get("env").and_then(Value::as_table) {
361 let environment: toml::map::Map<_, _> = environment
362 .iter()
363 .filter(|(name, _)| is_recipe_variable(name))
364 .map(|(name, value)| (name.clone(), value.clone()))
365 .collect();
366 if !environment.is_empty() {
367 selected.insert("env".to_owned(), Value::Table(environment));
368 }
369 }
370 (!selected.is_empty())
371 .then(|| Sha256Digest::compute(Value::Table(selected).to_string().as_bytes()))
372}