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 92d731f8d..777371c52 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 @@ -1,6 +1,6 @@ --- source: crates/pixi_manifest/src/manifests/workspace.rs -expression: "toml_edit::de::from_str::(&contents).expect(\"parsing should succeed!\").workspace.pypi_options.clone().unwrap()" +expression: "WorkspaceManifest::from_toml_str(&contents).expect(\"parsing should succeed!\").workspace.pypi_options.clone().unwrap()" --- index-url: "https://pypi.org/simple" extra-index-urls: @@ -10,3 +10,4 @@ find-links: - url: "https://example.com/bar" no-build-isolation: ~ index-strategy: ~ +no-build: ~ 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 3c897c775..95ffa8c83 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 @@ -8,3 +8,4 @@ extra-index-urls: find-links: ~ no-build-isolation: ~ index-strategy: ~ +no-build: ~ diff --git a/crates/pixi_manifest/src/pypi/pypi_options.rs b/crates/pixi_manifest/src/pypi/pypi_options.rs index a5d2bd80e..e941a2ca9 100644 --- a/crates/pixi_manifest/src/pypi/pypi_options.rs +++ b/crates/pixi_manifest/src/pypi/pypi_options.rs @@ -1,4 +1,4 @@ -use std::{hash::Hash, path::PathBuf}; +use std::{collections::HashSet, hash::Hash, path::PathBuf}; use indexmap::IndexSet; use serde::Serialize; @@ -48,6 +48,40 @@ pub enum FindLinksUrlOrPath { Url(Url), } +/// Don't build sdist for all or certain packages +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, strum::Display)] +#[strum(serialize_all = "kebab-case")] +#[serde(rename_all = "kebab-case")] +pub enum NoBuild { + /// Build any sdist we come across + #[default] + None, + /// Don't build any sdist + All, + /// Don't build sdist for specific packages + // Todo: would be nice to check if these are actually used at some point + Packages(HashSet), +} + +impl NoBuild { + /// Merges two `NoBuild` together, according to the following rules + /// - If either is `All`, the result is `All` + /// - If either is `None`, the result is the other + /// - If both are `Packages`, the result is the union of the two + pub fn union(&self, other: &NoBuild) -> NoBuild { + match (self, other) { + (NoBuild::All, _) | (_, NoBuild::All) => NoBuild::All, + (NoBuild::None, _) => other.clone(), + (_, NoBuild::None) => self.clone(), + (NoBuild::Packages(packages), NoBuild::Packages(other_packages)) => { + let mut packages = packages.clone(); + packages.extend(other_packages.iter().cloned()); + NoBuild::Packages(packages) + } + } + } +} + /// Specific options for a PyPI registries #[derive(Debug, Clone, PartialEq, Serialize, Eq, Default)] #[serde(rename_all = "kebab-case")] @@ -63,6 +97,8 @@ pub struct PypiOptions { pub no_build_isolation: Option>, /// The strategy to use when resolving against multiple index URLs. pub index_strategy: Option, + /// Don't build sdist for all or certain packages + pub no_build: Option, } /// Clones and deduplicates two iterators of values @@ -85,6 +121,7 @@ impl PypiOptions { flat_indexes: Option>, no_build_isolation: Option>, index_strategy: Option, + no_build: Option, ) -> Self { Self { index_url: index, @@ -92,6 +129,7 @@ impl PypiOptions { find_links: flat_indexes, no_build_isolation, index_strategy, + no_build, } } @@ -190,12 +228,21 @@ impl PypiOptions { }) .or_else(|| other.no_build_isolation.clone()); + // Set the no-build option + let no_build = match (self.no_build.as_ref(), other.no_build.as_ref()) { + (Some(a), Some(b)) => Some(a.union(b)), + (Some(a), None) => Some(a.clone()), + (None, Some(b)) => Some(b.clone()), + (None, None) => None, + }; + Ok(PypiOptions { index_url: index, extra_index_urls: extra_indexes, find_links: flat_indexes, no_build_isolation, index_strategy, + no_build, }) } } @@ -278,6 +325,7 @@ mod tests { ]), no_build_isolation: Some(vec!["foo".to_string(), "bar".to_string()]), index_strategy: None, + no_build: None, }; // Create the second set of options @@ -290,6 +338,7 @@ mod tests { ]), no_build_isolation: Some(vec!["foo".to_string()]), index_strategy: None, + no_build: Some(NoBuild::All), }; // Merge the two options @@ -298,6 +347,40 @@ mod tests { insta::assert_yaml_snapshot!(merged_opts); } + #[test] + fn test_no_build_union() { + // Case 1: One is `All`, result should be `All` + assert_eq!(NoBuild::All.union(&NoBuild::None), NoBuild::All); + assert_eq!(NoBuild::None.union(&NoBuild::All), NoBuild::All); + assert_eq!( + NoBuild::All.union(&NoBuild::Packages(HashSet::from_iter(["pkg1".to_string()]))), + NoBuild::All + ); + + // Case 2: One is `None`, result should be the other + assert_eq!(NoBuild::None.union(&NoBuild::None), NoBuild::None); + assert_eq!( + NoBuild::None.union(&NoBuild::Packages(HashSet::from_iter(["pkg1".to_string()]))), + NoBuild::Packages(HashSet::from_iter(["pkg1".to_string()])) + ); + assert_eq!( + NoBuild::Packages(HashSet::from_iter(["pkg1".to_string()])).union(&NoBuild::None), + NoBuild::Packages(HashSet::from_iter(["pkg1".to_string()])) + ); + + // Case 3: Both are `Packages`, result should be the union of the two + assert_eq!( + NoBuild::Packages(HashSet::from_iter(["pkg1".to_string(), "pkg2".to_string()])).union( + &NoBuild::Packages(HashSet::from_iter(["pkg2".to_string(), "pkg3".to_string()])) + ), + NoBuild::Packages(HashSet::from_iter([ + "pkg1".to_string(), + "pkg2".to_string(), + "pkg3".to_string() + ])) + ); + } + #[test] fn test_error_on_multiple_primary_indexes() { // Create the first set of options @@ -307,6 +390,7 @@ mod tests { find_links: None, no_build_isolation: None, index_strategy: None, + no_build: Default::default(), }; // Create the second set of options @@ -316,6 +400,7 @@ mod tests { find_links: None, no_build_isolation: None, index_strategy: None, + no_build: Default::default(), }; // Merge the two options @@ -333,6 +418,7 @@ mod tests { find_links: None, no_build_isolation: None, index_strategy: Some(IndexStrategy::FirstIndex), + no_build: Default::default(), }; // Create the second set of options @@ -342,6 +428,7 @@ mod tests { find_links: None, no_build_isolation: None, index_strategy: Some(IndexStrategy::UnsafeBestMatch), + no_build: Default::default(), }; // 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 4c7bcb8a6..a53d67d53 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 @@ -15,3 +15,4 @@ no-build-isolation: - foo - bar index-strategy: ~ +no-build: all diff --git a/crates/pixi_manifest/src/toml/pypi_options.rs b/crates/pixi_manifest/src/toml/pypi_options.rs index 0a9959b74..523c5c7f4 100644 --- a/crates/pixi_manifest/src/toml/pypi_options.rs +++ b/crates/pixi_manifest/src/toml/pypi_options.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{collections::HashSet, path::PathBuf}; use pixi_toml::{TomlEnum, TomlFromStr, TomlWith}; use toml_span::{ @@ -8,7 +8,45 @@ use toml_span::{ }; use url::Url; -use crate::pypi::pypi_options::{FindLinksUrlOrPath, PypiOptions}; +use crate::pypi::pypi_options::{FindLinksUrlOrPath, NoBuild, PypiOptions}; + +impl<'de> toml_span::Deserialize<'de> for NoBuild { + fn deserialize(value: &mut Value<'de>) -> Result { + // It can be either `true` or `false` or an array of strings + if value.as_bool().is_some() { + if bool::deserialize(value)? { + return Ok(NoBuild::All); + } else { + return Ok(NoBuild::None); + } + } + // We assume it's an array of strings + if value.as_array().is_some() { + match value.take() { + ValueInner::Array(array) => { + let mut packages = HashSet::with_capacity(array.len()); + for mut value in array { + packages.insert(value.take_string(None)?.into_owned()); + } + Ok(NoBuild::Packages(packages)) + } + _ => Err(expected( + "an array of packages e.g. [\"foo\", \"bar\"]", + value.take(), + value.span, + ) + .into()), + } + } else { + Err(expected( + r#"either "all", "none" or an array of packages e.g. ["foo", "bar"] "#, + value.take(), + value.span, + ) + .into()) + } + } +} impl<'de> toml_span::Deserialize<'de> for PypiOptions { fn deserialize(value: &mut Value<'de>) -> Result { @@ -26,6 +64,8 @@ impl<'de> toml_span::Deserialize<'de> for PypiOptions { .optional::>("index-strategy") .map(TomlEnum::into_inner); + let no_build = th.optional::("no-build"); + th.finalize(None)?; Ok(Self { @@ -34,6 +74,7 @@ impl<'de> toml_span::Deserialize<'de> for PypiOptions { find_links, no_build_isolation, index_strategy, + no_build, }) } } @@ -149,6 +190,7 @@ mod test { ]), no_build_isolation: Some(vec!["pkg1".to_string(), "pkg2".to_string()]), index_strategy: None, + no_build: Default::default(), }, ); } @@ -164,6 +206,16 @@ mod test { ] no-build-isolation = ["sigma"] index-strategy = "first-index" + no-build = true + "#; + let options = PypiOptions::from_toml_str(input).unwrap(); + assert_debug_snapshot!(options); + } + + #[test] + fn test_no_build_packages() { + let input = r#" + no-build = ["package1"] "#; let options = PypiOptions::from_toml_str(input).unwrap(); assert_debug_snapshot!(options); @@ -257,4 +309,19 @@ mod test { "### ) } + + #[test] + fn test_wrong_build_option_type() { + let input = r#"no-build = 3"#; + assert_snapshot!(format_parse_error( + input, + PypiOptions::from_toml_str(input).unwrap_err() + ), @r###" + × expected either "all", "none" or an array of packages e.g. ["foo", "bar"] , found integer + ╭─[pixi.toml:1:12] + 1 │ no-build = 3 + · ─ + ╰──── + "###) + } } 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 368388905..72f631be0 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 @@ -82,4 +82,7 @@ PypiOptions { index_strategy: Some( FirstIndex, ), + no_build: Some( + All, + ), } 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 new file mode 100644 index 000000000..58036b036 --- /dev/null +++ b/crates/pixi_manifest/src/toml/snapshots/pixi_manifest__toml__pypi_options__test__no_build_packages.snap @@ -0,0 +1,18 @@ +--- +source: crates/pixi_manifest/src/toml/pypi_options.rs +expression: options +--- +PypiOptions { + index_url: None, + extra_index_urls: None, + find_links: None, + no_build_isolation: None, + index_strategy: None, + no_build: Some( + Packages( + { + "package1", + }, + ), + ), +} diff --git a/crates/pixi_manifest/src/validation.rs b/crates/pixi_manifest/src/validation.rs index 90f17c1a4..39ebd2fa3 100644 --- a/crates/pixi_manifest/src/validation.rs +++ b/crates/pixi_manifest/src/validation.rs @@ -9,8 +9,8 @@ use std::{ use super::pypi::pypi_options::PypiOptions; use crate::{ - Environment, Feature, FeatureName, KnownPreviewFeature, SystemRequirements, TargetSelector, - WorkspaceManifest, + pypi::pypi_options::NoBuild, Environment, Feature, FeatureName, KnownPreviewFeature, + SystemRequirements, TargetSelector, WorkspaceManifest, }; impl WorkspaceManifest { @@ -236,7 +236,7 @@ impl WorkspaceManifest { } // Check if there are no conflicts in pypi options between features - features + let opts = features .iter() .chain(default) .filter_map(|feature| { @@ -250,6 +250,23 @@ impl WorkspaceManifest { .try_fold(PypiOptions::default(), |acc, opts| acc.union(opts)) .into_diagnostic()?; + // If no-build is set, check if the package names are pep508 compliant + if let Some(NoBuild::Packages(packages)) = opts.no_build { + let packages = packages + .iter() + .map(|p| pep508_rs::PackageName::new(p.clone())) + .collect::, _>>(); + if let Err(e) = packages { + return Err(miette::miette!( + labels = vec![LabeledSpan::at( + env.features_source_loc.clone().unwrap_or_default(), + "while resolving no-build packages array" + )], + "{e}", + )); + } + } + Ok(()) } } diff --git a/crates/pixi_uv_conversions/src/conversions.rs b/crates/pixi_uv_conversions/src/conversions.rs index a426d0b6c..79f2745ce 100644 --- a/crates/pixi_uv_conversions/src/conversions.rs +++ b/crates/pixi_uv_conversions/src/conversions.rs @@ -4,17 +4,20 @@ use std::str::FromStr; use pixi_git::sha::GitSha as PixiGitSha; use pixi_git::url::RepositoryUrl; use pixi_manifest::pypi::pypi_options::FindLinksUrlOrPath; -use pixi_manifest::pypi::pypi_options::{IndexStrategy, PypiOptions}; +use pixi_manifest::pypi::pypi_options::{IndexStrategy, NoBuild, PypiOptions}; use pixi_record::{LockedGitUrl, PinnedGitCheckout, PinnedGitSpec}; use pixi_spec::GitReference as PixiReference; use pixi_git::git::GitReference as PixiGitReference; use pep440_rs::VersionSpecifiers; +use uv_configuration::BuildOptions; use uv_distribution_types::{GitSourceDist, Index, IndexLocations, IndexUrl}; use uv_pep508::{InvalidNameError, PackageName, VerbatimUrl, VerbatimUrlError}; use uv_python::PythonEnvironment; +use crate::VersionError; + #[derive(thiserror::Error, Debug)] pub enum ConvertFlatIndexLocationError { #[error("could not convert path to flat index location {1}")] @@ -23,6 +26,23 @@ pub enum ConvertFlatIndexLocationError { NotAbsolute(PathBuf), } +/// Convert PyPI options to build options +pub fn no_build_to_build_options(no_build: &NoBuild) -> Result { + let uv_no_build = match no_build { + NoBuild::None => uv_configuration::NoBuild::None, + NoBuild::All => uv_configuration::NoBuild::All, + NoBuild::Packages(ref vec) => uv_configuration::NoBuild::Packages( + vec.iter() + .map(|s| PackageName::new(s.clone())) + .collect::, _>>()?, + ), + }; + Ok(BuildOptions::new( + uv_configuration::NoBinary::default(), + uv_no_build, + )) +} + /// Convert the subset of pypi-options to index locations pub fn pypi_options_to_index_locations( options: &PypiOptions, @@ -250,7 +270,7 @@ pub fn into_pinned_git_spec(dist: GitSourceDist) -> PinnedGitSpec { /// /// So we need to convert the locked git url into a parsed git url. /// which is used in the uv crate. -pub fn into_parsed_git_url( +pub fn to_parsed_git_url( locked_git_url: &LockedGitUrl, ) -> miette::Result { let git_source = PinnedGitCheckout::from_locked_url(locked_git_url)?; @@ -265,16 +285,138 @@ pub fn into_parsed_git_url( Ok(parsed_git_url) } -/// uv_pep440 and pep440 are very similar but not the same, this can convert to uv_pep440::VersionSpecifiers -pub fn as_uv_specifiers( +/// Converts from the open-source variant to the uv-specific variant, +/// these are incompatible types +pub fn to_uv_specifiers( specifiers: &VersionSpecifiers, ) -> Result { uv_pep440::VersionSpecifiers::from_str(specifiers.to_string().as_str()) } -/// uv_pep440 and pep440 are very similar but not the same, this can convert to uv_pep440::Version -pub fn as_uv_version( +pub fn to_requirements<'req>( + requirements: impl Iterator, +) -> Result, crate::ConversionError> { + let requirements: Result, _> = requirements + .map(|requirement| { + let requirement: uv_pep508::Requirement = + uv_pep508::Requirement::from(requirement.clone()); + pep508_rs::Requirement::from_str(&requirement.to_string()) + .map_err(crate::Pep508Error::Pep508Error) + }) + .collect(); + + Ok(requirements?) +} + +/// Convert back to PEP508 without the VerbatimParsedUrl +/// We need this function because we need to convert to the introduced +/// `VerbatimParsedUrl` back to crates.io `VerbatimUrl`, for the locking +pub fn convert_uv_requirements_to_pep508<'req>( + requires_dist: impl Iterator>, +) -> Result, crate::ConversionError> { + // Convert back top PEP508 Requirement + let requirements: Result, _> = requires_dist + .map(|r| { + let requirement = r.to_string(); + pep508_rs::Requirement::from_str(&requirement).map_err(crate::Pep508Error::Pep508Error) + }) + .collect(); + + Ok(requirements?) +} + +/// Converts `uv_normalize::PackageName` to `pep508_rs::PackageName` +pub fn to_normalize( + normalise: &uv_normalize::PackageName, +) -> Result { + Ok(pep508_rs::PackageName::from_str(normalise.as_str()) + .map_err(crate::NameError::PepNameError)?) +} + +/// Converts `pe508::PackageName` to `uv_normalize::PackageName` +pub fn to_uv_normalize( + normalise: &pep508_rs::PackageName, +) -> Result { + Ok( + uv_normalize::PackageName::from_str(normalise.to_string().as_str()) + .map_err(crate::NameError::UvNameError)?, + ) +} + +/// Converts `pep508_rs::ExtraName` to `uv_normalize::ExtraName` +pub fn to_uv_extra_name( + extra_name: &pep508_rs::ExtraName, +) -> Result { + Ok( + uv_normalize::ExtraName::from_str(extra_name.to_string().as_str()) + .map_err(crate::NameError::UvExtraNameError)?, + ) +} + +/// Converts `uv_normalize::ExtraName` to `pep508_rs::ExtraName` +pub fn to_extra_name( + extra_name: &uv_normalize::ExtraName, +) -> Result { + Ok( + pep508_rs::ExtraName::from_str(extra_name.to_string().as_str()) + .map_err(crate::NameError::PepExtraNameError)?, + ) +} + +/// Converts `pep440_rs::Version` to `uv_pep440::Version` +pub fn to_uv_version( version: &pep440_rs::Version, -) -> Result { - uv_pep440::Version::from_str(version.to_string().as_str()) +) -> Result { + Ok( + uv_pep440::Version::from_str(version.to_string().as_str()) + .map_err(VersionError::UvError)?, + ) +} + +/// Converts `pep508_rs::MarkerTree` to `uv_pep508::MarkerTree` +pub fn to_uv_marker_tree( + marker_tree: &pep508_rs::MarkerTree, +) -> Result { + let serialized = marker_tree.try_to_string(); + if let Some(serialized) = serialized { + Ok(uv_pep508::MarkerTree::from_str(serialized.as_str()) + .map_err(crate::Pep508Error::UvPep508)?) + } else { + Ok(uv_pep508::MarkerTree::default()) + } +} + +/// Converts `uv_pep508::MarkerTree` to `pep508_rs::MarkerTree` +pub fn to_marker_environment( + marker_env: &uv_pep508::MarkerEnvironment, +) -> Result { + let serde_str = serde_json::to_string(marker_env).expect("its valid"); + serde_json::from_str(&serde_str).map_err(crate::ConversionError::MarkerEnvironmentSerialization) +} + +/// Converts `pep440_rs::VersionSpecifiers` to `uv_pep440::VersionSpecifiers` +pub fn to_uv_version_specifiers( + version_specifier: &pep440_rs::VersionSpecifiers, +) -> Result { + Ok( + uv_pep440::VersionSpecifiers::from_str(&version_specifier.to_string()) + .map_err(crate::VersionSpecifiersError::UvVersionError)?, + ) +} + +/// Converts `uv_pep440::VersionSpecifiers` to `pep440_rs::VersionSpecifiers` +pub fn to_version_specifiers( + version_specifier: &uv_pep440::VersionSpecifiers, +) -> Result { + Ok( + pep440_rs::VersionSpecifiers::from_str(&version_specifier.to_string()) + .map_err(crate::VersionSpecifiersError::PepVersionError)?, + ) +} + +/// Converts trusted_host `string` to `uv_configuration::TrustedHost` +pub fn to_uv_trusted_host( + trusted_host: &str, +) -> Result { + Ok(uv_configuration::TrustedHost::from_str(trusted_host)?) } diff --git a/crates/pixi_uv_conversions/src/requirements.rs b/crates/pixi_uv_conversions/src/requirements.rs index 51ef862b9..c9965f086 100644 --- a/crates/pixi_uv_conversions/src/requirements.rs +++ b/crates/pixi_uv_conversions/src/requirements.rs @@ -46,7 +46,7 @@ fn create_uv_url( url.parse() } -fn to_version_specificers( +fn manifest_version_to_version_specifiers( version: &VersionOrStar, ) -> Result { match version { @@ -91,7 +91,7 @@ pub fn as_uv_req( PyPiRequirement::Version { version, index, .. } => { // TODO: implement index later RequirementSource::Registry { - specifier: to_version_specificers(version)?, + specifier: manifest_version_to_version_specifiers(version)?, index: index.clone(), conflict: None, } @@ -180,7 +180,7 @@ pub fn as_uv_req( ext: DistExtension::from_path(url.path())?, }, PyPiRequirement::RawVersion(version) => RequirementSource::Registry { - specifier: to_version_specificers(version)?, + specifier: manifest_version_to_version_specifiers(version)?, index: None, conflict: None, }, diff --git a/crates/pixi_uv_conversions/src/types.rs b/crates/pixi_uv_conversions/src/types.rs index 98af3b877..8f011b863 100644 --- a/crates/pixi_uv_conversions/src/types.rs +++ b/crates/pixi_uv_conversions/src/types.rs @@ -2,13 +2,9 @@ // use pep508_rs::PackageName; use std::error::Error; -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; +use std::fmt::{Debug, Display}; use thiserror::Error; use uv_pep440::VersionSpecifierBuildError; -use uv_pypi_types::VerbatimParsedUrl; #[derive(Debug)] pub enum NameError { @@ -144,127 +140,3 @@ pub enum ConversionError { #[error(transparent)] TrustedHostError(#[from] uv_configuration::TrustedHostError), } - -pub fn to_requirements<'req>( - requirements: impl Iterator, -) -> Result, ConversionError> { - let requirements: Result, _> = requirements - .map(|requirement| { - let requirement: uv_pep508::Requirement = - uv_pep508::Requirement::from(requirement.clone()); - pep508_rs::Requirement::from_str(&requirement.to_string()) - .map_err(Pep508Error::Pep508Error) - }) - .collect(); - - Ok(requirements?) -} - -/// Convert back to PEP508 without the VerbatimParsedUrl -/// We need this function because we need to convert to the introduced -/// `VerbatimParsedUrl` back to crates.io `VerbatimUrl`, for the locking -pub fn convert_uv_requirements_to_pep508<'req>( - requires_dist: impl Iterator>, -) -> Result, ConversionError> { - // Convert back top PEP508 Requirement - let requirements: Result, _> = requires_dist - .map(|r| { - let requirement = r.to_string(); - pep508_rs::Requirement::from_str(&requirement).map_err(Pep508Error::Pep508Error) - }) - .collect(); - - Ok(requirements?) -} - -/// Converts `uv_normalize::PackageName` to `pep508_rs::PackageName` -pub fn to_normalize( - normalise: &uv_normalize::PackageName, -) -> Result { - Ok(pep508_rs::PackageName::from_str(normalise.as_str()).map_err(NameError::PepNameError)?) -} - -/// Converts `pe508::PackageName` to `uv_normalize::PackageName` -pub fn to_uv_normalize( - normalise: &pep508_rs::PackageName, -) -> Result { - Ok( - uv_normalize::PackageName::from_str(normalise.to_string().as_str()) - .map_err(NameError::UvNameError)?, - ) -} - -/// Converts `pep508_rs::ExtraName` to `uv_normalize::ExtraName` -pub fn to_uv_extra_name( - extra_name: &pep508_rs::ExtraName, -) -> Result { - Ok( - uv_normalize::ExtraName::from_str(extra_name.to_string().as_str()) - .map_err(NameError::UvExtraNameError)?, - ) -} - -/// Converts `uv_normalize::ExtraName` to `pep508_rs::ExtraName` -pub fn to_extra_name( - extra_name: &uv_normalize::ExtraName, -) -> Result { - Ok( - pep508_rs::ExtraName::from_str(extra_name.to_string().as_str()) - .map_err(NameError::PepExtraNameError)?, - ) -} - -/// Converts `pep440_rs::Version` to `uv_pep440::Version` -pub fn to_uv_version(version: &pep440_rs::Version) -> Result { - Ok( - uv_pep440::Version::from_str(version.to_string().as_str()) - .map_err(VersionError::UvError)?, - ) -} - -/// Converts `pep508_rs::MarkerTree` to `uv_pep508::MarkerTree` -pub fn to_uv_marker_tree( - marker_tree: &pep508_rs::MarkerTree, -) -> Result { - let serialized = marker_tree.try_to_string(); - if let Some(serialized) = serialized { - Ok(uv_pep508::MarkerTree::from_str(serialized.as_str()).map_err(Pep508Error::UvPep508)?) - } else { - Ok(uv_pep508::MarkerTree::default()) - } -} - -/// Converts `uv_pep508::MarkerTree` to `pep508_rs::MarkerTree` -pub fn to_marker_environment( - marker_env: &uv_pep508::MarkerEnvironment, -) -> Result { - let serde_str = serde_json::to_string(marker_env).expect("its valid"); - serde_json::from_str(&serde_str).map_err(ConversionError::MarkerEnvironmentSerialization) -} - -/// Converts `pep440_rs::VersionSpecifiers` to `uv_pep440::VersionSpecifiers` -pub fn to_uv_version_specifiers( - version_specifier: &pep440_rs::VersionSpecifiers, -) -> Result { - Ok( - uv_pep440::VersionSpecifiers::from_str(&version_specifier.to_string()) - .map_err(VersionSpecifiersError::UvVersionError)?, - ) -} - -/// Converts `uv_pep440::VersionSpecifiers` to `pep440_rs::VersionSpecifiers` -pub fn to_version_specifiers( - version_specifier: &uv_pep440::VersionSpecifiers, -) -> Result { - Ok( - pep440_rs::VersionSpecifiers::from_str(&version_specifier.to_string()) - .map_err(VersionSpecifiersError::PepVersionError)?, - ) -} - -/// Converts trusted_host `string` to `uv_configuration::TrustedHost` -pub fn to_uv_trusted_host( - trusted_host: &str, -) -> Result { - Ok(uv_configuration::TrustedHost::from_str(trusted_host)?) -} diff --git a/docs/reference/pixi_manifest.md b/docs/reference/pixi_manifest.md index 0c49a97e6..e5e634800 100644 --- a/docs/reference/pixi_manifest.md +++ b/docs/reference/pixi_manifest.md @@ -281,6 +281,7 @@ The options that can be defined are: - `extra-index-urls`: adds an extra index url. - `find-links`: similar to `--find-links` option in `pip`. - `no-build-isolation`: disables build isolation, can only be set per package. +- `no-build`: don't build source distributions. - `index-strategy`: allows for specifying the index strategy to use. 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. @@ -335,6 +336,26 @@ detectron2 = { git = "https://github.com/facebookresearch/detectron2.git", rev = !!! tip "Conda dependencies define the build environment" To use `no-build-isolation` effectively, use conda dependencies to define the build environment. These are installed before the PyPI dependencies are resolved, this way these dependencies are available during the build process. In the example above adding `torch` as a PyPI dependency would be ineffective, as it would not yet be installed during the PyPI resolution phase. +### No Build +When enabled, resolving will not run arbitrary Python code. The cached wheels of already-built source distributions will be reused, but operations that require building distributions will exit with an error. + +Can be either set per package or globally. +```toml +[pypi-options] +# No sdists allowed +no-build = true # default is false +``` +or: +```toml +[pypi-options] +no-build = ["package1", "package2"] +``` + +When features are merged, the following priority is adhered: +`no-build = true` > `no-build = ["package1", "package2"]` > `no-build = false` +So, to expand: if `no-build = true` is set for *any* feature in the environment, this will be used as the setting for the environment. + + ### Index Strategy The strategy to use when resolving against multiple index URLs. Description modified from the [uv](https://docs.astral.sh/uv/reference/settings/#index-strategy) documentation: diff --git a/schema/examples/valid/full.toml b/schema/examples/valid/full.toml index 3713f6c1e..1a57bb656 100644 --- a/schema/examples/valid/full.toml +++ b/schema/examples/valid/full.toml @@ -17,6 +17,9 @@ readme = "README.md" repository = "https://github.com/author/project" version = "0.1.0" +[project.pypi-options] +no-build = false + [package] [package.build] @@ -55,6 +58,7 @@ git4 = { git = "https://github.com/prefix-dev/rattler", rev = "v0.1.0", subdirec #path2 = { path = "path/to/package" } [pypi-options] +no-build = ["foobar"] no-build-isolation = ["requests"] [pypi-dependencies] @@ -116,6 +120,9 @@ test = "*" [feature.test2.dependencies] test = "*" +[feature.yes-build.pypi-options] +no-build = true + [feature.prod] activation = { scripts = ["activate.sh", "deactivate.sh"] } channel-priority = "disabled" diff --git a/schema/model.py b/schema/model.py index 1ceb02254..1cc03b408 100644 --- a/schema/model.py +++ b/schema/model.py @@ -537,6 +537,11 @@ class PyPIOptions(StrictBaseModel): description="The strategy to use when resolving packages from multiple indexes", examples=["first-index", "unsafe-first-match", "unsafe-best-match"], ) + no_build: bool | list[PyPIPackageName] | None = Field( + None, + description="Packages that should NOT be built", + examples=["true", "false"], + ) ####################### diff --git a/schema/schema.json b/schema/schema.json index b858cd597..baf9cba3b 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -1168,6 +1168,26 @@ "https://pypi.org/simple" ] }, + "no-build": { + "title": "No-Build", + "description": "Packages that should NOT be built", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ], + "examples": [ + "true", + "false" + ] + }, "no-build-isolation": { "title": "No-Build-Isolation", "description": "Packages that should NOT be isolated during the build process", diff --git a/src/environment.rs b/src/environment.rs index f9d22eac1..a1675968e 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -512,6 +512,7 @@ pub async fn update_prefix_pypi( lock_file_dir: &Path, platform: Platform, non_isolated_packages: Option>, + no_build: &pixi_manifest::pypi::pypi_options::NoBuild, ) -> miette::Result<()> { // If we have changed interpreter, we need to uninstall all site-packages from // the old interpreter We need to do this before the pypi prefix update, @@ -577,6 +578,7 @@ pub async fn update_prefix_pypi( environment_variables, platform, non_isolated_packages, + no_build, ) }, ) diff --git a/src/install_pypi/conversions.rs b/src/install_pypi/conversions.rs index 9f6896397..0d852d04d 100644 --- a/src/install_pypi/conversions.rs +++ b/src/install_pypi/conversions.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use pixi_consts::consts; use pixi_record::LockedGitUrl; use pixi_uv_conversions::{ - into_parsed_git_url, to_uv_normalize, to_uv_version, to_uv_version_specifiers, ConversionError, + to_parsed_git_url, to_uv_normalize, to_uv_version, to_uv_version_specifiers, ConversionError, }; use rattler_lock::{PackageHashes, PypiPackageData, UrlOrPath}; use url::Url; @@ -99,7 +99,7 @@ pub fn convert_to_dist( if LockedGitUrl::is_locked_git_url(&url_without_direct) { let locked_git_url = LockedGitUrl::new(url_without_direct.clone().into_owned()); - let parsed_git_url = into_parsed_git_url(&locked_git_url).map_err(|err| { + let parsed_git_url = to_parsed_git_url(&locked_git_url).map_err(|err| { ConvertToUvDistError::LockedUrl( err.to_string(), locked_git_url.to_url().to_string(), diff --git a/src/install_pypi/mod.rs b/src/install_pypi/mod.rs index 8d5d6cc72..66ecf44c7 100644 --- a/src/install_pypi/mod.rs +++ b/src/install_pypi/mod.rs @@ -8,6 +8,7 @@ use pixi_manifest::SystemRequirements; use pixi_record::PixiRecord; use pixi_uv_conversions::{ isolated_names_to_packages, locked_indexes_to_index_locations, names_to_build_isolation, + no_build_to_build_options, }; use pypi_modifiers::pypi_tags::{get_pypi_tags, is_python_record}; use rattler_conda_types::Platform; @@ -43,7 +44,6 @@ pub(crate) mod utils; type CombinedPypiPackageData = (PypiPackageData, PypiPackageEnvironmentData); /// Installs and/or remove python distributions. -// TODO: refactor arguments in struct #[allow(clippy::too_many_arguments)] pub async fn update_python_distributions( lock_file_dir: &Path, @@ -57,6 +57,7 @@ pub async fn update_python_distributions( environment_variables: &HashMap, platform: Platform, non_isolated_packages: Option>, + no_build: &pixi_manifest::pypi::pypi_options::NoBuild, ) -> miette::Result<()> { let start = std::time::Instant::now(); @@ -75,6 +76,7 @@ pub async fn update_python_distributions( .map(|indexes| locked_indexes_to_index_locations(indexes, lock_file_dir)) .unwrap_or_else(|| Ok(IndexLocations::default())) .into_diagnostic()?; + let build_options = no_build_to_build_options(no_build).into_diagnostic()?; let registry_client = Arc::new( RegistryClientBuilder::new(uv_context.cache.clone()) @@ -95,7 +97,7 @@ pub async fn update_python_distributions( entries, Some(&tags), &uv_types::HashStrategy::None, - &uv_context.build_options, + &build_options, ) }; @@ -143,7 +145,7 @@ pub async fn update_python_distributions( &config_settings, build_isolation, LinkMode::default(), - &uv_context.build_options, + &build_options, &uv_context.hash_strategy, None, LowerBound::default(), @@ -323,7 +325,7 @@ pub async fn update_python_distributions( &uv_context.cache, &tags, &uv_types::HashStrategy::None, - &uv_context.build_options, + &build_options, distribution_database, ) .with_reporter(UvReporter::new(options)); diff --git a/src/install_pypi/plan/mod.rs b/src/install_pypi/plan/mod.rs index 8fe84b725..7ff7b2bce 100644 --- a/src/install_pypi/plan/mod.rs +++ b/src/install_pypi/plan/mod.rs @@ -7,7 +7,7 @@ use miette::IntoDiagnostic; use pixi_consts::consts; use pixi_git::url::RepositoryUrl; use pixi_record::LockedGitUrl; -use pixi_uv_conversions::{into_parsed_git_url, to_uv_version}; +use pixi_uv_conversions::{to_parsed_git_url, to_uv_version}; use rattler_lock::{PypiPackageData, UrlOrPath}; use url::Url; use uv_cache::Cache; @@ -415,7 +415,7 @@ fn need_reinstall( // is it a git url? if LockedGitUrl::is_locked_git_url(url) { let locked_git_url = LockedGitUrl::new(url.clone()); - into_parsed_git_url(&locked_git_url) + to_parsed_git_url(&locked_git_url) } else { // it is not a git url, so we fallback to use the url as is ParsedGitUrl::try_from(url.clone()).into_diagnostic() diff --git a/src/lock_file/outdated.rs b/src/lock_file/outdated.rs index a88bfcc97..13df0ea10 100644 --- a/src/lock_file/outdated.rs +++ b/src/lock_file/outdated.rs @@ -187,6 +187,12 @@ async fn find_unsatisfiable_targets<'p>( // If the indexes mismatched we also cannot trust any of the locked content. disregard_locked_content.pypi.insert(environment.clone()); } + EnvironmentUnsat::InvalidDistExtensionInNoBuild(_) => { + disregard_locked_content.pypi.insert(environment.clone()); + } + EnvironmentUnsat::NoBuildWithNonBinaryPackages(_) => { + disregard_locked_content.pypi.insert(environment.clone()); + } } continue; diff --git a/src/lock_file/resolve/pypi.rs b/src/lock_file/resolve/pypi.rs index 67dce4b55..759cbe1c2 100644 --- a/src/lock_file/resolve/pypi.rs +++ b/src/lock_file/resolve/pypi.rs @@ -17,8 +17,9 @@ use pixi_manifest::{pypi::pypi_options::PypiOptions, PyPiRequirement, SystemRequ use pixi_record::PixiRecord; use pixi_uv_conversions::{ as_uv_req, convert_uv_requirements_to_pep508, into_pinned_git_spec, isolated_names_to_packages, - names_to_build_isolation, pypi_options_to_index_locations, to_index_strategy, to_normalize, - to_requirements, to_uv_normalize, to_uv_version, to_version_specifiers, ConversionError, + names_to_build_isolation, no_build_to_build_options, pypi_options_to_index_locations, + to_index_strategy, to_normalize, to_requirements, to_uv_normalize, to_uv_version, + to_version_specifiers, ConversionError, }; use pypi_modifiers::{ pypi_marker_env::determine_marker_environment, @@ -293,6 +294,9 @@ pub async fn resolve_pypi( .connectivity(Connectivity::Online) .build(), ); + let build_options = + no_build_to_build_options(&pypi_options.no_build.clone().unwrap_or_default()) + .into_diagnostic()?; // Resolve the flat indexes from `--find-links`. let flat_index = { @@ -306,12 +310,7 @@ pub async fn resolve_pypi( .await .into_diagnostic() .wrap_err("failed to query find-links locations")?; - FlatIndex::from_entries( - entries, - Some(&tags), - &context.hash_strategy, - &context.build_options, - ) + FlatIndex::from_entries(entries, Some(&tags), &context.hash_strategy, &build_options) }; // Create a shared in-memory index. @@ -359,7 +358,7 @@ pub async fn resolve_pypi( &config_settings, build_isolation, LinkMode::default(), - &context.build_options, + &build_options, &context.hash_strategy, None, LowerBound::default(), @@ -466,7 +465,7 @@ pub async fn resolve_pypi( AllowedYanks::from_manifest(&manifest, &resolver_env, options.dependency_mode), &context.hash_strategy, options.exclude_newer, - &context.build_options, + &build_options, &context.capabilities, ); let package_requests = Rc::new(RefCell::new(Default::default())); diff --git a/src/lock_file/resolve/uv_resolution_context.rs b/src/lock_file/resolve/uv_resolution_context.rs index bf749fa72..f49be1368 100644 --- a/src/lock_file/resolve/uv_resolution_context.rs +++ b/src/lock_file/resolve/uv_resolution_context.rs @@ -1,6 +1,6 @@ use miette::{Context, IntoDiagnostic}; use uv_cache::Cache; -use uv_configuration::{BuildOptions, Concurrency, SourceStrategy, TrustedHost}; +use uv_configuration::{Concurrency, SourceStrategy, TrustedHost}; use uv_distribution_types::IndexCapabilities; use uv_types::{HashStrategy, InFlight}; @@ -14,7 +14,6 @@ use pixi_uv_conversions::{to_uv_trusted_host, ConversionError}; pub struct UvResolutionContext { pub cache: Cache, pub in_flight: InFlight, - pub build_options: BuildOptions, pub hash_strategy: HashStrategy, pub client: reqwest::Client, pub keyring_provider: uv_configuration::KeyringProviderType, @@ -66,7 +65,6 @@ impl UvResolutionContext { in_flight: InFlight::default(), hash_strategy: HashStrategy::None, client: project.client().clone(), - build_options: BuildOptions::default(), keyring_provider, concurrency: Concurrency::default(), source_strategy: SourceStrategy::Disabled, diff --git a/src/lock_file/satisfiability.rs b/src/lock_file/satisfiability.rs index e8162f402..152c1b626 100644 --- a/src/lock_file/satisfiability.rs +++ b/src/lock_file/satisfiability.rs @@ -16,12 +16,12 @@ use miette::Diagnostic; use pep440_rs::VersionSpecifiers; use pixi_git::url::RepositoryUrl; use pixi_glob::{GlobHashCache, GlobHashError, GlobHashKey}; -use pixi_manifest::FeaturesExt; +use pixi_manifest::{pypi::pypi_options::NoBuild, FeaturesExt}; use pixi_record::{LockedGitUrl, ParseLockFileError, PixiRecord, SourceMismatchError}; use pixi_spec::{PixiSpec, SourceSpec, SpecConversionError}; use pixi_uv_conversions::{ - as_uv_req, as_uv_specifiers, as_uv_version, into_pixi_reference, to_normalize, - to_uv_marker_tree, to_uv_version_specifiers, AsPep508Error, + as_uv_req, into_pixi_reference, to_normalize, to_uv_marker_tree, to_uv_specifiers, + to_uv_version, to_uv_version_specifiers, AsPep508Error, }; use pypi_modifiers::pypi_marker_env::determine_marker_environment; use rattler_conda_types::{ @@ -34,7 +34,7 @@ use rattler_lock::{ }; use thiserror::Error; use url::Url; -use uv_distribution_filename::DistExtension; +use uv_distribution_filename::{DistExtension, ExtensionError, SourceDistExtension}; use uv_git::GitReference; use uv_pypi_types::{ ParsedPathUrl, ParsedUrl, ParsedUrlError, RequirementSource, VerbatimParsedUrl, @@ -51,6 +51,14 @@ pub enum EnvironmentUnsat { #[error(transparent)] InvalidChannel(#[from] ParseChannelError), + + #[error(transparent)] + InvalidDistExtensionInNoBuild(#[from] ExtensionError), + + #[error( + "the lock-file contains non-binary package: '{0}', but the pypi-option `no-build` is set" + )] + NoBuildWithNonBinaryPackages(String), } #[derive(Debug, Error)] @@ -422,39 +430,149 @@ pub fn verify_environment_satisfiability( return Err(EnvironmentUnsat::ChannelsMismatch); } - // Check if the indexes in the lock file match our current configuration. + // Do some more checks if we have pypi dependencies + // 1. Check if the PyPI indexes are present and match + // 2. Check if we have a no-build option set, that we only have binary packages, or an editable source if !environment.pypi_dependencies(None).is_empty() { - let indexes = rattler_lock::PypiIndexes::from(grouped_env.pypi_options()); - match locked_environment.pypi_indexes() { - None => { - // Mismatch when there should be an index but there is not - if locked_environment - .lock_file() - .version() - .should_pypi_indexes_be_present() - && locked_environment - .pypi_packages_by_platform() - .any(|(_platform, mut packages)| packages.next().is_some()) - { - return Err(IndexesMismatch { - current: indexes, - previous: None, + let group_pypi_options = grouped_env.pypi_options(); + let indexes = rattler_lock::PypiIndexes::from(group_pypi_options.clone()); + + // Check if the indexes in the lock file match our current configuration. + verify_pypi_indexes(locked_environment, indexes)?; + + // Check that if `no-build` is set, we only have binary packages + // or that the package that we disallow are not built from source + if let Some(no_build) = group_pypi_options.no_build.as_ref() { + verify_pypi_no_build(no_build, locked_environment)?; + } + } + + Ok(()) +} + +fn verify_pypi_no_build( + no_build: &NoBuild, + locked_environment: rattler_lock::Environment<'_>, +) -> Result<(), EnvironmentUnsat> { + // Check if we are disallowing all source packages or only a subset + #[derive(Eq, PartialEq)] + enum Check { + All, + Packages(HashSet), + } + + let check = match no_build { + // Ok, so we are allowed to build any source package + NoBuild::None => return Ok(()), + // We are not allowed to build any source package + NoBuild::All => Check::All, + // We are not allowed to build a subset of source packages + NoBuild::Packages(hash_set) => { + let packages = hash_set + .iter() + .filter_map(|name| pep508_rs::PackageName::new(name.to_string()).ok()) + .collect(); + Check::Packages(packages) + } + }; + + // Small helper function to get the dist extension from a url + fn pypi_dist_extension_from_url(url: &Url) -> Result { + // Take the file name from the url + let path = url.path_segments().and_then(|s| s.last()).unwrap_or(""); + // Convert the path to a dist extension + DistExtension::from_path(Path::new(path)) + } + + // Determine if we do not accept non-wheels for all packages or only for a subset + // Check all the currently locked packages if we are making any violations + for (_, packages) in locked_environment.pypi_packages_by_platform() { + for (package, _) in packages { + let extension = match &package.location { + // Get the extension from the url + UrlOrPath::Url(url) => { + if url.scheme().starts_with("git+") { + // Just choose some source extension, does not really matter, cause it is + // actually a directory, this is just for the check + Ok(DistExtension::Source(SourceDistExtension::TarGz)) + } else { + pypi_dist_extension_from_url(url) } - .into()); } - } - Some(locked_indexes) => { - if locked_indexes != &indexes { - return Err(IndexesMismatch { - current: indexes, - previous: Some(locked_indexes.clone()), + UrlOrPath::Path(path) => { + let path = Path::new(path.as_str()); + if path.is_dir() { + // Editables are allowed with no-build + if package.editable { + continue; + } else { + // Non-editable source packages might not be allowed + Ok(DistExtension::Source(SourceDistExtension::TarGz)) + } + } else { + // Could be a reference to a wheel or sdist + DistExtension::from_path(path) } - .into()); } + }?; + + match extension { + // Wheels are fine + DistExtension::Wheel => continue, + // Check if we have a source package that we are not allowed to build + // it could be that we are only disallowing for certain source packages + DistExtension::Source(_) => match check { + Check::All => { + return Err(EnvironmentUnsat::NoBuildWithNonBinaryPackages( + package.name.to_string(), + )) + } + Check::Packages(ref hash_set) => { + if hash_set.contains(&package.name) { + return Err(EnvironmentUnsat::NoBuildWithNonBinaryPackages( + package.name.to_string(), + )); + } + } + }, } } } + Ok(()) +} +fn verify_pypi_indexes( + locked_environment: rattler_lock::Environment<'_>, + indexes: PypiIndexes, +) -> Result<(), EnvironmentUnsat> { + match locked_environment.pypi_indexes() { + None => { + // Mismatch when there should be an index but there is not + if locked_environment + .lock_file() + .version() + .should_pypi_indexes_be_present() + && locked_environment + .pypi_packages_by_platform() + .any(|(_platform, mut packages)| packages.next().is_some()) + { + return Err(IndexesMismatch { + current: indexes, + previous: None, + } + .into()); + } + } + Some(locked_indexes) => { + if locked_indexes != &indexes { + return Err(IndexesMismatch { + current: indexes, + previous: Some(locked_indexes.clone()), + } + .into()); + } + } + } Ok(()) } @@ -1062,14 +1180,14 @@ pub(crate) async fn verify_package_platform_satisfiability( // Ensure that the record matches the currently selected interpreter. if let Some(requires_python) = &record.0.requires_python { - let uv_specifier_requires_python = as_uv_specifiers(requires_python) + let uv_specifier_requires_python = to_uv_specifiers(requires_python) .expect("pep440 conversion should never fail"); let marker_version = pep440_rs::Version::from_str( &marker_environment.python_full_version().version.to_string(), ) .expect("cannot parse version"); - let uv_maker_version = as_uv_version(&marker_version) + let uv_maker_version = to_uv_version(&marker_version) .expect("cannot convert python marker version to uv_pep440"); let marker_requires_python = diff --git a/src/lock_file/snapshots/pixi__lock_file__satisfiability__tests__failing_satisiability@non-binary-no-build.snap b/src/lock_file/snapshots/pixi__lock_file__satisfiability__tests__failing_satisiability@non-binary-no-build.snap new file mode 100644 index 000000000..20471481a --- /dev/null +++ b/src/lock_file/snapshots/pixi__lock_file__satisfiability__tests__failing_satisiability@non-binary-no-build.snap @@ -0,0 +1,7 @@ +--- +source: src/lock_file/satisfiability.rs +expression: s +--- +environment 'default' does not satisfy the requirements of the project + Diagnostic severity: error + Caused by: the lock-file contains non-binary package: 'sdist', but the pypi-option `no-build` is set diff --git a/src/lock_file/update.rs b/src/lock_file/update.rs index 0a9752397..d6a4646d0 100644 --- a/src/lock_file/update.rs +++ b/src/lock_file/update.rs @@ -289,6 +289,11 @@ impl<'p> LockFileDerivedData<'p> { .await?; let non_isolated_packages = environment.pypi_options().no_build_isolation; + let no_build = environment + .pypi_options() + .no_build + .clone() + .unwrap_or_default(); // Update the prefix with Pypi records environment::update_prefix_pypi( environment.name(), @@ -304,6 +309,7 @@ impl<'p> LockFileDerivedData<'p> { self.project.root(), environment.best_platform(), non_isolated_packages, + &no_build, ) .await .with_context(|| { diff --git a/tests/data/non-satisfiability/non-binary-no-build/.gitattributes b/tests/data/non-satisfiability/non-binary-no-build/.gitattributes new file mode 100644 index 000000000..8f61a8e77 --- /dev/null +++ b/tests/data/non-satisfiability/non-binary-no-build/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting +pixi.lock linguist-language=YAML linguist-generated=true diff --git a/tests/data/non-satisfiability/non-binary-no-build/.gitignore b/tests/data/non-satisfiability/non-binary-no-build/.gitignore new file mode 100644 index 000000000..740bb7d1a --- /dev/null +++ b/tests/data/non-satisfiability/non-binary-no-build/.gitignore @@ -0,0 +1,4 @@ + +# pixi environments +.pixi +*.egg-info diff --git a/tests/data/non-satisfiability/non-binary-no-build/pixi.lock b/tests/data/non-satisfiability/non-binary-no-build/pixi.lock new file mode 100644 index 000000000..05160af0b --- /dev/null +++ b/tests/data/non-satisfiability/non-binary-no-build/pixi.lock @@ -0,0 +1,168 @@ +version: 6 +environments: + default: + channels: + - url: https://prefix.dev/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + osx-arm64: + - conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/ca-certificates-2024.12.14-hf0a4a13_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + - conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.6.3-h39f12f2_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.48.0-h3f77e49_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_2.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.12.8-hc22306f_1_cpython.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/e0/71/020fc6513cf4ef13b6d8ccea836c72828add6bbfecd344c59e26a6dc841b/sdist-0.0.0.tar.gz +packages: +- conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + sha256: adfa71f158cbd872a36394c56c3568e6034aa55c623634b37a4836bd036e6b91 + md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 122909 + timestamp: 1720974522888 +- conda: https://prefix.dev/conda-forge/osx-arm64/ca-certificates-2024.12.14-hf0a4a13_0.conda + sha256: 256be633fd0882ccc1a7a32bc278547e1703f85082c0789a87a603ee3ab8fb82 + md5: 7cb381a6783d91902638e4ed1ebd478e + license: ISC + purls: [] + size: 157091 + timestamp: 1734208344343 +- conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda + sha256: e42ab5ace927ee7c84e3f0f7d813671e1cf3529f5f06ee5899606630498c2745 + md5: 38d2656dd914feb0cab8c629370768bf + depends: + - __osx >=11.0 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + purls: [] + size: 64693 + timestamp: 1730967175868 +- conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + sha256: 41b3d13efb775e340e4dba549ab5c029611ea6918703096b2eaa9c015c0750ca + md5: 086914b672be056eb70fd4285b6783b6 + license: MIT + license_family: MIT + purls: [] + size: 39020 + timestamp: 1636488587153 +- conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.6.3-h39f12f2_1.conda + sha256: d863b8257406918ffdc50ae65502f2b2d6cede29404d09a094f59509d6a0aaf1 + md5: b2553114a7f5e20ccd02378a77d836aa + depends: + - __osx >=11.0 + constrains: + - xz ==5.6.3=*_1 + license: 0BSD + purls: [] + size: 99129 + timestamp: 1733407496073 +- conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.48.0-h3f77e49_1.conda + sha256: 17c06940cc2a13fd6a17effabd6881b1477db38b2cd3ee2571092d293d3fdd75 + md5: 4c55169502ecddf8077973a987d08f08 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + purls: [] + size: 852831 + timestamp: 1737564996616 +- conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 +- conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_2.conda + sha256: b45c73348ec9841d5c893acc2e97adff24127548fe8c786109d03c41ed564e91 + md5: f6f7c5b7d0983be186c46c4f6f8f9af8 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 796754 + timestamp: 1736683572099 +- conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda + sha256: 97772762abc70b3a537683ca9fc3ff3d6099eb64e4aba3b9c99e6fce48422d21 + md5: 22f971393637480bda8c679f374d8861 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 2936415 + timestamp: 1736086108693 +- conda: https://prefix.dev/conda-forge/osx-arm64/python-3.12.8-hc22306f_1_cpython.conda + build_number: 1 + sha256: 7586a711b1b08a9df8864e26efdc06980bdfb0e18d5ac4651d0fee30a8d3e3a0 + md5: 54ca5b5d92ef3a3ba61e195ee882a518 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.4,<3.0a0 + - libffi >=3.4,<4.0a0 + - liblzma >=5.6.3,<6.0a0 + - libsqlite >=3.47.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.4.0,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 12998673 + timestamp: 1733408900971 +- conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + sha256: a1dfa679ac3f6007362386576a704ad2d0d7a02e98f5d0b115f207a2da63e884 + md5: 8cbb776a2f641b943d413b3e19df71f4 + depends: + - ncurses >=6.3,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 250351 + timestamp: 1679532511311 +- pypi: https://files.pythonhosted.org/packages/e0/71/020fc6513cf4ef13b6d8ccea836c72828add6bbfecd344c59e26a6dc841b/sdist-0.0.0.tar.gz + name: sdist + version: 0.0.0 + sha256: c69b35cd5dc5c159a7a7d144d3ea0daad6c1e35f86a7354c219810e3f380c85b +- conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + sha256: 72457ad031b4c048e5891f3f6cb27a53cb479db68a52d965f796910e71a403a8 + md5: b50a57ba89c32b62428b71a875291c9b + depends: + - libzlib >=1.2.13,<2.0.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3145523 + timestamp: 1699202432999 +- conda: https://prefix.dev/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda + sha256: c4b1ae8a2931fe9b274c44af29c5475a85b37693999f8c792dad0f8c6734b1de + md5: dbcace4706afdfb7eb891f7b37d07c04 + license: LicenseRef-Public-Domain + purls: [] + size: 122921 + timestamp: 1737119101255 diff --git a/tests/data/non-satisfiability/non-binary-no-build/pixi.toml b/tests/data/non-satisfiability/non-binary-no-build/pixi.toml new file mode 100644 index 000000000..2af08e377 --- /dev/null +++ b/tests/data/non-satisfiability/non-binary-no-build/pixi.toml @@ -0,0 +1,17 @@ +[project] +authors = ["Tim de Jager "] +channels = ["https://prefix.dev/conda-forge"] +name = "non-binary-no-build" +platforms = ["osx-arm64"] +version = "0.1.0" + +[pypi-options] +no-build = true + +[tasks] + +[dependencies] +python = "3.12.*" + +[pypi-dependencies] +sdist = "*" diff --git a/tests/data/pixi_tomls/no_build.toml b/tests/data/pixi_tomls/no_build.toml new file mode 100644 index 000000000..36206ab6c --- /dev/null +++ b/tests/data/pixi_tomls/no_build.toml @@ -0,0 +1,15 @@ +[project] +channels = ["conda-forge"] +description = "Package management made easy!" +name = "no_build" +platforms = ["linux-64", "osx-arm64", "osx-64", "win-64"] +version = "0.1.0" + +[pypi-options] +no-build = ["sdist"] + +[dependencies] +python = "3.12.*" + +[pypi-dependencies] +sdist = "==0.0.0" diff --git a/tests/data/satisfiability/no-build-editable/.gitattributes b/tests/data/satisfiability/no-build-editable/.gitattributes new file mode 100644 index 000000000..8f61a8e77 --- /dev/null +++ b/tests/data/satisfiability/no-build-editable/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting +pixi.lock linguist-language=YAML linguist-generated=true diff --git a/tests/data/satisfiability/no-build-editable/.gitignore b/tests/data/satisfiability/no-build-editable/.gitignore new file mode 100644 index 000000000..740bb7d1a --- /dev/null +++ b/tests/data/satisfiability/no-build-editable/.gitignore @@ -0,0 +1,4 @@ + +# pixi environments +.pixi +*.egg-info diff --git a/tests/data/satisfiability/no-build-editable/pixi.lock b/tests/data/satisfiability/no-build-editable/pixi.lock new file mode 100644 index 000000000..ae07d478f --- /dev/null +++ b/tests/data/satisfiability/no-build-editable/pixi.lock @@ -0,0 +1,193 @@ +version: 6 +environments: + default: + channels: + - url: https://prefix.dev/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + osx-arm64: + - conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/ca-certificates-2024.12.14-hf0a4a13_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + - conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.6.3-h39f12f2_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libmpdec-4.0.0-h99b78c6_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.48.0-h3f77e49_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_2.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.13.1-h4f43103_105_cp313.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/python_abi-3.13-5_cp313.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda + - pypi: . +packages: +- conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + sha256: adfa71f158cbd872a36394c56c3568e6034aa55c623634b37a4836bd036e6b91 + md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 122909 + timestamp: 1720974522888 +- conda: https://prefix.dev/conda-forge/osx-arm64/ca-certificates-2024.12.14-hf0a4a13_0.conda + sha256: 256be633fd0882ccc1a7a32bc278547e1703f85082c0789a87a603ee3ab8fb82 + md5: 7cb381a6783d91902638e4ed1ebd478e + license: ISC + purls: [] + size: 157091 + timestamp: 1734208344343 +- conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda + sha256: e42ab5ace927ee7c84e3f0f7d813671e1cf3529f5f06ee5899606630498c2745 + md5: 38d2656dd914feb0cab8c629370768bf + depends: + - __osx >=11.0 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + purls: [] + size: 64693 + timestamp: 1730967175868 +- conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + sha256: 41b3d13efb775e340e4dba549ab5c029611ea6918703096b2eaa9c015c0750ca + md5: 086914b672be056eb70fd4285b6783b6 + license: MIT + license_family: MIT + purls: [] + size: 39020 + timestamp: 1636488587153 +- conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.6.3-h39f12f2_1.conda + sha256: d863b8257406918ffdc50ae65502f2b2d6cede29404d09a094f59509d6a0aaf1 + md5: b2553114a7f5e20ccd02378a77d836aa + depends: + - __osx >=11.0 + constrains: + - xz ==5.6.3=*_1 + license: 0BSD + purls: [] + size: 99129 + timestamp: 1733407496073 +- conda: https://prefix.dev/conda-forge/osx-arm64/libmpdec-4.0.0-h99b78c6_0.conda + sha256: f7917de9117d3a5fe12a39e185c7ce424f8d5010a6f97b4333e8a1dcb2889d16 + md5: 7476305c35dd9acef48da8f754eedb40 + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 69263 + timestamp: 1723817629767 +- conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.48.0-h3f77e49_1.conda + sha256: 17c06940cc2a13fd6a17effabd6881b1477db38b2cd3ee2571092d293d3fdd75 + md5: 4c55169502ecddf8077973a987d08f08 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + purls: [] + size: 852831 + timestamp: 1737564996616 +- conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 +- conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_2.conda + sha256: b45c73348ec9841d5c893acc2e97adff24127548fe8c786109d03c41ed564e91 + md5: f6f7c5b7d0983be186c46c4f6f8f9af8 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 796754 + timestamp: 1736683572099 +- pypi: . + name: no-build-editable + version: 0.1.0 + sha256: 36a293cb2033c1eedb5d407bee54e213ef0a3e2021c9fcc7d040203ad0802074 + requires_python: '>=3.11' + editable: true +- conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda + sha256: 97772762abc70b3a537683ca9fc3ff3d6099eb64e4aba3b9c99e6fce48422d21 + md5: 22f971393637480bda8c679f374d8861 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 2936415 + timestamp: 1736086108693 +- conda: https://prefix.dev/conda-forge/osx-arm64/python-3.13.1-h4f43103_105_cp313.conda + build_number: 105 + sha256: 7d27cc8ef214abbdf7dd8a5d473e744f4bd9beb7293214a73c58e4895c2830b8 + md5: 11d916b508764b7d881dd5c75d222d6e + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.4,<3.0a0 + - libffi >=3.4,<4.0a0 + - liblzma >=5.6.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.47.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.4.0,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 12919840 + timestamp: 1736761931666 +- conda: https://prefix.dev/conda-forge/osx-arm64/python_abi-3.13-5_cp313.conda + build_number: 5 + sha256: 4437198eae80310f40b23ae2f8a9e0a7e5c2b9ae411a8621eb03d87273666199 + md5: b8e82d0a5c1664638f87f63cc5d241fb + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6322 + timestamp: 1723823058879 +- conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + sha256: a1dfa679ac3f6007362386576a704ad2d0d7a02e98f5d0b115f207a2da63e884 + md5: 8cbb776a2f641b943d413b3e19df71f4 + depends: + - ncurses >=6.3,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 250351 + timestamp: 1679532511311 +- conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + sha256: 72457ad031b4c048e5891f3f6cb27a53cb479db68a52d965f796910e71a403a8 + md5: b50a57ba89c32b62428b71a875291c9b + depends: + - libzlib >=1.2.13,<2.0.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3145523 + timestamp: 1699202432999 +- conda: https://prefix.dev/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda + sha256: c4b1ae8a2931fe9b274c44af29c5475a85b37693999f8c792dad0f8c6734b1de + md5: dbcace4706afdfb7eb891f7b37d07c04 + license: LicenseRef-Public-Domain + purls: [] + size: 122921 + timestamp: 1737119101255 diff --git a/tests/data/satisfiability/no-build-editable/pyproject.toml b/tests/data/satisfiability/no-build-editable/pyproject.toml new file mode 100644 index 000000000..c4d087fbb --- /dev/null +++ b/tests/data/satisfiability/no-build-editable/pyproject.toml @@ -0,0 +1,23 @@ +[project] +authors = [{ name = "Tim de Jager", email = "tim@prefix.dev" }] +dependencies = [] +description = "Add a short description here" +name = "no-build-editable" +requires-python = ">= 3.11" +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.pixi.project] +channels = ["https://prefix.dev/conda-forge"] +platforms = ["osx-arm64"] + +[tool.pixi.pypi-options] +no-build = true + +[tool.pixi.pypi-dependencies] +no_build_editable = { path = ".", editable = true } + +[tool.pixi.tasks] diff --git a/tests/data/satisfiability/no-build-editable/src/no_build_editable/__init__.py b/tests/data/satisfiability/no-build-editable/src/no_build_editable/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration_python/test_pypi_options.py b/tests/integration_python/test_pypi_options.py new file mode 100644 index 000000000..3c4c8a6b9 --- /dev/null +++ b/tests/integration_python/test_pypi_options.py @@ -0,0 +1,22 @@ +from pathlib import Path +import pytest +from .common import verify_cli_command, ExitCode + + +@pytest.mark.extra_slow +def test_no_build_option(pixi: Path, tmp_pixi_workspace: Path) -> None: + """ + Tests the behavior of pixi install command when the no-build option is specified in pixi.toml. + This test verifies that the installation fails appropriately when attempting to install + packages that need to be built like `sdist`. + """ + test_data = Path(__file__).parent.parent / "data/pixi_tomls/no_build.toml" + manifest = tmp_pixi_workspace.joinpath("pixi.toml") + toml = test_data.read_text() + manifest.write_text(toml) + + # Run the installation + verify_cli_command( + [pixi, "install", "--manifest-path", manifest], + ExitCode.FAILURE, + )