use std::{collections::HashMap, path::PathBuf, sync::Arc};
use clap::ValueEnum;
use rattler::package_cache::PackageCache;
use rattler_conda_types::{ChannelConfig, Platform};
#[cfg(feature = "s3")]
use rattler_networking::s3_middleware;
use rattler_networking::{
AuthenticationStorage,
authentication_storage::{self, AuthenticationStorageError},
mirror_middleware,
};
use rattler_repodata_gateway::Gateway;
use rattler_solve::ChannelPriority;
#[cfg(feature = "s3")]
use thiserror::Error;
use url::Url;
use crate::console_utils::LoggingOutputHandler;
pub const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum SkipExisting {
None,
Local,
All,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum TestStrategy {
Skip,
Native,
#[default]
NativeAndEmulated,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContinueOnFailure {
Yes,
#[default]
No,
}
impl From<bool> for ContinueOnFailure {
fn from(value: bool) -> Self {
if value {
ContinueOnFailure::Yes
} else {
ContinueOnFailure::No
}
}
}
#[derive(Clone)]
pub struct Configuration {
pub fancy_log_handler: LoggingOutputHandler,
pub client: rattler_build_networking::BaseClient,
pub source_cache: Option<Arc<rattler_build_source_cache::SourceCache>>,
pub no_clean: bool,
pub test_strategy: TestStrategy,
pub use_zstd: bool,
pub use_bz2: bool,
pub use_sharded: bool,
pub skip_existing: SkipExisting,
pub noarch_build_platform: Option<Platform>,
pub channel_config: ChannelConfig,
pub compression_threads: Option<u32>,
pub io_concurrency_limit: Option<usize>,
pub package_cache: PackageCache,
pub repodata_gateway: Gateway,
pub channel_priority: ChannelPriority,
pub allow_insecure_host: Option<Vec<String>>,
pub continue_on_failure: ContinueOnFailure,
pub error_prefix_in_binary: bool,
pub allow_symlinks_on_windows: bool,
pub allow_absolute_license_paths: bool,
pub environments_externally_managed: bool,
}
pub fn get_auth_store(
auth_file: Option<PathBuf>,
) -> Result<AuthenticationStorage, AuthenticationStorageError> {
match auth_file {
Some(auth_file) => {
let mut store = AuthenticationStorage::empty();
store.add_backend(Arc::from(
authentication_storage::backends::file::FileStorage::from_path(auth_file)?,
));
Ok(store)
}
None => rattler_networking::AuthenticationStorage::from_env_and_defaults(),
}
}
pub fn reqwest_client_from_auth_storage(
auth_file: Option<PathBuf>,
#[cfg(feature = "s3")] s3_middleware_config: HashMap<String, s3_middleware::S3Config>,
mirror_middleware_config: HashMap<Url, Vec<mirror_middleware::Mirror>>,
allow_insecure_host: Option<Vec<String>>,
) -> Result<rattler_build_networking::BaseClient, AuthenticationStorageError> {
let auth_storage = get_auth_store(auth_file)?;
let builder = rattler_build_networking::BaseClient::builder()
.user_agent(APP_USER_AGENT)
.timeout(5 * 60)
.insecure_hosts(allow_insecure_host.unwrap_or_default())
.with_authentication(auth_storage);
#[cfg(feature = "s3")]
let builder = builder.with_s3(s3_middleware_config);
Ok(builder.with_mirrors(mirror_middleware_config).build())
}
pub struct ConfigurationBuilder {
cache_dir: Option<PathBuf>,
fancy_log_handler: Option<LoggingOutputHandler>,
client: Option<rattler_build_networking::BaseClient>,
no_clean: bool,
no_test: bool,
test_strategy: TestStrategy,
use_zstd: bool,
use_bz2: bool,
use_sharded: bool,
skip_existing: SkipExisting,
noarch_build_platform: Option<Platform>,
channel_config: Option<ChannelConfig>,
compression_threads: Option<u32>,
io_concurrency_limit: Option<usize>,
channel_priority: ChannelPriority,
allow_insecure_host: Option<Vec<String>>,
continue_on_failure: ContinueOnFailure,
error_prefix_in_binary: bool,
allow_symlinks_on_windows: bool,
allow_absolute_license_paths: bool,
environments_externally_managed: bool,
}
impl Configuration {
pub fn builder() -> ConfigurationBuilder {
ConfigurationBuilder::new()
}
}
impl ConfigurationBuilder {
fn new() -> Self {
Self {
cache_dir: None,
fancy_log_handler: None,
client: None,
no_clean: false,
no_test: false,
test_strategy: TestStrategy::default(),
use_zstd: true,
use_bz2: true,
use_sharded: true,
skip_existing: SkipExisting::None,
noarch_build_platform: None,
channel_config: None,
compression_threads: None,
io_concurrency_limit: None,
channel_priority: ChannelPriority::Strict,
allow_insecure_host: None,
continue_on_failure: ContinueOnFailure::No,
error_prefix_in_binary: false,
allow_symlinks_on_windows: false,
allow_absolute_license_paths: false,
environments_externally_managed: false,
}
}
pub fn with_cache_dir(self, cache_dir: PathBuf) -> Self {
Self {
cache_dir: Some(cache_dir),
..self
}
}
pub fn with_continue_on_failure(self, continue_on_failure: ContinueOnFailure) -> Self {
Self {
continue_on_failure,
..self
}
}
pub fn with_error_prefix_in_binary(self, error_prefix_in_binary: bool) -> Self {
Self {
error_prefix_in_binary,
..self
}
}
pub fn with_allow_symlinks_on_windows(self, allow_symlinks_on_windows: bool) -> Self {
Self {
allow_symlinks_on_windows,
..self
}
}
pub fn with_allow_absolute_license_paths(self, allow_absolute_license_paths: bool) -> Self {
Self {
allow_absolute_license_paths,
..self
}
}
pub fn with_opt_cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
Self { cache_dir, ..self }
}
pub fn with_logging_output_handler(self, fancy_log_handler: LoggingOutputHandler) -> Self {
Self {
fancy_log_handler: Some(fancy_log_handler),
..self
}
}
pub fn with_skip_existing(self, skip_existing: SkipExisting) -> Self {
Self {
skip_existing,
..self
}
}
pub fn with_channel_config(self, channel_config: ChannelConfig) -> Self {
Self {
channel_config: Some(channel_config),
..self
}
}
pub fn with_compression_threads(self, compression_threads: Option<u32>) -> Self {
Self {
compression_threads,
..self
}
}
pub fn with_io_concurrency_limit(self, io_concurrency_limit: Option<usize>) -> Self {
Self {
io_concurrency_limit,
..self
}
}
pub fn with_keep_build(self, keep_build: bool) -> Self {
Self {
no_clean: keep_build,
..self
}
}
pub fn with_reqwest_client(self, client: rattler_build_networking::BaseClient) -> Self {
Self {
client: Some(client),
..self
}
}
pub fn with_testing(self, testing_enabled: bool) -> Self {
Self {
no_test: !testing_enabled,
..self
}
}
pub fn with_test_strategy(self, test_strategy: TestStrategy) -> Self {
Self {
test_strategy,
..self
}
}
pub fn with_zstd_repodata_enabled(self, zstd_repodata_enabled: bool) -> Self {
Self {
use_zstd: zstd_repodata_enabled,
..self
}
}
pub fn with_bz2_repodata_enabled(self, bz2_repodata_enabled: bool) -> Self {
Self {
use_bz2: bz2_repodata_enabled,
..self
}
}
pub fn with_sharded_repodata_enabled(self, sharded_repodata_enabled: bool) -> Self {
Self {
use_sharded: sharded_repodata_enabled,
..self
}
}
pub fn with_noarch_build_platform(self, noarch_build_platform: Option<Platform>) -> Self {
Self {
noarch_build_platform,
..self
}
}
pub fn with_channel_priority(self, channel_priority: ChannelPriority) -> Self {
Self {
channel_priority,
..self
}
}
pub fn with_allow_insecure_host(self, allow_insecure_host: Option<Vec<String>>) -> Self {
Self {
allow_insecure_host,
..self
}
}
pub fn with_environments_externally_managed(
self,
environments_externally_managed: bool,
) -> Self {
Self {
environments_externally_managed,
..self
}
}
pub fn finish(self) -> Configuration {
let cache_dir = self.cache_dir.unwrap_or_else(|| {
rattler_cache::default_cache_dir().expect("failed to determine default cache directory")
});
let client = self.client.unwrap_or_default();
let package_cache = PackageCache::new(cache_dir.join(rattler_cache::PACKAGE_CACHE_DIR));
let channel_config = self.channel_config.unwrap_or_else(|| {
ChannelConfig::default_with_root_dir(
std::env::current_dir().unwrap_or_else(|_err| PathBuf::from("/")),
)
});
let repodata_gateway = Gateway::builder()
.with_cache_dir(cache_dir.join(rattler_cache::REPODATA_CACHE_DIR))
.with_package_cache(package_cache.clone())
.with_client(client.get_client().clone())
.with_channel_config(rattler_repodata_gateway::ChannelConfig {
default: rattler_repodata_gateway::SourceConfig {
zstd_enabled: self.use_zstd,
bz2_enabled: self.use_bz2,
sharded_enabled: self.use_sharded,
cache_action: Default::default(),
},
per_channel: Default::default(),
})
.finish();
let test_strategy = match self.no_test {
true => TestStrategy::Skip,
false => self.test_strategy,
};
Configuration {
fancy_log_handler: self.fancy_log_handler.unwrap_or_default(),
client,
source_cache: None, no_clean: self.no_clean,
test_strategy,
use_zstd: self.use_zstd,
use_bz2: self.use_bz2,
use_sharded: self.use_sharded,
skip_existing: self.skip_existing,
noarch_build_platform: self.noarch_build_platform,
channel_config,
compression_threads: self.compression_threads,
io_concurrency_limit: self.io_concurrency_limit,
package_cache,
repodata_gateway,
channel_priority: self.channel_priority,
allow_insecure_host: self.allow_insecure_host,
continue_on_failure: self.continue_on_failure,
error_prefix_in_binary: self.error_prefix_in_binary,
allow_symlinks_on_windows: self.allow_symlinks_on_windows,
allow_absolute_license_paths: self.allow_absolute_license_paths,
environments_externally_managed: self.environments_externally_managed,
}
}
}
#[cfg(feature = "s3")]
#[derive(Debug, Error)]
pub enum S3CredentialError {
#[error("Failed to get authentication storage: {0}")]
AuthStorageError(#[from] AuthenticationStorageError),
#[error("Failed to load credentials from AWS SDK: {0}")]
SdkError(#[from] rattler_s3::FromSDKError),
#[error(
"No S3 credentials found for bucket '{bucket}'. Please configure credentials via `rattler auth` or AWS environment variables."
)]
NoCredentials {
bucket: String,
},
}
#[cfg(feature = "s3")]
pub async fn resolve_s3_credentials(
s3_config: &HashMap<String, s3_middleware::S3Config>,
auth_file: Option<PathBuf>,
bucket_url: &Url,
) -> Result<rattler_s3::ResolvedS3Credentials, S3CredentialError> {
let bucket_name = bucket_url.host_str().unwrap_or_default();
if let Some(config) = s3_config.get(bucket_name)
&& let s3_middleware::S3Config::Custom {
endpoint_url,
region,
force_path_style,
} = config
{
let s3_creds = rattler_s3::S3Credentials {
endpoint_url: endpoint_url.clone(),
region: region.clone(),
addressing_style: if *force_path_style {
rattler_s3::S3AddressingStyle::Path
} else {
rattler_s3::S3AddressingStyle::VirtualHost
},
access_key_id: None,
secret_access_key: None,
session_token: None,
};
let auth_storage = get_auth_store(auth_file.clone())?;
if let Some(resolved) = s3_creds.resolve(bucket_url, &auth_storage) {
tracing::debug!(
"Resolved S3 credentials for bucket '{}' from config + auth storage",
bucket_name
);
return Ok(resolved);
}
}
tracing::debug!(
"Using AWS SDK default credential chain for bucket '{}'",
bucket_name
);
let resolved = rattler_s3::ResolvedS3Credentials::from_sdk().await?;
Ok(resolved)
}