From 0536133b83df03db9715e6bccc1d45f90a6efeb8 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Mon, 13 Oct 2025 16:01:42 +0200 Subject: [PATCH 1/5] feat: finalize zig build backend --- .github/workflows/build-upload.yml | 1 + Cargo.lock | 21 ++ README.md | 3 +- crates/pixi-build-zig/Cargo.toml | 28 ++ crates/pixi-build-zig/pixi.toml | 10 + crates/pixi-build-zig/src/build_script.j2 | 10 + crates/pixi-build-zig/src/build_script.rs | 56 ++++ crates/pixi-build-zig/src/config.rs | 164 +++++++++++ crates/pixi-build-zig/src/main.rs | 271 ++++++++++++++++++ ...build_script__test__build_script@bash.snap | 5 + ...ild_script__test__build_script@cmdexe.snap | 7 + ...st__build_script_with_extra_args@bash.snap | 5 + ...__build_script_with_extra_args@cmdexe.snap | 7 + ...xi_build_zig__tests__env_vars_are_set.snap | 8 + ...__tests__zig_is_in_build_requirements.snap | 29 ++ ...__zig_is_not_added_if_already_present.snap | 29 ++ docs/backends/pixi-build-zig.md | 252 ++++++++++++++++ mkdocs.yml | 1 + pixi.toml | 2 + 19 files changed, 908 insertions(+), 1 deletion(-) create mode 100644 crates/pixi-build-zig/Cargo.toml create mode 100644 crates/pixi-build-zig/pixi.toml create mode 100644 crates/pixi-build-zig/src/build_script.j2 create mode 100644 crates/pixi-build-zig/src/build_script.rs create mode 100644 crates/pixi-build-zig/src/config.rs create mode 100644 crates/pixi-build-zig/src/main.rs create mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script@bash.snap create mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script@cmdexe.snap create mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script_with_extra_args@bash.snap create mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script_with_extra_args@cmdexe.snap create mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__env_vars_are_set.snap create mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_in_build_requirements.snap create mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_not_added_if_already_present.snap create mode 100644 docs/backends/pixi-build-zig.md diff --git a/.github/workflows/build-upload.yml b/.github/workflows/build-upload.yml index 0e1104d0..7346f345 100644 --- a/.github/workflows/build-upload.yml +++ b/.github/workflows/build-upload.yml @@ -6,6 +6,7 @@ on: - "pixi-build-python-v[0-9]+.[0-9]+.[0-9]+" - "pixi-build-rattler-build-v[0-9]+.[0-9]+.[0-9]+" - "pixi-build-rust-v[0-9]+.[0-9]+.[0-9]+" + - "pixi-build-zig-v[0-9]+.[0-9]+.[0-9]+" - "pixi-build-mojo-v[0-9]+.[0-9]+.[0-9]+" - "pixi-build-ros-v[0-9]+.[0-9]+.[0-9]+" - "py-pixi-build-backend-v[0-9]+.[0-9]+.[0-9]+" diff --git a/Cargo.lock b/Cargo.lock index bc114251..d7b5c141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4333,6 +4333,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "pixi-build-zig" +version = "0.1.0" +dependencies = [ + "fs-err", + "indexmap 2.11.1", + "insta", + "miette", + "minijinja", + "pixi-build-backend", + "pixi_build_types", + "rattler_conda_types", + "recipe-stage0", + "rstest", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.16", + "tokio", +] + [[package]] name = "pixi_build_type_conversions" version = "0.1.0" diff --git a/README.md b/README.md index b0169f2b..3f7a6ba7 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,14 @@ This repository contains backend implementations designed to facilitate the building of pixi projects directly from their source code. These backends aim to enhance the functionality of Pixi, a cross-platform, multi-language package manager and workflow tool built on the foundation of the conda ecosystem. ## Available Build Backends -The idea is that a backend should be able to build a certain type of so +The idea is that a backend should be able to build a certain type of project. The repository provides the following build backends: 1. **pixi-build-python**: A backend tailored for building Python-based projects. 2. **pixi-build-cmake**: A backend designed for projects utilizing CMake as their build system. 3. **pixi-build-rattler-build**: A backend for building [`recipe.yaml`](https://rattler.build/latest/) directly 4. **pixi-build-rust**: A backend for building Rust projects. +5. **pixi-build-zig**: A backend for building Zig projects. These backends are located in the `crates/*` directory of the repository. diff --git a/crates/pixi-build-zig/Cargo.toml b/crates/pixi-build-zig/Cargo.toml new file mode 100644 index 00000000..e242fff4 --- /dev/null +++ b/crates/pixi-build-zig/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "pixi-build-zig" +version = "0.1.0" +description = "A Zig build backend for Pixi" +documentation = "https://prefix-dev.github.io/pixi-build-backends/backends/pixi-build-zig/" +repository.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +fs-err = { workspace = true } +indexmap = { workspace = true } +miette = { workspace = true } +minijinja = { workspace = true, features = ["json"] } +rattler_conda_types = { workspace = true } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["macros"] } +pixi-build-backend = { workspace = true } +pixi_build_types = { workspace = true } + +recipe-stage0 = { workspace = true } + +[dev-dependencies] +insta = { workspace = true, features = ["yaml", "redactions", "filters"] } +rstest = { workspace = true } +serde_json = { workspace = true } +tempfile = { workspace = true } diff --git a/crates/pixi-build-zig/pixi.toml b/crates/pixi-build-zig/pixi.toml new file mode 100644 index 00000000..20184726 --- /dev/null +++ b/crates/pixi-build-zig/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-zig/src/build_script.j2 b/crates/pixi-build-zig/src/build_script.j2 new file mode 100644 index 00000000..5af2f941 --- /dev/null +++ b/crates/pixi-build-zig/src/build_script.j2 @@ -0,0 +1,10 @@ +{%- macro env(key) -%} +{%- if is_bash %}{{ "$" ~key }}{% else %}{{ "%" ~ key ~ "%" }}{% endif -%} +{%- endmacro -%} +{%- set install_prefix = env("LIBRARY_PREFIX") if not is_bash else env("PREFIX") -%} + +zig build --prefix {{ install_prefix }}{% if extra_args | length > 0 %} {{ extra_args | join(" ") }}{% endif %} +{%- if not is_bash %} + +if errorlevel 1 exit 1 +{%- endif %} diff --git a/crates/pixi-build-zig/src/build_script.rs b/crates/pixi-build-zig/src/build_script.rs new file mode 100644 index 00000000..54655844 --- /dev/null +++ b/crates/pixi-build-zig/src/build_script.rs @@ -0,0 +1,56 @@ +use minijinja::Environment; +use serde::Serialize; + +#[derive(Serialize)] +pub struct BuildScriptContext { + /// Any additional args to pass to `zig build` + pub extra_args: Vec, + + /// The platform that is running the build. + pub is_bash: bool, +} + +impl BuildScriptContext { + pub fn render(&self) -> String { + let env = Environment::new(); + let template = env + .template_from_str(include_str!("build_script.j2")) + .unwrap(); + template.render(self).unwrap().trim().to_string() + } +} + +#[cfg(test)] +mod test { + use rstest::*; + + #[rstest] + fn test_build_script(#[values(true, false)] is_bash: bool) { + let context = super::BuildScriptContext { + extra_args: vec![], + is_bash, + }; + let script = context.render(); + + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_suffix(if is_bash { "bash" } else { "cmdexe" }); + settings.bind(|| { + insta::assert_snapshot!(script); + }); + } + + #[rstest] + fn test_build_script_with_extra_args(#[values(true, false)] is_bash: bool) { + let context = super::BuildScriptContext { + extra_args: vec!["-Doptimize=ReleaseFast".to_string()], + is_bash, + }; + let script = context.render(); + + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_suffix(if is_bash { "bash" } else { "cmdexe" }); + settings.bind(|| { + insta::assert_snapshot!(script); + }); + } +} diff --git a/crates/pixi-build-zig/src/config.rs b/crates/pixi-build-zig/src/config.rs new file mode 100644 index 00000000..8747f51e --- /dev/null +++ b/crates/pixi-build-zig/src/config.rs @@ -0,0 +1,164 @@ +use indexmap::IndexMap; +use pixi_build_backend::generated_recipe::BackendConfig; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct ZigBackendConfig { + /// Extra args to pass for zig build + #[serde(default)] + pub extra_args: Vec, + /// 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, +} + +impl BackendConfig for ZigBackendConfig { + 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 + 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 { + extra_args: if target_config.extra_args.is_empty() { + self.extra_args.clone() + } else { + target_config.extra_args.clone() + }, + 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() + }, + }) + } +} + +#[cfg(test)] +mod tests { + use super::ZigBackendConfig; + use pixi_build_backend::generated_recipe::BackendConfig; + use serde_json::json; + use std::path::PathBuf; + + #[test] + fn test_ensure_deseralize_from_empty() { + let json_data = json!({}); + serde_json::from_value::(json_data).unwrap(); + } + + #[test] + fn test_merge_with_target_config() { + let mut base_env = indexmap::IndexMap::new(); + base_env.insert("BASE_VAR".to_string(), "base_value".to_string()); + base_env.insert("SHARED_VAR".to_string(), "base_shared".to_string()); + + let base_config = ZigBackendConfig { + extra_args: vec!["--base-arg".to_string()], + env: base_env, + debug_dir: Some(PathBuf::from("/base/debug")), + extra_input_globs: vec!["*.base".to_string()], + }; + + let mut target_env = indexmap::IndexMap::new(); + target_env.insert("TARGET_VAR".to_string(), "target_value".to_string()); + target_env.insert("SHARED_VAR".to_string(), "target_shared".to_string()); + + let target_config = ZigBackendConfig { + extra_args: vec!["--target-arg".to_string()], + env: target_env, + debug_dir: None, + extra_input_globs: vec!["*.target".to_string()], + }; + + let merged = base_config + .merge_with_target_config(&target_config) + .unwrap(); + + // extra_args should be completely overridden + assert_eq!(merged.extra_args, vec!["--target-arg".to_string()]); + + // env should merge with target taking precedence + assert_eq!(merged.env.get("BASE_VAR"), Some(&"base_value".to_string())); + assert_eq!( + merged.env.get("TARGET_VAR"), + Some(&"target_value".to_string()) + ); + assert_eq!( + merged.env.get("SHARED_VAR"), + Some(&"target_shared".to_string()) + ); + + // debug_dir should use base value + assert_eq!(merged.debug_dir, Some(PathBuf::from("/base/debug"))); + + // extra_input_globs should be completely overridden + assert_eq!(merged.extra_input_globs, vec!["*.target".to_string()]); + } + + #[test] + fn test_merge_with_empty_target_config() { + let mut base_env = indexmap::IndexMap::new(); + base_env.insert("BASE_VAR".to_string(), "base_value".to_string()); + + let base_config = ZigBackendConfig { + extra_args: vec!["--base-arg".to_string()], + env: base_env, + debug_dir: Some(PathBuf::from("/base/debug")), + extra_input_globs: vec!["*.base".to_string()], + }; + + let empty_target_config = ZigBackendConfig::default(); + + let merged = base_config + .merge_with_target_config(&empty_target_config) + .unwrap(); + + // Should keep base values when target is empty + assert_eq!(merged.extra_args, vec!["--base-arg".to_string()]); + assert_eq!(merged.env.get("BASE_VAR"), Some(&"base_value".to_string())); + assert_eq!(merged.debug_dir, Some(PathBuf::from("/base/debug"))); + assert_eq!(merged.extra_input_globs, vec!["*.base".to_string()]); + } + + #[test] + fn test_merge_target_debug_dir_error() { + let base_config = ZigBackendConfig { + debug_dir: Some(PathBuf::from("/base/debug")), + ..Default::default() + }; + + let target_config = ZigBackendConfig { + debug_dir: Some(PathBuf::from("/target/debug")), + ..Default::default() + }; + + let result = base_config.merge_with_target_config(&target_config); + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("`debug_dir` cannot have a target specific value")); + } +} diff --git a/crates/pixi-build-zig/src/main.rs b/crates/pixi-build-zig/src/main.rs new file mode 100644 index 00000000..9480c5c1 --- /dev/null +++ b/crates/pixi-build-zig/src/main.rs @@ -0,0 +1,271 @@ +mod build_script; +mod config; + +use build_script::BuildScriptContext; +use config::ZigBackendConfig; +use miette::IntoDiagnostic; +use pixi_build_backend::variants::NormalizedKey; +use pixi_build_backend::{ + generated_recipe::{DefaultMetadataProvider, GenerateRecipe, GeneratedRecipe, PythonParams}, + intermediate_backend::IntermediateBackendInstantiator, +}; +use pixi_build_types::ProjectModelV1; +use rattler_conda_types::{MatchSpec, PackageName, Platform}; +use recipe_stage0::matchspec::PackageDependency; +use recipe_stage0::recipe::{ConditionalRequirements, Script}; +use std::collections::HashSet; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, + sync::Arc, +}; + +#[derive(Default, Clone)] +pub struct ZigGenerator {} + +impl GenerateRecipe for ZigGenerator { + type Config = ZigBackendConfig; + + fn generate_recipe( + &self, + model: &ProjectModelV1, + config: &Self::Config, + _manifest_root: PathBuf, + host_platform: Platform, + _python_params: Option, + _variants: &HashSet, + ) -> miette::Result { + // Create the recipe using the default metadata provider + 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 zig to build requirements if not already present + let zig_name = PackageName::new_unchecked("zig"); + if !resolved_requirements.build.contains_key(&zig_name) { + requirements.build.push( + PackageDependency::Binary( + MatchSpec::from_str("zig", rattler_conda_types::ParseStrictness::Lenient) + .into_diagnostic()?, + ) + .into(), + ); + } + + let build_script = BuildScriptContext { + extra_args: config.extra_args.clone(), + is_bash: !Platform::current().is_windows(), + } + .render(); + + generated_recipe.recipe.build.script = Script { + content: build_script, + env: config.env.clone(), + ..Default::default() + }; + + Ok(generated_recipe) + } + + /// Returns the build input globs used by the backend. + fn extract_input_globs_from_build( + &self, + config: &Self::Config, + _workdir: impl AsRef, + _editable: bool, + ) -> miette::Result> { + Ok([ + "**/*.zig", + // Zig build files + "build.zig", + "build.zig.zon", + ] + .iter() + .map(|s| s.to_string()) + .chain(config.extra_input_globs.clone()) + .collect()) + } +} + +#[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); + } +} + +#[cfg(test)] +mod tests { + use indexmap::IndexMap; + + use super::*; + + #[macro_export] + macro_rules! project_fixture { + ($($json:tt)+) => { + serde_json::from_value::( + serde_json::json!($($json)+) + ).expect("Failed to create TestProjectModel from JSON fixture.") + }; + } + + #[test] + fn test_input_globs_includes_extra_globs() { + let config = ZigBackendConfig { + extra_input_globs: vec!["custom/*.txt".to_string(), "extra/**/*.zig".to_string()], + ..Default::default() + }; + + let generator = ZigGenerator::default(); + + let result = generator + .extract_input_globs_from_build(&config, PathBuf::new(), false) + .unwrap(); + + // Verify that all extra globs are included in the result + for extra_glob in &config.extra_input_globs { + assert!( + result.contains(extra_glob), + "Result should contain extra glob: {}", + extra_glob + ); + } + + // Verify that default globs are still present + assert!(result.contains("**/*.zig")); + assert!(result.contains("build.zig")); + assert!(result.contains("build.zig.zon")); + } + + #[test] + fn test_zig_is_in_build_requirements() { + let project_model = project_fixture!({ + "name": "foobar", + "version": "0.1.0", + "targets": { + "defaultTarget": { + "runDependencies": { + "boltons": { + "binary": { + "version": "*" + } + } + } + }, + } + }); + + let generated_recipe = ZigGenerator::default() + .generate_recipe( + &project_model, + &ZigBackendConfig::default(), + PathBuf::from("."), + Platform::Linux64, + None, + &HashSet::new(), + ) + .expect("Failed to generate recipe"); + + insta::assert_yaml_snapshot!(generated_recipe.recipe, { + ".source[0].path" => "[ ... path ... ]", + ".build.script" => "[ ... script ... ]", + }); + } + + #[test] + fn test_zig_is_not_added_if_already_present() { + let project_model = project_fixture!({ + "name": "foobar", + "version": "0.1.0", + "targets": { + "defaultTarget": { + "runDependencies": { + "boltons": { + "binary": { + "version": "*" + } + } + }, + "buildDependencies": { + "zig": { + "binary": { + "version": "*" + } + } + } + }, + } + }); + + let generated_recipe = ZigGenerator::default() + .generate_recipe( + &project_model, + &ZigBackendConfig::default(), + PathBuf::from("."), + Platform::Linux64, + None, + &HashSet::new(), + ) + .expect("Failed to generate recipe"); + + insta::assert_yaml_snapshot!(generated_recipe.recipe, { + ".source[0].path" => "[ ... path ... ]", + ".build.script" => "[ ... script ... ]", + }); + } + + #[test] + fn test_env_vars_are_set() { + let project_model = project_fixture!({ + "name": "foobar", + "version": "0.1.0", + "targets": { + "defaultTarget": { + "runDependencies": { + "boltons": { + "binary": { + "version": "*" + } + } + } + }, + } + }); + + let env = IndexMap::from([("foo".to_string(), "bar".to_string())]); + + let generated_recipe = ZigGenerator::default() + .generate_recipe( + &project_model, + &ZigBackendConfig { + env: env.clone(), + ..Default::default() + }, + PathBuf::from("."), + Platform::Linux64, + None, + &HashSet::new(), + ) + .expect("Failed to generate recipe"); + + insta::assert_yaml_snapshot!(generated_recipe.recipe.build.script, + { + ".content" => "[ ... script ... ]", + }); + } +} diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script@bash.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script@bash.snap new file mode 100644 index 00000000..88ff9416 --- /dev/null +++ b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script@bash.snap @@ -0,0 +1,5 @@ +--- +source: crates/pixi-build-zig/src/build_script.rs +expression: script +--- +zig build --prefix $PREFIX diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script@cmdexe.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script@cmdexe.snap new file mode 100644 index 00000000..bfd824d1 --- /dev/null +++ b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script@cmdexe.snap @@ -0,0 +1,7 @@ +--- +source: crates/pixi-build-zig/src/build_script.rs +expression: script +--- +zig build --prefix %LIBRARY_PREFIX% + +if errorlevel 1 exit 1 diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script_with_extra_args@bash.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script_with_extra_args@bash.snap new file mode 100644 index 00000000..fb7c8c3f --- /dev/null +++ b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script_with_extra_args@bash.snap @@ -0,0 +1,5 @@ +--- +source: crates/pixi-build-zig/src/build_script.rs +expression: script +--- +zig build --prefix $PREFIX -Doptimize=ReleaseFast diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script_with_extra_args@cmdexe.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script_with_extra_args@cmdexe.snap new file mode 100644 index 00000000..55fc6d19 --- /dev/null +++ b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__build_script__test__build_script_with_extra_args@cmdexe.snap @@ -0,0 +1,7 @@ +--- +source: crates/pixi-build-zig/src/build_script.rs +expression: script +--- +zig build --prefix %LIBRARY_PREFIX% -Doptimize=ReleaseFast + +if errorlevel 1 exit 1 diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__env_vars_are_set.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__env_vars_are_set.snap new file mode 100644 index 00000000..0d8e3bed --- /dev/null +++ b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__env_vars_are_set.snap @@ -0,0 +1,8 @@ +--- +source: crates/pixi-build-zig/src/main.rs +expression: generated_recipe.recipe.build.script +--- +content: "[ ... script ... ]" +env: + foo: bar +secrets: [] diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_in_build_requirements.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_in_build_requirements.snap new file mode 100644 index 00000000..26021008 --- /dev/null +++ b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_in_build_requirements.snap @@ -0,0 +1,29 @@ +--- +source: crates/pixi-build-zig/src/main.rs +expression: generated_recipe.recipe +--- +context: {} +package: + name: foobar + version: 0.1.0 +source: [] +build: + number: ~ + script: "[ ... script ... ]" +requirements: + build: + - zig + host: [] + run: + - boltons + run_constraints: [] +tests: [] +about: + homepage: ~ + license: ~ + license_file: ~ + summary: ~ + description: ~ + documentation: ~ + repository: ~ +extra: ~ diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_not_added_if_already_present.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_not_added_if_already_present.snap new file mode 100644 index 00000000..26021008 --- /dev/null +++ b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_not_added_if_already_present.snap @@ -0,0 +1,29 @@ +--- +source: crates/pixi-build-zig/src/main.rs +expression: generated_recipe.recipe +--- +context: {} +package: + name: foobar + version: 0.1.0 +source: [] +build: + number: ~ + script: "[ ... script ... ]" +requirements: + build: + - zig + host: [] + run: + - boltons + run_constraints: [] +tests: [] +about: + homepage: ~ + license: ~ + license_file: ~ + summary: ~ + description: ~ + documentation: ~ + repository: ~ +extra: ~ diff --git a/docs/backends/pixi-build-zig.md b/docs/backends/pixi-build-zig.md new file mode 100644 index 00000000..bbcf01e3 --- /dev/null +++ b/docs/backends/pixi-build-zig.md @@ -0,0 +1,252 @@ +# pixi-build-zig + +The `pixi-build-zig` backend is designed for building Zig projects using the [Zig build system](https://ziglang.org/learn/build-system/). It provides seamless integration with Pixi's package management workflow while maintaining cross-platform compatibility. + +!!! warning + `pixi-build` is a preview feature, and will change until it is stabilized. + This is why we require users to opt in to that feature by adding "pixi-build" to `workspace.preview`. + + ```toml + [workspace] + preview = ["pixi-build"] + ``` + + +## Overview + +This backend automatically generates conda packages from Zig projects by: + +- **Using Zig Build**: Leverages Zig's native build system for compilation and installation +- **Cross-platform support**: Works consistently across Linux, macOS, and Windows +- **Standard installation**: Uses `zig build --prefix` to install artifacts to the conda prefix +- **Flexible configuration**: Supports custom build arguments and environment variables + +## Basic Usage + +To use the Zig backend in your `pixi.toml`, add it to your package's build configuration: + +```toml +[package] +name = "zig_package" +version = "0.1.0" + +[package.build] +backend = { name = "pixi-build-zig", version = "*" } +channels = ["https://prefix.dev/conda-forge"] +``` + +### build.zig Requirements + +Your Zig project must use `b.installArtifact()` in `build.zig` to mark which artifacts should be installed. The Zig build system will automatically place them in the correct locations: + +- **Executables** → `$PREFIX/bin/` +- **Libraries** → `$PREFIX/lib/` +- **Headers** → `$PREFIX/include/` + +Example `build.zig`: + +```zig +pub fn build(b: *std.Build) void { + const exe = b.addExecutable(.{ + .name = "my-tool", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + }), + }); + + // This marks the executable for installation + b.installArtifact(exe); +} +``` + +### Required Dependencies + +The backend automatically includes the following build tools: + +- `zig` - The Zig compiler and build system + +The backend will automatically add `zig` to your build dependencies if it's not already specified. You can add it explicitly if you need a specific version: + +```toml +[package.build-dependencies] +zig = ">=0.14.0,<0.14" +``` + +## Configuration Options + +You can customize the Zig backend behavior using the `[package.build.config]` section in your `pixi.toml`. The backend supports the following configuration options: + +### `extra-args` + +- **Type**: `Array` +- **Default**: `[]` +- **Target Merge Behavior**: `Overwrite` - Platform-specific arguments completely replace base arguments + +Additional command-line arguments to pass to the `zig build` command. These arguments are appended to the build command. + +```toml +[package.build.config] +extra-args = [ + "-Doptimize=ReleaseFast", + "-Dtarget=x86_64-linux-gnu" +] +``` + +For target-specific configuration, platform arguments completely replace the base configuration: + +```toml +[package.build.config] +extra-args = ["-Doptimize=Debug"] + +[package.build.config.targets.linux-64] +extra-args = ["-Doptimize=ReleaseFast", "-Dtarget=x86_64-linux-gnu"] +# Result for linux-64: ["-Doptimize=ReleaseFast", "-Dtarget=x86_64-linux-gnu"] +``` + +### `env` + +- **Type**: `Map` +- **Default**: `{}` +- **Target Merge Behavior**: `Merge` - Platform environment variables override base variables with same name, others are merged + +Environment variables to set during the build process. These variables are available during compilation. + +```toml +[package.build.config] +env = { ZIG_GLOBAL_CACHE_DIR = "/tmp/zig-cache", CUSTOM_VAR = "value" } +``` + +For target-specific configuration, platform environment variables are merged with base variables: + +```toml +[package.build.config] +env = { COMMON_VAR = "base", ZIG_LOCAL_CACHE_DIR = ".zig-cache" } + +[package.build.config.targets.linux-64] +env = { COMMON_VAR = "linux", ZIG_SYSTEM_LINKER_HACK = "1" } +# Result for linux-64: { COMMON_VAR = "linux", ZIG_LOCAL_CACHE_DIR = ".zig-cache", ZIG_SYSTEM_LINKER_HACK = "1" } +``` + +### `debug-dir` + +- **Type**: `String` (path) +- **Default**: Not set +- **Target Merge Behavior**: Not allowed - Cannot have target specific value + +If specified, internal build state and debug information will be written to this directory. Useful for troubleshooting build issues. + +```toml +[package.build.config] +debug-dir = ".build-debug" +``` + +### `extra-input-globs` + +- **Type**: `Array` +- **Default**: `[]` +- **Target Merge Behavior**: `Overwrite` - Platform-specific globs completely replace base globs + +Additional glob patterns to include as input files for the build process. These patterns are added to the default input globs that include Zig source files (`**/*.zig`), build configuration files (`build.zig`, `build.zig.zon`), and other build-related files. + +```toml +[package.build.config] +extra-input-globs = [ + "assets/**/*", + "data/*.json", + "*.md" +] +``` + +For target-specific configuration, platform-specific globs completely replace the base: + +```toml +[package.build.config] +extra-input-globs = ["*.txt"] + +[package.build.config.targets.linux-64] +extra-input-globs = ["*.txt", "*.so", "linux-configs/**/*"] +# Result for linux-64: ["*.txt", "*.so", "linux-configs/**/*"] +``` + +## Build Process + +The Zig backend follows this build process: + +1. **Environment Setup**: Configures environment variables if specified in the configuration +2. **Build and Install**: Executes `zig build` with the following behavior: + - `--prefix $PREFIX` (Unix/macOS) or `--prefix %LIBRARY_PREFIX%` (Windows): Install to the correct conda package location + - Additional arguments from `extra-args` configuration +3. **Artifact Installation**: The Zig build system automatically places installed artifacts in the correct subdirectories based on their type + +### Windows Considerations + +On Windows, the backend uses `%LIBRARY_PREFIX%` instead of `%PREFIX%` to follow conda's convention for Unix-style packages. This means your artifacts will be installed to: + +- **Executables** → `%LIBRARY_PREFIX%\bin\` (i.e., `%PREFIX%\Library\bin\`) +- **Libraries** → `%LIBRARY_PREFIX%\lib\` (i.e., `%PREFIX%\Library\lib\`) +- **Headers** → `%LIBRARY_PREFIX%\include\` (i.e., `%PREFIX%\Library\include\`) + +## Accessing Host Dependencies + +If your Zig project depends on C libraries from conda (e.g., SDL3, OpenSSL), you need to configure `build.zig` to find them. During the build, dependencies are available in the `$PREFIX` environment variable: + +```zig +fn getLibraryAndIncludePath(b: *std.Build) ?EnvPaths { + // During build (via pixi-build-zig backend), PREFIX points to host dependencies + // During development (via pixi shell), CONDA_PREFIX points to the environment + const env_var = if (std.process.getEnvVarOwned(b.allocator, "PREFIX")) |prefix| + prefix + else |_| blk: { + break :blk std.process.getEnvVarOwned(b.allocator, "CONDA_PREFIX") catch { + return null; + }; + }; + defer b.allocator.free(env_var); + + const include_subdir = if (builtin.os.tag == .windows) "Library/include" else "include"; + const lib_subdir = if (builtin.os.tag == .windows) "Library/lib" else "lib"; + + const include_path = std.fs.path.join(b.allocator, &.{ env_var, include_subdir }) catch return null; + const lib_path = std.fs.path.join(b.allocator, &.{ env_var, lib_subdir }) catch return null; + + return EnvPaths{ + .includePath = include_path, + .libraryPath = lib_path, + }; +} +``` + +## Example Configuration + +Here's a complete example for a Zig project with SDL3: + +```toml +[package] +name = "zig-sdl-game" +version = "0.1.0" + +[package.build] +backend = { name = "pixi-build-zig", version = "*" } +channels = ["https://prefix.dev/conda-forge"] + +[package.build.config] +extra-args = ["-Doptimize=ReleaseFast"] + +[package.host-dependencies] +sdl3 = ">=3.0.0,<4" + +[package.run-dependencies] +sdl3 = ">=3.0.0,<4" +``` + +## Limitations + +- Requires Zig projects to use `b.installArtifact()` in `build.zig` (standard practice) +- No automatic metadata extraction from `build.zig.zon` (must be specified in `pixi.toml`) +- The backend runs `zig build` from the project root directory + +## See Also + +- [Zig Build System Documentation](https://ziglang.org/learn/build-system/) - Official Zig build system guide +- [Zig Documentation](https://ziglang.org/documentation/master/) - Official Zig documentation +- [zig.guide](https://zig.guide/) - Community-maintained Zig learning resource diff --git a/mkdocs.yml b/mkdocs.yml index 4135711d..866b1a80 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -107,6 +107,7 @@ nav: - pixi-build-rattler-build: backends/pixi-build-rattler-build.md - pixi-build-ros: backends/pixi-build-ros.md - pixi-build-rust: backends/pixi-build-rust.md + - pixi-build-zig: backends/pixi-build-zig.md - pixi-build-mojo: backends/pixi-build-mojo.md - Key Concepts: - Compilers: key_concepts/compilers.md diff --git a/pixi.toml b/pixi.toml index 1412e3ff..0e218ed4 100644 --- a/pixi.toml +++ b/pixi.toml @@ -28,6 +28,7 @@ install-pixi-build-cmake = { cmd = "cargo install --path crates/pixi-build-cmake install-pixi-build-rattler-build = { cmd = "cargo install --path crates/pixi-build-rattler-build --locked --force" } install-pixi-build-rust = { cmd = "cargo install --path crates/pixi-build-rust --locked --force" } install-pixi-build-mojo = { cmd = "cargo install --path crates/pixi-build-mojo --locked --force" } +install-pixi-build-zig = { cmd = "cargo install --path crates/pixi-build-zig --locked --force" } install-pixi-build-ros = { cmd = "pixi global install --force-reinstall --path backends/pixi-build-ros --channel https://prefix.dev/pixi-build-backends --channel https://prefix.dev/conda-forge" } install-pixi-backends = { depends-on = [ "install-pixi-build-python", @@ -35,6 +36,7 @@ install-pixi-backends = { depends-on = [ "install-pixi-build-rattler-build", "install-pixi-build-rust", "install-pixi-build-mojo", + "install-pixi-build-zig", "install-pixi-build-ros", ] } From 2e7935f5e8f0866f2da3cabd704f71567637cb2b Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Mon, 13 Oct 2025 16:46:57 +0200 Subject: [PATCH 2/5] feat: add to index --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 3869f2a9..519103ca 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,6 +33,7 @@ The repository currently provides the following specialized build backends: | [**`pixi-build-ros`**](./backends/pixi-build-ros.md) | ROS (Robot Operating System) packages | | [**`pixi-build-rust`**](./backends/pixi-build-rust.md) | Cargo-based Rust applications and libraries | | [**`pixi-build-mojo`**](./backends/pixi-build-mojo.md) | Mojo applications and packages | +| [**`pixi-build-zig`**](./backends/pixi-build-zig.md) | Zig (with build.zig) applications and libraries | All backends are available through the [prefix.dev/conda-forge](https://prefix.dev/channels/conda-forge) conda channel and work across multiple platforms (Linux, macOS, Windows). For the latest backend versions, you can extend the channel list with the [prefix.dev/pixi-build-backends](https://prefix.dev/channels/pixi-build-backends) conda channel, here we push the latest versions of the backends. From fae340e92c7476365c5727b8147db0d8137259be Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 14 Oct 2025 13:36:36 +0200 Subject: [PATCH 3/5] Cargo clippy --- crates/pixi-build-zig/src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/pixi-build-zig/src/main.rs b/crates/pixi-build-zig/src/main.rs index 9480c5c1..fcfe21ef 100644 --- a/crates/pixi-build-zig/src/main.rs +++ b/crates/pixi-build-zig/src/main.rs @@ -141,8 +141,7 @@ mod tests { for extra_glob in &config.extra_input_globs { assert!( result.contains(extra_glob), - "Result should contain extra glob: {}", - extra_glob + "Result should contain extra glob: {extra_glob}" ); } From 725720a220dd16b5cf1a696003359bdb666df9e5 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 14 Oct 2025 13:46:06 +0200 Subject: [PATCH 4/5] Move to inline snapshots --- crates/pixi-build-zig/src/main.rs | 63 ++++++++++++++++++- ...xi_build_zig__tests__env_vars_are_set.snap | 8 --- ...__tests__zig_is_in_build_requirements.snap | 29 --------- ...__zig_is_not_added_if_already_present.snap | 29 --------- 4 files changed, 60 insertions(+), 69 deletions(-) delete mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__env_vars_are_set.snap delete mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_in_build_requirements.snap delete mode 100644 crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_not_added_if_already_present.snap diff --git a/crates/pixi-build-zig/src/main.rs b/crates/pixi-build-zig/src/main.rs index fcfe21ef..3afa7453 100644 --- a/crates/pixi-build-zig/src/main.rs +++ b/crates/pixi-build-zig/src/main.rs @@ -183,7 +183,33 @@ mod tests { insta::assert_yaml_snapshot!(generated_recipe.recipe, { ".source[0].path" => "[ ... path ... ]", ".build.script" => "[ ... script ... ]", - }); + }, @r#" + context: {} + package: + name: foobar + version: 0.1.0 + source: [] + build: + number: ~ + script: "[ ... script ... ]" + requirements: + build: + - zig + host: [] + run: + - boltons + run_constraints: [] + tests: [] + about: + homepage: ~ + license: ~ + license_file: ~ + summary: ~ + description: ~ + documentation: ~ + repository: ~ + extra: ~ + "#); } #[test] @@ -225,7 +251,33 @@ mod tests { insta::assert_yaml_snapshot!(generated_recipe.recipe, { ".source[0].path" => "[ ... path ... ]", ".build.script" => "[ ... script ... ]", - }); + }, @r#" + context: {} + package: + name: foobar + version: 0.1.0 + source: [] + build: + number: ~ + script: "[ ... script ... ]" + requirements: + build: + - zig + host: [] + run: + - boltons + run_constraints: [] + tests: [] + about: + homepage: ~ + license: ~ + license_file: ~ + summary: ~ + description: ~ + documentation: ~ + repository: ~ + extra: ~ + "#); } #[test] @@ -265,6 +317,11 @@ mod tests { insta::assert_yaml_snapshot!(generated_recipe.recipe.build.script, { ".content" => "[ ... script ... ]", - }); + }, @r#" + content: "[ ... script ... ]" + env: + foo: bar + secrets: [] + "#); } } diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__env_vars_are_set.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__env_vars_are_set.snap deleted file mode 100644 index 0d8e3bed..00000000 --- a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__env_vars_are_set.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/pixi-build-zig/src/main.rs -expression: generated_recipe.recipe.build.script ---- -content: "[ ... script ... ]" -env: - foo: bar -secrets: [] diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_in_build_requirements.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_in_build_requirements.snap deleted file mode 100644 index 26021008..00000000 --- a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_in_build_requirements.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: crates/pixi-build-zig/src/main.rs -expression: generated_recipe.recipe ---- -context: {} -package: - name: foobar - version: 0.1.0 -source: [] -build: - number: ~ - script: "[ ... script ... ]" -requirements: - build: - - zig - host: [] - run: - - boltons - run_constraints: [] -tests: [] -about: - homepage: ~ - license: ~ - license_file: ~ - summary: ~ - description: ~ - documentation: ~ - repository: ~ -extra: ~ diff --git a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_not_added_if_already_present.snap b/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_not_added_if_already_present.snap deleted file mode 100644 index 26021008..00000000 --- a/crates/pixi-build-zig/src/snapshots/pixi_build_zig__tests__zig_is_not_added_if_already_present.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: crates/pixi-build-zig/src/main.rs -expression: generated_recipe.recipe ---- -context: {} -package: - name: foobar - version: 0.1.0 -source: [] -build: - number: ~ - script: "[ ... script ... ]" -requirements: - build: - - zig - host: [] - run: - - boltons - run_constraints: [] -tests: [] -about: - homepage: ~ - license: ~ - license_file: ~ - summary: ~ - description: ~ - documentation: ~ - repository: ~ -extra: ~ From a2ea0d1cd868c9af76332605f935e03e358fccb3 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Tue, 14 Oct 2025 19:30:59 +0200 Subject: [PATCH 5/5] feat: add zig build recipe --- recipe/pixi-build-zig/recipe.yaml | 65 +++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 recipe/pixi-build-zig/recipe.yaml diff --git a/recipe/pixi-build-zig/recipe.yaml b/recipe/pixi-build-zig/recipe.yaml new file mode 100644 index 00000000..e7b09082 --- /dev/null +++ b/recipe/pixi-build-zig/recipe.yaml @@ -0,0 +1,65 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json +context: + name: pixi-build-zig + version: "${{ env.get('PIXI_BUILD_RUST_VERSION', default='0.1.0dev') }}" + +package: + name: ${{ name }} + version: ${{ version }} + +source: + path: ../.. + +build: + script: + env: + CARGO_PROFILE_RELEASE_STRIP: symbols + CARGO_PROFILE_RELEASE_LTO: fat + content: + - if: osx and x86_64 + then: + # use the default linker for osx-64 as we are hitting a bug with the conda-forge linker + # https://github.com/rust-lang/rust/issues/140686 + - unset CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER + + - if: unix + then: + - export OPENSSL_DIR="$PREFIX" + - cargo auditable install --locked --no-track --bins --root ${{ PREFIX }} --path crates/${{name}} + - cargo-bundle-licenses --format yaml --output ./THIRDPARTY.yml + files: + - bin/${{ name }} + - bin/${{ name }}.exe + +requirements: + build: + - ${{ compiler("rust") }} + - ${{ stdlib("c") }} + - cargo-bundle-licenses + - cargo-auditable + host: + - pkg-config + - libzlib + - liblzma + - if: unix + then: openssl + run: + - pixi-build-api-version >=2,<3 + +tests: + - script: ${{ name }} --help + - package_contents: + bin: + - ${{ name }} + +about: + homepage: https://github.com/prefix-dev/pixi-build-backends + summary: A pixi build backend to build Rust packages. + description: | + This package provides a build backend for pixi that allows building Rust packages. + license: BSD-3-Clause + license_file: + - LICENSE + - THIRDPARTY.yml + documentation: https://prefix-dev.github.io/pixi-build-backends + repository: https://github.com/prefix-dev/pixi-build-backends