Skip to content
24 changes: 4 additions & 20 deletions crates/pixi_build_python/src/build_script.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use std::path::PathBuf;

use minijinja::Environment;
use serde::Serialize;
use serde::{Deserialize, Serialize};

const UV: &str = "uv";
#[derive(Serialize)]
pub struct BuildScriptContext {
pub installer: Installer,
Expand All @@ -13,12 +12,12 @@ pub struct BuildScriptContext {
pub manifest_root: PathBuf,
}

#[derive(Default, Serialize)]
#[derive(Default, Serialize, Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum Installer {
Uv,
#[default]
Pip,
#[default]
Uv,
}

impl Installer {
Expand All @@ -28,21 +27,6 @@ impl Installer {
Installer::Pip => "pip",
}
}

/// Determine the installer from an iterator of dependency package names.
/// Checks if "uv" is present in the package names.
pub fn determine_installer_from_names<'a>(
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.

Why remove? Cant we check if an installer is set based on the dependencies and use that, otherwise use the default?

mut package_names: impl Iterator<Item = &'a str>,
) -> Installer {
// Check all dependency names for "uv" package
let has_uv = package_names.any(|name| name == UV);

if has_uv {
Installer::Uv
} else {
Installer::Pip
}
}
}

#[derive(Serialize)]
Expand Down
33 changes: 32 additions & 1 deletion crates/pixi_build_python/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::build_script::Installer;
use indexmap::IndexMap;
use pixi_build_backend::generated_recipe::BackendConfig;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -39,6 +40,13 @@ pub struct PythonBackendConfig {
/// Only meaningful for packages with compiled extensions (non-noarch).
#[serde(default)]
pub abi3: Option<bool>,

/// The package installer to use for building.
/// Defaults to `uv` (recommended). Use `pip` only when needed for
/// compatibility with packages that require pip-specific behavior.
/// Supported values: `"uv"` (default), `"pip"`
#[serde(default)]
pub installer: Option<Installer>,
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.

I dont see why we need this. Lets just keepnthe behavior where this is derived

}

impl PythonBackendConfig {
Expand All @@ -56,9 +64,10 @@ impl PythonBackendConfig {
/// Creates a new [`PythonBackendConfig`] with default values and
/// `ignore_pyproject_manifest` set to `true`.
#[cfg(test)]
pub fn default_with_ignore_pyproject_manifest() -> Self {
pub fn default_with_ignore_pyproject_manifest(installer: Option<Installer>) -> Self {
Self {
ignore_pyproject_manifest: Some(true),
installer,
..Default::default()
}
}
Expand Down Expand Up @@ -110,12 +119,16 @@ impl BackendConfig for PythonBackendConfig {
.ignore_pypi_mapping
.or(self.ignore_pypi_mapping),
abi3: target_config.abi3.or(self.abi3),
installer: target_config.installer.clone().or(self.installer.clone()),
})
}
}

#[cfg(test)]
mod tests {

use crate::build_script::Installer;

use super::PythonBackendConfig;
use pixi_build_backend::generated_recipe::BackendConfig;
use serde_json::json;
Expand Down Expand Up @@ -143,6 +156,7 @@ mod tests {
ignore_pyproject_manifest: Some(true),
ignore_pypi_mapping: Some(true),
abi3: Some(true),
installer: None,
};

let mut target_env = indexmap::IndexMap::new();
Expand All @@ -159,6 +173,7 @@ mod tests {
ignore_pyproject_manifest: Some(false),
ignore_pypi_mapping: Some(false),
abi3: Some(false),
installer: None,
};

let merged = base_config
Expand Down Expand Up @@ -213,6 +228,7 @@ mod tests {
ignore_pyproject_manifest: Some(true),
ignore_pypi_mapping: Some(true),
abi3: None,
installer: None,
};

let empty_target_config = PythonBackendConfig::default();
Expand Down Expand Up @@ -348,4 +364,19 @@ mod tests {
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("`debug_dir` cannot have a target specific value"));
}

#[test]
fn test_deserialize_installer_field() {
let json_data = json!({"installer": "uv"});
let config: PythonBackendConfig = serde_json::from_value(json_data).unwrap();
assert_eq!(config.installer, Some(Installer::Uv));

let json_data = json!({"installer": "pip"});
let config: PythonBackendConfig = serde_json::from_value(json_data).unwrap();
assert_eq!(config.installer, Some(Installer::Pip));

let json_data = json!({});
let config: PythonBackendConfig = serde_json::from_value(json_data).unwrap();
assert_eq!(config.installer, None);
}
}
38 changes: 31 additions & 7 deletions crates/pixi_build_python/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod config;
mod metadata;
mod pypi_mapping;

use build_script::{BuildPlatform, BuildScriptContext, Installer};
use build_script::{BuildPlatform, BuildScriptContext};
use config::PythonBackendConfig;
use fs_err as fs;
use miette::IntoDiagnostic;
Expand Down Expand Up @@ -156,8 +156,7 @@ impl GenerateRecipe for PythonGenerator {
// are added to the `host` requirements, while for cmake/rust they are
// added to the `build` requirements.
// We only check build and host dependencies for the installer.
let installer =
Installer::determine_installer_from_names(model_dependencies.build_and_host_names());
let installer = config.installer.clone().unwrap_or_default();

let installer_name = installer.package_name().to_string();
let installer_pkg = pixi_build_types::SourcePackageName::from(installer_name.as_str());
Expand Down Expand Up @@ -651,7 +650,9 @@ version = "0.1.0"
let generated_recipe = PythonGenerator::default()
.generate_recipe(
&project_model,
&PythonBackendConfig::default_with_ignore_pyproject_manifest(),
&PythonBackendConfig::default_with_ignore_pyproject_manifest(Some(
build_script::Installer::Pip,
)),
PathBuf::from("."),
Platform::Linux64,
None,
Expand Down Expand Up @@ -696,7 +697,7 @@ version = "0.1.0"
let generated_recipe = PythonGenerator::default()
.generate_recipe(
&project_model,
&PythonBackendConfig::default_with_ignore_pyproject_manifest(),
&PythonBackendConfig::default_with_ignore_pyproject_manifest(None),
PathBuf::from("."),
Platform::Linux64,
None,
Expand Down Expand Up @@ -1089,8 +1090,8 @@ build-backend = "hatchling.build"

assert_eq!(
host_deps,
vec!["pip", "python"],
"host deps should only contain pip and python when ignore_pypi_mapping=true"
vec!["uv", "python"],
"host deps should only contain uv and python when ignore_pypi_mapping=true"
);
}

Expand Down Expand Up @@ -1257,4 +1258,27 @@ build-backend = "setuptools.build_meta"
"ignore_pypi_mapping should default to true"
);
}

#[tokio::test]
async fn test_uv_is_default_installer_in_host_requirements() {
let config = PythonBackendConfig::default_with_ignore_pyproject_manifest(None);
let recipe = generate_test_recipe(&config)
.await
.expect("Failed to generate recipe");
let host_deps: Vec<String> = recipe
.recipe
.requirements
.host
.iter()
.map(|item| item.to_string())
.collect();
assert!(
host_deps.contains(&"uv".to_string()),
"uv should be in host deps when installer not specified, got: {host_deps:?}"
);
assert!(
!host_deps.contains(&"pip".to_string()),
"pip should NOT be in host deps when installer not specified, got: {host_deps:?}"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ build:
requirements:
build: []
host:
- pip
- uv
- python
run:
- boltons
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ requirements:
build: []
host:
- python
- pip
- uv
run:
- boltons
- python
Expand Down
Loading