#[cfg(test)]
mod tests;
use ic_host_artifacts::artifact::Sha256Digest;
use std::{
borrow::Cow,
collections::BTreeSet,
ffi::OsStr,
fmt, fs, io,
path::{Path, PathBuf},
};
use toml::Value;
pub struct ConfigurationFile {
pub path: PathBuf,
pub location: PathBuf,
pub sha256: [u8; 32],
pub compiler_overrides: Vec<&'static str>,
}
#[derive(Debug)]
pub enum ConfigurationError {
Io {
operation: &'static str,
path: PathBuf,
source: io::Error,
},
Invalid {
path: PathBuf,
message: String,
},
}
impl fmt::Display for ConfigurationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io {
operation,
path,
source,
} => write!(f, "{operation} {}: {source}", path.display()),
Self::Invalid { path, message } => write!(
f,
"invalid Cargo configuration {}: {message}",
path.display()
),
}
}
}
impl std::error::Error for ConfigurationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
Self::Invalid { .. } => None,
}
}
}
pub fn configuration_directories(
invocation: &Path,
cargo_home: Option<&OsStr>,
default_home: Option<&OsStr>,
) -> io::Result<Vec<PathBuf>> {
if !invocation.is_absolute() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Cargo invocation directory must be absolute",
));
}
let mut directories: Vec<_> = invocation
.ancestors()
.map(|path| path.join(".cargo"))
.collect();
let home = cargo_home
.map(PathBuf::from)
.or_else(|| default_home.map(|home| PathBuf::from(home).join(".cargo")));
if let Some(home) = home {
directories.push(if home.is_absolute() {
home
} else {
invocation.join(home)
});
}
Ok(directories)
}
#[must_use]
pub fn preferred_configuration(directory: &Path) -> Option<PathBuf> {
let extensionless = directory.join("config");
if extensionless.exists() {
return Some(extensionless);
}
let toml = directory.join("config.toml");
toml.exists().then_some(toml)
}
pub fn observe_configurations(
roots: &[PathBuf],
) -> Result<Vec<ConfigurationFile>, ConfigurationError> {
let mut files = Vec::new();
let mut active = BTreeSet::new();
for root in roots {
append_configuration(&mut files, root, &mut active, false)?;
}
Ok(files)
}
#[must_use]
pub fn is_compiler_override(name: &str) -> bool {
matches!(
name,
"RUSTC"
| "RUSTC_WRAPPER"
| "RUSTC_WORKSPACE_WRAPPER"
| "RUSTDOC"
| "CARGO_BUILD_RUSTC"
| "CARGO_BUILD_RUSTC_WRAPPER"
| "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"
| "CARGO_BUILD_RUSTDOC"
)
}
#[must_use]
pub fn is_build_variable(name: &str) -> bool {
matches!(
name,
"RUSTFLAGS" | "RUSTDOCFLAGS" | "CARGO_ENCODED_RUSTFLAGS" | "CARGO_ENCODED_RUSTDOCFLAGS"
) || name.starts_with("CARGO_BUILD_")
|| name.starts_with("CARGO_PROFILE_")
|| name.starts_with("CARGO_TARGET_") && name != "CARGO_TARGET_DIR"
}
fn append_configuration(
files: &mut Vec<ConfigurationFile>,
config: &Path,
active: &mut BTreeSet<PathBuf>,
optional: bool,
) -> Result<(), ConfigurationError> {
if !config.is_absolute() {
return Err(invalid_configuration(
config,
"configuration path must be absolute",
));
}
if files.iter().any(|file| file.path == config) {
return Ok(());
}
let contents = match fs::read_to_string(config) {
Ok(contents) => contents,
Err(error) if optional && error.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(source) => {
return Err(ConfigurationError::Io {
operation: "read Cargo configuration",
path: config.to_owned(),
source,
});
}
};
let parent = config.parent().ok_or_else(|| {
invalid_configuration(config, "configuration path has no parent directory")
})?;
let location =
parent
.canonicalize()
.map_err(|source| ConfigurationError::Io {
operation: "resolve Cargo configuration directory",
path: parent.to_owned(),
source,
})?
.join(config.file_name().ok_or_else(|| {
invalid_configuration(config, "configuration path has no file name")
})?);
if !active.insert(location.clone()) {
return Ok(());
}
let configuration = toml::from_str::<Value>(&contents)
.map_err(|_| invalid_configuration(config, "invalid TOML"))?;
let compiler_overrides = [
"rustc",
"rustc-wrapper",
"rustc-workspace-wrapper",
"rustdoc",
]
.into_iter()
.filter(|field| {
configuration
.get("build")
.and_then(|build| build.get(field))
.is_some()
})
.collect();
files.push(ConfigurationFile {
path: config.to_owned(),
location: location.clone(),
sha256: *Sha256Digest::compute(contents.as_bytes()).as_bytes(),
compiler_overrides,
});
if let Some(include) = configuration.get("include") {
for (included, optional) in configuration_includes(include, config)? {
let included = if included.is_absolute() {
Cow::Borrowed(included)
} else {
Cow::Owned(parent.join(included))
};
append_configuration(files, &included, active, optional)?;
}
}
active.remove(&location);
Ok(())
}
fn configuration_includes<'a>(
include: &'a Value,
config: &Path,
) -> Result<Vec<(&'a Path, bool)>, ConfigurationError> {
let values = match include {
Value::Array(values) => values.as_slice(),
value => std::slice::from_ref(value),
};
values
.iter()
.map(|value| match value {
Value::String(path) => Ok((Path::new(path), false)),
Value::Table(table) => {
let path = table.get("path").and_then(Value::as_str).ok_or_else(|| {
invalid_configuration(
config,
"Cargo configuration include table requires a string `path`",
)
})?;
let optional = table
.get("optional")
.map(|value| {
value.as_bool().ok_or_else(|| {
invalid_configuration(
config,
"Cargo configuration include `optional` must be a boolean",
)
})
})
.transpose()?
.unwrap_or(false);
Ok((Path::new(path), optional))
}
_ => Err(invalid_configuration(
config,
"Cargo configuration `include` must contain paths or include tables",
)),
})
.collect()
}
fn invalid_configuration(path: &Path, message: &str) -> ConfigurationError {
ConfigurationError::Invalid {
path: path.to_owned(),
message: message.to_owned(),
}
}