diff --git a/Cargo.lock b/Cargo.lock index d315bfb844..e1f07f29e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6091,6 +6091,7 @@ dependencies = [ "uv-distribution", "uv-distribution-filename", "uv-distribution-types", + "uv-flags", "uv-install-wheel", "uv-installer", "uv-normalize", diff --git a/Cargo.toml b/Cargo.toml index 2b9d21c0fe..09a40ef12f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -221,6 +221,7 @@ uv-dispatch = { git = "https://github.com/astral-sh/uv", tag = "0.9.5" } uv-distribution = { git = "https://github.com/astral-sh/uv", tag = "0.9.5" } uv-distribution-filename = { git = "https://github.com/astral-sh/uv", tag = "0.9.5" } uv-distribution-types = { git = "https://github.com/astral-sh/uv", tag = "0.9.5" } +uv-flags = { git = "https://github.com/astral-sh/uv", tag = "0.9.5" } uv-git = { git = "https://github.com/astral-sh/uv", tag = "0.9.5" } uv-git-types = { git = "https://github.com/astral-sh/uv", tag = "0.9.5" } uv-install-wheel = { git = "https://github.com/astral-sh/uv", tag = "0.9.5" } diff --git a/crates/pixi/tests/integration_rust/common/pypi_index.rs b/crates/pixi/tests/integration_rust/common/pypi_index.rs index 564764734f..d0ffad7563 100644 --- a/crates/pixi/tests/integration_rust/common/pypi_index.rs +++ b/crates/pixi/tests/integration_rust/common/pypi_index.rs @@ -304,6 +304,74 @@ fn build_module(pkg: &PyPIPackage) -> (String, Vec) { (path, content) } +/// Write a malformed wheel where the filename version doesn't match the METADATA version. +/// This is used to test the UV_SKIP_WHEEL_FILENAME_CHECK environment variable. +pub fn write_malformed_wheel( + out_dir: &Path, + filename_version: &str, + metadata_version: &str, + name: &str, +) -> miette::Result { + let wheel_name = format!( + "{}-{}-py3-none-any.whl", + normalize_dist_name(name), + filename_version + ); + let wheel_path = out_dir.join(&wheel_name); + + let file = std::fs::File::create(&wheel_path).into_diagnostic()?; + let mut zip = ZipWriter::new(file); + let options = SimpleFileOptions::default(); + + let dist_info = format!( + "{}-{}.dist-info", + normalize_dist_name(name), + metadata_version + ); + + // METADATA with different version than filename + let metadata = format!( + "Metadata-Version: 2.1\nName: {}\nVersion: {}\nSummary: Malformed test wheel\n", + name, metadata_version + ); + + // WHEEL file + let wheel_content = "Wheel-Version: 1.0\nGenerator: pixi-tests-malformed\nRoot-Is-Purelib: true\nTag: py3-none-any\n"; + + // Module file + let module_dir = normalize_dist_name(name).to_string(); + let module_path = format!("{module_dir}/__init__.py"); + let module_content = format!( + "# malformed test package\n__version__ = \"{}\"\n", + metadata_version + ); + + // Write module + zip.start_file(&module_path, options).into_diagnostic()?; + zip.write_all(module_content.as_bytes()).into_diagnostic()?; + + // Write METADATA + let metadata_path = format!("{dist_info}/METADATA"); + zip.start_file(&metadata_path, options).into_diagnostic()?; + zip.write_all(metadata.as_bytes()).into_diagnostic()?; + + // Write WHEEL + let wheel_file_path = format!("{dist_info}/WHEEL"); + zip.start_file(&wheel_file_path, options) + .into_diagnostic()?; + zip.write_all(wheel_content.as_bytes()).into_diagnostic()?; + + // Build and write RECORD + let record_path = format!("{dist_info}/RECORD"); + let record = + format!("{module_path},,\n{metadata_path},,\n{wheel_file_path},,\n{record_path},,\n"); + zip.start_file(&record_path, options).into_diagnostic()?; + zip.write_all(record.as_bytes()).into_diagnostic()?; + + zip.finish().into_diagnostic()?; + Ok(wheel_path) +} + /// Write a wheel to `out_dir` for the package. fn write_wheel(out_dir: &Path, pkg: &PyPIPackage) -> miette::Result { let wheel_name = wheel_filename(pkg); diff --git a/crates/pixi/tests/integration_rust/install_tests.rs b/crates/pixi/tests/integration_rust/install_tests.rs index 0efa5b9135..71c05e074a 100644 --- a/crates/pixi/tests/integration_rust/install_tests.rs +++ b/crates/pixi/tests/integration_rust/install_tests.rs @@ -6,6 +6,7 @@ use std::{ sync::LazyLock, }; +use dunce::canonicalize; use fs_err::tokio as tokio_fs; use pixi_cli::run::{self, Args}; use pixi_cli::{ @@ -1561,3 +1562,149 @@ async fn test_exclude_newer_pypi() { "boltons ==20.2.1".parse().unwrap() )); } + +/// Test that UV_SKIP_WHEEL_FILENAME_CHECK environment variable and pypi-option are respected +/// when installing wheels with version mismatch between filename and metadata +#[tokio::test] +#[cfg_attr( + any(not(feature = "online_tests"), not(feature = "slow_integration_tests")), + ignore +)] +async fn test_uv_skip_wheel_filename_check() { + setup_tracing(); + + // Create a malformed wheel with version mismatch + // Filename says 1.0.0, but METADATA says 2.0.0 + let wheels_dir = tempdir().unwrap(); + crate::common::pypi_index::write_malformed_wheel( + wheels_dir.path(), + "1.0.0", // filename version + "2.0.0", // metadata version + "test-malformed", + ) + .unwrap(); + + let current_platform = Platform::current(); + let wheel_path = canonicalize( + wheels_dir + .path() + .join("test_malformed-1.0.0-py3-none-any.whl"), + ) + .expect("failed to canonicalize wheel path") + .display() + .to_string() + .replace('\\', "/"); // Convert Windows backslashes to forward slashes for TOML + + // Test 1: Environment variable UV_SKIP_WHEEL_FILENAME_CHECK=1 + let manifest_env_var = format!( + r#" + [project] + name = "test-malformed-wheel-env" + channels = ["https://prefix.dev/conda-forge"] + platforms = ["{current_platform}"] + + [dependencies] + python = "3.12.*" + + [pypi-dependencies] + test-malformed = {{ path = "{wheel_path}" }} + "# + ); + + let pixi = + PixiControl::from_manifest(&manifest_env_var).expect("cannot instantiate pixi project"); + + // Installation should succeed with UV_SKIP_WHEEL_FILENAME_CHECK=1 + temp_env::async_with_vars([("UV_SKIP_WHEEL_FILENAME_CHECK", Some("1"))], async { + pixi.install() + .await + .expect("Installation should succeed with UV_SKIP_WHEEL_FILENAME_CHECK=1"); + }) + .await; + + // Verify the package is installed + let prefix_path = pixi.default_env_path().unwrap(); + let cache = uv_cache::Cache::temp().unwrap(); + let env = create_uv_environment(&prefix_path, &cache); + assert!( + is_pypi_package_installed(&env, "test-malformed"), + "Package should be installed with UV_SKIP_WHEEL_FILENAME_CHECK=1" + ); + + // Test 2: pypi-option skip-wheel-filename-check = true + let manifest_pypi_option = format!( + r#" + [project] + name = "test-malformed-wheel-option" + channels = ["https://prefix.dev/conda-forge"] + platforms = ["{current_platform}"] + + [dependencies] + python = "3.12.*" + + [pypi-options] + skip-wheel-filename-check = true + + [pypi-dependencies] + test-malformed = {{ path = "{wheel_path}" }} + "# + ); + + let pixi_option = + PixiControl::from_manifest(&manifest_pypi_option).expect("cannot instantiate pixi project"); + + // Installation should succeed with pypi-option + pixi_option + .install() + .await + .expect("Installation should succeed with skip-wheel-filename-check = true"); + + // Verify the package is installed + let prefix_path = pixi_option.default_env_path().unwrap(); + let cache = uv_cache::Cache::temp().unwrap(); + let env = create_uv_environment(&prefix_path, &cache); + assert!( + is_pypi_package_installed(&env, "test-malformed"), + "Package should be installed with skip-wheel-filename-check = true" + ); + + // Test 3: Environment variable takes precedence over pypi-option + let manifest_precedence = format!( + r#" + [project] + name = "test-malformed-wheel-precedence" + channels = ["https://prefix.dev/conda-forge"] + platforms = ["{current_platform}"] + + [dependencies] + python = "3.12.*" + + [pypi-options] + skip-wheel-filename-check = false + + [pypi-dependencies] + test-malformed = {{ path = "{wheel_path}" }} + "# + ); + + let pixi_precedence = + PixiControl::from_manifest(&manifest_precedence).expect("cannot instantiate pixi project"); + + // Installation should succeed because env var overrides pypi-option + temp_env::async_with_vars([("UV_SKIP_WHEEL_FILENAME_CHECK", Some("1"))], async { + pixi_precedence + .install() + .await + .expect("Installation should succeed when env var overrides pypi-option"); + }) + .await; + + // Verify the package is installed + let prefix_path = pixi_precedence.default_env_path().unwrap(); + let cache = uv_cache::Cache::temp().unwrap(); + let env = create_uv_environment(&prefix_path, &cache); + assert!( + is_pypi_package_installed(&env, "test-malformed"), + "Package should be installed when env var takes precedence" + ); +} diff --git a/crates/pixi_core/src/lock_file/resolve/build_dispatch.rs b/crates/pixi_core/src/lock_file/resolve/build_dispatch.rs index b9a3c40787..d2acfd7b92 100644 --- a/crates/pixi_core/src/lock_file/resolve/build_dispatch.rs +++ b/crates/pixi_core/src/lock_file/resolve/build_dispatch.rs @@ -28,6 +28,7 @@ use crate::{ }; use async_once_cell::OnceCell as AsyncCell; use once_cell::sync::OnceCell; +use pixi_install_pypi::initialize_uv_flags; use pixi_manifest::EnvironmentName; use pixi_manifest::pypi::pypi_options::NoBuildIsolation; use pixi_record::PixiRecord; @@ -315,6 +316,8 @@ impl<'a> LazyBuildDispatch<'a> { async fn get_or_try_init(&self) -> Result<&BuildDispatch<'a>, LazyBuildDispatchError> { self.build_dispatch .get_or_try_init(async { + initialize_uv_flags(None); + // Disallow installing if the flag is set. if self.disallow_install_conda_prefix { return Err(LazyBuildDispatchError::InstallationRequiredButDisallowed); diff --git a/crates/pixi_core/src/lock_file/update.rs b/crates/pixi_core/src/lock_file/update.rs index abcce33531..bb3e075b3f 100644 --- a/crates/pixi_core/src/lock_file/update.rs +++ b/crates/pixi_core/src/lock_file/update.rs @@ -772,6 +772,8 @@ impl<'p> LockFileDerivedData<'p> { let pypi_indexes = self.locked_env(environment)?.pypi_indexes().cloned(); let index_strategy = environment.pypi_options().index_strategy.clone(); let exclude_newer = environment.exclude_newer(); + let skip_wheel_filename_check = + environment.pypi_options().skip_wheel_filename_check; let config = PyPIUpdateConfig { environment_name: environment.name(), @@ -787,6 +789,7 @@ impl<'p> LockFileDerivedData<'p> { no_binary: &no_binary, index_strategy: index_strategy.as_ref(), exclude_newer: exclude_newer.as_ref(), + skip_wheel_filename_check, }; let lazy_env_vars = LazyPixiEnvironmentVars { diff --git a/crates/pixi_install_pypi/Cargo.toml b/crates/pixi_install_pypi/Cargo.toml index 7d56f315c6..0e6dffc7cc 100644 --- a/crates/pixi_install_pypi/Cargo.toml +++ b/crates/pixi_install_pypi/Cargo.toml @@ -51,6 +51,7 @@ uv-dispatch = { workspace = true } uv-distribution = { workspace = true } uv-distribution-filename = { workspace = true } uv-distribution-types = { workspace = true } +uv-flags = { workspace = true } uv-install-wheel = { workspace = true } uv-installer = { workspace = true } uv-normalize = { workspace = true } diff --git a/crates/pixi_install_pypi/src/lib.rs b/crates/pixi_install_pypi/src/lib.rs index dd52a13f39..dc31b6b691 100644 --- a/crates/pixi_install_pypi/src/lib.rs +++ b/crates/pixi_install_pypi/src/lib.rs @@ -176,6 +176,7 @@ pub struct PyPIBuildConfig<'a> { pub no_binary: &'a NoBinary, pub index_strategy: Option<&'a pixi_manifest::pypi::pypi_options::IndexStrategy>, pub exclude_newer: Option<&'a DateTime>, + pub skip_wheel_filename_check: Option, } /// Configuration for PyPI context, grouping uv and environment settings @@ -262,6 +263,9 @@ impl<'a> PyPIEnvironmentUpdater<'a> { pixi_records: &[PixiRecord], pypi_records: &[PyPIRecords], ) -> miette::Result<()> { + // Initialize UV flags from environment variables and pypi-options before any operations + initialize_uv_flags(self.build_config.skip_wheel_filename_check); + let python_info = match on_python_interpreter_change(python_status, self.config.prefix, pypi_records) .await? @@ -1024,3 +1028,33 @@ impl<'a> PyPIEnvironmentUpdater<'a> { Ok(()) } } + +/// Initialize UV flags from environment variables and pypi-options. +/// +/// This function reads UV-related environment variables and pypi-options +/// to initialize the global uv_flags state. Environment variables take +/// precedence over pypi-options. +/// It's safe to call multiple times as the global flag can only be initialized once. +pub fn initialize_uv_flags(skip_wheel_filename_check_option: Option) { + let mut flags = uv_flags::EnvironmentFlags::empty(); + + // Determine if we should skip wheel filename check + // Environment variable takes precedence over pypi-option + let should_skip = if let Ok(env_value) = std::env::var("UV_SKIP_WHEEL_FILENAME_CHECK") { + // Environment variable is set - use it (takes precedence) + matches!( + env_value.to_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) + } else { + // No environment variable - use pypi-option if set, otherwise false + skip_wheel_filename_check_option.unwrap_or(false) + }; + + if should_skip { + flags.insert(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK); + } + + // Initialize the global flags (ignore error if already initialized) + let _ = uv_flags::init(flags); +} diff --git a/crates/pixi_manifest/src/manifests/snapshots/pixi_manifest__manifests__workspace__tests__pypi_options_default_feature.snap b/crates/pixi_manifest/src/manifests/snapshots/pixi_manifest__manifests__workspace__tests__pypi_options_default_feature.snap index caa8cc4890..1faf95e9c3 100644 --- a/crates/pixi_manifest/src/manifests/snapshots/pixi_manifest__manifests__workspace__tests__pypi_options_default_feature.snap +++ b/crates/pixi_manifest/src/manifests/snapshots/pixi_manifest__manifests__workspace__tests__pypi_options_default_feature.snap @@ -14,3 +14,4 @@ prerelease-mode: ~ no-build: ~ dependency-overrides: ~ no-binary: ~ +skip-wheel-filename-check: ~ diff --git a/crates/pixi_manifest/src/manifests/snapshots/pixi_manifest__manifests__workspace__tests__pypy_options_project_and_default_feature.snap b/crates/pixi_manifest/src/manifests/snapshots/pixi_manifest__manifests__workspace__tests__pypy_options_project_and_default_feature.snap index 80f4cc0d87..24c91c28ad 100644 --- a/crates/pixi_manifest/src/manifests/snapshots/pixi_manifest__manifests__workspace__tests__pypy_options_project_and_default_feature.snap +++ b/crates/pixi_manifest/src/manifests/snapshots/pixi_manifest__manifests__workspace__tests__pypy_options_project_and_default_feature.snap @@ -12,3 +12,4 @@ prerelease-mode: ~ no-build: ~ dependency-overrides: ~ no-binary: ~ +skip-wheel-filename-check: ~ diff --git a/crates/pixi_manifest/src/pypi/pypi_options.rs b/crates/pixi_manifest/src/pypi/pypi_options.rs index d2187dd697..bd054dc05e 100644 --- a/crates/pixi_manifest/src/pypi/pypi_options.rs +++ b/crates/pixi_manifest/src/pypi/pypi_options.rs @@ -173,6 +173,8 @@ pub struct PypiOptions { pub dependency_overrides: Option>, /// Don't use pre-built wheels all or certain packages pub no_binary: Option, + /// Skip wheel filename validation + pub skip_wheel_filename_check: Option, } use crate::pypi::merge::{ @@ -191,6 +193,7 @@ impl PypiOptions { no_build: Option, dependency_overrides: Option>, no_binary: Option, + skip_wheel_filename_check: Option, ) -> Self { Self { index_url: index, @@ -202,6 +205,7 @@ impl PypiOptions { no_build, dependency_overrides, no_binary, + skip_wheel_filename_check, } } @@ -257,6 +261,15 @@ impl PypiOptions { } })?; + let skip_wheel_filename_check = merge_single_option( + &self.skip_wheel_filename_check, + &other.skip_wheel_filename_check, + |a, b| PypiOptionsMergeError::MultipleSkipWheelFilenameCheck { + first: *a, + second: *b, + }, + )?; + // Ordered lists, deduplicated let extra_indexes = merge_list_dedup(&self.extra_index_urls, &other.extra_index_urls); let flat_indexes = merge_list_dedup(&self.find_links, &other.find_links); @@ -280,6 +293,7 @@ impl PypiOptions { no_build, dependency_overrides, no_binary, + skip_wheel_filename_check, }) } } @@ -395,6 +409,10 @@ pub enum PypiOptionsMergeError { "multiple prerelease modes are not supported, found both {first} and {second} across multiple pypi options" )] MultiplePrereleaseModes { first: String, second: String }, + #[error( + "multiple skip-wheel-filename-check values are not supported, found both {first} and {second} across multiple pypi options" + )] + MultipleSkipWheelFilenameCheck { first: bool, second: bool }, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -505,6 +523,7 @@ mod tests { ), ])), no_binary: Default::default(), + skip_wheel_filename_check: Some(true), }; // Create the second set of options @@ -537,6 +556,7 @@ mod tests { ])), no_binary: Default::default(), + skip_wheel_filename_check: None, }; // Merge the two options @@ -634,6 +654,7 @@ mod tests { no_build: Default::default(), dependency_overrides: None, no_binary: Default::default(), + skip_wheel_filename_check: None, }; // Create the second set of options @@ -647,6 +668,7 @@ mod tests { no_build: Default::default(), dependency_overrides: None, no_binary: Default::default(), + skip_wheel_filename_check: None, }; // Merge the two options @@ -668,6 +690,7 @@ mod tests { no_build: Default::default(), dependency_overrides: None, no_binary: Default::default(), + skip_wheel_filename_check: None, }; // Create the second set of options @@ -681,6 +704,7 @@ mod tests { no_build: Default::default(), dependency_overrides: None, no_binary: Default::default(), + skip_wheel_filename_check: None, }; // Merge the two options @@ -702,6 +726,7 @@ mod tests { no_build: Default::default(), dependency_overrides: None, no_binary: Default::default(), + skip_wheel_filename_check: None, }; // Create the second set of options @@ -715,6 +740,7 @@ mod tests { no_build: Default::default(), dependency_overrides: None, no_binary: Default::default(), + skip_wheel_filename_check: None, }; // Merge the two options diff --git a/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__merge_pypi_options.snap b/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__merge_pypi_options.snap index 761c23af93..6df7b97800 100644 --- a/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__merge_pypi_options.snap +++ b/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__merge_pypi_options.snap @@ -25,3 +25,4 @@ dependency-overrides: pkg2: version: "==2.0.0" no-binary: ~ +skip-wheel-filename-check: true diff --git a/crates/pixi_manifest/src/toml/pypi_options.rs b/crates/pixi_manifest/src/toml/pypi_options.rs index efb265f786..e0ba150ec1 100644 --- a/crates/pixi_manifest/src/toml/pypi_options.rs +++ b/crates/pixi_manifest/src/toml/pypi_options.rs @@ -133,6 +133,8 @@ impl<'de> toml_span::Deserialize<'de> for PypiOptions { let no_binary = th.optional::("no-binary"); + let skip_wheel_filename_check = th.optional::("skip-wheel-filename-check"); + th.finalize(None)?; Ok(Self { @@ -145,6 +147,7 @@ impl<'de> toml_span::Deserialize<'de> for PypiOptions { no_build, dependency_overrides, no_binary, + skip_wheel_filename_check, }) } } @@ -300,6 +303,7 @@ mod test { }) )]),), no_binary: Default::default(), + skip_wheel_filename_check: None, }, ); } diff --git a/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__full.snap b/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__full.snap index 38f6810793..8365ce19f4 100644 --- a/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__full.snap +++ b/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__full.snap @@ -103,4 +103,5 @@ PypiOptions { }, ), ), + skip_wheel_filename_check: None, } diff --git a/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_binary_packages.snap b/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_binary_packages.snap index 8a6e12bb5a..1597e79213 100644 --- a/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_binary_packages.snap +++ b/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_binary_packages.snap @@ -22,4 +22,5 @@ PypiOptions { }, ), ), + skip_wheel_filename_check: None, } diff --git a/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_build_isolation_boolean.snap b/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_build_isolation_boolean.snap index 4f52a68030..f09ebb78a0 100644 --- a/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_build_isolation_boolean.snap +++ b/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_build_isolation_boolean.snap @@ -12,4 +12,5 @@ PypiOptions { no_build: None, dependency_overrides: None, no_binary: None, + skip_wheel_filename_check: None, } diff --git a/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_build_packages.snap b/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_build_packages.snap index f60e6ae0e1..7c7c4188bf 100644 --- a/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_build_packages.snap +++ b/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_build_packages.snap @@ -22,4 +22,5 @@ PypiOptions { ), dependency_overrides: None, no_binary: None, + skip_wheel_filename_check: None, } diff --git a/docs/reference/pixi_manifest.md b/docs/reference/pixi_manifest.md index 58c9449d8e..4667e5829f 100644 --- a/docs/reference/pixi_manifest.md +++ b/docs/reference/pixi_manifest.md @@ -461,6 +461,7 @@ The options that can be defined are: - `no-binary`: don't use pre-build wheels. - `index-strategy`: allows for specifying the index strategy to use. - `prerelease-mode`: controls whether pre-release versions are allowed during dependency resolution. +- `skip-wheel-filename-check`: allows installing wheels with version mismatches between filename and metadata. These options are explained in the sections below. Most of these options are taken directly or with slight modifications from the [uv settings](https://docs.astral.sh/uv/reference/settings/). If any are missing that you need feel free to create an issue [requesting](https://github.com/prefix-dev/pixi/issues) them. @@ -621,6 +622,39 @@ Example: prerelease-mode = "allow" # Allow all pre-release versions ``` +### Skip Wheel Filename Check + +By default, `uv` validates that wheel filenames match the package metadata (name and version) inside the wheel. This validation ensures that wheels are correctly named and helps prevent installation of malformed packages. + +However, in some cases you may need to install wheels where the filename version doesn't match the metadata version. The `skip-wheel-filename-check` option allows you to disable this validation. + +!!! warning "One skip-wheel-filename-check per environment" + Only one `skip-wheel-filename-check` can be defined per environment or solve-group, otherwise, an error will be shown. + +#### Possible values: + +- **`false`** (default): Perform wheel filename validation. Installation will fail if filename and metadata don't match. +- **`true`**: Skip wheel filename validation. Allow installing wheels with mismatched filename and metadata versions. + +#### Precedence + +The `UV_SKIP_WHEEL_FILENAME_CHECK` environment variable takes precedence over the `skip-wheel-filename-check` pypi-option. This allows for temporary overrides without modifying the manifest. + + +Example: +```toml +[pypi-options] +# Allow installing malformed wheels +skip-wheel-filename-check = true +``` + +Or set per feature: +```toml +[feature.special.pypi-options] +# Only for this feature's environment +skip-wheel-filename-check = true +``` + ## The `dependencies` table(s) ??? info "Details regarding the dependencies" For more detail regarding the dependency types, make sure to check the [Run, Host, Build](../build/dependency_types.md) dependency documentation. diff --git a/schema/examples/valid/full.toml b/schema/examples/valid/full.toml index b73bbfcd10..ea9461ed92 100644 --- a/schema/examples/valid/full.toml +++ b/schema/examples/valid/full.toml @@ -71,6 +71,7 @@ git4 = { git = "https://github.com/prefix-dev/rattler", rev = "v0.1.0", subdirec no-binary = ["testpypi"] no-build = ["foobar"] no-build-isolation = ["requests"] +skip-wheel-filename-check = false [pypi-dependencies] requests = { version = ">= 2.8.1, ==2.8.*", extras = [ @@ -134,6 +135,7 @@ test = "*" [feature.yes-build.pypi-options] dependency-overrides = { numpy = ">=2.0.0" } no-build = true +skip-wheel-filename-check = true [feature.prod] activation = { scripts = ["activate.sh", "deactivate.sh"] } diff --git a/schema/model.py b/schema/model.py index fe656d724c..8b706fec36 100644 --- a/schema/model.py +++ b/schema/model.py @@ -723,6 +723,11 @@ class PyPIOptions(StrictBaseModel): description="The strategy to use when considering pre-release versions", examples=["disallow", "allow", "if-necessary", "explicit", "if-necessary-or-explicit"], ) + skip_wheel_filename_check: bool | None = Field( + None, + description="Skip wheel filename validation, allowing installation of wheels with version mismatches between filename and metadata", + examples=[True, False], + ) ####################### diff --git a/schema/schema.json b/schema/schema.json index 731638fb69..eebb791672 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -1648,6 +1648,15 @@ "explicit", "if-necessary-or-explicit" ] + }, + "skip-wheel-filename-check": { + "title": "Skip-Wheel-Filename-Check", + "description": "Skip wheel filename validation, allowing installation of wheels with version mismatches between filename and metadata", + "type": "boolean", + "examples": [ + true, + false + ] } } },