diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index 6baca1c1f..bc0fcf59c 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -203,7 +203,11 @@ impl serde::ser::Serialize for IndexUrl { where S: serde::ser::Serializer, { - self.to_string().serialize(serializer) + match self { + Self::Pypi(url) => url.without_credentials().serialize(serializer), + Self::Url(url) => url.without_credentials().serialize(serializer), + Self::Path(url) => url.without_credentials().serialize(serializer), + } } } @@ -404,6 +408,9 @@ impl<'a> IndexLocations { } else { let mut indexes = vec![]; + // TODO(charlie): By only yielding the first default URL, we'll drop credentials if, + // e.g., an authenticated default URL is provided in a configuration file, but an + // unauthenticated default URL is present in the receipt. let mut seen = FxHashSet::default(); let mut default = false; for index in { diff --git a/crates/uv-redacted/src/lib.rs b/crates/uv-redacted/src/lib.rs index 5c9a8e278..a0534c46d 100644 --- a/crates/uv-redacted/src/lib.rs +++ b/crates/uv-redacted/src/lib.rs @@ -1,5 +1,6 @@ use ref_cast::RefCast; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; use std::str::FromStr; @@ -98,6 +99,24 @@ impl DisplaySafeUrl { let _ = self.0.set_password(None); } + /// Returns the URL with any credentials removed. + pub fn without_credentials(&self) -> Cow<'_, Url> { + if self.0.password().is_none() && self.0.username() == "" { + return Cow::Borrowed(&self.0); + } + + // For URLs that use the `git` convention (i.e., `ssh://git@github.com/...`), avoid dropping the + // username. + if is_ssh_git_username(&self.0) { + return Cow::Borrowed(&self.0); + } + + let mut url = self.0.clone(); + let _ = url.set_username(""); + let _ = url.set_password(None); + Cow::Owned(url) + } + /// Returns [`Display`] implementation that doesn't mask credentials. #[inline] pub fn displayable_with_credentials(&self) -> impl Display { diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index 6a2d38db8..00cc17c04 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -3629,3 +3629,83 @@ fn tool_install_mismatched_name() { error: Package name (`black`) provided with `--from` does not match install request (`flask`) "###); } + +/// When installing from an authenticated index, the credentials should be omitted from the receipt. +#[test] +fn tool_install_credentials() { + let context = TestContext::new("3.12") + .with_exclude_newer("2025-01-18T00:00:00Z") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Install `executable-application` + uv_snapshot!(context.filters(), context.tool_install() + .arg("executable-application") + .arg("--index") + .arg("https://public:heron@pypi-proxy.fly.dev/basic-auth/simple") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .env(EnvVars::PATH, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + executable-application==0.3.0 + Installed 1 executable: app + "###); + + tool_dir + .child("executable-application") + .assert(predicate::path::is_dir()); + tool_dir + .child("executable-application") + .child("uv-receipt.toml") + .assert(predicate::path::exists()); + + let executable = bin_dir.child(format!("app{}", std::env::consts::EXE_SUFFIX)); + assert!(executable.exists()); + + // On Windows, we can't snapshot an executable file. + #[cfg(not(windows))] + insta::with_settings!({ + filters => context.filters(), + }, { + // Should run black in the virtual environment + assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###" + #![TEMP_DIR]/tools/executable-application/bin/python + # -*- coding: utf-8 -*- + import sys + from executable_application import main + if __name__ == "__main__": + if sys.argv[0].endswith("-script.pyw"): + sys.argv[0] = sys.argv[0][:-11] + elif sys.argv[0].endswith(".exe"): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) + "###); + + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + // We should have a tool receipt + assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#" + [tool] + requirements = [{ name = "executable-application" }] + entrypoints = [ + { name = "app", install-path = "[TEMP_DIR]/bin/app" }, + ] + + [tool.options] + index = [{ url = "https://pypi-proxy.fly.dev/basic-auth/simple", explicit = false, default = false, format = "simple", authenticate = "auto" }] + exclude-newer = "2025-01-18T00:00:00Z" + "#); + }); +}