use std::{
collections::BTreeSet,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
};
use indexmap::IndexMap;
use rattler_conda_types::{ChannelConfig, NamedChannelOrUrl};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use thiserror::Error;
use url::Url;
use crate::config::s3::S3OptionsMap;
use crate::config::{
build::BuildConfig, concurrency::ConcurrencyConfig, index::IndexConfig, proxy::ProxyConfig,
repodata_config::RepodataConfig, run_post_link_scripts::RunPostLinkScripts,
};
pub mod build;
pub mod channel_config;
pub mod concurrency;
pub mod index;
pub mod proxy;
pub mod repodata_config;
pub mod run_post_link_scripts;
pub mod s3;
pub mod tls;
use crate::config::channel_config::default_channel_config;
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum ValidationError {
#[error("Missing required field: {0}")]
MissingRequiredField(String),
#[error("Invalid value for field {0}: {1}")]
InvalidValue(String, String),
#[error("Invalid configuration: {0}")]
Invalid(String),
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum MergeError {
#[error("Error merging configurations: {0}")]
Error(String),
}
#[derive(Error, Debug)]
pub enum LoadError {
#[error("Error merging configuration files: {0} ({1})")]
MergeError(MergeError, PathBuf),
#[error("IO error while reading configuration file: {0}")]
IoError(#[from] std::io::Error),
#[error("Error parsing configuration file: {0}")]
ParseError(#[from] toml::de::Error),
#[error("Error validating configuration: {0}")]
ValidationError(#[from] ValidationError),
}
pub trait Config:
Serialize + DeserializeOwned + std::fmt::Debug + Clone + PartialEq + Default
{
fn merge_config(self, other: &Self) -> Result<Self, MergeError>;
fn validate(&self) -> Result<(), ValidationError> {
Ok(())
}
fn is_default(&self) -> bool {
self == &Self::default()
}
fn keys(&self) -> Vec<String> {
Vec::new()
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct NoExtension {}
impl Config for NoExtension {
fn merge_config(self, _other: &Self) -> Result<Self, MergeError> {
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CommonConfig {
#[serde(default)]
#[serde(alias = "default_channels")] #[serde(skip_serializing_if = "Option::is_none")]
pub default_channels: Option<Vec<NamedChannelOrUrl>>,
#[serde(default)]
#[serde(alias = "authentication_override_file")] #[serde(skip_serializing_if = "Option::is_none")]
pub authentication_override_file: Option<PathBuf>,
#[serde(default)]
#[serde(alias = "tls_no_verify")] #[serde(skip_serializing_if = "Option::is_none")]
pub tls_no_verify: Option<bool>,
#[serde(default)]
#[serde(alias = "tls_root_certs")] #[serde(skip_serializing_if = "Option::is_none")]
pub tls_root_certs: Option<tls::TlsRootCerts>,
#[serde(default)]
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub mirrors: IndexMap<Url, Vec<Url>>,
#[serde(default, skip_serializing_if = "BuildConfig::is_default")]
pub build: BuildConfig,
#[serde(skip, default = "default_channel_config")]
pub channel_config: ChannelConfig,
#[serde(alias = "repodata_config")] #[serde(default, skip_serializing_if = "RepodataConfig::is_empty")]
pub repodata_config: RepodataConfig,
#[serde(default)]
#[serde(skip_serializing_if = "ConcurrencyConfig::is_default")]
pub concurrency: ConcurrencyConfig,
#[serde(default)]
#[serde(skip_serializing_if = "ProxyConfig::is_default")]
pub proxy_config: ProxyConfig,
#[serde(default)]
#[serde(skip_serializing_if = "S3OptionsMap::is_default")]
pub s3_options: S3OptionsMap,
#[serde(default, skip_serializing_if = "IndexConfig::is_empty")]
pub index_config: IndexConfig,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub run_post_link_scripts: Option<RunPostLinkScripts>,
#[serde(default)]
#[serde(alias = "allow_symbolic_links")] #[serde(skip_serializing_if = "Option::is_none")]
pub allow_symbolic_links: Option<bool>,
#[serde(default)]
#[serde(alias = "allow_hard_links")] #[serde(skip_serializing_if = "Option::is_none")]
pub allow_hard_links: Option<bool>,
#[serde(default)]
#[serde(alias = "allow_ref_links")] #[serde(skip_serializing_if = "Option::is_none")]
pub allow_ref_links: Option<bool>,
}
impl Default for CommonConfig {
fn default() -> Self {
Self {
default_channels: None,
authentication_override_file: None,
tls_no_verify: None,
tls_root_certs: None,
mirrors: IndexMap::new(),
build: BuildConfig::default(),
channel_config: default_channel_config(),
repodata_config: RepodataConfig::default(),
concurrency: ConcurrencyConfig::default(),
proxy_config: ProxyConfig::default(),
s3_options: S3OptionsMap::default(),
index_config: IndexConfig::default(),
run_post_link_scripts: None,
allow_symbolic_links: None,
allow_hard_links: None,
allow_ref_links: None,
}
}
}
fn prefixed_keys(prefix: &str, keys: Vec<String>) -> Vec<String> {
keys.into_iter().map(|k| format!("{prefix}.{k}")).collect()
}
fn covered_by(key: &str, ignored: &BTreeSet<String>) -> bool {
ignored.iter().any(|path| {
key == path
|| key
.strip_prefix(path.as_str())
.is_some_and(|rest| rest.starts_with('.'))
})
}
impl Config for CommonConfig {
fn merge_config(self, other: &Self) -> Result<Self, MergeError> {
Ok(Self {
default_channels: other
.default_channels
.as_ref()
.or(self.default_channels.as_ref())
.cloned(),
authentication_override_file: other
.authentication_override_file
.as_ref()
.or(self.authentication_override_file.as_ref())
.cloned(),
tls_no_verify: other.tls_no_verify.or(self.tls_no_verify),
tls_root_certs: other.tls_root_certs.or(self.tls_root_certs),
mirrors: self
.mirrors
.iter()
.chain(other.mirrors.iter())
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
build: self.build.merge_config(&other.build)?,
channel_config: self.channel_config,
repodata_config: self.repodata_config.merge_config(&other.repodata_config)?,
concurrency: self.concurrency.merge_config(&other.concurrency)?,
proxy_config: self.proxy_config.merge_config(&other.proxy_config)?,
s3_options: self.s3_options.merge_config(&other.s3_options)?,
index_config: self.index_config.merge_config(&other.index_config)?,
run_post_link_scripts: other
.run_post_link_scripts
.clone()
.or(self.run_post_link_scripts),
allow_symbolic_links: other.allow_symbolic_links.or(self.allow_symbolic_links),
allow_hard_links: other.allow_hard_links.or(self.allow_hard_links),
allow_ref_links: other.allow_ref_links.or(self.allow_ref_links),
})
}
fn validate(&self) -> Result<(), ValidationError> {
self.build.validate()?;
self.repodata_config.validate()?;
self.concurrency.validate()?;
self.proxy_config.validate()?;
self.s3_options.validate()?;
self.index_config.validate()?;
Ok(())
}
fn keys(&self) -> Vec<String> {
let mut keys = vec![
"default-channels".to_string(),
"authentication-override-file".to_string(),
"tls-no-verify".to_string(),
"tls-root-certs".to_string(),
"mirrors".to_string(),
"run-post-link-scripts".to_string(),
"allow-symbolic-links".to_string(),
"allow-hard-links".to_string(),
"allow-ref-links".to_string(),
"s3-options".to_string(),
"index-config".to_string(),
];
keys.extend(prefixed_keys("build", self.build.keys()));
keys.extend(prefixed_keys(
"repodata-config",
self.repodata_config.keys(),
));
keys.extend(prefixed_keys("concurrency", self.concurrency.keys()));
keys.extend(prefixed_keys("proxy-config", self.proxy_config.keys()));
keys.extend(prefixed_keys("s3-options", self.s3_options.keys()));
keys
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConfigBase<T = NoExtension> {
#[serde(flatten)]
pub common: CommonConfig,
#[serde(flatten)]
pub extensions: T,
#[serde(skip)]
pub loaded_from: Vec<PathBuf>,
}
impl<T> Deref for ConfigBase<T> {
type Target = CommonConfig;
fn deref(&self) -> &Self::Target {
&self.common
}
}
impl<T> DerefMut for ConfigBase<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.common
}
}
impl<T> Config for ConfigBase<T>
where
T: Config,
{
fn merge_config(self, other: &Self) -> Result<Self, MergeError> {
Ok(Self {
common: self.common.merge_config(&other.common)?,
extensions: self.extensions.merge_config(&other.extensions)?,
loaded_from: self
.loaded_from
.iter()
.chain(&other.loaded_from)
.cloned()
.collect(),
})
}
fn validate(&self) -> Result<(), ValidationError> {
self.common.validate()?;
self.extensions.validate()
}
fn keys(&self) -> Vec<String> {
let mut keys = self.common.keys();
keys.extend(self.extensions.keys());
keys
}
}
impl<T> ConfigBase<T>
where
T: Config,
{
pub fn from_toml_str(input: &str) -> Result<(Self, BTreeSet<String>), toml::de::Error> {
let mut unknown_to_common = BTreeSet::new();
let common: CommonConfig = serde_ignored::deserialize(
toml::de::Deserializer::parse(input)?,
|path: serde_ignored::Path<'_>| {
unknown_to_common.insert(path.to_string());
},
)?;
let mut unknown_to_extension = BTreeSet::new();
let extensions: T = serde_ignored::deserialize(
toml::de::Deserializer::parse(input)?,
|path: serde_ignored::Path<'_>| {
unknown_to_extension.insert(path.to_string());
},
)?;
let unused = unknown_to_common
.iter()
.filter(|key| covered_by(key, &unknown_to_extension))
.chain(
unknown_to_extension
.iter()
.filter(|key| covered_by(key, &unknown_to_common)),
)
.cloned()
.collect::<BTreeSet<String>>();
let unused = unused
.iter()
.filter(|key| {
!unused.iter().any(|other| {
other
.strip_prefix(key.as_str())
.is_some_and(|rest| rest.starts_with('.'))
})
})
.cloned()
.collect();
Ok((
Self {
common,
extensions,
loaded_from: Vec::new(),
},
unused,
))
}
pub fn load_from_files<I, P>(paths: I) -> Result<Self, LoadError>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let mut config = Self::default();
for path in paths {
let path = path.as_ref();
let content = fs_err::read_to_string(path)?;
let (mut other, unused) = Self::from_toml_str(&content)?;
for key in &unused {
tracing::warn!(
"Ignoring unknown configuration key `{key}` in {}",
path.display()
);
}
other.loaded_from.push(path.to_path_buf());
config = config
.merge_config(&other)
.map_err(|e| LoadError::MergeError(e, path.to_path_buf()))?;
}
config.validate()?;
Ok(config)
}
pub fn load_from_default_locations(tool_dirs: &[&str]) -> Result<Self, LoadError> {
Self::load_from_files(
crate::locations::config_search_paths(tool_dirs)
.into_iter()
.filter(|path| path.is_file()),
)
}
}