diff --git a/Cargo.lock b/Cargo.lock index 05904fe589..dc0642af75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1208,9 +1208,9 @@ checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "minijinja" -version = "1.0.21" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e877d961d4f96ce13615862322df7c0b6d169d40cab71a7ef3f9b9e594451e" +checksum = "7165d0e94806d52ad5295e4b54a95176d831814840bc067298ca647e1c956338" dependencies = [ "serde", "serde_json", diff --git a/rye/Cargo.toml b/rye/Cargo.toml index 72431ba886..204f3006d1 100644 --- a/rye/Cargo.toml +++ b/rye/Cargo.toml @@ -24,7 +24,7 @@ git-testament = "0.2.4" globset = "0.4.10" indicatif = "0.17.3" license = { version = "3.1.1", features = ["offline"] } -minijinja = { version = "1.0.0", features = ["json"] } +minijinja = { version = "2.0.1", features = ["json"] } once_cell = "1.17.1" pathdiff = "0.2.1" pep440_rs = "0.4.0" 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/init.rs b/rye/src/cli/init.rs index 907f9292b1..e88608357a 100644 --- a/rye/src/cli/init.rs +++ b/rye/src/cli/init.rs @@ -253,7 +253,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { } if metadata.author.is_none() { is_metadata_author_none = true; - metadata.author = author.clone(); + metadata.author.clone_from(&author); } if metadata.requires_python.is_none() { metadata.requires_python = Some(requires_python); diff --git a/rye/src/cli/lock.rs b/rye/src/cli/lock.rs index e12e0b6ccb..ecbeef1316 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,12 +34,18 @@ 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, /// Use this pyproject.toml file #[arg(long, value_name = "PYPROJECT_TOML")] pyproject: Option, + /// Set to true to lock with hashes in the lockfile. + #[arg(long)] + generate_hashes: bool, } pub fn execute(cmd: Args) -> Result<(), Error> { @@ -55,8 +61,10 @@ pub fn execute(cmd: Args) -> Result<(), Error> { all_features: cmd.all_features, with_sources: cmd.with_sources, reset: cmd.reset, + generate_hashes: cmd.generate_hashes, }, 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..15b12bec76 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,12 +43,18 @@ 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, /// Do not reuse (reset) prior lock options. #[arg(long)] reset: bool, + /// Set to true to lock with hashes in the lockfile. + #[arg(long)] + generate_hashes: bool, } pub fn execute(cmd: Args) -> Result<(), Error> { @@ -71,7 +77,9 @@ pub fn execute(cmd: Args) -> Result<(), Error> { all_features: cmd.all_features, with_sources: cmd.with_sources, reset: cmd.reset, + generate_hashes: cmd.generate_hashes, }, + 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..d910401274 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; @@ -35,6 +36,7 @@ static REQUIREMENTS_HEADER: &str = r#"# generated by rye # features: {{ lock_options.features|tojson }} # all-features: {{ lock_options.all_features|tojson }} # with-sources: {{ lock_options.with_sources|tojson }} +# generate-hashes: {{ lock_options.generate_hashes|tojson }} "#; static PARAM_RE: Lazy = @@ -59,6 +61,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 { @@ -76,6 +90,8 @@ pub struct LockOptions { pub with_sources: bool, /// Do not reuse (reset) prior lock options. pub reset: bool, + /// Generate hashes in the lock file. + pub generate_hashes: bool, } impl LockOptions { @@ -128,6 +144,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 +153,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 +207,7 @@ pub fn update_workspace_lockfile( &lock_options, &exclusions, true, + keyring_provider, )?; Ok(()) @@ -308,6 +327,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 +336,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 +377,7 @@ pub fn update_single_project_lockfile( &lock_options, &exclusions, false, + keyring_provider, )?; Ok(()) @@ -372,6 +394,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 +432,13 @@ fn generate_lockfile( lock_options.pre, env::var("__RYE_UV_EXCLUDE_NEWER").ok(), upgrade, + keyring_provider, + lock_options.generate_hashes, )?; } 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 { @@ -431,6 +459,10 @@ fn generate_lockfile( if lock_options.pre { cmd.arg("--pre"); } + if lock_options.generate_hashes { + cmd.arg("--generate-hashes"); + cmd.arg("--reuse-hashes"); + } cmd.arg(if output == CommandOutput::Verbose { "--verbose" 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..303fe9a9d8 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,8 @@ struct UvCompileOptions { pub upgrade: UvPackageUpgrade, pub no_deps: bool, pub no_header: bool, + pub keyring_provider: KeyringProvider, + pub generate_hashes: bool, } impl UvCompileOptions { @@ -50,6 +52,10 @@ impl UvCompileOptions { cmd.arg("--no-deps"); } + if self.generate_hashes { + cmd.arg("--generate-hashes"); + } + if self.allow_prerelease { cmd.arg("--prerelease=allow"); } @@ -69,6 +75,13 @@ impl UvCompileOptions { } UvPackageUpgrade::Nothing => {} } + + match self.keyring_provider { + KeyringProvider::Disabled => {} + KeyringProvider::Subprocess => { + cmd.arg("--keyring-provider").arg("subprocess"); + } + } } } @@ -80,6 +93,8 @@ impl Default for UvCompileOptions { upgrade: UvPackageUpgrade::Nothing, no_deps: false, no_header: false, + generate_hashes: false, + keyring_provider: KeyringProvider::Disabled, } } } @@ -311,6 +326,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 +335,8 @@ impl Uv { allow_prerelease: bool, exclude_newer: Option, upgrade: UvPackageUpgrade, + keyring_provider: KeyringProvider, + generate_hashes: bool, ) -> Result<(), Error> { let options = UvCompileOptions { allow_prerelease, @@ -326,6 +344,8 @@ impl Uv { upgrade, no_deps: false, no_header: true, + generate_hashes, + keyring_provider, }; let mut cmd = self.cmd(); @@ -560,6 +580,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 +589,8 @@ impl UvWithVenv { upgrade: UvPackageUpgrade::Nothing, no_deps: true, no_header: true, + generate_hashes: false, + keyring_provider, }; cmd.arg("pip").arg("compile"); diff --git a/rye/tests/test_sync.rs b/rye/tests/test_sync.rs index 92cadf625a..61bf9e768b 100644 --- a/rye/tests/test_sync.rs +++ b/rye/tests/test_sync.rs @@ -189,6 +189,7 @@ fn test_autosync_remember() { # features: [] # all-features: true # with-sources: true + # generate-hashes: false --index-url https://pypi.org/simple/ @@ -246,6 +247,7 @@ fn test_autosync_remember() { # features: [] # all-features: true # with-sources: true + # generate-hashes: false --index-url https://pypi.org/simple/