diff --git a/Cargo.lock b/Cargo.lock index bc114251..48364c88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4262,6 +4262,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "pixi-build-pixi" +version = "0.3.3" +dependencies = [ + "indexmap 2.11.1", + "insta", + "miette", + "minijinja", + "pixi-build-backend", + "pixi_build_types", + "rattler-build", + "rattler_conda_types", + "recipe-stage0", + "rstest", + "serde", + "serde_json", + "strum", + "tokio", +] + [[package]] name = "pixi-build-python" version = "0.4.0" diff --git a/crates/pixi-build-pixi/Cargo.toml b/crates/pixi-build-pixi/Cargo.toml new file mode 100644 index 00000000..c3bda663 --- /dev/null +++ b/crates/pixi-build-pixi/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pixi-build-pixi" +version = "0.3.3" +description = "Pixi build backend for Pixi" +edition.workspace = true + +[dependencies] +indexmap = { workspace = true } +miette = { workspace = true } +minijinja = { workspace = true } +pixi-build-backend = { workspace = true } +pixi_build_types = { workspace = true } +rattler-build = { workspace = true } +rattler_conda_types = { workspace = true } +recipe-stage0 = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["macros"] } + +[dev-dependencies] +insta = { workspace = true, features = ["yaml", "redactions", "filters"] } +rstest = { workspace = true } +strum = { workspace = true } diff --git a/crates/pixi-build-pixi/pixi.toml b/crates/pixi-build-pixi/pixi.toml new file mode 100644 index 00000000..20184726 --- /dev/null +++ b/crates/pixi-build-pixi/pixi.toml @@ -0,0 +1,10 @@ +[package.build.backend] +name = "pixi-build-rust" +version = "*" +channels = [ + "https://prefix.dev/pixi-build-backends", + "https://prefix.dev/conda-forge", +] + +[package.run-dependencies] +pixi-build-api-version = ">=2,<3" diff --git a/crates/pixi-build-pixi/src/build_script.rs b/crates/pixi-build-pixi/src/build_script.rs new file mode 100644 index 00000000..0920a63b --- /dev/null +++ b/crates/pixi-build-pixi/src/build_script.rs @@ -0,0 +1,17 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct BuildScriptContext { + pub build_task: String, + pub manifest_root: std::path::PathBuf, +} + +impl BuildScriptContext { + pub fn render(&self) -> String { + format!( + "pixi run --as-is --manifest-path {} {}", + self.manifest_root.to_string_lossy(), + self.build_task + ) + } +} diff --git a/crates/pixi-build-pixi/src/config.rs b/crates/pixi-build-pixi/src/config.rs new file mode 100644 index 00000000..a3b63e26 --- /dev/null +++ b/crates/pixi-build-pixi/src/config.rs @@ -0,0 +1,63 @@ +use std::path::{Path, PathBuf}; + +use indexmap::IndexMap; +use pixi_build_backend::generated_recipe::BackendConfig; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize, Clone)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct PixiBackendConfig { + /// Environment Variables + #[serde(default)] + pub env: IndexMap, + /// If set, internal state will be logged as files in that directory + pub debug_dir: Option, + /// Extra input globs to include in addition to the default ones + #[serde(default)] + pub extra_input_globs: Vec, + /// Name of the build task in pixi.toml (defaults to "build") + #[serde(default = "default_build_task")] + pub build_task: String, +} + +fn default_build_task() -> String { + "build".to_string() +} + +impl BackendConfig for PixiBackendConfig { + fn debug_dir(&self) -> Option<&Path> { + self.debug_dir.as_deref() + } + + /// Merge this configuration with a target-specific configuration. + /// Target-specific values override base values using the following rules: + /// - extra_args: Platform-specific completely replaces base + /// - env: Platform env vars override base, others merge + /// - debug_dir: Not allowed to have target specific value + /// - extra_input_globs: Platform-specific completely replaces base + /// - compilers: Platform-specific completely replaces base + fn merge_with_target_config(&self, target_config: &Self) -> miette::Result { + if target_config.debug_dir.is_some() { + miette::bail!("`debug_dir` cannot have a target specific value"); + } + + Ok(Self { + env: { + let mut merged_env = self.env.clone(); + merged_env.extend(target_config.env.clone()); + merged_env + }, + debug_dir: self.debug_dir.clone(), + extra_input_globs: if target_config.extra_input_globs.is_empty() { + self.extra_input_globs.clone() + } else { + target_config.extra_input_globs.clone() + }, + build_task: if target_config.build_task == default_build_task() { + self.build_task.clone() + } else { + target_config.build_task.clone() + }, + }) + } +} diff --git a/crates/pixi-build-pixi/src/main.rs b/crates/pixi-build-pixi/src/main.rs new file mode 100644 index 00000000..8e06090c --- /dev/null +++ b/crates/pixi-build-pixi/src/main.rs @@ -0,0 +1,105 @@ +mod build_script; +mod config; + +use build_script::BuildScriptContext; +use config::PixiBackendConfig; +use miette::IntoDiagnostic; +use pixi_build_backend::{ + generated_recipe::{DefaultMetadataProvider, GenerateRecipe, GeneratedRecipe, PythonParams}, + intermediate_backend::IntermediateBackendInstantiator, +}; +use rattler_build::{NormalizedKey, recipe::variable::Variable}; +use rattler_conda_types::{PackageName, Platform}; +use recipe_stage0::recipe::{ConditionalRequirements, Script}; +use std::collections::HashSet; +use std::{ + collections::{BTreeMap, BTreeSet}, + path::Path, + sync::Arc, +}; + +#[derive(Default, Clone)] +pub struct PixiGenerator {} + +impl GenerateRecipe for PixiGenerator { + type Config = PixiBackendConfig; + + fn generate_recipe( + &self, + model: &pixi_build_types::ProjectModelV1, + config: &Self::Config, + manifest_root: std::path::PathBuf, + host_platform: rattler_conda_types::Platform, + _python_params: Option, + _variants: &HashSet, + ) -> miette::Result { + let mut generated_recipe = + GeneratedRecipe::from_model(model.clone(), &mut DefaultMetadataProvider) + .into_diagnostic()?; + + let requirements = &mut generated_recipe.recipe.requirements; + + let resolved_requirements = ConditionalRequirements::resolve( + requirements.build.as_ref(), + requirements.host.as_ref(), + requirements.run.as_ref(), + requirements.run_constraints.as_ref(), + Some(host_platform), + ); + + // Add pixi as a build dependency + let pixi_name = PackageName::new_unchecked("pixi"); + if !resolved_requirements.build.contains_key(&pixi_name) { + requirements.build.push("pixi".parse().into_diagnostic()?); + } + + let build_script = BuildScriptContext { + build_task: config.build_task.clone(), + manifest_root, + } + .render(); + + generated_recipe.recipe.build.script = Script { + content: build_script, + env: config.env.clone(), + ..Default::default() + }; + + Ok(generated_recipe) + } + + fn extract_input_globs_from_build( + &self, + config: &Self::Config, + _workdir: impl AsRef, + _editable: bool, + ) -> miette::Result> { + Ok(["pixi.toml", "pixi.lock"] + .iter() + .map(|s: &&str| s.to_string()) + .chain(config.extra_input_globs.clone()) + .collect()) + } + + fn default_variants( + &self, + _host_platform: Platform, + ) -> miette::Result>> { + let variants = BTreeMap::new(); + + // No default variants needed for pixi builds + Ok(variants) + } +} + +#[tokio::main] +pub async fn main() { + if let Err(err) = pixi_build_backend::cli::main(|log| { + IntermediateBackendInstantiator::::new(log, Arc::default()) + }) + .await + { + eprintln!("{err:?}"); + std::process::exit(1); + } +} diff --git a/pixi.toml b/pixi.toml index 1412e3ff..487d121e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -23,6 +23,7 @@ test = [ ] generate-matrix = "python scripts/generate-matrix.py" +install-pixi-build-pixi = { cmd = "cargo install --path crates/pixi-build-pixi --locked --force" } install-pixi-build-python = { cmd = "cargo install --path crates/pixi-build-python --locked --force" } install-pixi-build-cmake = { cmd = "cargo install --path crates/pixi-build-cmake --locked --force" } install-pixi-build-rattler-build = { cmd = "cargo install --path crates/pixi-build-rattler-build --locked --force" }