diff --git a/rye/src/cli/add.rs b/rye/src/cli/add.rs index 4c956c5d92..3e357c9b0b 100644 --- a/rye/src/cli/add.rs +++ b/rye/src/cli/add.rs @@ -13,6 +13,7 @@ use url::Url; use crate::bootstrap::ensure_self_venv; use crate::config::Config; use crate::consts::VENV_BIN; +use crate::lock::KeyringProvider; use crate::pyproject::{BuildSystem, DependencyKind, ExpandedSources, PyProject}; use crate::sources::py::PythonVersion; use crate::sync::{autosync, sync, SyncOptions}; @@ -137,7 +138,7 @@ impl ReqExtras { }; req.version_or_url = match req.version_or_url { Some(_) => bail!("requirement already has a version marker"), - None => Some(pep508_rs::VersionOrUrl::Url( + None => Some(VersionOrUrl::Url( format!("git+{}{}", git, suffix).parse().with_context(|| { format!("unable to interpret '{}{}' as git reference", git, suffix) })?, @@ -146,10 +147,11 @@ impl ReqExtras { } else if let Some(ref url) = self.url { req.version_or_url = match req.version_or_url { Some(_) => bail!("requirement already has a version marker"), - None => Some(pep508_rs::VersionOrUrl::Url( - url.parse() - .with_context(|| format!("unable to parse '{}' as url", url))?, - )), + None => { + Some(VersionOrUrl::Url(url.parse().with_context(|| { + format!("unable to parse '{}' as url", url) + })?)) + } }; } else if let Some(ref path) = self.path { // For hatchling build backend, it use {root:uri} for file relative path, @@ -175,7 +177,7 @@ impl ReqExtras { }; req.version_or_url = match req.version_or_url { Some(_) => bail!("requirement already has a version marker"), - None => Some(pep508_rs::VersionOrUrl::Url(file_url)), + None => Some(VersionOrUrl::Url(file_url)), }; } for feature in self.features.iter().flat_map(|x| x.split(',')) { @@ -212,6 +214,9 @@ pub struct Args { /// Overrides the pin operator #[arg(long)] pin: Option, + /// Attempt to use `keyring` for authentication for index URLs. + #[arg(long, value_enum, default_value_t)] + keyring_provider: KeyringProvider, /// Runs `sync` even if auto-sync is disabled. #[arg(long)] sync: bool, @@ -259,6 +264,8 @@ pub fn execute(cmd: Args) -> Result<(), Error> { requirements.push(requirement); } + let keyring_provider = cmd.keyring_provider; + if !cmd.excluded { if cfg.use_uv() { sync(SyncOptions::python_only().pyproject(None)) @@ -270,8 +277,12 @@ pub fn execute(cmd: Args) -> Result<(), Error> { cmd.pre, output, &default_operator, + keyring_provider, )?; } else { + if keyring_provider != KeyringProvider::Disabled { + bail!("`--keyring-provider` option requires the uv backend"); + } for requirement in &mut requirements { resolve_requirements_with_unearth( &pyproject_toml, @@ -303,7 +314,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { } if (cfg.autosync() && !cmd.no_sync) || cmd.sync { - autosync(&pyproject_toml, output)?; + autosync(&pyproject_toml, output, keyring_provider)?; } Ok(()) @@ -448,6 +459,7 @@ fn resolve_requirements_with_uv( pre: bool, output: CommandOutput, default_operator: &Operator, + keyring_provider: KeyringProvider, ) -> Result<(), Error> { let venv_path = pyproject_toml.venv_path(); let py_bin = get_venv_python_bin(&venv_path); @@ -460,7 +472,13 @@ fn resolve_requirements_with_uv( .venv(&venv_path, &py_bin, py_ver, None)?; for req in requirements { - let mut new_req = uv.resolve(py_ver, req, pre, env::var("__RYE_UV_EXCLUDE_NEWER").ok())?; + let mut new_req = uv.resolve( + py_ver, + req, + pre, + env::var("__RYE_UV_EXCLUDE_NEWER").ok(), + keyring_provider, + )?; // if a version or URL is already provided we just use the normalized package name but // retain all old information. diff --git a/rye/src/cli/lock.rs b/rye/src/cli/lock.rs index e12e0b6ccb..dbc54cfaa2 100644 --- a/rye/src/cli/lock.rs +++ b/rye/src/cli/lock.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use anyhow::Error; use clap::Parser; -use crate::lock::LockOptions; +use crate::lock::{KeyringProvider, LockOptions}; use crate::sync::{sync, SyncMode, SyncOptions}; use crate::utils::CommandOutput; @@ -34,6 +34,9 @@ pub struct Args { /// Set to true to lock with sources in the lockfile. #[arg(long)] with_sources: bool, + /// Attempt to use `keyring` for authentication for index URLs. + #[arg(long, value_enum, default_value_t)] + keyring_provider: KeyringProvider, /// Reset prior lock options. #[arg(long)] reset: bool, @@ -57,6 +60,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { reset: cmd.reset, }, pyproject: cmd.pyproject, + keyring_provider: cmd.keyring_provider, ..SyncOptions::default() })?; Ok(()) diff --git a/rye/src/cli/remove.rs b/rye/src/cli/remove.rs index 73adaa2632..f47812b652 100644 --- a/rye/src/cli/remove.rs +++ b/rye/src/cli/remove.rs @@ -5,6 +5,7 @@ use clap::Parser; use pep508_rs::Requirement; use crate::config::Config; +use crate::lock::KeyringProvider; use crate::pyproject::{DependencyKind, PyProject}; use crate::sync::autosync; use crate::utils::{format_requirement, CommandOutput}; @@ -27,6 +28,9 @@ pub struct Args { /// Does not run `sync` even if auto-sync is enabled. #[arg(long, conflicts_with = "sync")] no_sync: bool, + /// Attempt to use `keyring` for authentication for index URLs. + #[arg(long, value_enum, default_value_t)] + keyring_provider: KeyringProvider, /// Enables verbose diagnostics. #[arg(short, long)] verbose: bool, @@ -65,7 +69,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { } if (Config::current().autosync() && !cmd.no_sync) || cmd.sync { - autosync(&pyproject_toml, output)?; + autosync(&pyproject_toml, output, cmd.keyring_provider)?; } Ok(()) diff --git a/rye/src/cli/sync.rs b/rye/src/cli/sync.rs index aaaea1cc82..4e5fa22871 100644 --- a/rye/src/cli/sync.rs +++ b/rye/src/cli/sync.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use anyhow::Error; use clap::Parser; -use crate::lock::LockOptions; +use crate::lock::{KeyringProvider, LockOptions}; use crate::sync::{sync, SyncMode, SyncOptions}; use crate::utils::CommandOutput; @@ -43,6 +43,9 @@ pub struct Args { /// Set to true to lock with sources in the lockfile. #[arg(long)] with_sources: bool, + /// Attempt to use `keyring` for authentication for index URLs. + #[arg(long, value_enum, default_value_t)] + keyring_provider: KeyringProvider, /// Use this pyproject.toml file #[arg(long, value_name = "PYPROJECT_TOML")] pyproject: Option, @@ -72,6 +75,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { with_sources: cmd.with_sources, reset: cmd.reset, }, + keyring_provider: cmd.keyring_provider, pyproject: cmd.pyproject, })?; Ok(()) diff --git a/rye/src/cli/test.rs b/rye/src/cli/test.rs index ff81f820a0..5221121ce7 100644 --- a/rye/src/cli/test.rs +++ b/rye/src/cli/test.rs @@ -10,6 +10,7 @@ use same_file::is_same_file; use crate::config::Config; use crate::consts::VENV_BIN; +use crate::lock::KeyringProvider; use crate::pyproject::{locate_projects, normalize_package_name, DependencyKind, PyProject}; use crate::sync::autosync; use crate::utils::{CommandOutput, QuietExit}; @@ -28,6 +29,9 @@ pub struct Args { /// Use this pyproject.toml file #[arg(long, value_name = "PYPROJECT_TOML")] pyproject: Option, + /// Attempt to use `keyring` for authentication for index URLs. + #[arg(long, value_enum, default_value_t)] + keyring_provider: KeyringProvider, // Disable test output capture to stdout #[arg(long = "no-capture", short = 's')] no_capture: bool, @@ -73,7 +77,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { let has_pytest = has_pytest_dependency(&projects)?; if has_pytest { if Config::current().autosync() { - autosync(&projects[0], output)?; + autosync(&projects[0], output, cmd.keyring_provider)?; } else { bail!("pytest not installed but in dependencies. Run `rye sync`.") } diff --git a/rye/src/lock.rs b/rye/src/lock.rs index bb30e03f04..38e31f7168 100644 --- a/rye/src/lock.rs +++ b/rye/src/lock.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use std::{env, fmt, fs}; use anyhow::{anyhow, bail, Context, Error}; +use clap::ValueEnum; use minijinja::render; use once_cell::sync::Lazy; use pep508_rs::Requirement; @@ -59,6 +60,18 @@ impl fmt::Display for LockMode { } } +/// Keyring provider type to use for credential lookup. +#[derive(ValueEnum, Copy, Clone, Serialize, Debug, Default, PartialEq)] +#[value(rename_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum KeyringProvider { + /// Do not use keyring for credential lookup. + #[default] + Disabled, + /// Use the `keyring` command for credential lookup. + Subprocess, +} + /// Controls how locking should work. #[derive(Debug, Clone, Default, Serialize)] pub struct LockOptions { @@ -128,6 +141,7 @@ impl LockOptions { } /// Creates lockfiles for all projects in the workspace. +#[allow(clippy::too_many_arguments)] pub fn update_workspace_lockfile( py_ver: &PythonVersion, workspace: &Arc, @@ -136,6 +150,7 @@ pub fn update_workspace_lockfile( output: CommandOutput, sources: &ExpandedSources, lock_options: &LockOptions, + keyring_provider: KeyringProvider, ) -> Result<(), Error> { echo!(if output, "Generating {} lockfile: {}", lock_mode, lockfile.display()); @@ -189,6 +204,7 @@ pub fn update_workspace_lockfile( &lock_options, &exclusions, true, + keyring_provider, )?; Ok(()) @@ -308,6 +324,7 @@ fn dump_dependencies( } /// Updates the lockfile of the current project. +#[allow(clippy::too_many_arguments)] pub fn update_single_project_lockfile( py_ver: &PythonVersion, pyproject: &PyProject, @@ -316,6 +333,7 @@ pub fn update_single_project_lockfile( output: CommandOutput, sources: &ExpandedSources, lock_options: &LockOptions, + keyring_provider: KeyringProvider, ) -> Result<(), Error> { echo!(if output, "Generating {} lockfile: {}", lock_mode, lockfile.display()); @@ -356,6 +374,7 @@ pub fn update_single_project_lockfile( &lock_options, &exclusions, false, + keyring_provider, )?; Ok(()) @@ -372,6 +391,7 @@ fn generate_lockfile( lock_options: &LockOptions, exclusions: &HashSet, no_deps: bool, + keyring_provider: KeyringProvider, ) -> Result<(), Error> { let use_uv = Config::current().use_uv(); let scratch = tempfile::tempdir()?; @@ -409,8 +429,12 @@ fn generate_lockfile( lock_options.pre, env::var("__RYE_UV_EXCLUDE_NEWER").ok(), upgrade, + keyring_provider, )?; } else { + if keyring_provider != KeyringProvider::Disabled { + bail!("`--keyring-provider` option requires the uv backend"); + } let mut cmd = Command::new(get_pip_compile(py_ver, output)?); // legacy pip tools requires some extra parameters if get_pip_tools_version(py_ver) == PipToolsVersion::Legacy { diff --git a/rye/src/sync.rs b/rye/src/sync.rs index 2acf751a15..0c644fb5bf 100644 --- a/rye/src/sync.rs +++ b/rye/src/sync.rs @@ -13,7 +13,7 @@ use crate::config::Config; use crate::consts::VENV_BIN; use crate::lock::{ make_project_root_fragment, update_single_project_lockfile, update_workspace_lockfile, - LockMode, LockOptions, + KeyringProvider, LockMode, LockOptions, }; use crate::piptools::{get_pip_sync, get_pip_tools_venv_path}; use crate::platform::get_toolchain_python_bin; @@ -56,6 +56,8 @@ pub struct SyncOptions { pub lock_options: LockOptions, /// Explicit pyproject location (Only usable by PythonOnly mode) pub pyproject: Option, + /// Keyring provider to use for credential lookup. + pub keyring_provider: KeyringProvider, } impl SyncOptions { @@ -197,6 +199,7 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> { cmd.output, &sources, &cmd.lock_options, + cmd.keyring_provider, ) .context("could not write production lockfile for workspace")?; update_workspace_lockfile( @@ -207,6 +210,7 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> { cmd.output, &sources, &cmd.lock_options, + cmd.keyring_provider, ) .context("could not write dev lockfile for workspace")?; } else { @@ -219,6 +223,7 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> { cmd.output, &sources, &cmd.lock_options, + cmd.keyring_provider, ) .context("could not write production lockfile for project")?; update_single_project_lockfile( @@ -229,6 +234,7 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> { cmd.output, &sources, &cmd.lock_options, + cmd.keyring_provider, ) .context("could not write dev lockfile for project")?; } @@ -310,7 +316,11 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> { } /// Performs an autosync. -pub fn autosync(pyproject: &PyProject, output: CommandOutput) -> Result<(), Error> { +pub fn autosync( + pyproject: &PyProject, + output: CommandOutput, + keyring_provider: KeyringProvider, +) -> Result<(), Error> { sync(SyncOptions { output, dev: true, @@ -319,6 +329,7 @@ pub fn autosync(pyproject: &PyProject, output: CommandOutput) -> Result<(), Erro no_lock: false, lock_options: LockOptions::default(), pyproject: Some(pyproject.toml_path().to_path_buf()), + keyring_provider, }) } diff --git a/rye/src/uv.rs b/rye/src/uv.rs index da9ed10e91..e4f4e6a1b3 100644 --- a/rye/src/uv.rs +++ b/rye/src/uv.rs @@ -1,5 +1,5 @@ use crate::bootstrap::download_url; -use crate::lock::make_project_root_fragment; +use crate::lock::{make_project_root_fragment, KeyringProvider}; use crate::platform::get_app_dir; use crate::pyproject::{read_venv_marker, write_venv_marker, ExpandedSources}; use crate::sources::py::PythonVersion; @@ -38,6 +38,7 @@ struct UvCompileOptions { pub upgrade: UvPackageUpgrade, pub no_deps: bool, pub no_header: bool, + pub keyring_provider: KeyringProvider, } impl UvCompileOptions { @@ -69,6 +70,13 @@ impl UvCompileOptions { } UvPackageUpgrade::Nothing => {} } + + match self.keyring_provider { + KeyringProvider::Disabled => {} + KeyringProvider::Subprocess => { + cmd.arg("--keyring-provider").arg("subprocess"); + } + } } } @@ -80,6 +88,7 @@ impl Default for UvCompileOptions { upgrade: UvPackageUpgrade::Nothing, no_deps: false, no_header: false, + keyring_provider: KeyringProvider::Disabled, } } } @@ -311,6 +320,7 @@ impl Uv { Ok(UvWithVenv::new(self.clone(), venv_dir, version)) } + #[allow(clippy::too_many_arguments)] pub fn lockfile( &self, py_version: &PythonVersion, @@ -319,6 +329,7 @@ impl Uv { allow_prerelease: bool, exclude_newer: Option, upgrade: UvPackageUpgrade, + keyring_provider: KeyringProvider, ) -> Result<(), Error> { let options = UvCompileOptions { allow_prerelease, @@ -326,6 +337,7 @@ impl Uv { upgrade, no_deps: false, no_header: true, + keyring_provider, }; let mut cmd = self.cmd(); @@ -560,6 +572,7 @@ impl UvWithVenv { requirement: &Requirement, allow_prerelease: bool, exclude_newer: Option, + keyring_provider: KeyringProvider, ) -> Result { let mut cmd = self.venv_cmd(); let options = UvCompileOptions { @@ -568,6 +581,7 @@ impl UvWithVenv { upgrade: UvPackageUpgrade::Nothing, no_deps: true, no_header: true, + keyring_provider, }; cmd.arg("pip").arg("compile");