Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
68 changes: 68 additions & 0 deletions crates/pixi/tests/integration_rust/common/pypi_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,74 @@ fn build_module(pkg: &PyPIPackage) -> (String, Vec<u8>) {
(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<PathBuf> {
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<PathBuf> {
let wheel_name = wheel_filename(pkg);
Expand Down
147 changes: 147 additions & 0 deletions crates/pixi/tests/integration_rust/install_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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"
);
}
3 changes: 3 additions & 0 deletions crates/pixi_core/src/lock_file/resolve/build_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions crates/pixi_core/src/lock_file/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/pixi_install_pypi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
34 changes: 34 additions & 0 deletions crates/pixi_install_pypi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Utc>>,
pub skip_wheel_filename_check: Option<bool>,
}

/// Configuration for PyPI context, grouping uv and environment settings
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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<bool>) {
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ prerelease-mode: ~
no-build: ~
dependency-overrides: ~
no-binary: ~
skip-wheel-filename-check: ~
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ prerelease-mode: ~
no-build: ~
dependency-overrides: ~
no-binary: ~
skip-wheel-filename-check: ~
Loading
Loading