Skip to content
Open
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
168 changes: 160 additions & 8 deletions crates/pixi_build_python/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use pixi_build_backend::{
};
use pyproject_toml::PyProjectToml;
use rattler_build_recipe::stage0::{
ConditionalList, Item, PythonBuild, Script, SerializableMatchSpec, Value,
ConditionalList, Item, JinjaTemplate, PythonBuild, Script, SerializableMatchSpec, Value,
};
use rattler_conda_types::{
ChannelUrl, NoArchType, Platform, Version, VersionBumpType, package::EntryPoint,
Expand All @@ -39,6 +39,34 @@ use crate::pypi_mapping::{

const CYTHON_INPUT_GLOBS: &[&str] = &["**/*.{pyx,pxd,pxi}"];

/// Build-script env entry pinning the macOS deployment target for compiled
/// (non-noarch) macOS builds (pixi issue #6175).
///
/// rattler-build hard-codes `MACOSX_DEPLOYMENT_TARGET` (11.0/10.9) and ignores
/// the `c_stdlib_version` variant, so maturin/PEP 517 wheels get the host SDK
/// tag. We override it with `${{ c_stdlib_version }}` (rendered lazily by
/// rattler-build), which pixi derives from the platform's `__osx` requirement.
/// `None` off macOS, for noarch, or when the variant is absent.
fn macos_deployment_target_env(
host_platform: Platform,
variants: &HashSet<NormalizedKey>,
is_noarch: bool,
) -> Option<(String, Value<String>)> {
if is_noarch
|| !host_platform.is_osx()
|| !variants.contains(&NormalizedKey::from("c_stdlib_version".to_string()))
{
return None;
}

let template =
JinjaTemplate::new("${{ c_stdlib_version }}".to_string()).expect("valid jinja template");
Some((
"MACOSX_DEPLOYMENT_TARGET".to_string(),
Value::new_template(template, None),
))
}
Comment on lines +42 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed because this is done by the action script of the macos_deployment_target package.


/// Compute the `python_abi` version spec from an optional `requires-python`
/// specifier string.
///
Expand Down Expand Up @@ -400,14 +428,19 @@ impl GenerateRecipe for PythonGenerator {
generated_recipe.recipe.build.python = python;
generated_recipe.recipe.build.noarch = noarch_kind;

let mut script_env: indexmap::IndexMap<String, Value<String>> = config
.env
.iter()
.map(|(k, v)| (k.clone(), Value::new_concrete(v.clone(), None)))
.collect();
// An explicit `MACOSX_DEPLOYMENT_TARGET` in the backend config wins.
if let Some((key, value)) = macos_deployment_target_env(host_platform, variants, is_noarch)
{
script_env.entry(key).or_insert(value);
}

generated_recipe.recipe.build.script = Script::from_content(build_script)
.with_env(
config
.env
.iter()
.map(|(k, v)| (k.clone(), Value::new_concrete(v.clone(), None)))
.collect(),
)
.with_env(script_env)
.with_secrets(model.secrets.iter().cloned().collect());

// Add the metadata input globs from the MetadataProvider
Expand Down Expand Up @@ -816,6 +849,125 @@ version = "0.1.0"
});
}

fn stdlib_version_variants() -> HashSet<NormalizedKey> {
HashSet::from([NormalizedKey::from("c_stdlib_version".to_string())])
}

#[test]
fn macos_deployment_target_set_for_compiled_osx() {
let entry = macos_deployment_target_env(
Platform::OsxArm64,
&stdlib_version_variants(),
/* is_noarch */ false,
)
.expect("a compiled macOS build should pin the deployment target");

assert_eq!(entry.0, "MACOSX_DEPLOYMENT_TARGET");
assert_eq!(
entry.1.as_template().map(|t| t.to_string()),
Some("${{ c_stdlib_version }}".to_string())
);
}

#[test]
fn macos_deployment_target_skipped_for_noarch() {
assert!(
macos_deployment_target_env(Platform::OsxArm64, &stdlib_version_variants(), true)
.is_none()
);
}

#[test]
fn macos_deployment_target_skipped_off_macos() {
assert!(
macos_deployment_target_env(Platform::Linux64, &stdlib_version_variants(), false)
.is_none()
);
}

#[test]
fn macos_deployment_target_skipped_without_variant() {
assert!(macos_deployment_target_env(Platform::OsxArm64, &HashSet::new(), false).is_none());
}

/// End-to-end through `generate_recipe`: a compiled macOS build gets the
/// deployment target wired into the build-script env as a lazily-rendered
/// `${{ c_stdlib_version }}`, overriding rattler-build's hard-coded default.
#[tokio::test]
async fn test_macos_deployment_target_in_generated_recipe() {
let generated_recipe = PythonGenerator::default()
.generate_recipe(
&minimal_project(),
&PythonBackendConfig {
compilers: Some(vec!["rust".to_string()]),
ignore_pyproject_manifest: Some(true),
..Default::default()
},
PathBuf::from("."),
Platform::OsxArm64,
None,
&stdlib_version_variants(),
vec![],
None,
None,
None,
None,
)
.await
.expect("Failed to generate recipe");

let dep_target = generated_recipe
.recipe
.build
.script
.env
.get("MACOSX_DEPLOYMENT_TARGET")
.expect("MACOSX_DEPLOYMENT_TARGET should be set for a compiled macOS build");
assert_eq!(
dep_target.as_template().map(|t| t.to_string()),
Some("${{ c_stdlib_version }}".to_string())
);
}

/// A user-provided `MACOSX_DEPLOYMENT_TARGET` in the backend config is left
/// untouched -- the derived value only fills the gap.
#[tokio::test]
async fn test_macos_deployment_target_respects_user_config() {
let generated_recipe = PythonGenerator::default()
.generate_recipe(
&minimal_project(),
&PythonBackendConfig {
compilers: Some(vec!["rust".to_string()]),
ignore_pyproject_manifest: Some(true),
env: IndexMap::from([(
"MACOSX_DEPLOYMENT_TARGET".to_string(),
"12.0".to_string(),
)]),
..Default::default()
},
PathBuf::from("."),
Platform::OsxArm64,
None,
&stdlib_version_variants(),
vec![],
None,
None,
None,
None,
)
.await
.expect("Failed to generate recipe");

let dep_target = generated_recipe
.recipe
.build
.script
.env
.get("MACOSX_DEPLOYMENT_TARGET")
.expect("explicit MACOSX_DEPLOYMENT_TARGET should be kept");
assert_eq!(dep_target.as_concrete().map(String::as_str), Some("12.0"));
}

#[tokio::test]
async fn test_multiple_compilers_configuration() {
let project_model = project_fixture!({
Expand Down
51 changes: 51 additions & 0 deletions crates/pixi_core/src/workspace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod has_project_ref;
pub mod registry;
mod repodata;
mod solve_group;
mod stdlib_variants;
pub mod virtual_packages;
mod workspace_mut;

Expand Down Expand Up @@ -724,6 +725,15 @@ impl Workspace {
}
}

// Derive `c_stdlib` variants from the platform's system requirements,
// filling only keys an explicit `[workspace.build-variants]` entry
// didn't already set -- a hand-written variant always wins.
for (key, value) in stdlib_variants::derive_stdlib_variants(platform) {
variant_configuration
.entry(key)
.or_insert_with(|| vec![value]);
}

// Collect absolute variant file paths without reading their content.
let variant_files = self
.workspace
Expand Down Expand Up @@ -1581,6 +1591,47 @@ mod tests {
);
}

/// An explicit `[workspace.build-variants]` entry wins over the value
/// derived from the platform's system requirements, while a key the user
/// did not set (`c_stdlib`) is still filled in from the platform.
#[test]
fn explicit_build_variant_overrides_derived_stdlib() {
let file_contents = r#"
[workspace]
name = "foo"
channels = []
platforms = ["osx-arm64"]
build-variants = { c_stdlib_version = ["99.0"] }
"#;
let workspace = Workspace::from_str(Path::new("pixi.toml"), file_contents).unwrap();

let platform = pixi_manifest::PixiPlatform::new(
pixi_manifest::PixiPlatformName::from_str("mac").unwrap(),
Platform::OsxArm64,
vec![GenericVirtualPackage {
name: "__osx".parse().unwrap(),
version: Version::from_str("13.5").unwrap(),
build_string: "0".to_string(),
}],
)
.unwrap();

let variants = workspace.variants(&platform).unwrap().variant_configuration;

// Explicit override is kept verbatim, not replaced by the derived 13.5.
assert_eq!(
variants.get("c_stdlib_version"),
Some(&vec![VariantValue::String("99.0".to_string())])
);
// The provider key the user didn't set is derived from the platform.
assert_eq!(
variants.get("c_stdlib"),
Some(&vec![VariantValue::String(
"macosx_deployment_target".to_string()
)])
);
}

#[test]
fn test_mapping_location() {
let file_contents = r#"
Expand Down
Loading
Loading