From cf548aa6f255d13c7e023fc331b456cd48cbe8bd Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Thu, 14 Mar 2024 13:19:23 +0100 Subject: [PATCH] feat: implement global config options (#960) Co-authored-by: Ruben Arts --- docs/advanced/global_configuration.md | 32 +++ docs/configuration.md | 4 + mkdocs.yml | 3 +- src/cli/add.rs | 7 +- src/cli/global/common.rs | 53 ++-- src/cli/global/install.rs | 17 +- src/cli/global/list.rs | 9 +- src/cli/global/upgrade.rs | 12 +- src/cli/global/upgrade_all.rs | 17 +- src/cli/info.rs | 16 +- src/cli/init.rs | 33 +-- src/cli/install.rs | 4 + src/cli/project/channel/add.rs | 6 +- src/cli/project/channel/remove.rs | 6 +- src/cli/remove.rs | 7 +- src/cli/run.rs | 7 +- src/cli/search.rs | 62 +++-- src/cli/shell.rs | 79 ++---- src/config.rs | 255 +++++++++++++++++- src/consts.rs | 5 +- src/project/mod.rs | 52 ++-- src/prompt.rs | 5 - .../pixi__config__tests__config_merge.snap | 38 +++ src/utils/mod.rs | 1 + src/utils/reqwest.rs | 43 +++ tests/common/mod.rs | 3 + tests/config/config_1.toml | 2 + tests/config/config_2.toml | 2 + 28 files changed, 589 insertions(+), 191 deletions(-) create mode 100644 docs/advanced/global_configuration.md create mode 100644 src/snapshots/pixi__config__tests__config_merge.snap create mode 100644 src/utils/reqwest.rs create mode 100644 tests/config/config_1.toml create mode 100644 tests/config/config_2.toml diff --git a/docs/advanced/global_configuration.md b/docs/advanced/global_configuration.md new file mode 100644 index 000000000..c27f0b98d --- /dev/null +++ b/docs/advanced/global_configuration.md @@ -0,0 +1,32 @@ +# Global configuration in pixi + +Pixi supports some global configuration options, as well as project-scoped configuration (that does not belong into the project file). +The configuration is loaded in the following order: + +1. Global configuration folder (e.g. `~/.config/pixi/config.toml` on Linux, dependent on XDG_CONFIG_HOME) +2. Global .pixi folder: `~/.pixi/config.toml` (or `$PIXI_HOME/config.toml` if the `PIXI_HOME` environment variable is set) +3. Project-local .pixi folder: `$PIXI_PROJECT/.pixi/config.toml` +4. Command line arguments (`--tls-no-verify`, `--change-ps1=false` etc.) + +!!! note + To find the locations where `pixi` looks for configuration files, run `pixi` with `-v` or `--verbose`. + +## Reference + +The following reference describes all available configuration options. + +```toml +# The default channels to select when running `pixi init` or `pixi global install`. +# This defaults to only conda-forge. +default_channels = ["conda-forge"] + +# When set to false, the `(pixi)` prefix in the shell prompt is removed. +# This applies to the `pixi shell` subcommand. +# You can override this from the CLI with `--change-ps1`. +change_ps1 = true + +# When set to true, the TLS certificates are not verified. Note that this is a +# security risk and should only be used for testing purposes or internal networks. +# You can override this from the CLI with `--tls-no-verify`. +tls_no_verify = false +``` diff --git a/docs/configuration.md b/docs/configuration.md index 06425bb53..63558870c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -503,3 +503,7 @@ test = {features = ["test"], solve-group = "test"} prod = {features = ["prod"], solve-group = "test"} lint = ["lint"] ``` + +## Global configuration + +The global configuration options are documented in the [global configuration](advanced/global_configuration.md) section. diff --git a/mkdocs.yml b/mkdocs.yml index c7cacdcfe..3cd906306 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -109,9 +109,10 @@ nav: - Authentication: advanced/authentication.md - Tasks: advanced/advanced_tasks.md - Multi Platform: advanced/multi_platform_configuration.md - - Info command: advanced/explain_info_command.md + - Info Command: advanced/explain_info_command.md - Channel Logic: advanced/channel_priority.md - GitHub Actions: advanced/github_actions.md + - Global Configuration: advanced/global_configuration.md - Examples: - C++/Cmake: examples/cpp-sdl.md - OpenCV: examples/opencv.md diff --git a/src/cli/add.rs b/src/cli/add.rs index 92bdd1b61..a1bfd5fbb 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -1,4 +1,5 @@ use crate::{ + config::ConfigCli, environment::{get_up_to_date_prefix, verify_prefix_location_unchanged, LockFileUsage}, project::{manifest::PyPiRequirement, DependencyType, Project, SpecType}, FeatureName, @@ -91,6 +92,9 @@ pub struct Args { /// The feature for which the dependency should be added #[arg(long, short)] pub feature: Option, + + #[clap(flatten)] + pub config: ConfigCli, } impl DependencyType { @@ -108,7 +112,8 @@ impl DependencyType { } pub async fn execute(args: Args) -> miette::Result<()> { - let mut project = Project::load_or_else_discover(args.manifest_path.as_deref())?; + let mut project = Project::load_or_else_discover(args.manifest_path.as_deref())? + .with_cli_config(args.config.clone()); let dependency_type = DependencyType::from_args(&args); let spec_platforms = &args.platform; diff --git a/src/cli/global/common.rs b/src/cli/global/common.rs index cbe985327..ec0ce1271 100644 --- a/src/cli/global/common.rs +++ b/src/cli/global/common.rs @@ -11,7 +11,7 @@ use rattler_repodata_gateway::sparse::SparseRepoData; use rattler_solve::{resolvo, SolverImpl, SolverTask}; use reqwest_middleware::ClientWithMiddleware; -use crate::{prefix::Prefix, repodata}; +use crate::{config::home_path, prefix::Prefix, repodata}; /// Global binaries directory, default to `$HOME/.pixi/bin` pub struct BinDir(pub PathBuf); @@ -19,7 +19,9 @@ pub struct BinDir(pub PathBuf); impl BinDir { /// Create the Binary Executable directory pub async fn create() -> miette::Result { - let bin_dir = bin_dir()?; + let bin_dir = bin_dir().ok_or(miette::miette!( + "could not determine global binary executable directory" + ))?; tokio::fs::create_dir_all(&bin_dir) .await .into_diagnostic()?; @@ -28,7 +30,9 @@ impl BinDir { /// Get the Binary Executable directory, erroring if it doesn't already exist. pub async fn from_existing() -> miette::Result { - let bin_dir = bin_dir()?; + let bin_dir = bin_dir().ok_or(miette::miette!( + "could not find global binary executable directory" + ))?; if tokio::fs::try_exists(&bin_dir).await.into_diagnostic()? { Ok(Self(bin_dir)) } else { @@ -39,39 +43,17 @@ impl BinDir { } } -/// Get pixi home directory, default to `$HOME/.pixi` -/// -/// It may be overridden by the `PIXI_HOME` environment variable. -/// -/// # Returns -/// -/// The pixi home directory -pub fn home_path() -> miette::Result { - if let Some(path) = std::env::var_os("PIXI_HOME") { - Ok(PathBuf::from(path)) - } else { - dirs::home_dir() - .map(|path| path.join(".pixi")) - .ok_or_else(|| miette::miette!("could not find home directory")) - } -} - -/// Global binaries directory, default to `$HOME/.pixi/bin` -/// -/// # Returns -/// -/// The global binaries directory -pub fn bin_dir() -> miette::Result { - home_path().map(|path| path.join("bin")) -} - /// Global binary environments directory, default to `$HOME/.pixi/envs` pub struct BinEnvDir(pub PathBuf); impl BinEnvDir { /// Construct the path to the env directory for the binary package `package_name`. fn package_bin_env_dir(package_name: &PackageName) -> miette::Result { - Ok(bin_env_dir()?.join(package_name.as_normalized())) + Ok(bin_env_dir() + .ok_or(miette::miette!( + "could not find global binary environment directory" + ))? + .join(package_name.as_normalized())) } /// Get the Binary Environment directory, erroring if it doesn't already exist. @@ -100,12 +82,21 @@ impl BinEnvDir { } } +/// Global binaries directory, default to `$HOME/.pixi/bin` +/// +/// # Returns +/// +/// The global binaries directory +pub fn bin_dir() -> Option { + home_path().map(|path| path.join("bin")) +} + /// Global binary environments directory, default to `$HOME/.pixi/envs` /// /// # Returns /// /// The global binary environments directory -pub fn bin_env_dir() -> miette::Result { +pub fn bin_env_dir() -> Option { home_path().map(|path| path.join("envs")) } diff --git a/src/cli/global/install.rs b/src/cli/global/install.rs index f00ec13a5..852daca6e 100644 --- a/src/cli/global/install.rs +++ b/src/cli/global/install.rs @@ -2,6 +2,7 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::Arc; +use crate::config::Config; use crate::install::execute_transaction; use crate::{config, prefix::Prefix, progress::await_in_progress}; use clap::Parser; @@ -10,8 +11,7 @@ use miette::IntoDiagnostic; use rattler::install::Transaction; use rattler::package_cache::PackageCache; use rattler_conda_types::{ - Channel, ChannelConfig, MatchSpec, PackageName, ParseStrictness, Platform, PrefixRecord, - RepoDataRecord, + MatchSpec, PackageName, ParseStrictness, Platform, PrefixRecord, RepoDataRecord, }; use rattler_shell::{ activation::{ActivationVariables, Activator, PathModificationBehavior}, @@ -41,7 +41,7 @@ pub struct Args { /// For example: `pixi global install --channel conda-forge --channel bioconda`. /// /// By default, if no channel is provided, `conda-forge` is used. - #[clap(short, long, default_values = ["conda-forge"])] + #[clap(short, long)] channel: Vec, } @@ -231,13 +231,8 @@ pub(super) async fn create_executable_scripts( /// Install a global command pub async fn execute(args: Args) -> miette::Result<()> { // Figure out what channels we are using - let channel_config = ChannelConfig::default(); - let channels = args - .channel - .iter() - .map(|c| Channel::from_str(c, &channel_config)) - .collect::, _>>() - .into_diagnostic()?; + let config = Config::load_global(); + let channels = config.compute_channels(&args.channel).into_diagnostic()?; // Find the MatchSpec we want to install let specs = args @@ -258,7 +253,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let (prefix_package, scripts, _) = globally_install_package(&package_name, records, authenticated_client.clone()).await?; - let channel_name = channel_name_from_prefix(&prefix_package, &channel_config); + let channel_name = channel_name_from_prefix(&prefix_package, config.channel_config()); let record = &prefix_package.repodata_record.package_record; // Warn if no executables were created for the package diff --git a/src/cli/global/list.rs b/src/cli/global/list.rs index 8e075301e..1a6e1684c 100644 --- a/src/cli/global/list.rs +++ b/src/cli/global/list.rs @@ -6,9 +6,10 @@ use itertools::Itertools; use miette::IntoDiagnostic; use rattler_conda_types::PackageName; +use crate::config::home_path; use crate::prefix::Prefix; -use super::common::{bin_env_dir, find_designated_package, home_path, BinDir, BinEnvDir}; +use super::common::{bin_env_dir, find_designated_package, BinDir, BinEnvDir}; use super::install::{find_and_map_executable_scripts, BinScriptMapping}; /// Lists all packages previously installed into a globally accessible location via `pixi global install`. @@ -90,7 +91,7 @@ pub async fn execute(_args: Args) -> miette::Result<()> { if package_info.is_empty() { print_no_packages_found_message(); } else { - let path = home_path()?; + let path = home_path().ok_or(miette::miette!("Could not determine home directory"))?; let len = package_info.len(); let mut message = String::new(); for (idx, pkgi) in package_info.into_iter().enumerate() { @@ -139,7 +140,9 @@ pub async fn execute(_args: Args) -> miette::Result<()> { /// A list of all globally installed packages represented as [`PackageName`]s pub(super) async fn list_global_packages() -> miette::Result> { let mut packages = vec![]; - let Ok(mut dir_contents) = tokio::fs::read_dir(bin_env_dir()?).await else { + let bin_env_dir = + bin_env_dir().ok_or(miette::miette!("Could not determine global envs directory"))?; + let Ok(mut dir_contents) = tokio::fs::read_dir(bin_env_dir).await else { return Ok(vec![]); }; diff --git a/src/cli/global/upgrade.rs b/src/cli/global/upgrade.rs index 92d6c1eb8..35956182a 100644 --- a/src/cli/global/upgrade.rs +++ b/src/cli/global/upgrade.rs @@ -4,10 +4,11 @@ use clap::Parser; use indicatif::ProgressBar; use itertools::Itertools; use miette::IntoDiagnostic; -use rattler_conda_types::{Channel, ChannelConfig, MatchSpec, PackageName, Version}; +use rattler_conda_types::{Channel, MatchSpec, PackageName, Version}; use rattler_conda_types::{ParseStrictness, RepoDataRecord}; use reqwest_middleware::ClientWithMiddleware; +use crate::config::Config; use crate::progress::{global_multi_progress, long_running_progress_style}; use super::common::{ @@ -32,7 +33,7 @@ pub struct Args { /// /// By default, if no channel is provided, `conda-forge` is used, the channel /// the package was installed from will always be used. - #[clap(short, long, default_values = ["conda-forge"])] + #[clap(short, long)] channel: Vec, } @@ -62,11 +63,12 @@ pub async fn execute(args: Args) -> miette::Result<()> { .version .into_version(); + let config = Config::load_global(); + // Figure out what channels we are using - let channel_config = ChannelConfig::default(); let last_installed_channel = Channel::from_str( prefix_record.repodata_record.channel.clone(), - &channel_config, + config.channel_config(), ) .into_diagnostic()?; @@ -74,7 +76,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let input_channels = args .channel .iter() - .map(|c| Channel::from_str(c, &channel_config)) + .map(|c| Channel::from_str(c, config.channel_config())) .collect::, _>>() .into_diagnostic()?; channels.extend(input_channels); diff --git a/src/cli/global/upgrade_all.rs b/src/cli/global/upgrade_all.rs index 593d7a32e..9294bf4de 100644 --- a/src/cli/global/upgrade_all.rs +++ b/src/cli/global/upgrade_all.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use clap::Parser; use itertools::Itertools; use miette::IntoDiagnostic; -use rattler_conda_types::{Channel, ChannelConfig, MatchSpec, ParseStrictness}; +use rattler_conda_types::{Channel, MatchSpec, ParseStrictness}; + +use crate::config::Config; use super::{ common::{find_installed_package, get_client_and_sparse_repodata, load_package_records}, @@ -23,19 +25,14 @@ pub struct Args { /// /// By default, if no channel is provided, `conda-forge` is used, the channel /// the package was installed from will always be used. - #[clap(short, long, default_values = ["conda-forge"])] + #[clap(short, long)] channel: Vec, } pub async fn execute(args: Args) -> miette::Result<()> { let packages = list_global_packages().await?; - let channel_config = ChannelConfig::default(); - let mut channels = args - .channel - .iter() - .map(|c| Channel::from_str(c, &channel_config)) - .collect::, _>>() - .into_diagnostic()?; + let config = Config::load_global(); + let mut channels = config.compute_channels(&args.channel).into_diagnostic()?; let mut installed_versions = HashMap::with_capacity(packages.len()); @@ -43,7 +40,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let prefix_record = find_installed_package(package_name).await?; let last_installed_channel = Channel::from_str( prefix_record.repodata_record.channel.clone(), - &channel_config, + config.channel_config(), ) .into_diagnostic()?; diff --git a/src/cli/info.rs b/src/cli/info.rs index 1254c0b08..7185f7282 100644 --- a/src/cli/info.rs +++ b/src/cli/info.rs @@ -39,6 +39,7 @@ pub struct ProjectInfo { last_updated: Option, pixi_folder_size: Option, version: Option, + configuration: Vec, } #[derive(Serialize)] @@ -184,7 +185,6 @@ impl Display for Info { } writeln!(f, "{:>WIDTH$}: {}", bold.apply_to("Cache dir"), cache_dir)?; - if let Some(cache_size) = &self.cache_size { writeln!(f, "{:>WIDTH$}: {}", bold.apply_to("Cache size"), cache_size)?; } @@ -208,6 +208,19 @@ impl Display for Info { pi.manifest_path.to_string_lossy() )?; + let config_locations = pi + .configuration + .iter() + .map(|p| p.to_string_lossy()) + .join(", "); + + writeln!( + f, + "{:>WIDTH$}: {}", + bold.apply_to("Config locations"), + config_locations + )?; + if let Some(update_time) = &pi.last_updated { writeln!( f, @@ -282,6 +295,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { manifest_path: p.root().to_path_buf().join("pixi.toml"), last_updated: last_updated(p.lock_file_path()).ok(), pixi_folder_size, + configuration: p.config().loaded_from.clone(), version: p.version().clone().map(|v| v.to_string()), }); diff --git a/src/cli/init.rs b/src/cli/init.rs index 45679789c..1040926a5 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use crate::environment::{get_up_to_date_prefix, LockFileUsage}; use crate::project::manifest::python::PyPiPackageName; use crate::project::manifest::PyPiRequirement; @@ -10,7 +11,7 @@ use itertools::Itertools; use miette::IntoDiagnostic; use minijinja::{context, Environment}; use rattler_conda_types::ParseStrictness::{Lenient, Strict}; -use rattler_conda_types::{Channel, ChannelConfig, MatchSpec, Platform}; +use rattler_conda_types::{Channel, MatchSpec, Platform}; use regex::Regex; use std::io::{Error, ErrorKind, Write}; use std::path::Path; @@ -38,9 +39,6 @@ pub struct Args { pub env_file: Option, } -/// The default channels to use for a new project. -const DEFAULT_CHANNELS: &[&str] = &["conda-forge"]; - /// The pixi.toml template /// /// This uses a template just to simplify the flexibility of emitting it. @@ -76,6 +74,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let manifest_path = dir.join(consts::PROJECT_MANIFEST); let gitignore_path = dir.join(".gitignore"); let gitattributes_path = dir.join(".gitattributes"); + let config = Config::load_global(); // Check if the project file doesn't already exist. We don't want to overwrite it. if fs::metadata(&manifest_path).map_or(false, |x| x.is_file()) { @@ -106,7 +105,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { // TODO: Improve this: // - Use .condarc as channel config // - Implement it for `[crate::project::manifest::ProjectManifest]` to do this for other filetypes, e.g. (pyproject.toml, requirements.txt) - let (conda_deps, pypi_deps, channels) = conda_env_to_manifest(conda_env_file)?; + let (conda_deps, pypi_deps, channels) = conda_env_to_manifest(conda_env_file, &config)?; let rv = env .render_named_str( @@ -176,11 +175,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let channels = if let Some(channels) = args.channels { channels } else { - DEFAULT_CHANNELS - .iter() - .copied() - .map(ToOwned::to_owned) - .collect() + config.default_channels().to_vec() }; let rv = env @@ -278,15 +273,16 @@ type ParsedDependencies = (Vec, Vec, Vec>); fn conda_env_to_manifest( env_info: CondaEnvFile, + config: &Config, ) -> miette::Result<(Vec, Vec, Vec)> { let channels = parse_channels(env_info.channels().clone()); let (conda_deps, pip_deps, mut extra_channels) = parse_dependencies(env_info.dependencies().clone())?; - let channel_config = ChannelConfig::default(); + extra_channels.extend( channels .into_iter() - .map(|c| Arc::new(Channel::from_str(c, &channel_config).unwrap())), + .map(|c| Arc::new(Channel::from_str(c, config.channel_config()).unwrap())), ); let mut channels: Vec<_> = extra_channels .into_iter() @@ -294,7 +290,7 @@ fn conda_env_to_manifest( .map(|c| { if c.base_url() .as_str() - .starts_with(channel_config.channel_alias.as_str()) + .starts_with(config.channel_config().channel_alias.as_str()) { c.name().to_string() } else { @@ -303,15 +299,12 @@ fn conda_env_to_manifest( }) .collect(); if channels.is_empty() { - channels = DEFAULT_CHANNELS - .iter() - .copied() - .map(ToOwned::to_owned) - .collect() + channels = config.default_channels(); } Ok((conda_deps, pip_deps, channels)) } + fn parse_dependencies(deps: Vec) -> miette::Result { let mut conda_deps = vec![]; let mut pip_deps = vec![]; @@ -420,7 +413,9 @@ mod tests { ] ); - let (conda_deps, pip_deps, channels) = conda_env_to_manifest(conda_env_file_data).unwrap(); + let config = Config::default(); + let (conda_deps, pip_deps, channels) = + conda_env_to_manifest(conda_env_file_data, &config).unwrap(); assert_eq!( channels, diff --git a/src/cli/install.rs b/src/cli/install.rs index 4e8e6d0b8..958c06c56 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -1,3 +1,4 @@ +use crate::config::ConfigCli; use crate::environment::get_up_to_date_prefix; use crate::project::manifest::EnvironmentName; use crate::Project; @@ -17,6 +18,9 @@ pub struct Args { #[arg(long, short)] pub environment: Option, + + #[clap(flatten)] + pub config: ConfigCli, } pub async fn execute(args: Args) -> miette::Result<()> { diff --git a/src/cli/project/channel/add.rs b/src/cli/project/channel/add.rs index e952cc8c4..b24ee72a4 100644 --- a/src/cli/project/channel/add.rs +++ b/src/cli/project/channel/add.rs @@ -5,7 +5,7 @@ use crate::Project; use clap::Parser; use indexmap::IndexMap; use miette::IntoDiagnostic; -use rattler_conda_types::{Channel, ChannelConfig}; +use rattler_conda_types::Channel; #[derive(Parser, Debug, Default)] pub struct Args { /// The channel name or URL @@ -27,12 +27,12 @@ pub async fn execute(mut project: Project, args: Args) -> miette::Result<()> { .map_or(FeatureName::Default, FeatureName::Named); // Determine which channels are missing - let channel_config = ChannelConfig::default(); let channels = args .channel .into_iter() .map(|channel_str| { - Channel::from_str(&channel_str, &channel_config).map(|channel| (channel_str, channel)) + Channel::from_str(&channel_str, project.config().channel_config()) + .map(|channel| (channel_str, channel)) }) .collect::, _>>() .into_diagnostic()?; diff --git a/src/cli/project/channel/remove.rs b/src/cli/project/channel/remove.rs index 89b414592..619f8e580 100644 --- a/src/cli/project/channel/remove.rs +++ b/src/cli/project/channel/remove.rs @@ -6,7 +6,7 @@ use crate::Project; use clap::Parser; use indexmap::IndexMap; use miette::IntoDiagnostic; -use rattler_conda_types::{Channel, ChannelConfig}; +use rattler_conda_types::Channel; #[derive(Parser, Debug, Default)] pub struct Args { @@ -29,12 +29,12 @@ pub async fn execute(mut project: Project, args: Args) -> miette::Result<()> { .map_or(FeatureName::Default, FeatureName::Named); // Determine which channels to remove - let channel_config = ChannelConfig::default(); let channels = args .channel .into_iter() .map(|channel_str| { - Channel::from_str(&channel_str, &channel_config).map(|channel| (channel_str, channel)) + Channel::from_str(&channel_str, project.config().channel_config()) + .map(|channel| (channel_str, channel)) }) .collect::, _>>() .into_diagnostic()?; diff --git a/src/cli/remove.rs b/src/cli/remove.rs index 3ac84ce4c..b6db74cc2 100644 --- a/src/cli/remove.rs +++ b/src/cli/remove.rs @@ -6,6 +6,7 @@ use indexmap::IndexMap; use miette::miette; use rattler_conda_types::Platform; +use crate::config::ConfigCli; use crate::environment::{get_up_to_date_prefix, LockFileUsage}; use crate::project::manifest::python::PyPiPackageName; use crate::project::manifest::FeatureName; @@ -41,6 +42,9 @@ pub struct Args { /// The feature for which the dependency should be removed #[arg(long, short)] pub feature: Option, + + #[clap(flatten)] + pub config: ConfigCli, } fn convert_pkg_name(deps: &[String]) -> miette::Result> @@ -56,7 +60,8 @@ where } pub async fn execute(args: Args) -> miette::Result<()> { - let mut project = Project::load_or_else_discover(args.manifest_path.as_deref())?; + let mut project = Project::load_or_else_discover(args.manifest_path.as_deref())? + .with_cli_config(args.config.clone()); let deps = args.deps; let spec_type = if args.host { SpecType::Host diff --git a/src/cli/run.rs b/src/cli/run.rs index c77d82b1f..c123a5200 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -4,6 +4,7 @@ use std::convert::identity; use std::str::FromStr; use std::{collections::HashMap, path::PathBuf, string::String}; +use crate::config::ConfigCli; use clap::Parser; use dialoguer::theme::ColorfulTheme; use itertools::Itertools; @@ -44,13 +45,17 @@ pub struct Args { #[arg(long, short)] pub environment: Option, + + #[clap(flatten)] + pub config: ConfigCli, } /// CLI entry point for `pixi run` /// When running the sigints are ignored and child can react to them. As it pleases. pub async fn execute(args: Args) -> miette::Result<()> { // Load the project - let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; + let project = Project::load_or_else_discover(args.manifest_path.as_deref())? + .with_cli_config(args.config.clone()); // Sanity check of prefix location verify_prefix_location_unchanged(project.default_environment().dir().as_path())?; diff --git a/src/cli/search.rs b/src/cli/search.rs index 0b199cfb7..6facad793 100644 --- a/src/cli/search.rs +++ b/src/cli/search.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::io::{self, Write}; use std::sync::Arc; use std::{cmp::Ordering, path::PathBuf}; @@ -7,7 +6,7 @@ use clap::Parser; use indexmap::IndexMap; use itertools::Itertools; use miette::IntoDiagnostic; -use rattler_conda_types::{Channel, ChannelConfig, PackageName, Platform, RepoDataRecord}; +use rattler_conda_types::{Channel, PackageName, Platform, RepoDataRecord}; use rattler_networking::AuthenticationMiddleware; use rattler_repodata_gateway::sparse::SparseRepoData; use regex::Regex; @@ -15,6 +14,7 @@ use regex::Regex; use strsim::jaro; use tokio::task::spawn_blocking; +use crate::config::Config; use crate::{progress::await_in_progress, repodata::fetch_sparse_repodata, Project}; /// Search a package, output will list the latest version of package @@ -90,22 +90,49 @@ pub async fn execute(args: Args) -> miette::Result<()> { let stdout = io::stdout(); let project = Project::load_or_else_discover(args.manifest_path.as_deref()).ok(); - let channel_config = ChannelConfig::default(); - let channels = match (args.channel, project.as_ref()) { // if user passes channels through the channel flag - (Some(c), _) => c - .iter() - .map(|c| Channel::from_str(c, &channel_config)) - .map_ok(Cow::Owned) - .collect::, _>>() - .into_diagnostic()?, + (Some(c), Some(p)) => { + let channels = p.config().compute_channels(&c).into_diagnostic()?; + eprintln!( + "Using channels from arguments ({}): {:?}", + p.name(), + channels.iter().map(|c| c.name()).join(", ") + ); + channels + } + // No project -> use the global config + (Some(c), None) => { + let channels = Config::load_global() + .compute_channels(&c) + .into_diagnostic()?; + eprintln!( + "Using channels from arguments: {}", + channels.iter().map(|c| c.name()).join(", ") + ); + channels + } // if user doesn't pass channels and we are in a project - (None, Some(p)) => p.channels().into_iter().map(Cow::Borrowed).collect(), + (None, Some(p)) => { + let channels: Vec<_> = p.channels().into_iter().cloned().collect(); + eprintln!( + "Using channels from project ({}): {}", + p.name(), + channels.iter().map(|c| c.name()).join(", ") + ); + channels + } // if user doesn't pass channels and we are not in project - (None, None) => vec![Cow::Owned( - Channel::from_str("conda-forge", &channel_config).into_diagnostic()?, - )], + (None, None) => { + let channels = Config::load_global() + .compute_channels(&[]) + .into_diagnostic()?; + eprintln!( + "Using channels from global config: {}", + channels.iter().map(|c| c.name()).join(", ") + ); + channels + } }; let package_name_filter = args.package; @@ -114,12 +141,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { .with_arc(Arc::new(AuthenticationMiddleware::default())) .build(); let repo_data = Arc::new( - fetch_sparse_repodata( - channels.iter().map(AsRef::as_ref), - [args.platform], - &authenticated_client, - ) - .await?, + fetch_sparse_repodata(channels.iter(), [args.platform], &authenticated_client).await?, ); // When package name filter contains * (wildcard), it will search and display a list of packages matching this filter diff --git a/src/cli/shell.rs b/src/cli/shell.rs index 386657d91..c132441f8 100644 --- a/src/cli/shell.rs +++ b/src/cli/shell.rs @@ -1,4 +1,5 @@ use crate::activation::get_activation_env; +use crate::config::ConfigCliPrompt; use crate::{prompt, Project}; use clap::Parser; use miette::IntoDiagnostic; @@ -29,6 +30,9 @@ pub struct Args { #[arg(long, short)] environment: Option, + + #[clap(flatten)] + config: ConfigCliPrompt, } fn start_powershell( @@ -195,7 +199,8 @@ async fn start_nu_shell( } pub async fn execute(args: Args) -> miette::Result<()> { - let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; + let project = Project::load_or_else_discover(args.manifest_path.as_deref())? + .with_cli_config(args.config.clone()); let environment_name = args .environment .map_or_else(|| EnvironmentName::Default, EnvironmentName::Named); @@ -217,19 +222,25 @@ pub async fn execute(args: Args) -> miette::Result<()> { .or_else(ShellEnum::from_env) .unwrap_or_default(); + let prompt = if project.config().change_ps1() { + match interactive_shell { + ShellEnum::NuShell(_) => prompt::get_nu_prompt(prompt_name.as_str()), + ShellEnum::PowerShell(_) => prompt::get_powershell_prompt(prompt_name.as_str()), + ShellEnum::Bash(_) => prompt::get_bash_prompt(prompt_name.as_str()), + ShellEnum::Zsh(_) => prompt::get_zsh_prompt(prompt_name.as_str()), + ShellEnum::Fish(_) => prompt::get_fish_prompt(prompt_name.as_str()), + ShellEnum::Xonsh(_) => prompt::get_xonsh_prompt(), + ShellEnum::CmdExe(_) => prompt::get_cmd_prompt(prompt_name.as_str()), + } + } else { + "".to_string() + }; + #[cfg(target_family = "windows")] let res = match interactive_shell { - ShellEnum::NuShell(nushell) => { - start_nu_shell(nushell, env, prompt::get_nu_prompt(prompt_name.as_str())).await - } - ShellEnum::PowerShell(pwsh) => start_powershell( - pwsh, - env, - prompt::get_powershell_prompt(prompt_name.as_str()), - ), - ShellEnum::CmdExe(cmdexe) => { - start_cmdexe(cmdexe, env, prompt::get_cmd_prompt(prompt_name.as_str())) - } + ShellEnum::NuShell(nushell) => start_nu_shell(nushell, env, prompt).await, + ShellEnum::PowerShell(pwsh) => start_powershell(pwsh, env, prompt), + ShellEnum::CmdExe(cmdexe) => start_cmdexe(cmdexe, env, prompt), _ => { miette::bail!("Unsupported shell: {:?}", interactive_shell); } @@ -237,44 +248,12 @@ pub async fn execute(args: Args) -> miette::Result<()> { #[cfg(target_family = "unix")] let res = match interactive_shell { - ShellEnum::NuShell(nushell) => { - start_nu_shell(nushell, env, prompt::get_nu_prompt(prompt_name.as_str())).await - } - ShellEnum::PowerShell(pwsh) => start_powershell( - pwsh, - env, - prompt::get_powershell_prompt(prompt_name.as_str()), - ), - ShellEnum::Bash(bash) => { - start_unix_shell( - bash, - vec!["-l", "-i"], - env, - prompt::get_bash_prompt(prompt_name.as_str()), - ) - .await - } - ShellEnum::Zsh(zsh) => { - start_unix_shell( - zsh, - vec!["-l", "-i"], - env, - prompt::get_zsh_prompt(prompt_name.as_str()), - ) - .await - } - ShellEnum::Fish(fish) => { - start_unix_shell( - fish, - vec![], - env, - prompt::get_fish_prompt(prompt_name.as_str()), - ) - .await - } - ShellEnum::Xonsh(xonsh) => { - start_unix_shell(xonsh, vec![], env, prompt::get_xonsh_prompt()).await - } + ShellEnum::NuShell(nushell) => start_nu_shell(nushell, env, prompt).await, + ShellEnum::PowerShell(pwsh) => start_powershell(pwsh, env, prompt), + ShellEnum::Bash(bash) => start_unix_shell(bash, vec!["-l", "-i"], env, prompt).await, + ShellEnum::Zsh(zsh) => start_unix_shell(zsh, vec!["-l", "-i"], env, prompt).await, + ShellEnum::Fish(fish) => start_unix_shell(fish, vec![], env, prompt).await, + ShellEnum::Xonsh(xonsh) => start_unix_shell(xonsh, vec![], env, prompt).await, _ => { miette::bail!("Unsupported shell: {:?}", interactive_shell) } diff --git a/src/config.rs b/src/config.rs index 64fac0dbd..fc6ad34b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,13 @@ -use std::path::PathBuf; +use clap::{ArgAction, Parser}; +use miette::{Context, IntoDiagnostic}; +use rattler_conda_types::{Channel, ChannelConfig, ParseChannelError}; +use serde::Deserialize; +use std::fs; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use crate::consts; + /// Determines the default author based on the default git author. Both the name and the email /// address of the author are returned. pub fn get_default_author() -> Option<(String, String)> { @@ -30,6 +37,21 @@ pub fn get_default_author() -> Option<(String, String)> { Some((name?, email.unwrap_or_else(|| "".into()))) } +/// Get pixi home directory, default to `$HOME/.pixi` +/// +/// It may be overridden by the `PIXI_HOME` environment variable. +/// +/// # Returns +/// +/// The pixi home directory +pub fn home_path() -> Option { + if let Some(path) = std::env::var_os("PIXI_HOME") { + Some(PathBuf::from(path)) + } else { + dirs::home_dir().map(|path| path.join(consts::PIXI_DIR)) + } +} + /// Returns the default cache directory. /// Most important is the `PIXI_CACHE_DIR` environment variable. /// If that is not set, the `RATTLER_CACHE_DIR` environment variable is used. @@ -43,3 +65,234 @@ pub fn get_cache_dir() -> miette::Result { .map_err(|_| miette::miette!("could not determine default cache directory")) }) } +#[derive(Parser, Debug, Default, Clone)] +pub struct ConfigCli { + /// Do not verify the TLS certificate of the server. + #[arg(long, action = ArgAction::SetTrue)] + tls_no_verify: bool, +} + +#[derive(Parser, Debug, Default, Clone)] +pub struct ConfigCliPrompt { + #[clap(flatten)] + config: ConfigCli, + + /// Do not change the PS1 variable when starting a prompt. + #[arg(long)] + change_ps1: Option, +} + +#[derive(Clone, Default, Debug, Deserialize)] +pub struct Config { + #[serde(default)] + pub default_channels: Vec, + + /// If set to true, pixi will set the PS1 environment variable to a custom value. + #[serde(default)] + change_ps1: Option, + + /// If set to true, pixi will not verify the TLS certificate of the server. + #[serde(default)] + tls_no_verify: Option, + + #[serde(skip)] + pub loaded_from: Vec, + + #[serde(skip)] + pub channel_config: ChannelConfig, +} + +impl From for Config { + fn from(cli: ConfigCli) -> Self { + Self { + tls_no_verify: if cli.tls_no_verify { Some(true) } else { None }, + ..Default::default() + } + } +} + +impl From for Config { + fn from(cli: ConfigCliPrompt) -> Self { + let mut config: Config = cli.config.into(); + config.change_ps1 = cli.change_ps1; + config + } +} + +impl Config { + /// Parse the given toml string and return a Config instance. + pub fn from_toml(toml: &str, location: &Path) -> miette::Result { + let mut config: Config = toml_edit::de::from_str(toml) + .into_diagnostic() + .context(format!("Failed to parse {}", consts::CONFIG_FILE))?; + + config.loaded_from.push(location.to_path_buf()); + + Ok(config) + } + + /// Load the global config file from the home directory (~/.pixi/config.toml) + pub fn load_global() -> Config { + let global_locations = vec![ + dirs::config_dir().map(|d| d.join("pixi").join(consts::CONFIG_FILE)), + home_path().map(|d| d.join(consts::CONFIG_FILE)), + ]; + let mut merged_config = Config::default(); + for location in global_locations.into_iter().flatten() { + if location.exists() { + tracing::info!("Loading global config from {}", location.display()); + let global_config = fs::read_to_string(&location).unwrap_or_default(); + if let Ok(config) = Config::from_toml(&global_config, &location) { + merged_config.merge_config(&config); + } else { + tracing::warn!( + "Could not load global config (invalid toml): {}", + location.display() + ); + } + } else { + tracing::info!("Global config not found at {}", location.display()); + } + } + merged_config + } + + /// Load the config from the given path pixi folder and merge it with the global config. + pub fn load(p: &Path) -> miette::Result { + let local_config = p.join(consts::CONFIG_FILE); + let mut config = Self::load_global(); + + if local_config.exists() { + let s = fs::read_to_string(&local_config).into_diagnostic()?; + let local = Config::from_toml(&s, &local_config)?; + config.merge_config(&local); + } + + Ok(config) + } + + pub fn from_path(p: &Path) -> miette::Result { + let s = fs::read_to_string(p).into_diagnostic()?; + Config::from_toml(&s, p) + } + + /// Merge the given config into the current one. + pub fn merge_config(&mut self, other: &Config) { + if !other.default_channels.is_empty() { + self.default_channels = other.default_channels.clone(); + } + + if other.change_ps1.is_some() { + self.change_ps1 = other.change_ps1; + } + + if other.tls_no_verify.is_some() { + self.tls_no_verify = other.tls_no_verify; + } + + self.loaded_from.extend(other.loaded_from.iter().cloned()); + } + + /// Retrieve the value for the default_channels field (defaults to the ["conda-forge"]). + pub fn default_channels(&self) -> Vec { + if self.default_channels.is_empty() { + consts::DEFAULT_CHANNELS + .iter() + .map(|s| s.to_string()) + .collect() + } else { + self.default_channels.clone() + } + } + + /// Retrieve the value for the tls_no_verify field (defaults to false). + pub fn tls_no_verify(&self) -> bool { + self.tls_no_verify.unwrap_or(false) + } + + /// Retrieve the value for the change_ps1 field (defaults to true). + pub fn change_ps1(&self) -> bool { + self.change_ps1.unwrap_or(true) + } + + pub fn channel_config(&self) -> &ChannelConfig { + &self.channel_config + } + + pub fn compute_channels( + &self, + cli_channels: &[String], + ) -> Result, ParseChannelError> { + let channels = if cli_channels.is_empty() { + self.default_channels() + } else { + cli_channels.to_vec() + }; + + channels + .iter() + .map(|c| Channel::from_str(c, &self.channel_config)) + .collect::, _>>() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_config_parse() { + let toml = r#" + default_channels = ["conda-forge"] + tls_no_verify = true + "#; + let config = Config::from_toml(toml, &PathBuf::from("")).unwrap(); + assert_eq!(config.default_channels, vec!["conda-forge"]); + assert_eq!(config.tls_no_verify, Some(true)); + } + + #[test] + fn test_config_from_cli() { + let cli = ConfigCli { + tls_no_verify: true, + }; + let config = Config::from(cli); + assert_eq!(config.tls_no_verify, Some(true)); + + let cli = ConfigCli { + tls_no_verify: false, + }; + + let config = Config::from(cli); + assert_eq!(config.tls_no_verify, None); + } + + #[test] + fn test_config_merge() { + let mut config = Config::default(); + let other = Config { + default_channels: vec!["conda-forge".to_string()], + tls_no_verify: Some(true), + ..Default::default() + }; + config.merge_config(&other); + assert_eq!(config.default_channels, vec!["conda-forge"]); + assert_eq!(config.tls_no_verify, Some(true)); + + let d = Path::new(&env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("config"); + + let config_1 = Config::from_path(&d.join("config_1.toml")).unwrap(); + let config_2 = Config::from_path(&d.join("config_2.toml")).unwrap(); + + let mut merged = config_1.clone(); + merged.merge_config(&config_2); + + let debug = format!("{:#?}", merged); + let debug = debug.replace("\\\\", "/"); + // replace the path with a placeholder + let debug = debug.replace(&d.to_str().unwrap().replace('\\', "/"), "path"); + insta::assert_snapshot!(debug); + } +} diff --git a/src/consts.rs b/src/consts.rs index fa8e38133..71db1d857 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -4,13 +4,16 @@ use lazy_static::lazy_static; pub const PROJECT_MANIFEST: &str = "pixi.toml"; pub const PROJECT_LOCK_FILE: &str = "pixi.lock"; pub const PIXI_DIR: &str = ".pixi"; +pub const CONFIG_FILE: &str = "config.toml"; pub const PREFIX_FILE_NAME: &str = "pixi_env_prefix"; pub const ENVIRONMENTS_DIR: &str = "envs"; pub const SOLVE_GROUP_ENVIRONMENTS_DIR: &str = "solve-group-envs"; pub const PYPI_DEPENDENCIES: &str = "pypi-dependencies"; - pub const DEFAULT_ENVIRONMENT_NAME: &str = "default"; +/// The default channels to use for a new project. +pub const DEFAULT_CHANNELS: &[&str] = &["conda-forge"]; + pub const DEFAULT_FEATURE_NAME: &str = DEFAULT_ENVIRONMENT_NAME; lazy_static! { diff --git a/src/project/mod.rs b/src/project/mod.rs index d1b2e7e6b..4f7f2b87c 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -12,8 +12,6 @@ use indexmap::{Equivalent, IndexMap, IndexSet}; use miette::{IntoDiagnostic, NamedSource, WrapErr}; use rattler_conda_types::{Channel, GenericVirtualPackage, Platform, Version}; -use rattler_networking::AuthenticationMiddleware; -use reqwest::Client; use reqwest_middleware::ClientWithMiddleware; use std::hash::Hash; @@ -28,8 +26,10 @@ use std::{ }; use crate::activation::{get_environment_variables, run_activation}; +use crate::config::Config; use crate::project::grouped_environment::GroupedEnvironment; use crate::task::TaskName; +use crate::utils::reqwest::build_reqwest_clients; use crate::{ consts::{self, PROJECT_MANIFEST}, task::Task, @@ -102,6 +102,8 @@ pub struct Project { pub(crate) manifest: Manifest, /// The cache that contains environment variables env_vars: HashMap>>>, + /// The global configuration as loaded from the config file(s) + config: Config, } impl Debug for Project { @@ -116,16 +118,21 @@ impl Debug for Project { impl Project { /// Constructs a new instance from an internal manifest representation pub fn from_manifest(manifest: Manifest) -> Self { - let (client, authenticated_client) = build_reqwest_clients(); - let env_vars = Project::init_env_vars(&manifest.parsed.environments); + let root = manifest.path.parent().unwrap_or(Path::new("")).to_owned(); + + let config = + Config::load(&root.join(consts::PIXI_DIR)).unwrap_or_else(|_| Config::load_global()); + + let (client, authenticated_client) = build_reqwest_clients(Some(&config)); Self { - root: manifest.path.parent().unwrap_or(Path::new("")).to_owned(), + root, client, authenticated_client, manifest, env_vars, + config, } } @@ -188,7 +195,9 @@ impl Project { let env_vars = Project::init_env_vars(&manifest.parsed.environments); - let (client, authenticated_client) = build_reqwest_clients(); + let config = Config::load(&root.join(consts::PIXI_DIR))?; + + let (client, authenticated_client) = build_reqwest_clients(Some(&config)); Ok(Self { root: root.to_owned(), @@ -196,6 +205,7 @@ impl Project { authenticated_client, manifest, env_vars, + config, }) } @@ -208,6 +218,14 @@ impl Project { Ok(project) } + pub fn with_cli_config(mut self, config: C) -> Self + where + C: Into, + { + self.config.merge_config(&config.into()); + self + } + /// Returns the name of the project pub fn name(&self) -> &str { &self.manifest.parsed.project.name @@ -431,6 +449,10 @@ impl Project { &self.authenticated_client } + pub fn config(&self) -> &Config { + &self.config + } + /// Return a combination of static environment variables generated from the project and the environment /// and from running activation script pub async fn get_env_variables( @@ -459,24 +481,6 @@ impl Project { } } -fn build_reqwest_clients() -> (Client, ClientWithMiddleware) { - static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); - - let timeout = 5 * 60; - let client = Client::builder() - .pool_max_idle_per_host(20) - .user_agent(APP_USER_AGENT) - .timeout(std::time::Duration::from_secs(timeout)) - .build() - .expect("failed to create reqwest Client"); - - let authenticated_client = reqwest_middleware::ClientBuilder::new(client.clone()) - .with_arc(Arc::new(AuthenticationMiddleware::default())) - .build(); - - (client, authenticated_client) -} - /// Iterates over the current directory and all its parent directories and returns the first /// directory path that contains the [`consts::PROJECT_MANIFEST`]. pub fn find_project_root() -> Option { diff --git a/src/prompt.rs b/src/prompt.rs index 42510995b..1c680dda8 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -1,17 +1,14 @@ /// Set default pixi prompt for the bash shell -#[cfg(target_family = "unix")] pub fn get_bash_prompt(env_name: &str) -> String { format!("export PS1=\"({}) $PS1\"", env_name) } /// Set default pixi prompt for the zsh shell -#[cfg(target_family = "unix")] pub fn get_zsh_prompt(env_name: &str) -> String { format!("export PS1=\"({}) $PS1\"", env_name) } /// Set default pixi prompt for the fish shell -#[cfg(target_family = "unix")] pub fn get_fish_prompt(env_name: &str) -> String { format!( "functions -c fish_prompt old_fish_prompt; \ @@ -23,7 +20,6 @@ pub fn get_fish_prompt(env_name: &str) -> String { } /// Set default pixi prompt for the xonsh shell -#[cfg(target_family = "unix")] pub fn get_xonsh_prompt() -> String { // Xonsh' default prompt can find the environment for some reason. "".to_string() @@ -48,7 +44,6 @@ pub fn get_nu_prompt(env_name: &str) -> String { } /// Set default pixi prompt for the cmd.exe command prompt -#[cfg(target_family = "windows")] pub fn get_cmd_prompt(env_name: &str) -> String { format!(r"@PROMPT ({}) $P$G", env_name) } diff --git a/src/snapshots/pixi__config__tests__config_merge.snap b/src/snapshots/pixi__config__tests__config_merge.snap new file mode 100644 index 000000000..066465599 --- /dev/null +++ b/src/snapshots/pixi__config__tests__config_merge.snap @@ -0,0 +1,38 @@ +--- +source: src/config.rs +expression: debug +--- +Config { + default_channels: [ + "conda-forge", + "bioconda", + "defaults", + ], + change_ps1: Some( + true, + ), + tls_no_verify: Some( + false, + ), + loaded_from: [ + "path/config_1.toml", + "path/config_2.toml", + ], + channel_config: ChannelConfig { + channel_alias: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "conda.anaconda.org", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, + }, +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ac400c733..8836f6477 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,6 @@ mod barrier_cell; pub mod conda_environment_file; +pub mod reqwest; pub mod spanned; pub use barrier_cell::BarrierCell; diff --git a/src/utils/reqwest.rs b/src/utils/reqwest.rs new file mode 100644 index 000000000..8874a8844 --- /dev/null +++ b/src/utils/reqwest.rs @@ -0,0 +1,43 @@ +use std::{sync::Arc, time::Duration}; + +use rattler_networking::{retry_policies::ExponentialBackoff, AuthenticationMiddleware}; +use reqwest::Client; +use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; + +use crate::config::Config; + +/// The default retry policy employed by pixi. +/// TODO: At some point we might want to make this configurable. +pub fn default_retry_policy() -> ExponentialBackoff { + ExponentialBackoff::builder().build_with_max_retries(3) +} + +pub(crate) fn build_reqwest_clients(config: Option<&Config>) -> (Client, ClientWithMiddleware) { + static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + + // If we do not have a config, we will just load the global default. + let config = if let Some(config) = config { + config.clone() + } else { + Config::load_global() + }; + + if config.tls_no_verify() { + tracing::warn!("TLS verification is disabled. This is insecure and should only be used for testing or internal networks."); + } + + let timeout = 5 * 60; + let client = Client::builder() + .pool_max_idle_per_host(20) + .user_agent(APP_USER_AGENT) + .danger_accept_invalid_certs(config.tls_no_verify()) + .timeout(Duration::from_secs(timeout)) + .build() + .expect("failed to create reqwest Client"); + + let authenticated_client = ClientBuilder::new(client.clone()) + .with_arc(Arc::new(AuthenticationMiddleware::default())) + .build(); + + (client, authenticated_client) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 7b711c97a..cee0710e4 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -227,6 +227,7 @@ impl PixiControl { platform: Default::default(), pypi: false, feature: None, + config: Default::default(), }, } } @@ -242,6 +243,7 @@ impl PixiControl { pypi: false, platform: Default::default(), feature: None, + config: Default::default(), }, } } @@ -331,6 +333,7 @@ impl PixiControl { frozen: false, locked: false, }, + config: Default::default(), }, } } diff --git a/tests/config/config_1.toml b/tests/config/config_1.toml new file mode 100644 index 000000000..7ec6a40d0 --- /dev/null +++ b/tests/config/config_1.toml @@ -0,0 +1,2 @@ +default_channels = ["conda-forge", "bioconda", "defaults"] +tls_no_verify = true diff --git a/tests/config/config_2.toml b/tests/config/config_2.toml new file mode 100644 index 000000000..90f64b623 --- /dev/null +++ b/tests/config/config_2.toml @@ -0,0 +1,2 @@ +tls_no_verify = false +change_ps1 = true