diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index adf87ba757ea..1e5d95b2054c 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -236,13 +236,17 @@ impl LoweredRequirement { .find(|Index { name, .. }| { name.as_ref().is_some_and(|name| *name == index) }) - .map(|Index { url: index, .. }| index.clone()) else { return Err(LoweringError::MissingIndex( requirement.name.clone(), index, )); }; + let url = if let Some(credentials) = index.credentials() { + credentials.apply(index.url.clone().into_url()) + } else { + index.url.clone().into_url() + }; let conflict = project_name.and_then(|project_name| { if let Some(extra) = extra { Some(ConflictItem::from((project_name.clone(), extra))) @@ -252,12 +256,7 @@ impl LoweredRequirement { }) } }); - let source = registry_source( - &requirement, - index.into_url(), - conflict, - lower_bound, - ); + let source = registry_source(&requirement, url, conflict, lower_bound); (source, marker) } Source::Workspace { @@ -465,20 +464,19 @@ impl LoweredRequirement { .find(|Index { name, .. }| { name.as_ref().is_some_and(|name| *name == index) }) - .map(|Index { url: index, .. }| index.clone()) else { return Err(LoweringError::MissingIndex( requirement.name.clone(), index, )); }; + let url = if let Some(credentials) = index.credentials() { + credentials.apply(index.url.clone().into_url()) + } else { + index.url.clone().into_url() + }; let conflict = None; - let source = registry_source( - &requirement, - index.into_url(), - conflict, - lower_bound, - ); + let source = registry_source(&requirement, url, conflict, lower_bound); (source, marker) } Source::Workspace { .. } => { 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]