use std::path::PathBuf;
use anyhow::Context;
use clap::{arg, Parser, Subcommand};
use clap_verbosity_flag::Verbosity;
use rattler_conda_types::Platform;
use rattler_config::config::concurrency::default_max_concurrent_solves;
use rattler_index::{index_fs, index_s3, IndexFsConfig, IndexS3Config};
use rattler_networking::AuthenticationStorage;
use rattler_s3::S3Credentials;
use url::Url;
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(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>,
#[arg(long)]
config: Option<PathBuf>,
}
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
enum Commands {
#[command(name = "fs")]
FileSystem {
#[arg()]
channel: std::path::PathBuf,
},
S3 {
#[arg(value_parser = parse_s3_url)]
channel: Url,
#[clap(flatten)]
credentials: rattler_s3::clap::S3CredentialsOpts,
},
}
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();
let config = if let Some(config_path) = cli.config {
Some(Config::load_from_files(vec![config_path])?)
} else {
None
};
let max_parallel = cli
.max_parallel
.or(config.as_ref().map(|c| c.concurrency.downloads))
.unwrap_or_else(default_max_concurrent_solves);
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),
force: cli.force,
max_parallel,
multi_progress: Some(multi_progress),
})
.await
}
Commands::S3 {
channel,
mut credentials,
} => {
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),
force: cli.force,
max_parallel,
multi_progress: Some(multi_progress),
})
.await
}
}?;
println!("Finished indexing channel.");
Ok(())
}