diff --git a/crates/uv/src/commands/project/install_target.rs b/crates/uv/src/commands/project/install_target.rs index 78d88640960f..066afe2cc373 100644 --- a/crates/uv/src/commands/project/install_target.rs +++ b/crates/uv/src/commands/project/install_target.rs @@ -3,7 +3,7 @@ use std::path::Path; use std::str::FromStr; use itertools::Either; - +use uv_distribution_types::Index; use uv_normalize::PackageName; use uv_pypi_types::{LenientRequirement, VerbatimParsedUrl}; use uv_resolver::{Installable, Lock, Package}; @@ -88,6 +88,38 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> { } impl<'lock> InstallTarget<'lock> { + /// Return an iterator over the [`Index`] definitions in the target. + pub(crate) fn indexes(self) -> impl Iterator { + match self { + Self::Project { workspace, .. } + | Self::Workspace { workspace, .. } + | Self::NonProjectWorkspace { workspace, .. } => { + Either::Left(workspace.indexes().iter().chain( + workspace.packages().values().flat_map(|member| { + member + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.index.as_ref()) + .into_iter() + .flatten() + }), + )) + } + Self::Script { script, .. } => Either::Right( + script + .metadata + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.top_level.index.as_deref()) + .into_iter() + .flatten(), + ), + } + } + /// Return an iterator over all [`Sources`] defined by the target. pub(crate) fn sources(&self) -> impl Iterator { match self { diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 845f15a041e3..1f7ece16db63 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -468,6 +468,12 @@ async fn do_lock( } } + for index in target.indexes() { + if let Some(credentials) = index.credentials() { + uv_auth::store_credentials(index.raw_url(), credentials); + } + } + // Initialize the registry client. let client = RegistryClientBuilder::new(cache.clone()) .native_tls(native_tls) diff --git a/crates/uv/src/commands/project/lock_target.rs b/crates/uv/src/commands/project/lock_target.rs index ba3e9727b88c..e81ca37e79b3 100644 --- a/crates/uv/src/commands/project/lock_target.rs +++ b/crates/uv/src/commands/project/lock_target.rs @@ -5,7 +5,7 @@ use itertools::Either; use uv_configuration::{LowerBound, SourceStrategy}; use uv_distribution::LoweredRequirement; -use uv_distribution_types::IndexLocations; +use uv_distribution_types::{Index, IndexLocations}; use uv_normalize::{GroupName, PackageName}; use uv_pep508::RequirementOrigin; use uv_pypi_types::{Conflicts, Requirement, SupportedEnvironments, VerbatimParsedUrl}; @@ -159,6 +159,34 @@ impl<'lock> LockTarget<'lock> { } } + /// Return an iterator over the [`Index`] definitions in the [`LockTarget`]. + pub(crate) fn indexes(self) -> impl Iterator { + match self { + Self::Workspace(workspace) => Either::Left(workspace.indexes().iter().chain( + workspace.packages().values().flat_map(|member| { + member + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.index.as_ref()) + .into_iter() + .flatten() + }), + )), + Self::Script(script) => Either::Right( + script + .metadata + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.top_level.index.as_deref()) + .into_iter() + .flatten(), + ), + } + } + /// Return the `Requires-Python` bound for the [`LockTarget`]. pub(crate) fn requires_python(self) -> Option { match self { diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 4d1da92e07ed..8558e07b4f78 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -363,8 +363,8 @@ pub(super) async fn do_sync( } } - // Populate credentials from the workspace. - store_credentials_from_workspace(target); + // Populate credentials from the target. + store_credentials_from_target(target); // Initialize the registry client. let client = RegistryClientBuilder::new(cache.clone()) @@ -522,7 +522,14 @@ fn apply_editable_mode(resolution: Resolution, editable: EditableMode) -> Resolu /// /// These credentials can come from any of `tool.uv.sources`, `tool.uv.dev-dependencies`, /// `project.dependencies`, and `project.optional-dependencies`. -fn store_credentials_from_workspace(target: InstallTarget<'_>) { +fn store_credentials_from_target(target: InstallTarget<'_>) { + // Iterate over any idnexes in the target. + for index in target.indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + // Iterate over any sources in the target. for source in target.sources() { match source { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index f872839a7927..cc9137f04efa 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -7503,6 +7503,146 @@ fn lock_peer_member() -> Result<()> { Ok(()) } +/// Lock a workspace in which a member defines an explicit index that requires authentication. +#[test] +fn lock_index_workspace_member() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["child"] + + [tool.uv.workspace] + members = ["child"] + + [tool.uv.sources] + child = { workspace = true } + "#, + )?; + + let child = context.temp_dir.child("child"); + fs_err::create_dir_all(&child)?; + + let pyproject_toml = child.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>=2"] + + [[tool.uv.index]] + name = "my-index" + url = "https://pypi-proxy.fly.dev/basic-auth/simple" + explicit = true + + [tool.uv.sources] + iniconfig = { index = "my-index" } + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // Locking without the necessary credentials should fail. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because iniconfig was not found in the package registry and child depends on iniconfig>=2, we can conclude that child's requirements are unsatisfiable. + And because your workspace requires child, we can conclude that your workspace's requirements are unsatisfiable. + "###); + + uv_snapshot!(context.filters(), context.lock() + .env("UV_INDEX_MY_INDEX_USERNAME", "public") + .env("UV_INDEX_MY_INDEX_PASSWORD", "heron"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + members = [ + "child", + "project", + ] + + [[package]] + name = "child" + version = "0.1.0" + source = { editable = "child" } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig", specifier = ">=2", index = "https://pypi-proxy.fly.dev/basic-auth/simple" }] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi-proxy.fly.dev/basic-auth/simple" } + sdist = { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "child" }, + ] + + [package.metadata] + requires-dist = [{ name = "child", editable = "child" }] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock() + .env("UV_INDEX_MY_INDEX_USERNAME", "public") + .env("UV_INDEX_MY_INDEX_PASSWORD", "heron") + .arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + Ok(()) +} + /// Ensure that development dependencies are omitted for non-workspace members. Below, `bar` depends /// on `foo`, but `bar/uv.lock` should omit `anyio`, but should include `typing-extensions`. #[test]