use std::path::PathBuf;
#[cfg(all(feature = "s3", feature = "rattler_config"))]
use anyhow::Context;
use clap::{Parser, Subcommand, ValueEnum};
use clap_verbosity_flag::Verbosity;
use rattler_conda_types::Platform;
#[cfg(feature = "rattler_config")]
use rattler_config::config::concurrency::default_max_concurrent_solves;
use rattler_index::{
index_fs, IndexFsConfig, PackageRevisionAssignment, RepodataRevision, RepodataRevisionInfo,
};
#[cfg(feature = "s3")]
use rattler_index::{index_s3, IndexS3Config, PreconditionChecks};
#[cfg(feature = "s3")]
use rattler_networking::AuthenticationStorage;
#[cfg(feature = "s3")]
use rattler_s3::S3Credentials;
#[cfg(feature = "s3")]
use url::Url;
#[cfg(feature = "s3")]
fn parse_s3_url(value: &str) -> Result<Url, String> {
let url: Url = Url::parse(value).map_err(|e| format!("`{value}` isn't a valid URL: {e}"))?;
if url.scheme() == "s3" && url.host_str().is_some() {
Ok(url)
} else {
Err(format!(
"Only S3 URLs of format s3://bucket/... can be used, not `{value}`"
))
}
}
#[derive(Parser)]
#[command(name = "rattler-index", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[command(flatten)]
verbosity: Verbosity,
#[arg(long, default_value = "true", global = true)]
write_zst: Option<bool>,
#[arg(long, default_value = "true", global = true)]
write_shards: Option<bool>,
#[arg(long, global = true)]
repodata_revision: Vec<RepodataRevision>,
#[arg(long, value_enum, default_value = "from-index-json", global = true)]
package_revision_assignment: CliPackageRevisionAssignment,
#[arg(short, long, default_value = "false", global = true)]
force: bool,
#[arg(long, global = true)]
max_parallel: Option<usize>,
#[arg(long, global = true)]
target_platform: Option<Platform>,
#[arg(long, global = true)]
repodata_patch: Option<String>,
#[cfg(feature = "s3")]
#[arg(long, default_value = "false", global = true)]
disable_precondition_checks: bool,
#[arg(long)]
config: Option<PathBuf>,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")]
enum CliPackageRevisionAssignment {
FromIndexJson,
Latest,
}
impl From<CliPackageRevisionAssignment> for PackageRevisionAssignment {
fn from(value: CliPackageRevisionAssignment) -> Self {
match value {
CliPackageRevisionAssignment::FromIndexJson => PackageRevisionAssignment::FromIndexJson,
CliPackageRevisionAssignment::Latest => PackageRevisionAssignment::Latest,
}
}
}
fn repodata_revisions(revisions: &[RepodataRevision]) -> Vec<RepodataRevisionInfo> {
revisions
.iter()
.copied()
.map(|revision| RepodataRevisionInfo {
revision,
n_packages: None,
oldest: None,
newest: None,
})
.collect()
}
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
enum Commands {
#[command(name = "fs")]
FileSystem {
#[arg()]
channel: std::path::PathBuf,
},
#[cfg(feature = "s3")]
S3 {
#[arg(value_parser = parse_s3_url)]
channel: Url,
#[clap(flatten)]
credentials: rattler_s3::clap::S3CredentialsOpts,
},
}
#[cfg(feature = "rattler_config")]
pub type Config = rattler_config::config::ConfigBase<()>;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(cli.verbosity)
.init();
let multi_progress = indicatif::MultiProgress::new();
#[cfg(feature = "rattler_config")]
let config = if let Some(config_path) = cli.config {
Some(Config::load_from_files(vec![config_path])?)
} else {
None
};
#[cfg(feature = "rattler_config")]
let max_parallel = cli
.max_parallel
.or(config.as_ref().map(|c| c.concurrency.downloads))
.unwrap_or_else(default_max_concurrent_solves);
#[cfg(not(feature = "rattler_config"))]
let max_parallel = cli.max_parallel.unwrap_or(10);
#[cfg(feature = "s3")]
let precondition_checks = if cli.disable_precondition_checks {
PreconditionChecks::Disabled
} else {
PreconditionChecks::Enabled
};
match cli.command {
Commands::FileSystem { channel } => {
index_fs(IndexFsConfig {
channel,
target_platform: cli.target_platform,
repodata_patch: cli.repodata_patch,
write_zst: cli.write_zst.unwrap_or(true),
write_shards: cli.write_shards.unwrap_or(true),
repodata_revisions: repodata_revisions(&cli.repodata_revision),
package_revision_assignment: cli.package_revision_assignment.into(),
force: cli.force,
max_parallel,
multi_progress: Some(multi_progress),
})
.await
}
#[cfg(feature = "s3")]
Commands::S3 {
channel,
#[cfg(feature = "rattler_config")]
mut credentials,
#[cfg(not(feature = "rattler_config"))]
credentials,
} => {
#[cfg(feature = "rattler_config")]
{
let bucket = channel.host().context("Invalid S3 url")?.to_string();
let s3_config = config
.as_ref()
.and_then(|config| config.s3_options.0.get(&bucket));
credentials.region = credentials.region.or(s3_config.map(|c| c.region.clone()));
credentials.endpoint_url = credentials
.endpoint_url
.or(s3_config.map(|c| c.endpoint_url.clone()));
}
let credentials = match Option::<S3Credentials>::from(credentials) {
Some(credentials) => {
let auth_storage = AuthenticationStorage::from_env_and_defaults()?;
credentials.resolve(&channel, &auth_storage).ok_or_else(|| anyhow::anyhow!("Could not find S3 credentials in the authentication storage, and no credentials were provided via the command line."))?
}
None => rattler_s3::ResolvedS3Credentials::from_sdk().await?,
};
index_s3(IndexS3Config {
channel,
credentials,
target_platform: cli.target_platform,
repodata_patch: cli.repodata_patch,
write_zst: cli.write_zst.unwrap_or(true),
write_shards: cli.write_shards.unwrap_or(true),
repodata_revisions: repodata_revisions(&cli.repodata_revision),
package_revision_assignment: cli.package_revision_assignment.into(),
force: cli.force,
max_parallel,
multi_progress: Some(multi_progress),
precondition_checks,
})
.await
}
}?;
println!("Finished indexing channel.");
Ok(())
}