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 000000000..cff4cc596 Binary files /dev/null and b/scripts/links/basic_app-0.1.0-py3-none-any.whl differ