From 6f925d0b6330c4fcd9caaec424068f4ad9679977 Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Sun, 22 Mar 2026 20:48:25 +0100 Subject: [PATCH 1/5] Fix abi3 run export handling for pixi-build-python --- .../src/specs_conversion.rs | 1 + crates/pixi_build_python/src/config.rs | 4 +- crates/pixi_build_python/src/main.rs | 222 ++++++++++++------ crates/recipe_stage0/src/marked_yaml.rs | 56 ++++- crates/recipe_stage0/src/recipe.rs | 40 +++- docs/build/backends/pixi-build-python.md | 20 +- .../src/recipe_stage0/recipe.rs | 1 + 7 files changed, 258 insertions(+), 86 deletions(-) diff --git a/crates/pixi_build_backend/src/specs_conversion.rs b/crates/pixi_build_backend/src/specs_conversion.rs index c9a098c2c6..0bac36723d 100644 --- a/crates/pixi_build_backend/src/specs_conversion.rs +++ b/crates/pixi_build_backend/src/specs_conversion.rs @@ -174,6 +174,7 @@ pub fn from_targets_v1_to_conditional_requirements(targets: &Targets) -> Conditi host: host_items, run: run_items, run_constraints: run_constraints_items, + ..Default::default() } } diff --git a/crates/pixi_build_python/src/config.rs b/crates/pixi_build_python/src/config.rs index bd16e6402d..5f28f84e82 100644 --- a/crates/pixi_build_python/src/config.rs +++ b/crates/pixi_build_python/src/config.rs @@ -35,7 +35,9 @@ pub struct PythonBackendConfig { #[serde(default)] pub ignore_pypi_mapping: Option, /// Whether the package uses the Python Stable ABI (abi3). - /// When true, adds `python_abi` to host requirements. + /// When true, marks the package as version-independent, adds `python-abi3` + /// to the host requirements, and suppresses CPython ABI run exports from + /// `host: python`. /// Only meaningful for packages with compiled extensions (non-noarch). #[serde(default)] pub abi3: Option, diff --git a/crates/pixi_build_python/src/main.rs b/crates/pixi_build_python/src/main.rs index f53f65248e..b9488ff42a 100644 --- a/crates/pixi_build_python/src/main.rs +++ b/crates/pixi_build_python/src/main.rs @@ -15,9 +15,9 @@ use pixi_build_backend::{ traits::ProjectModel, }; use pyproject_toml::PyProjectToml; -use rattler_conda_types::{ChannelUrl, Platform, Version, VersionBumpType, package::EntryPoint}; +use rattler_conda_types::{ChannelUrl, PackageName, Platform, Version, package::EntryPoint}; use recipe_stage0::matchspec::PackageDependency; -use recipe_stage0::recipe::{Item, NoArchKind, Python, Script}; +use recipe_stage0::recipe::{Item, NoArchKind, Python, Script, Value}; use std::collections::HashSet; use std::{ collections::{BTreeMap, BTreeSet}, @@ -32,16 +32,16 @@ use crate::pypi_mapping::{ map_requirements_with_channels, }; -/// Compute the `python_abi` version spec from an optional `requires-python` +/// Compute the `python-abi3` version spec from an optional `requires-python` /// specifier string. /// /// Extracts the lower bound (first `>=` specifier) and pins it to a single /// minor version: -/// - `">=3.9"` → `">=3.9,<3.10.0a0"` -/// - `">=3.9.3"` → `">=3.9.3,<3.10.0a0"` -/// - `">=3.11,<4"` → `">=3.11,<3.12.0a0"` -/// - `None` → `">=3.8,<3.9.0a0"` (default) -fn python_abi_spec_from_requires_python(requires_python: Option<&str>) -> miette::Result { +/// - `">=3.9"` → `"3.9.*"` +/// - `">=3.9.3"` → `"3.9.*"` +/// - `">=3.11,<4"` → `"3.11.*"` +/// - `None` → `"3.8.*"` (default) +fn python_abi3_spec_from_requires_python(requires_python: Option<&str>) -> miette::Result { let lower_bound = requires_python .and_then(|s| { let specifiers = pep440_rs::VersionSpecifiers::from_str(s).ok()?; @@ -50,27 +50,34 @@ fn python_abi_spec_from_requires_python(requires_python: Option<&str>) -> miette .find(|spec| *spec.operator() == pep440_rs::Operator::GreaterThanEqual) .map(|spec| { let pep_version = spec.version(); - // Convert pep440 version to rattler Version via string round-trip Version::from_str(&pep_version.to_string()) .expect("pep440 version should be a valid conda version") }) }) .unwrap_or_else(|| Version::from_str("3.8").expect("valid version")); - // Truncate to major.minor for the upper bound computation + let segment_count = std::cmp::min(lower_bound.segment_count(), 2); let major_minor = lower_bound - .clone() - .with_segments(..std::cmp::min(lower_bound.segment_count(), 2)) + .with_segments(..segment_count) .ok_or_else(|| miette::miette!("failed to truncate version to major.minor"))?; - let upper_bound = major_minor - .bump(VersionBumpType::Minor) - .into_diagnostic()? - .with_alpha() - .remove_local() - .into_owned(); + Ok(format!("{major_minor}.*")) +} - Ok(format!(">={lower_bound},<{upper_bound}")) +fn requirement_contains_package( + requirements: &[Item], + package_name: &str, +) -> bool { + requirements.iter().any(|item| match item { + Item::Value(Value::Concrete(dep)) => dep.package_name().as_normalized() == package_name, + Item::Value(Value::Template(spec)) => spec.split_whitespace().next() == Some(package_name), + Item::Conditional(cond) => cond + .then + .0 + .iter() + .chain(cond.else_value.0.iter()) + .any(|dep| dep.package_name().as_normalized() == package_name), + }) } #[derive(Default, Clone)] @@ -227,13 +234,24 @@ impl GenerateRecipe for PythonGenerator { ); } - // Add python_abi host dependency when abi3 is enabled + // ABI3 packages should not inherit CPython ABI pins from `host: python`. if config.abi3 == Some(true) { - let requires_python_str = pyproject_metadata_provider.requires_python().ok().flatten(); - let abi_spec = python_abi_spec_from_requires_python(requires_python_str.as_deref())?; - let python_abi_req: Item = - format!("python_abi {abi_spec}").parse().into_diagnostic()?; - requirements.host.push(python_abi_req); + if !requirement_contains_package(&requirements.host, "python-abi3") { + let requires_python_str = pyproject_metadata_provider.requires_python().ok().flatten(); + let abi3_spec = python_abi3_spec_from_requires_python(requires_python_str.as_deref())?; + let python_abi3_req: Item = + format!("python-abi3 {abi3_spec}").parse().into_diagnostic()?; + requirements.host.push(python_abi3_req); + } + + let python_package = PackageName::from_str("python").into_diagnostic()?; + if !requirements + .ignore_run_exports + .from_package + .contains(&python_package) + { + requirements.ignore_run_exports.from_package.push(python_package); + } } // Use NoArch platform for mapping if this is a noarch package @@ -1094,42 +1112,8 @@ build-backend = "hatchling.build" ); } - #[test] - fn test_python_abi_spec_from_requires_python() { - // Basic lower bound - assert_eq!( - python_abi_spec_from_requires_python(Some(">=3.9")).unwrap(), - ">=3.9,<3.10.0a0" - ); - // With patch version - assert_eq!( - python_abi_spec_from_requires_python(Some(">=3.9.3")).unwrap(), - ">=3.9.3,<3.10.0a0" - ); - // Multiple specifiers - uses the >= bound - assert_eq!( - python_abi_spec_from_requires_python(Some(">=3.11,<4")).unwrap(), - ">=3.11,<3.12.0a0" - ); - // 3.8 lower bound - assert_eq!( - python_abi_spec_from_requires_python(Some(">=3.8")).unwrap(), - ">=3.8,<3.9.0a0" - ); - // None defaults to 3.8 - assert_eq!( - python_abi_spec_from_requires_python(None).unwrap(), - ">=3.8,<3.9.0a0" - ); - // Extra segments are preserved in lower bound but upper bound still pins to major.minor - assert_eq!( - python_abi_spec_from_requires_python(Some(">=3.9.3.4")).unwrap(), - ">=3.9.3.4,<3.10.0a0" - ); - } - #[tokio::test] - async fn test_abi3_adds_python_abi_to_host() { + async fn test_abi3_marks_recipe_version_independent_and_ignores_python_run_exports() { let project_model = project_fixture!({ "name": "foobar", "version": "0.1.0", @@ -1185,20 +1169,31 @@ build-backend = "setuptools.build_meta" .collect(); assert!( - host_deps.iter().any(|d| d.contains("python_abi")), - "host deps should contain python_abi when abi3=true, got: {host_deps:?}" + host_deps.iter().any(|d| d == "python-abi3 3.9.*"), + "host deps should contain python-abi3 3.9.* when abi3=true, got: {host_deps:?}" ); - // Check the version spec - let abi_dep = host_deps.iter().find(|d| d.contains("python_abi")).unwrap(); assert!( - abi_dep.contains(">=3.9") && abi_dep.contains("<3.10.0a0"), - "python_abi should have >=3.9,<3.10.0a0 spec, got: {abi_dep}" + !host_deps.iter().any(|d| d.contains("python_abi")), + "host deps should not contain python_abi when abi3=true, got: {host_deps:?}" ); - // Check version_independent is set assert!( generated_recipe.recipe.build.python.version_independent, "version_independent should be true when abi3=true" ); + + let ignored_packages = &generated_recipe.recipe.requirements.ignore_run_exports.from_package; + assert!( + ignored_packages + .iter() + .any(|name| name.as_normalized() == "python"), + "ignore_run_exports.from_package should contain python when abi3=true, got: {ignored_packages:?}" + ); + + let recipe_yaml = generated_recipe.recipe.to_yaml_pretty().unwrap(); + assert!( + recipe_yaml.contains("ignore_run_exports:"), + "serialized recipe should include ignore_run_exports when abi3=true, got:\n{recipe_yaml}" + ); } #[tokio::test] @@ -1236,15 +1231,96 @@ build-backend = "setuptools.build_meta" .map(|item| item.to_string()) .collect(); - let abi_dep = host_deps.iter().find(|d| d.contains("python_abi")); assert!( - abi_dep.is_some(), - "host deps should contain python_abi, got: {host_deps:?}" + host_deps.iter().any(|d| d == "python-abi3 3.8.*"), + "host deps should contain python-abi3 3.8.* when abi3=true, got: {host_deps:?}" ); - let abi_dep = abi_dep.unwrap(); assert!( - abi_dep.contains(">=3.8") && abi_dep.contains("<3.9.0a0"), - "python_abi should default to >=3.8,<3.9.0a0, got: {abi_dep}" + !host_deps.iter().any(|d| d.contains("python_abi")), + "host deps should not contain python_abi when abi3=true, got: {host_deps:?}" + ); + assert!( + generated_recipe + .recipe + .requirements + .ignore_run_exports + .from_package + .iter() + .any(|name| name.as_normalized() == "python"), + "ignore_run_exports.from_package should contain python when abi3=true" + ); + } + + #[tokio::test] + async fn test_abi3_does_not_duplicate_explicit_python_abi3_dependency() { + let project_model = project_fixture!({ + "name": "foobar", + "version": "0.1.0", + "targets": { + "defaultTarget": { + "hostDependencies": { + "python-abi3": { + "binary": { + "version": "*" + } + } + } + } + } + }); + + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + fs::write( + temp_dir.path().join("pyproject.toml"), + r#"[project] +name = "foobar" +version = "0.1.0" +requires-python = ">=3.9" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" +"#, + ) + .await + .expect("Failed to write pyproject.toml"); + + let config = PythonBackendConfig { + abi3: Some(true), + noarch: Some(false), + compilers: Some(vec!["c".to_string()]), + ..Default::default() + }; + + let generated_recipe = PythonGenerator::default() + .generate_recipe( + &project_model, + &config, + temp_dir.path().to_path_buf(), + Platform::Linux64, + None, + &HashSet::new(), + vec![], + None, + ) + .await + .expect("Failed to generate recipe"); + + let host_deps: Vec = generated_recipe + .recipe + .requirements + .host + .iter() + .map(|item| item.to_string()) + .collect(); + + assert_eq!( + host_deps + .iter() + .filter(|dep| dep.starts_with("python-abi3")) + .count(), + 1, + "host deps should contain exactly one python-abi3 entry when it is explicitly declared, got: {host_deps:?}" ); } diff --git a/crates/recipe_stage0/src/marked_yaml.rs b/crates/recipe_stage0/src/marked_yaml.rs index b016abfe0f..448664c990 100644 --- a/crates/recipe_stage0/src/marked_yaml.rs +++ b/crates/recipe_stage0/src/marked_yaml.rs @@ -5,8 +5,9 @@ use marked_yaml::{Node as MarkedNode, Span}; pub type MappingHash = LinkedHashMap; use crate::recipe::{ - About, Build, Conditional, ConditionalList, ConditionalRequirements, Extra, IntermediateRecipe, - Item, ListOrItem, Package, PackageContents, Source, Test, Value, + About, Build, Conditional, ConditionalList, ConditionalRequirements, Extra, + IgnoreRunExports, IntermediateRecipe, Item, ListOrItem, Package, PackageContents, Source, + Test, Value, }; // Trait for converting to marked YAML nodes @@ -176,6 +177,57 @@ impl ToMarkedYaml for ConditionalRequirements { ); } + if !self.ignore_run_exports.is_empty() { + mapping.insert( + MarkedScalarNode::new(Span::new_blank(), "ignore_run_exports"), + self.ignore_run_exports.to_marked_yaml(), + ); + } + + MarkedNode::Mapping(MarkedMappingNode::new(Span::new_blank(), mapping)) + } +} + +impl ToMarkedYaml for IgnoreRunExports { + fn to_marked_yaml(&self) -> MarkedNode { + let mut mapping = MappingHash::new(); + + if !self.by_name.is_empty() { + mapping.insert( + MarkedScalarNode::new(Span::new_blank(), "by_name"), + MarkedNode::Sequence(MarkedSequenceNode::new( + Span::new_blank(), + self.by_name + .iter() + .map(|name| { + MarkedNode::Scalar(MarkedScalarNode::new( + Span::new_blank(), + name.as_normalized().to_string(), + )) + }) + .collect(), + )), + ); + } + + if !self.from_package.is_empty() { + mapping.insert( + MarkedScalarNode::new(Span::new_blank(), "from_package"), + MarkedNode::Sequence(MarkedSequenceNode::new( + Span::new_blank(), + self.from_package + .iter() + .map(|name| { + MarkedNode::Scalar(MarkedScalarNode::new( + Span::new_blank(), + name.as_normalized().to_string(), + )) + }) + .collect(), + )), + ); + } + MarkedNode::Mapping(MarkedMappingNode::new(Span::new_blank(), mapping)) } } diff --git a/crates/recipe_stage0/src/recipe.rs b/crates/recipe_stage0/src/recipe.rs index 42804d4bb6..6b947bc0c5 100644 --- a/crates/recipe_stage0/src/recipe.rs +++ b/crates/recipe_stage0/src/recipe.rs @@ -569,6 +569,20 @@ pub struct ResolvedRequirements { pub run_constraints: Vec, } +#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)] +pub struct IgnoreRunExports { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub by_name: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub from_package: Vec, +} + +impl IgnoreRunExports { + pub fn is_empty(&self) -> bool { + self.by_name.is_empty() && self.from_package.is_empty() + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] pub enum Target { Default, @@ -586,6 +600,8 @@ pub struct ConditionalRequirements { pub run: ConditionalList, #[serde(default)] pub run_constraints: ConditionalList, + #[serde(default, skip_serializing_if = "IgnoreRunExports::is_empty")] + pub ignore_run_exports: IgnoreRunExports, } impl ConditionalRequirements { @@ -656,12 +672,31 @@ impl Display for ConditionalRequirements { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{{ build: {}, host: {}, run: {}, run_constraints: {} }}", + "{{ build: {}, host: {}, run: {}, run_constraints: {}", self.build.iter().format(", "), self.host.iter().format(", "), self.run.iter().format(", "), self.run_constraints.iter().format(", "), - ) + )?; + + if !self.ignore_run_exports.is_empty() { + write!( + f, + ", ignore_run_exports: {{ by_name: {}, from_package: {} }}", + self.ignore_run_exports + .by_name + .iter() + .map(|name| name.as_normalized()) + .format(", "), + self.ignore_run_exports + .from_package + .iter() + .map(|name| name.as_normalized()) + .format(", "), + )?; + } + + write!(f, " }}") } } @@ -854,6 +889,7 @@ mod tests { ], run: vec!["xtl >=0.7,<0.8".parse().unwrap()], run_constraints: vec!["xsimd >=8.0.3,<10".parse().unwrap()], + ..Default::default() }, about: Some(About { homepage: Some(Value::Concrete( diff --git a/docs/build/backends/pixi-build-python.md b/docs/build/backends/pixi-build-python.md index 172e9f0fc3..69b3704289 100644 --- a/docs/build/backends/pixi-build-python.md +++ b/docs/build/backends/pixi-build-python.md @@ -199,9 +199,19 @@ compilers = ["c", "cxx"] - **Default**: `false` - **Target Merge Behavior**: `Overwrite` - Platform-specific setting takes precedence over base -Controls whether the package uses the [Python Stable ABI (abi3)](https://docs.python.org/3/c-api/stable.html). When set to `true`, a `python_abi` dependency is added to the host requirements with version bounds derived from `requires-python` in your `pyproject.toml`. +Controls whether the package uses the [Python Stable ABI (abi3)](https://docs.python.org/3/c-api/stable.html). When set to `true`, pixi: -The `python_abi` package has `run_exports` that automatically propagate the ABI constraint to the run environment, so only a host dependency is needed. +- marks the recipe as `build.python.version_independent: true` +- adds `python-abi3` to the host requirements +- suppresses the normal CPython ABI run exports from `host: python` + +The `python-abi3` version is derived from the lower bound of `requires-python`: + +- `requires-python = ">=3.9"` → `python-abi3 3.9.*` +- `requires-python = ">=3.11,<4"` → `python-abi3 3.11.*` +- If `requires-python` is not specified, defaults to `python-abi3 3.8.*` + +If `python-abi3` is already declared in your host requirements, pixi does not add a duplicate entry. ```toml [package.build.config] @@ -209,12 +219,6 @@ abi3 = true compilers = ["c"] ``` -The version bounds are computed from the lower bound of `requires-python`: - -- `requires-python = ">=3.9"` → `python_abi >=3.9,<3.10.0a0` -- `requires-python = ">=3.11,<4"` → `python_abi >=3.11,<3.12.0a0` -- If `requires-python` is not specified, defaults to `python_abi >=3.8,<3.9.0a0` - !!! warning "Incompatible with noarch" Setting `abi3 = true` with `noarch = true` will produce an error, since the stable ABI is only meaningful for packages with compiled extensions. diff --git a/pixi-build-backends/py-pixi-build-backend/src/recipe_stage0/recipe.rs b/pixi-build-backends/py-pixi-build-backend/src/recipe_stage0/recipe.rs index 843c4d4477..b0e71fda86 100644 --- a/pixi-build-backends/py-pixi-build-backend/src/recipe_stage0/recipe.rs +++ b/pixi-build-backends/py-pixi-build-backend/src/recipe_stage0/recipe.rs @@ -885,6 +885,7 @@ impl PyConditionalRequirements { .clone() .into_iter() .collect(), + ..Default::default() } } From f159d657b83a5dd2bd8b3960d4d1c865f924926d Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Sun, 22 Mar 2026 23:00:42 +0100 Subject: [PATCH 2/5] fmt --- crates/pixi_build_python/src/main.rs | 22 ++++++++++++++++------ crates/recipe_stage0/src/marked_yaml.rs | 5 ++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/pixi_build_python/src/main.rs b/crates/pixi_build_python/src/main.rs index b9488ff42a..bacf4e33bf 100644 --- a/crates/pixi_build_python/src/main.rs +++ b/crates/pixi_build_python/src/main.rs @@ -237,10 +237,13 @@ impl GenerateRecipe for PythonGenerator { // ABI3 packages should not inherit CPython ABI pins from `host: python`. if config.abi3 == Some(true) { if !requirement_contains_package(&requirements.host, "python-abi3") { - let requires_python_str = pyproject_metadata_provider.requires_python().ok().flatten(); - let abi3_spec = python_abi3_spec_from_requires_python(requires_python_str.as_deref())?; - let python_abi3_req: Item = - format!("python-abi3 {abi3_spec}").parse().into_diagnostic()?; + let requires_python_str = + pyproject_metadata_provider.requires_python().ok().flatten(); + let abi3_spec = + python_abi3_spec_from_requires_python(requires_python_str.as_deref())?; + let python_abi3_req: Item = format!("python-abi3 {abi3_spec}") + .parse() + .into_diagnostic()?; requirements.host.push(python_abi3_req); } @@ -250,7 +253,10 @@ impl GenerateRecipe for PythonGenerator { .from_package .contains(&python_package) { - requirements.ignore_run_exports.from_package.push(python_package); + requirements + .ignore_run_exports + .from_package + .push(python_package); } } @@ -1181,7 +1187,11 @@ build-backend = "setuptools.build_meta" "version_independent should be true when abi3=true" ); - let ignored_packages = &generated_recipe.recipe.requirements.ignore_run_exports.from_package; + let ignored_packages = &generated_recipe + .recipe + .requirements + .ignore_run_exports + .from_package; assert!( ignored_packages .iter() diff --git a/crates/recipe_stage0/src/marked_yaml.rs b/crates/recipe_stage0/src/marked_yaml.rs index 448664c990..8cf0203fe8 100644 --- a/crates/recipe_stage0/src/marked_yaml.rs +++ b/crates/recipe_stage0/src/marked_yaml.rs @@ -5,9 +5,8 @@ use marked_yaml::{Node as MarkedNode, Span}; pub type MappingHash = LinkedHashMap; use crate::recipe::{ - About, Build, Conditional, ConditionalList, ConditionalRequirements, Extra, - IgnoreRunExports, IntermediateRecipe, Item, ListOrItem, Package, PackageContents, Source, - Test, Value, + About, Build, Conditional, ConditionalList, ConditionalRequirements, Extra, IgnoreRunExports, + IntermediateRecipe, Item, ListOrItem, Package, PackageContents, Source, Test, Value, }; // Trait for converting to marked YAML nodes From 8b84a4516aebf697500aaea3bb35c7dd6210a4ab Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Sat, 13 Jun 2026 22:35:13 +0200 Subject: [PATCH 3/5] fix abi3 recipe serialization test --- crates/pixi_build_python/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/pixi_build_python/src/main.rs b/crates/pixi_build_python/src/main.rs index 5283ca66ae..689ca5f2c9 100644 --- a/crates/pixi_build_python/src/main.rs +++ b/crates/pixi_build_python/src/main.rs @@ -1434,10 +1434,10 @@ build-backend = "setuptools.build_meta" "ignore_run_exports.from_package should contain python when abi3=true, got: {ignored_packages:?}" ); - let recipe_yaml = generated_recipe.recipe.to_yaml_pretty().unwrap(); + let recipe_json = serde_json::to_string(&generated_recipe.recipe).unwrap(); assert!( - recipe_yaml.contains("ignore_run_exports:"), - "serialized recipe should include ignore_run_exports when abi3=true, got:\n{recipe_yaml}" + recipe_json.contains("ignore_run_exports"), + "serialized recipe should include ignore_run_exports when abi3=true, got:\n{recipe_json}" ); } From 1f210ab314a435f1f776dfaaa74d2e8fb5f51139 Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Sat, 13 Jun 2026 23:15:10 +0200 Subject: [PATCH 4/5] mention cep 20 --- docs/build/backends/pixi-build-python.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/build/backends/pixi-build-python.md b/docs/build/backends/pixi-build-python.md index dc72a8a3f3..47bf4922b6 100644 --- a/docs/build/backends/pixi-build-python.md +++ b/docs/build/backends/pixi-build-python.md @@ -205,6 +205,8 @@ Controls whether the package uses the [Python Stable ABI (abi3)](https://docs.py - adds `python-abi3` to the host requirements - suppresses the normal CPython ABI run exports from `host: python` +This follows [CEP 20](https://github.com/conda/ceps/blob/main/cep-0020.md), which defines conda ecosystem support for `abi3` Python packages. + The `python-abi3` version is derived from the lower bound of `requires-python`: - `requires-python = ">=3.9"` → `python-abi3 3.9.*` From 1d804b068eb8eab804919e4a5d60d8f88be66aef Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Sat, 13 Jun 2026 23:20:11 +0200 Subject: [PATCH 5/5] Fix python-abi3 fallback version --- crates/pixi_build_python/src/main.rs | 8 ++++---- docs/build/backends/pixi-build-python.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/pixi_build_python/src/main.rs b/crates/pixi_build_python/src/main.rs index 689ca5f2c9..a895775a3c 100644 --- a/crates/pixi_build_python/src/main.rs +++ b/crates/pixi_build_python/src/main.rs @@ -47,7 +47,7 @@ const CYTHON_INPUT_GLOBS: &[&str] = &["**/*.{pyx,pxd,pxi}"]; /// - `">=3.9"` → `"3.9.*"` /// - `">=3.9.3"` → `"3.9.*"` /// - `">=3.11,<4"` → `"3.11.*"` -/// - `None` → `"3.8.*"` (default) +/// - `None` → `"3.9.*"` (default) fn python_abi3_spec_from_requires_python(requires_python: Option<&str>) -> miette::Result { let lower_bound = requires_python .and_then(|s| { @@ -61,7 +61,7 @@ fn python_abi3_spec_from_requires_python(requires_python: Option<&str>) -> miett .expect("pep440 version should be a valid conda version") }) }) - .unwrap_or_else(|| Version::from_str("3.8").expect("valid version")); + .unwrap_or_else(|| Version::from_str("3.9").expect("valid version")); let segment_count = std::cmp::min(lower_bound.segment_count(), 2); let major_minor = lower_bound @@ -1477,8 +1477,8 @@ build-backend = "setuptools.build_meta" .collect(); assert!( - host_deps.iter().any(|d| d == "python-abi3 3.8.*"), - "host deps should contain python-abi3 3.8.* when abi3=true, got: {host_deps:?}" + host_deps.iter().any(|d| d == "python-abi3 3.9.*"), + "host deps should contain python-abi3 3.9.* when abi3=true, got: {host_deps:?}" ); assert!( !host_deps.iter().any(|d| d.contains("python_abi")), diff --git a/docs/build/backends/pixi-build-python.md b/docs/build/backends/pixi-build-python.md index 47bf4922b6..43fd680bb6 100644 --- a/docs/build/backends/pixi-build-python.md +++ b/docs/build/backends/pixi-build-python.md @@ -211,7 +211,7 @@ The `python-abi3` version is derived from the lower bound of `requires-python`: - `requires-python = ">=3.9"` → `python-abi3 3.9.*` - `requires-python = ">=3.11,<4"` → `python-abi3 3.11.*` -- If `requires-python` is not specified, defaults to `python-abi3 3.8.*` +- If `requires-python` is not specified, defaults to `python-abi3 3.9.*`, the oldest available `python-abi3` package on conda-forge If `python-abi3` is already declared in your host requirements, pixi does not add a duplicate entry.