From 170ab1cd7fb795bdfa228162ace3328c7ccf0d12 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 29 Sep 2025 13:23:18 -0400 Subject: [PATCH] Ignore origin when comparing installed tools (#16055) ## Summary This field gets dropped when you serialize and deserialize, so we should ignore it when comparing indexes. Closes https://github.com/astral-sh/uv/issues/16051. --- crates/uv-distribution-types/src/index.rs | 88 +++++++++++++- crates/uv/tests/it/tool_install.rs | 111 +++++++++++++++++- .../links/basic_app-0.1.0-py3-none-any.whl | Bin 0 -> 1562 bytes 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 scripts/links/basic_app-0.1.0-py3-none-any.whl diff --git a/crates/uv-distribution-types/src/index.rs b/crates/uv-distribution-types/src/index.rs index 3853915fb..2090f8ba8 100644 --- a/crates/uv-distribution-types/src/index.rs +++ b/crates/uv-distribution-types/src/index.rs @@ -48,7 +48,7 @@ impl IndexCacheControl { } } -#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case")] pub struct Index { @@ -156,6 +156,92 @@ pub struct Index { pub cache_control: Option, } +impl PartialEq for Index { + fn eq(&self, other: &Self) -> bool { + let Self { + name, + url, + explicit, + default, + origin: _, + format, + publish_url, + authenticate, + ignore_error_codes, + cache_control, + } = self; + *url == other.url + && *name == other.name + && *explicit == other.explicit + && *default == other.default + && *format == other.format + && *publish_url == other.publish_url + && *authenticate == other.authenticate + && *ignore_error_codes == other.ignore_error_codes + && *cache_control == other.cache_control + } +} + +impl Eq for Index {} + +impl PartialOrd for Index { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Index { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let Self { + name, + url, + explicit, + default, + origin: _, + format, + publish_url, + authenticate, + ignore_error_codes, + cache_control, + } = self; + url.cmp(&other.url) + .then_with(|| name.cmp(&other.name)) + .then_with(|| explicit.cmp(&other.explicit)) + .then_with(|| default.cmp(&other.default)) + .then_with(|| format.cmp(&other.format)) + .then_with(|| publish_url.cmp(&other.publish_url)) + .then_with(|| authenticate.cmp(&other.authenticate)) + .then_with(|| ignore_error_codes.cmp(&other.ignore_error_codes)) + .then_with(|| cache_control.cmp(&other.cache_control)) + } +} + +impl std::hash::Hash for Index { + fn hash(&self, state: &mut H) { + let Self { + name, + url, + explicit, + default, + origin: _, + format, + publish_url, + authenticate, + ignore_error_codes, + cache_control, + } = self; + url.hash(state); + name.hash(state); + explicit.hash(state); + default.hash(state); + format.hash(state); + publish_url.hash(state); + authenticate.hash(state); + ignore_error_codes.hash(state); + cache_control.hash(state); + } +} + #[derive( Default, Debug, diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index bb7f45b32..ced7a7782 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -3906,7 +3906,6 @@ fn tool_install_default_credentials() -> Result<()> { sys.argv[0] = sys.argv[0][:-4] sys.exit(main()) "###); - }); insta::with_settings!({ @@ -4094,6 +4093,116 @@ fn tool_install_with_executables_from_no_entrypoints() { "###); } +#[test] +fn tool_install_find_links() { + let context = TestContext::new("3.13").with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Run with `--find-links`. + uv_snapshot!(context.filters(), context.tool_run() + .arg("--find-links") + .arg(context.workspace_root.join("scripts/links/")) + .arg("basic-app") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + Hello from basic-app! + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + basic-app==0.1.0 + "); + + // Install with `--find-links`. + uv_snapshot!(context.filters(), context.tool_install() + .arg("--find-links") + .arg(context.workspace_root.join("scripts/links/")) + .arg("basic-app") + .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 1 package in [TIME] + Installed 1 package in [TIME] + + basic-app==0.1.0 + Installed 1 executable: basic-app + "); + + tool_dir + .child("basic-app") + .assert(predicate::path::is_dir()); + tool_dir + .child("basic-app") + .child("uv-receipt.toml") + .assert(predicate::path::exists()); + + let executable = bin_dir.child(format!("basic-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 basic-app in the virtual environment + assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r#" + #![TEMP_DIR]/tools/basic-app/bin/python + # -*- coding: utf-8 -*- + import sys + from basic_app 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()) + "#); + }); + + // Run the installed version with `--find-links` on the CLI again. + uv_snapshot!(context.filters(), context.tool_run() + .arg("--offline") + .arg("--find-links") + .arg(context.workspace_root.join("scripts/links/")) + .arg("basic-app") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + Hello from basic-app! + + ----- stderr ----- + "); + + // Run the installed version without `--find-links`. + uv_snapshot!(context.filters(), context.tool_run() + .arg("--offline") + .arg("basic-app") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving tool dependencies: + ╰─▶ Because only basic-app==0.1 is available and basic-app==0.1 needs to be downloaded from a registry, we can conclude that all versions of basic-app cannot be used. + And because you require basic-app, we can conclude that your requirements are unsatisfiable. + + hint: Packages were unavailable because the network was disabled. When the network is disabled, registry packages may only be read from the cache. + "); +} + #[test] fn tool_install_python_platform() { let context = TestContext::new("3.12") diff --git a/scripts/links/basic_app-0.1.0-py3-none-any.whl b/scripts/links/basic_app-0.1.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..cff4cc59630c04c36d66d0ec87b78602a8a3494b GIT binary patch literal 1562 zcmWIWW@Zs#fB;1(4WYPzR8nGbW^#ODL4ke%R2>IURFNS!XO^@jkYx_UqA2R(<1_Oz zOXB183M#!leS=T*Ldb=!!5>ba_ul#_C?Fs}L*vvL?TZ1QJkFjx8SbHd>U^k& zm(Dq#YrZ-f0U?*3FfqXF0vZgo3mHfOIdC`W8t5778R(^C7MJK|=B4GMxpD7Pf44xO zdVe66$Ez>g!`0P?`@Dzm*^9j1x?1PXoZlQ|aK-pRk^fn5T`!%J`kR6@bb}2pY`$P@ z^oZ-!dHsuDb*}Mh=z5(!=>wF~)bf4G<>|9MNMnV`wX;BPUiCe*ng#9)NQgb^ej%a* zbcG%eYvFZGYF=pyhpKS8d~w`S+T- zg4TDfsED6^l*Kp5JbFvMzH8tIi~HsVn-v|~n|`^(IQfY!TNV`Elk%`f)kNxcSnbmr zK4&6icS%gF=GK0`;Jj?};!6b?s!v$v&A75kc>YbNw-fUl6PjcGv}bim$pJ#OZ=yFTe3=sHvTxTP53jvw z-jq!clHg`!U}ykh1-#x2a&`6(a;Z6MIjzZnf#Jfv%`7Y_l3kiJPBfg*)4TDcwNO!s z(UaxNm*9(4jsLmNoRB}1Ywg|l@=I60+OS46Dv~YUbzVh7L^Ftcv^_$PlJG)ghM${v5 zU3&M!)0f3crmT)ncUV@PY4+Y>dP8De!1*cV`d7?aiqt+$^l!A@aLMk+d-wH^658`V z-u36u%KAKKLg8Z$?WOyrZYwseVOsT1k>kyosHmBy2Tz_i)}8TahF_%g_oC$oum1~_ z^Uu4vlkMANo?VHh)$N6InNt<(Cj?8oy%cJ=X%7sc|Np@RGcX_-nM9az=YOCwFnH?- zqTo3XnA2eTu$KoAT?`CM8Z}V#A(ajYBS4B3VLHGxD4WB;TgNFtCfsT)nI6?C=-D1- z&XUG6c+CM90tiQ8&$S4{qJW7Fx1-SWFuJ+uITK;-IlSh=@+rD`=ve?^-c}}j?u2BB U0B=?{kPdbrWCN=I#|+{D0GHj}0RR91 literal 0 HcmV?d00001