diff --git a/crates/uv-install-wheel/src/wheel.rs b/crates/uv-install-wheel/src/wheel.rs index 6cf90e50636c1..191d7d94efd6b 100644 --- a/crates/uv-install-wheel/src/wheel.rs +++ b/crates/uv-install-wheel/src/wheel.rs @@ -201,21 +201,25 @@ impl<'script> ValidatedScript<'script> { // Apply the Windows launcher normalization before checking so wheel validity is portable. // FIXME: What are the in-reality rules here for name normalization? let normalized_name = name.strip_suffix(".py").unwrap_or(name.as_str()); - if RESERVED_SCRIPT_NAMES_ERROR.contains(&normalized_name) - || normalized_name + let lowercase_name = name.to_ascii_lowercase(); + let reserved_name = lowercase_name + .strip_suffix(".py") + .unwrap_or(&lowercase_name); + if RESERVED_SCRIPT_NAMES_ERROR.contains(&reserved_name) + || reserved_name .strip_prefix(RESERVED_VERSIONED_SCRIPT_NAME_PREFIX_ERROR) .is_some_and(|minor| minor.parse::().is_ok()) || RESERVED_FREE_THREADED_SCRIPT_NAME_PREFIXES_ERROR .iter() .any(|prefix| { - normalized_name + reserved_name .strip_prefix(prefix) .and_then(|minor| minor.strip_suffix('t')) .is_some_and(|minor| minor.parse::().is_ok()) }) { return Err(Error::ReservedScriptName { - reserved: normalized_name.to_string(), + reserved: reserved_name.to_string(), declared: script.name.clone(), }); } diff --git a/crates/uv/tests/pip_install/pip_install.rs b/crates/uv/tests/pip_install/pip_install.rs index 118570ccacec7..e83e8cec08d78 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -14252,6 +14252,56 @@ fn reject_normalized_reserved_wheel_entrypoint_name() -> Result<()> { Ok(()) } +#[test] +fn reject_case_variant_reserved_wheel_entrypoint_name() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let repacked_wheel = repacked_wheel_with_entrypoint(&context, "console_scripts", "Python")?; + + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: foo-0.1.0-py3-none-any.whl (foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl)) + Caused by: Scripts must not use the reserved name `python`, got: `Python` + "); + + let context = uv_test::test_context!("3.12"); + let repacked_wheel = repacked_wheel_with_entrypoint(&context, "console_scripts", "Python.PY")?; + + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: foo-0.1.0-py3-none-any.whl (foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl)) + Caused by: Scripts must not use the reserved name `python`, got: `Python.PY` + "); + + let context = uv_test::test_context!("3.12"); + let repacked_wheel = repacked_wheel_with_entrypoint(&context, "console_scripts", "python.Py")?; + + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: foo-0.1.0-py3-none-any.whl (foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl)) + Caused by: Scripts must not use the reserved name `python`, got: `python.Py` + "); + + Ok(()) +} + #[test] fn reject_normalized_reserved_gui_wheel_entrypoint_name() -> Result<()> { let context = uv_test::test_context!("3.12");