From a1ef5a9d2d34dbc6c527666d4605ae6e5fbf8f93 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Thu, 24 Jul 2025 21:16:17 +0000 Subject: [PATCH 01/22] feat: first pass at simple pass through build backend --- Cargo.lock | 24 ++ crates/pixi-build-mojo/Cargo.toml | 29 ++ crates/pixi-build-mojo/src/build_script.j2 | 3 + crates/pixi-build-mojo/src/build_script.rs | 30 ++ crates/pixi-build-mojo/src/config.rs | 35 ++ crates/pixi-build-mojo/src/main.rs | 307 ++++++++++++++++++ ...ild_mojo__tests__env_vars_are_set.snap.new | 9 + ..._input_globs_includes_extra_globs.snap.new | 8 + ...sts__max_is_in_build_requirements.snap.new | 25 ++ ...t_added_if_max_is_already_present.snap.new | 25 ++ 10 files changed, 495 insertions(+) create mode 100644 crates/pixi-build-mojo/Cargo.toml create mode 100644 crates/pixi-build-mojo/src/build_script.j2 create mode 100644 crates/pixi-build-mojo/src/build_script.rs create mode 100644 crates/pixi-build-mojo/src/config.rs create mode 100644 crates/pixi-build-mojo/src/main.rs create mode 100644 crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap.new create mode 100644 crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap.new create mode 100644 crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap.new create mode 100644 crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap.new diff --git a/Cargo.lock b/Cargo.lock index 6bd41d3f..e214a92d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3979,6 +3979,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "pixi-build-mojo" +version = "0.2.0" +dependencies = [ + "async-trait", + "chrono", + "indexmap 2.9.0", + "insta", + "miette", + "minijinja", + "pixi-build-backend", + "pixi_build_type_conversions", + "pixi_build_types", + "pixi_manifest", + "rattler-build", + "rattler_conda_types", + "rattler_package_streaming", + "recipe-stage0", + "serde", + "serde_json", + "tempfile", + "tokio", +] + [[package]] name = "pixi-build-python" version = "0.3.0" diff --git a/crates/pixi-build-mojo/Cargo.toml b/crates/pixi-build-mojo/Cargo.toml new file mode 100644 index 00000000..17fe65ef --- /dev/null +++ b/crates/pixi-build-mojo/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "pixi-build-mojo" +version = "0.2.0" +edition.workspace = true + +[dependencies] +async-trait = { workspace = true } +chrono = { workspace = true } +indexmap = { workspace = true } +miette = { workspace = true } +minijinja = { workspace = true } +rattler_conda_types = { workspace = true } +rattler_package_streaming = { workspace = true } +rattler-build = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["macros"] } + +pixi-build-backend = { workspace = true } + +pixi_build_types = { workspace = true } +pixi_manifest = { workspace = true } +pixi_build_type_conversions = { workspace = true } + +recipe-stage0 = { workspace = true } + +[dev-dependencies] +insta = { version = "1.42.1", features = ["yaml", "redactions", "filters"] } diff --git a/crates/pixi-build-mojo/src/build_script.j2 b/crates/pixi-build-mojo/src/build_script.j2 new file mode 100644 index 00000000..a0750edd --- /dev/null +++ b/crates/pixi-build-mojo/src/build_script.j2 @@ -0,0 +1,3 @@ +mojo --version + +mojo {{ extra_args | join(" ") }} diff --git a/crates/pixi-build-mojo/src/build_script.rs b/crates/pixi-build-mojo/src/build_script.rs new file mode 100644 index 00000000..23ca035b --- /dev/null +++ b/crates/pixi-build-mojo/src/build_script.rs @@ -0,0 +1,30 @@ +use minijinja::Environment; +use serde::Serialize; + +#[derive(Serialize)] +pub struct BuildScriptContext { + pub build_platform: BuildPlatform, + pub source_dir: String, + pub extra_args: Vec, + /// The package has a host dependency on Python. + /// This is used to determine if the build script + /// should include Python-related logic. + pub has_host_python: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum BuildPlatform { + Unix, +} + +impl BuildScriptContext { + pub fn render(&self) -> Vec { + let env = Environment::new(); + let template = env + .template_from_str(include_str!("build_script.j2")) + .unwrap(); + let rendered = template.render(self).unwrap().to_string(); + rendered.lines().map(|s| s.to_string()).collect() + } +} diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs new file mode 100644 index 00000000..1dcc8917 --- /dev/null +++ b/crates/pixi-build-mojo/src/config.rs @@ -0,0 +1,35 @@ +use std::path::Path; + +use indexmap::IndexMap; +use pixi_build_backend::generated_recipe::BackendConfig; +use serde::Deserialize; + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct MojoBackendConfig { + /// Extra args for mojo invocation + #[serde(default)] + pub extra_args: Vec, + /// Environment Variables + #[serde(default)] + pub env: IndexMap, +} + +impl BackendConfig for MojoBackendConfig { + fn debug_dir(&self) -> Option<&Path> { + None + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::MojoBackendConfig; + + #[test] + fn test_ensure_deseralize_from_empty() { + let json_data = json!({}); + serde_json::from_value::(json_data).unwrap(); + } +} diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs new file mode 100644 index 00000000..35c10890 --- /dev/null +++ b/crates/pixi-build-mojo/src/main.rs @@ -0,0 +1,307 @@ +mod build_script; +mod config; + +use std::{collections::BTreeMap, path::Path}; + +use build_script::{BuildPlatform, BuildScriptContext}; +use config::MojoBackendConfig; +use miette::{Error, IntoDiagnostic}; +use pixi_build_backend::{ + generated_recipe::{GenerateRecipe, GeneratedRecipe, PythonParams}, + intermediate_backend::IntermediateBackendInstantiator, +}; +use rattler_build::{recipe::variable::Variable, NormalizedKey}; +use rattler_conda_types::{PackageName, Platform}; +use recipe_stage0::recipe::Script; + +#[derive(Default, Clone)] +pub struct MojoGenerator {} + +impl GenerateRecipe for MojoGenerator { + type Config = MojoBackendConfig; + + 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, + ) -> miette::Result { + let mut generated_recipe = + GeneratedRecipe::from_model(model.clone(), manifest_root.clone()); + + // we need to add compilers + let requirements = &mut generated_recipe.recipe.requirements; + let resolved_requirements = requirements.resolve(Some(&host_platform)); + + // Ensure the compiler function is added to the build requirements + // only if a specific compiler is not already present. + let mojo_compiler_pkg = "max".to_string(); + + let build_platform = Platform::current(); + + if !resolved_requirements + .build + .contains_key(&PackageName::new_unchecked(&mojo_compiler_pkg)) + { + requirements + .build + .push(mojo_compiler_pkg.parse().into_diagnostic()?); + } + + // Check if the host platform has a host python dependency + // TODO: surely this will be needed for compiling bindings or something? or maybe those + // will be handled by uv? + let has_host_python = resolved_requirements.contains(&PackageName::new_unchecked("python")); + + let build_script = BuildScriptContext { + build_platform: if build_platform.is_windows() { + return Err(Error::msg( + "Windows is not a supported build platform for mojo", + )); + } else { + BuildPlatform::Unix + }, + source_dir: manifest_root.display().to_string(), + extra_args: config.extra_args.clone(), + has_host_python, + } + .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( + _config: &Self::Config, + _workdir: impl AsRef, + _editable: bool, + ) -> Vec { + [ + // Source files + "**/*.{mojo,🔥}", + ] + .iter() + .map(|s: &&str| s.to_string()) + // May want a special area for defining includes? + .collect() + } + + fn default_variants(&self, _host_platform: Platform) -> BTreeMap> { + BTreeMap::new() + } +} + +#[tokio::main] +pub async fn main() { + if let Err(err) = + pixi_build_backend::cli::main(IntermediateBackendInstantiator::::new).await + { + eprintln!("{err:?}"); + std::process::exit(1); + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use indexmap::IndexMap; + use pixi_build_types::ProjectModelV1; + + use super::*; + + #[test] + fn test_input_globs_includes_extra_globs() { + let config = MojoBackendConfig { + ..Default::default() + }; + + let result = MojoGenerator::extract_input_globs_from_build(&config, PathBuf::new(), false); + + insta::assert_debug_snapshot!(result); + } + + #[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_max_is_in_build_requirements() { + let project_model = project_fixture!({ + "name": "foobar", + "version": "0.1.0", + "targets": { + "defaultTarget": { + "runDependencies": { + "boltons": { + "binary": { + "version": "*" + } + } + } + }, + } + }); + + let generated_recipe = MojoGenerator::default() + .generate_recipe( + &project_model, + &MojoBackendConfig::default(), + PathBuf::from("."), + Platform::Linux64, + None, + ) + .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 = MojoGenerator::default() + .generate_recipe( + &project_model, + &MojoBackendConfig { + env: env.clone(), + ..Default::default() + }, + PathBuf::from("."), + Platform::Linux64, + None, + ) + .expect("Failed to generate recipe"); + + insta::assert_yaml_snapshot!(generated_recipe.recipe.build.script, + { + ".content" => "[ ... script ... ]", + }); + } + + #[test] + fn test_has_python_is_set_in_build_script() { + let project_model = project_fixture!({ + "name": "foobar", + "version": "0.1.0", + "targets": { + "defaultTarget": { + "runDependencies": { + "boltons": { + "binary": { + "version": "*" + } + } + }, + "hostDependencies": { + "python": { + "binary": { + "version": "*" + } + } + } + }, + } + }); + + let generated_recipe = MojoGenerator::default() + .generate_recipe( + &project_model, + &MojoBackendConfig::default(), + PathBuf::from("."), + Platform::Linux64, + None, + ) + .expect("Failed to generate recipe"); + + // we want to check that + // -DPython_EXECUTABLE=$PYTHON is set in the build script + insta::assert_yaml_snapshot!(generated_recipe.recipe.build, + + { + ".script.content" => insta::dynamic_redaction(|value, _path| { + dbg!(&value); + // assert that the value looks like a uuid here + assert!(value + .as_slice() + .unwrap() + .iter() + .any(|c| c.as_str().unwrap().contains("-DPython_EXECUTABLE")) + ); + "[content]" + }) + }); + } + + #[test] + fn test_max_is_not_added_if_max_is_already_present() { + let project_model = project_fixture!({ + "name": "foobar", + "version": "0.1.0", + "targets": { + "defaultTarget": { + "runDependencies": { + "boltons": { + "binary": { + "version": "*" + } + } + }, + "buildDependencies": { + "max": { + "binary": { + "version": "*" + } + } + } + }, + } + }); + + let generated_recipe = MojoGenerator::default() + .generate_recipe( + &project_model, + &MojoBackendConfig::default(), + PathBuf::from("."), + Platform::Linux64, + None, + ) + .expect("Failed to generate recipe"); + + insta::assert_yaml_snapshot!(generated_recipe.recipe, { + ".source[0].path" => "[ ... path ... ]", + ".build.script" => "[ ... script ... ]", + }); + } +} diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap.new b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap.new new file mode 100644 index 00000000..a56b3305 --- /dev/null +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap.new @@ -0,0 +1,9 @@ +--- +source: crates/pixi-build-mojo/src/main.rs +assertion_line: 207 +expression: generated_recipe.recipe.build.script +--- +content: "[ ... script ... ]" +env: + foo: bar +secrets: [] diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap.new b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap.new new file mode 100644 index 00000000..4c82a4fc --- /dev/null +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap.new @@ -0,0 +1,8 @@ +--- +source: crates/pixi-build-mojo/src/main.rs +assertion_line: 128 +expression: result +--- +[ + "**/*.{mojo,🔥}", +] diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap.new b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap.new new file mode 100644 index 00000000..323415f1 --- /dev/null +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap.new @@ -0,0 +1,25 @@ +--- +source: crates/pixi-build-mojo/src/main.rs +assertion_line: 168 +expression: generated_recipe.recipe +--- +context: {} +package: + name: foobar + version: 0.1.0 +source: + - path: "[ ... path ... ]" + sha256: ~ +build: + number: ~ + script: "[ ... script ... ]" +requirements: + build: + - max + host: [] + run: + - boltons + run_constraints: [] +tests: [] +about: ~ +extra: ~ diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap.new b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap.new new file mode 100644 index 00000000..2ba37ef3 --- /dev/null +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap.new @@ -0,0 +1,25 @@ +--- +source: crates/pixi-build-mojo/src/main.rs +assertion_line: 302 +expression: generated_recipe.recipe +--- +context: {} +package: + name: foobar + version: 0.1.0 +source: + - path: "[ ... path ... ]" + sha256: ~ +build: + number: ~ + script: "[ ... script ... ]" +requirements: + build: + - max + host: [] + run: + - boltons + run_constraints: [] +tests: [] +about: ~ +extra: ~ From e343d8c509ae32979ff59ee1fbee44fdd81e8173 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Fri, 25 Jul 2025 14:49:18 +0000 Subject: [PATCH 02/22] feat: minimal working example --- crates/pixi-build-mojo/Cargo.toml | 4 + crates/pixi-build-mojo/src/config.rs | 5 +- crates/pixi-build-mojo/src/main.rs | 112 +++++++++--------- ..._build_mojo__tests__env_vars_are_set.snap} | 1 - ...ts__input_globs_includes_extra_globs.snap} | 1 - ..._tests__max_is_in_build_requirements.snap} | 1 - ..._not_added_if_max_is_already_present.snap} | 1 - pixi.toml | 2 + 8 files changed, 67 insertions(+), 60 deletions(-) rename crates/pixi-build-mojo/src/snapshots/{pixi_build_mojo__tests__env_vars_are_set.snap.new => pixi_build_mojo__tests__env_vars_are_set.snap} (88%) rename crates/pixi-build-mojo/src/snapshots/{pixi_build_mojo__tests__input_globs_includes_extra_globs.snap.new => pixi_build_mojo__tests__input_globs_includes_extra_globs.snap} (83%) rename crates/pixi-build-mojo/src/snapshots/{pixi_build_mojo__tests__max_is_in_build_requirements.snap.new => pixi_build_mojo__tests__max_is_in_build_requirements.snap} (94%) rename crates/pixi-build-mojo/src/snapshots/{pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap.new => pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap} (94%) diff --git a/crates/pixi-build-mojo/Cargo.toml b/crates/pixi-build-mojo/Cargo.toml index 17fe65ef..2dd5449c 100644 --- a/crates/pixi-build-mojo/Cargo.toml +++ b/crates/pixi-build-mojo/Cargo.toml @@ -3,6 +3,10 @@ name = "pixi-build-mojo" version = "0.2.0" edition.workspace = true +[profile.dev.package] +insta.opt-level = 3 +similar.opt-level = 3 + [dependencies] async-trait = { workspace = true } chrono = { workspace = true } diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 1dcc8917..2f985046 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use indexmap::IndexMap; use pixi_build_backend::generated_recipe::BackendConfig; @@ -13,11 +13,12 @@ pub struct MojoBackendConfig { /// Environment Variables #[serde(default)] pub env: IndexMap, + pub debug_dir: Option, } impl BackendConfig for MojoBackendConfig { fn debug_dir(&self) -> Option<&Path> { - None + self.debug_dir.as_deref() } } diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 35c10890..a01992e7 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -49,6 +49,7 @@ impl GenerateRecipe for MojoGenerator { .build .push(mojo_compiler_pkg.parse().into_diagnostic()?); } + eprintln!("resolved compiler"); // Check if the host platform has a host python dependency // TODO: surely this will be needed for compiling bindings or something? or maybe those @@ -68,12 +69,14 @@ impl GenerateRecipe for MojoGenerator { has_host_python, } .render(); + eprintln!("rendered build script"); generated_recipe.recipe.build.script = Script { content: build_script, env: config.env.clone(), ..Default::default() }; + eprintln!("Recipe script created"); Ok(generated_recipe) } @@ -85,7 +88,7 @@ impl GenerateRecipe for MojoGenerator { ) -> Vec { [ // Source files - "**/*.{mojo,🔥}", + "**/*.mojo", ] .iter() .map(|s: &&str| s.to_string()) @@ -210,59 +213,60 @@ mod tests { }); } - #[test] - fn test_has_python_is_set_in_build_script() { - let project_model = project_fixture!({ - "name": "foobar", - "version": "0.1.0", - "targets": { - "defaultTarget": { - "runDependencies": { - "boltons": { - "binary": { - "version": "*" - } - } - }, - "hostDependencies": { - "python": { - "binary": { - "version": "*" - } - } - } - }, - } - }); - - let generated_recipe = MojoGenerator::default() - .generate_recipe( - &project_model, - &MojoBackendConfig::default(), - PathBuf::from("."), - Platform::Linux64, - None, - ) - .expect("Failed to generate recipe"); - - // we want to check that - // -DPython_EXECUTABLE=$PYTHON is set in the build script - insta::assert_yaml_snapshot!(generated_recipe.recipe.build, - - { - ".script.content" => insta::dynamic_redaction(|value, _path| { - dbg!(&value); - // assert that the value looks like a uuid here - assert!(value - .as_slice() - .unwrap() - .iter() - .any(|c| c.as_str().unwrap().contains("-DPython_EXECUTABLE")) - ); - "[content]" - }) - }); - } + // I think we'll want this back at some point + // #[test] + // fn test_has_python_is_set_in_build_script() { + // let project_model = project_fixture!({ + // "name": "foobar", + // "version": "0.1.0", + // "targets": { + // "defaultTarget": { + // "runDependencies": { + // "boltons": { + // "binary": { + // "version": "*" + // } + // } + // }, + // "hostDependencies": { + // "python": { + // "binary": { + // "version": "*" + // } + // } + // } + // }, + // } + // }); + // + // let generated_recipe = MojoGenerator::default() + // .generate_recipe( + // &project_model, + // &MojoBackendConfig::default(), + // PathBuf::from("."), + // Platform::Linux64, + // None, + // ) + // .expect("Failed to generate recipe"); + // + // // we want to check that + // // -DPython_EXECUTABLE=$PYTHON is set in the build script + // insta::assert_yaml_snapshot!(generated_recipe.recipe.build, + // + // { + // ".script.content" => insta::dynamic_redaction(|value, _path| { + // dbg!(&value); + // // assert that the value looks like a uuid here + // assert!(value + // .as_slice() + // .unwrap() + // .iter() + // .any(|c| c.as_str().unwrap().contains("-DPython_EXECUTABLE")) + // ); + // "[content]" + // }) + // }); + // } #[test] fn test_max_is_not_added_if_max_is_already_present() { diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap.new b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap similarity index 88% rename from crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap.new rename to crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap index a56b3305..d8a41120 100644 --- a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap.new +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__env_vars_are_set.snap @@ -1,6 +1,5 @@ --- source: crates/pixi-build-mojo/src/main.rs -assertion_line: 207 expression: generated_recipe.recipe.build.script --- content: "[ ... script ... ]" diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap.new b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap similarity index 83% rename from crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap.new rename to crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap index 4c82a4fc..6c84f252 100644 --- a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap.new +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap @@ -1,6 +1,5 @@ --- source: crates/pixi-build-mojo/src/main.rs -assertion_line: 128 expression: result --- [ diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap.new b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap similarity index 94% rename from crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap.new rename to crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap index 323415f1..1cde0bcc 100644 --- a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap.new +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_in_build_requirements.snap @@ -1,6 +1,5 @@ --- source: crates/pixi-build-mojo/src/main.rs -assertion_line: 168 expression: generated_recipe.recipe --- context: {} diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap.new b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap similarity index 94% rename from crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap.new rename to crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap index 2ba37ef3..1cde0bcc 100644 --- a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap.new +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__max_is_not_added_if_max_is_already_present.snap @@ -1,6 +1,5 @@ --- source: crates/pixi-build-mojo/src/main.rs -assertion_line: 302 expression: generated_recipe.recipe --- context: {} diff --git a/pixi.toml b/pixi.toml index 142bd43a..e7f5ccf4 100644 --- a/pixi.toml +++ b/pixi.toml @@ -21,11 +21,13 @@ install-pixi-build-python = { cmd = "cargo install --path crates/pixi-build-pyth 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" } 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-backends = { depends-on = [ "install-pixi-build-python", "install-pixi-build-cmake", "install-pixi-build-rattler-build", "install-pixi-build-rust", + #"install-pixi-build-mojo", ] } From 4dd9fc320d6bfdd20bf1e990c46165e03b17461a Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Fri, 25 Jul 2025 16:22:59 +0000 Subject: [PATCH 03/22] feat: binary in conda env --- crates/pixi-build-mojo/src/build_script.j2 | 12 +++++++++++- crates/pixi-build-mojo/src/config.rs | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/pixi-build-mojo/src/build_script.j2 b/crates/pixi-build-mojo/src/build_script.j2 index a0750edd..29499220 100644 --- a/crates/pixi-build-mojo/src/build_script.j2 +++ b/crates/pixi-build-mojo/src/build_script.j2 @@ -1,3 +1,13 @@ +{%- set is_cmd_exe = build_platform == "windows" -%} +{%- macro env(key) -%} +{%- if is_cmd_exe %}{{ "%" ~ key ~ "%" }}{% else %}{{ "$" ~key }}{% endif -%} +{% endmacro -%} + +{# - Set up common variables -#} +{%- set build_dir = "build" -%} +{%- set source_dir = env("SRC_DIR") -%} +{%- set library_prefix = "%LIBRARY_PREFIX%" if build_platform == "windows" else "$PREFIX" -%} + mojo --version -mojo {{ extra_args | join(" ") }} +mojo {{ extra_args | join(" ") }} -o {{ library_prefix }}/main diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 2f985046..89ee313e 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -13,6 +13,7 @@ pub struct MojoBackendConfig { /// Environment Variables #[serde(default)] pub env: IndexMap, + pub debug_dir: Option, } From 293c3ebbf2e0fd1b81eccd9623bcdc3a0ae2b749 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Fri, 25 Jul 2025 18:26:33 +0000 Subject: [PATCH 04/22] feat: it works --- crates/pixi-build-mojo/src/build_script.j2 | 17 +++++-- crates/pixi-build-mojo/src/build_script.rs | 16 ++++-- crates/pixi-build-mojo/src/config.rs | 58 ++++++++++++++++++++-- crates/pixi-build-mojo/src/main.rs | 9 ++-- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/crates/pixi-build-mojo/src/build_script.j2 b/crates/pixi-build-mojo/src/build_script.j2 index 29499220..dd467f4c 100644 --- a/crates/pixi-build-mojo/src/build_script.j2 +++ b/crates/pixi-build-mojo/src/build_script.j2 @@ -4,10 +4,21 @@ {% endmacro -%} {# - Set up common variables -#} -{%- set build_dir = "build" -%} -{%- set source_dir = env("SRC_DIR") -%} {%- set library_prefix = "%LIBRARY_PREFIX%" if build_platform == "windows" else "$PREFIX" -%} mojo --version +mkdir -p {{ source_dir }}/{{ dist }} -mojo {{ extra_args | join(" ") }} -o {{ library_prefix }}/main +{#- Build any binaries -#} +{% if bins %} + {% for bin in bins %} + mojo build {{ bin.extra_args | join(" ") }} {{ bin.path }} -o {{ library_prefix }}/{{ bin.name }} + cp {{ library_prefix }}/{{ bin.name }} {{ source_dir }}/{{ dist }} + {% endfor %} +{% endif %} + +{#- Build pkg -#} +{% if pkg %} + mojo package {{ pkg.extra_args | join(" ") }} {{ pkg.path }} -o {{ library_prefix }}/{{ pkg.name}}.mojopkg + cp {{ library_prefix }}/{{ pkg.name }}.mojopkg {{ source_dir }}/{{ dist }} +{% endif %} diff --git a/crates/pixi-build-mojo/src/build_script.rs b/crates/pixi-build-mojo/src/build_script.rs index 23ca035b..2b58c286 100644 --- a/crates/pixi-build-mojo/src/build_script.rs +++ b/crates/pixi-build-mojo/src/build_script.rs @@ -1,18 +1,28 @@ +use super::config::{MojoBinConfig, MojoPkgConfig}; use minijinja::Environment; use serde::Serialize; -#[derive(Serialize)] +#[derive(Debug, Serialize)] pub struct BuildScriptContext { + /// The platform that the build is taking place on. pub build_platform: BuildPlatform, + /// The directory where the source code is located, the manifest root. pub source_dir: String, - pub extra_args: Vec, + /// The directory name to place output artifacts, will be created in `source_dir`. + pub dist: String, + /// Any executable artifacts to create. + pub bins: Option>, + /// Any packages to create. + pub pkg: Option, + + /// Not currenlty used /// The package has a host dependency on Python. /// This is used to determine if the build script /// should include Python-related logic. pub has_host_python: bool, } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "kebab-case")] pub enum BuildPlatform { Unix, diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 89ee313e..a03ddf98 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -2,19 +2,29 @@ use std::path::{Path, PathBuf}; use indexmap::IndexMap; use pixi_build_backend::generated_recipe::BackendConfig; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct MojoBackendConfig { - /// Extra args for mojo invocation - #[serde(default)] - pub extra_args: Vec, /// Environment Variables #[serde(default)] pub env: IndexMap, + /// Directory that will be created to place output artifacts. + /// + /// This is releative to the manifest dir. + #[serde(default = "default_dist_dir")] + pub dist_dir: PathBuf, + + /// Dir that can be specified for outputting pixi debug state. pub debug_dir: Option, + + /// Binary executables to produce. + pub bins: Option>, + + /// Packages to produce. + pub pkg: Option, } impl BackendConfig for MojoBackendConfig { @@ -23,6 +33,44 @@ impl BackendConfig for MojoBackendConfig { } } +impl Default for MojoBackendConfig { + fn default() -> Self { + Self { + env: Default::default(), + dist_dir: default_dist_dir(), + debug_dir: Default::default(), + bins: Default::default(), + pkg: Default::default(), + } + } +} + +fn default_dist_dir() -> PathBuf { + PathBuf::from("target") +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct MojoBinConfig { + pub name: String, + pub path: String, + #[serde(default, rename(serialize = "extra_args"))] + pub extra_args: Option>, + #[serde(default)] + pub env: IndexMap, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct MojoPkgConfig { + pub name: String, + pub path: String, + #[serde(default, rename(serialize = "extra_args"))] + pub extra_args: Option>, + #[serde(default)] + pub env: IndexMap, +} + #[cfg(test)] mod tests { use serde_json::json; diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index a01992e7..cac43b11 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -49,13 +49,14 @@ impl GenerateRecipe for MojoGenerator { .build .push(mojo_compiler_pkg.parse().into_diagnostic()?); } - eprintln!("resolved compiler"); // Check if the host platform has a host python dependency // TODO: surely this will be needed for compiling bindings or something? or maybe those // will be handled by uv? let has_host_python = resolved_requirements.contains(&PackageName::new_unchecked("python")); + // TODO: have different build scripts based on configuration? + let build_script = BuildScriptContext { build_platform: if build_platform.is_windows() { return Err(Error::msg( @@ -65,18 +66,18 @@ impl GenerateRecipe for MojoGenerator { BuildPlatform::Unix }, source_dir: manifest_root.display().to_string(), - extra_args: config.extra_args.clone(), + dist: config.dist_dir.display().to_string(), + bins: config.bins.clone(), + pkg: config.pkg.clone(), has_host_python, } .render(); - eprintln!("rendered build script"); generated_recipe.recipe.build.script = Script { content: build_script, env: config.env.clone(), ..Default::default() }; - eprintln!("Recipe script created"); Ok(generated_recipe) } From 8af4f87f06c97873039cc257f08d8355b32319ed Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Fri, 25 Jul 2025 23:25:19 +0000 Subject: [PATCH 05/22] chore: cleanup --- crates/pixi-build-mojo/src/build_script.rs | 8 -------- crates/pixi-build-mojo/src/config.rs | 10 ++++++++++ crates/pixi-build-mojo/src/main.rs | 18 +++--------------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/crates/pixi-build-mojo/src/build_script.rs b/crates/pixi-build-mojo/src/build_script.rs index 2b58c286..185d8e0b 100644 --- a/crates/pixi-build-mojo/src/build_script.rs +++ b/crates/pixi-build-mojo/src/build_script.rs @@ -4,8 +4,6 @@ use serde::Serialize; #[derive(Debug, Serialize)] pub struct BuildScriptContext { - /// The platform that the build is taking place on. - pub build_platform: BuildPlatform, /// The directory where the source code is located, the manifest root. pub source_dir: String, /// The directory name to place output artifacts, will be created in `source_dir`. @@ -22,12 +20,6 @@ pub struct BuildScriptContext { pub has_host_python: bool, } -#[derive(Debug, Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum BuildPlatform { - Unix, -} - impl BuildScriptContext { pub fn render(&self) -> Vec { let env = Environment::new(); diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index a03ddf98..fc3ca3d6 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -49,24 +49,34 @@ fn default_dist_dir() -> PathBuf { PathBuf::from("target") } +/// Config object for a Mojo binary. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct MojoBinConfig { + /// Name of the binary. pub name: String, + /// Path to file that has the `main` method. pub path: String, + /// Extra args to pass to the compiler. #[serde(default, rename(serialize = "extra_args"))] pub extra_args: Option>, + /// Env vars to set. #[serde(default)] pub env: IndexMap, } +/// Config object for a Mojo package. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct MojoPkgConfig { + /// Name to give the mojo package (.mojopkg suffix will be added). pub name: String, + /// Path to the directory that constitutes the package. pub path: String, + /// Extra args to pass to the compiler. #[serde(default, rename(serialize = "extra_args"))] pub extra_args: Option>, + /// Env vars to set. #[serde(default)] pub env: IndexMap, } diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index cac43b11..383c8db3 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -3,9 +3,9 @@ mod config; use std::{collections::BTreeMap, path::Path}; -use build_script::{BuildPlatform, BuildScriptContext}; +use build_script::BuildScriptContext; use config::MojoBackendConfig; -use miette::{Error, IntoDiagnostic}; +use miette::IntoDiagnostic; use pixi_build_backend::{ generated_recipe::{GenerateRecipe, GeneratedRecipe, PythonParams}, intermediate_backend::IntermediateBackendInstantiator, @@ -39,8 +39,6 @@ impl GenerateRecipe for MojoGenerator { // only if a specific compiler is not already present. let mojo_compiler_pkg = "max".to_string(); - let build_platform = Platform::current(); - if !resolved_requirements .build .contains_key(&PackageName::new_unchecked(&mojo_compiler_pkg)) @@ -55,16 +53,7 @@ impl GenerateRecipe for MojoGenerator { // will be handled by uv? let has_host_python = resolved_requirements.contains(&PackageName::new_unchecked("python")); - // TODO: have different build scripts based on configuration? - let build_script = BuildScriptContext { - build_platform: if build_platform.is_windows() { - return Err(Error::msg( - "Windows is not a supported build platform for mojo", - )); - } else { - BuildPlatform::Unix - }, source_dir: manifest_root.display().to_string(), dist: config.dist_dir.display().to_string(), bins: config.bins.clone(), @@ -89,11 +78,10 @@ impl GenerateRecipe for MojoGenerator { ) -> Vec { [ // Source files - "**/*.mojo", + "**/*.mojo,*.🔥", ] .iter() .map(|s: &&str| s.to_string()) - // May want a special area for defining includes? .collect() } From 6d80d70973154a01eb694f622da981cd2c4045ac Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Sat, 26 Jul 2025 11:05:04 +0000 Subject: [PATCH 06/22] make target dir optional --- crates/pixi-build-mojo/src/build_script.j2 | 17 ++++++++++++----- crates/pixi-build-mojo/src/build_script.rs | 2 +- crates/pixi-build-mojo/src/config.rs | 21 ++------------------- crates/pixi-build-mojo/src/main.rs | 22 +++++++++++++++++----- 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/crates/pixi-build-mojo/src/build_script.j2 b/crates/pixi-build-mojo/src/build_script.j2 index dd467f4c..0762782d 100644 --- a/crates/pixi-build-mojo/src/build_script.j2 +++ b/crates/pixi-build-mojo/src/build_script.j2 @@ -7,18 +7,25 @@ {%- set library_prefix = "%LIBRARY_PREFIX%" if build_platform == "windows" else "$PREFIX" -%} mojo --version -mkdir -p {{ source_dir }}/{{ dist }} + +{% if dist %} + mkdir -p {{ source_dir }}/{{ dist }} +{% endif %} {#- Build any binaries -#} {% if bins %} {% for bin in bins %} - mojo build {{ bin.extra_args | join(" ") }} {{ bin.path }} -o {{ library_prefix }}/{{ bin.name }} - cp {{ library_prefix }}/{{ bin.name }} {{ source_dir }}/{{ dist }} + mojo build {{ bin.extra_args | join(" ") }} {{ bin.path }} -o {{ library_prefix }}/bin/{{ bin.name }} + {% if dist %} + cp {{ library_prefix }}/bin/{{ bin.name }} {{ source_dir }}/{{ dist }} + {% endif %} {% endfor %} {% endif %} {#- Build pkg -#} {% if pkg %} - mojo package {{ pkg.extra_args | join(" ") }} {{ pkg.path }} -o {{ library_prefix }}/{{ pkg.name}}.mojopkg - cp {{ library_prefix }}/{{ pkg.name }}.mojopkg {{ source_dir }}/{{ dist }} + mojo package {{ pkg.extra_args | join(" ") }} {{ pkg.path }} -o {{ library_prefix }}/lib/mojo/{{ pkg.name}}.mojopkg + {% if dist %} + cp {{ library_prefix }}/lib/mojo/{{ pkg.name }}.mojopkg {{ source_dir }}/{{ dist }} + {% endif %} {% endif %} diff --git a/crates/pixi-build-mojo/src/build_script.rs b/crates/pixi-build-mojo/src/build_script.rs index 185d8e0b..d832dc84 100644 --- a/crates/pixi-build-mojo/src/build_script.rs +++ b/crates/pixi-build-mojo/src/build_script.rs @@ -7,7 +7,7 @@ pub struct BuildScriptContext { /// The directory where the source code is located, the manifest root. pub source_dir: String, /// The directory name to place output artifacts, will be created in `source_dir`. - pub dist: String, + pub dist: Option, /// Any executable artifacts to create. pub bins: Option>, /// Any packages to create. diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index fc3ca3d6..40b0ec8c 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -4,7 +4,7 @@ use indexmap::IndexMap; use pixi_build_backend::generated_recipe::BackendConfig; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct MojoBackendConfig { /// Environment Variables @@ -14,8 +14,7 @@ pub struct MojoBackendConfig { /// Directory that will be created to place output artifacts. /// /// This is releative to the manifest dir. - #[serde(default = "default_dist_dir")] - pub dist_dir: PathBuf, + pub dist_dir: Option, /// Dir that can be specified for outputting pixi debug state. pub debug_dir: Option, @@ -33,22 +32,6 @@ impl BackendConfig for MojoBackendConfig { } } -impl Default for MojoBackendConfig { - fn default() -> Self { - Self { - env: Default::default(), - dist_dir: default_dist_dir(), - debug_dir: Default::default(), - bins: Default::default(), - pkg: Default::default(), - } - } -} - -fn default_dist_dir() -> PathBuf { - PathBuf::from("target") -} - /// Config object for a Mojo binary. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 383c8db3..da374cee 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -55,7 +55,7 @@ impl GenerateRecipe for MojoGenerator { let build_script = BuildScriptContext { source_dir: manifest_root.display().to_string(), - dist: config.dist_dir.display().to_string(), + dist: config.dist_dir.clone().map(|d| d.display().to_string()), bins: config.bins.clone(), pkg: config.pkg.clone(), has_host_python, @@ -68,6 +68,9 @@ impl GenerateRecipe for MojoGenerator { ..Default::default() }; + // How do I set globs on the build?? + generated_recipe.build_input_globs = Self::globs(); + Ok(generated_recipe) } @@ -76,18 +79,27 @@ impl GenerateRecipe for MojoGenerator { _workdir: impl AsRef, _editable: bool, ) -> Vec { + Self::globs() + } + + fn default_variants(&self, _host_platform: Platform) -> BTreeMap> { + BTreeMap::new() + } +} + +impl MojoGenerator { + fn globs() -> Vec { [ // Source files "**/*.mojo,*.🔥", + "**/pixi.toml", + "**/pixi.lock", + "**/recipe.yaml", ] .iter() .map(|s: &&str| s.to_string()) .collect() } - - fn default_variants(&self, _host_platform: Platform) -> BTreeMap> { - BTreeMap::new() - } } #[tokio::main] From b6b103bfc425decbdb73e2314f7df999a2d33928 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Sat, 26 Jul 2025 15:33:54 +0000 Subject: [PATCH 07/22] fix: globs were bad --- crates/pixi-build-backend/src/generated_recipe.rs | 5 ++++- crates/pixi-build-mojo/src/config.rs | 4 ++++ crates/pixi-build-mojo/src/main.rs | 11 ++++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/pixi-build-backend/src/generated_recipe.rs b/crates/pixi-build-backend/src/generated_recipe.rs index fa89a14e..d3918986 100644 --- a/crates/pixi-build-backend/src/generated_recipe.rs +++ b/crates/pixi-build-backend/src/generated_recipe.rs @@ -32,6 +32,7 @@ pub struct PythonParams { pub trait GenerateRecipe { type Config: BackendConfig; +<<<<<<< Updated upstream /// Generates an [`IntermediateRecipe`] from a [`ProjectModelV1`]. fn generate_recipe( &self, @@ -82,7 +83,9 @@ pub trait BackendConfig: DeserializeOwned + Clone { fn merge_with_target_config(&self, target_config: &Self) -> miette::Result; } -#[derive(Default, Clone)] +======= +>>>>>>> Stashed changes +#[derive(Default)] pub struct GeneratedRecipe { pub recipe: IntermediateRecipe, pub metadata_input_globs: BTreeSet, diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 40b0ec8c..2fa30083 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -19,6 +19,10 @@ pub struct MojoBackendConfig { /// Dir that can be specified for outputting pixi debug state. pub debug_dir: Option, + /// Extra input globs to include in addition to the default ones. + #[serde(default)] + pub extra_input_globs: Vec, + /// Binary executables to produce. pub bins: Option>, diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index da374cee..72da5b0f 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -69,17 +69,19 @@ impl GenerateRecipe for MojoGenerator { }; // How do I set globs on the build?? - generated_recipe.build_input_globs = Self::globs(); + generated_recipe.build_input_globs = Self::globs().collect::>(); Ok(generated_recipe) } fn extract_input_globs_from_build( - _config: &Self::Config, + config: &Self::Config, _workdir: impl AsRef, _editable: bool, ) -> Vec { Self::globs() + .chain(config.extra_input_globs.clone()) + .collect::>() } fn default_variants(&self, _host_platform: Platform) -> BTreeMap> { @@ -88,17 +90,16 @@ impl GenerateRecipe for MojoGenerator { } impl MojoGenerator { - fn globs() -> Vec { + fn globs() -> impl Iterator { [ // Source files - "**/*.mojo,*.🔥", + "**/*.{mojo,🔥}", "**/pixi.toml", "**/pixi.lock", "**/recipe.yaml", ] .iter() .map(|s: &&str| s.to_string()) - .collect() } } From fd0669eb9c78232c95d0b30f3d23e66a0a7ffe25 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Sat, 26 Jul 2025 16:19:25 +0000 Subject: [PATCH 08/22] feat: working --- Cargo.lock | 2 +- crates/pixi-build-backend/src/generated_recipe.rs | 5 +---- crates/pixi-build-mojo/src/main.rs | 13 ++++++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e214a92d..c5ce056c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3985,7 +3985,7 @@ version = "0.2.0" dependencies = [ "async-trait", "chrono", - "indexmap 2.9.0", + "indexmap 2.10.0", "insta", "miette", "minijinja", diff --git a/crates/pixi-build-backend/src/generated_recipe.rs b/crates/pixi-build-backend/src/generated_recipe.rs index d3918986..211cb2d2 100644 --- a/crates/pixi-build-backend/src/generated_recipe.rs +++ b/crates/pixi-build-backend/src/generated_recipe.rs @@ -5,7 +5,7 @@ use std::{ }; use pixi_build_types::ProjectModelV1; -use rattler_build::{NormalizedKey, recipe::variable::Variable}; +use rattler_build::{recipe::variable::Variable, NormalizedKey}; use rattler_conda_types::Platform; use recipe_stage0::recipe::{ConditionalList, IntermediateRecipe, Item, Package, Source, Value}; use serde::de::DeserializeOwned; @@ -32,7 +32,6 @@ pub struct PythonParams { pub trait GenerateRecipe { type Config: BackendConfig; -<<<<<<< Updated upstream /// Generates an [`IntermediateRecipe`] from a [`ProjectModelV1`]. fn generate_recipe( &self, @@ -83,8 +82,6 @@ pub trait BackendConfig: DeserializeOwned + Clone { fn merge_with_target_config(&self, target_config: &Self) -> miette::Result; } -======= ->>>>>>> Stashed changes #[derive(Default)] pub struct GeneratedRecipe { pub recipe: IntermediateRecipe, diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 72da5b0f..2b97cead 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -1,7 +1,10 @@ mod build_script; mod config; -use std::{collections::BTreeMap, path::Path}; +use std::{ + collections::{BTreeMap, BTreeSet}, + path::Path, +}; use build_script::BuildScriptContext; use config::MojoBackendConfig; @@ -33,7 +36,7 @@ impl GenerateRecipe for MojoGenerator { // we need to add compilers let requirements = &mut generated_recipe.recipe.requirements; - let resolved_requirements = requirements.resolve(Some(&host_platform)); + let resolved_requirements = requirements.resolve(Some(host_platform)); // Ensure the compiler function is added to the build requirements // only if a specific compiler is not already present. @@ -69,7 +72,7 @@ impl GenerateRecipe for MojoGenerator { }; // How do I set globs on the build?? - generated_recipe.build_input_globs = Self::globs().collect::>(); + generated_recipe.build_input_globs = Self::globs().collect::>(); Ok(generated_recipe) } @@ -78,10 +81,10 @@ impl GenerateRecipe for MojoGenerator { config: &Self::Config, _workdir: impl AsRef, _editable: bool, - ) -> Vec { + ) -> BTreeSet { Self::globs() .chain(config.extra_input_globs.clone()) - .collect::>() + .collect() } fn default_variants(&self, _host_platform: Platform) -> BTreeMap> { From ad4e920ef086d0f17a010861b27341d807696e8a Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Sat, 26 Jul 2025 16:35:06 +0000 Subject: [PATCH 09/22] feat: improved tests --- crates/pixi-build-mojo/src/main.rs | 91 +++++++++++++++++++ ...sts__input_globs_includes_extra_globs.snap | 8 +- ...xi_build_mojo__tests__mojo_bin_is_set.snap | 34 +++++++ ...xi_build_mojo__tests__mojo_pkg_is_set.snap | 37 ++++++++ pixi.toml | 2 +- 5 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_bin_is_set.snap create mode 100644 crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_pkg_is_set.snap diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 2b97cead..1e87849f 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -123,11 +123,14 @@ mod tests { use indexmap::IndexMap; use pixi_build_types::ProjectModelV1; + use crate::config::{MojoBinConfig, MojoPkgConfig}; + use super::*; #[test] fn test_input_globs_includes_extra_globs() { let config = MojoBackendConfig { + extra_input_globs: vec![String::from("**/.c")], ..Default::default() }; @@ -145,6 +148,94 @@ mod tests { }; } + #[test] + fn test_mojo_bin_is_set() { + let project_model = project_fixture!({ + "name": "foobar", + "version": "0.1.0", + "targets": { + "defaultTarget": { + "runDependencies": { + "boltons": { + "binary": { + "version": "*" + } + } + } + }, + } + }); + + let generated_recipe = MojoGenerator::default() + .generate_recipe( + &project_model, + &MojoBackendConfig { + bins: Some(vec![MojoBinConfig { + name: String::from("example"), + path: String::from("./main.mojo"), + extra_args: Some(vec![String::from("-I"), String::from(".")]), + env: IndexMap::new(), + }]), + ..Default::default() + }, + PathBuf::from("."), + Platform::Linux64, + None, + ) + .expect("Failed to generate recipe"); + + insta::assert_yaml_snapshot!(generated_recipe.recipe, { + ".source[0].path" => "[ ... path ... ]", + }); + } + + #[test] + fn test_mojo_pkg_is_set() { + let project_model = project_fixture!({ + "name": "foobar", + "version": "0.1.0", + "targets": { + "defaultTarget": { + "runDependencies": { + "boltons": { + "binary": { + "version": "*" + } + } + } + }, + } + }); + + let generated_recipe = MojoGenerator::default() + .generate_recipe( + &project_model, + &MojoBackendConfig { + bins: Some(vec![MojoBinConfig { + name: String::from("example"), + path: String::from("./main.mojo"), + extra_args: Some(vec![String::from("-i"), String::from(".")]), + env: IndexMap::new(), + }]), + pkg: Some(MojoPkgConfig { + name: String::from("lib"), + path: String::from("mylib"), + extra_args: Some(vec![String::from("-i"), String::from(".")]), + env: IndexMap::new(), + }), + ..Default::default() + }, + PathBuf::from("."), + Platform::Linux64, + None, + ) + .expect("Failed to generate recipe"); + + insta::assert_yaml_snapshot!(generated_recipe.recipe, { + ".source[0].path" => "[ ... path ... ]", + }); + } + #[test] fn test_max_is_in_build_requirements() { let project_model = project_fixture!({ diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap index 6c84f252..7dd9a067 100644 --- a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap @@ -2,6 +2,10 @@ source: crates/pixi-build-mojo/src/main.rs expression: result --- -[ +{ "**/*.{mojo,🔥}", -] + "**/.c", + "**/pixi.lock", + "**/pixi.toml", + "**/recipe.yaml", +} diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_bin_is_set.snap b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_bin_is_set.snap new file mode 100644 index 00000000..7c04020a --- /dev/null +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_bin_is_set.snap @@ -0,0 +1,34 @@ +--- +source: crates/pixi-build-mojo/src/main.rs +expression: generated_recipe.recipe +--- +context: {} +package: + name: foobar + version: 0.1.0 +source: + - path: "[ ... path ... ]" + sha256: ~ +build: + number: ~ + script: + content: + - mojo --version + - "" + - "" + - "\t" + - "\t\tmojo build -I . ./main.mojo -o $PREFIX/bin/example" + - "\t\t" + - "\t" + env: {} + secrets: [] +requirements: + build: + - max + host: [] + run: + - boltons + run_constraints: [] +tests: [] +about: ~ +extra: ~ diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_pkg_is_set.snap b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_pkg_is_set.snap new file mode 100644 index 00000000..8640c2de --- /dev/null +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_pkg_is_set.snap @@ -0,0 +1,37 @@ +--- +source: crates/pixi-build-mojo/src/main.rs +expression: generated_recipe.recipe +--- +context: {} +package: + name: foobar + version: 0.1.0 +source: + - path: "[ ... path ... ]" + sha256: ~ +build: + number: ~ + script: + content: + - mojo --version + - "" + - "" + - "\t" + - "\t\tmojo build -i . ./main.mojo -o $PREFIX/bin/example" + - "\t\t" + - "\t" + - "" + - "\tmojo package -i . mylib -o $PREFIX/lib/mojo/lib.mojopkg" + - "\t" + env: {} + secrets: [] +requirements: + build: + - max + host: [] + run: + - boltons + run_constraints: [] +tests: [] +about: ~ +extra: ~ diff --git a/pixi.toml b/pixi.toml index e7f5ccf4..f7daea28 100644 --- a/pixi.toml +++ b/pixi.toml @@ -27,7 +27,7 @@ install-pixi-backends = { depends-on = [ "install-pixi-build-cmake", "install-pixi-build-rattler-build", "install-pixi-build-rust", - #"install-pixi-build-mojo", + "install-pixi-build-mojo", ] } From 28db9893f997e585601548b8eb1bdc341b3cd9dc Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Sat, 26 Jul 2025 17:07:26 +0000 Subject: [PATCH 10/22] chore: first pass at docs --- docs/backends/pixi-build-mojo.md | 123 +++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 docs/backends/pixi-build-mojo.md diff --git a/docs/backends/pixi-build-mojo.md b/docs/backends/pixi-build-mojo.md new file mode 100644 index 00000000..d4e5fbcb --- /dev/null +++ b/docs/backends/pixi-build-mojo.md @@ -0,0 +1,123 @@ +# pixi-build-mojo + +The `pixi-build-mojo` backend is designed for building Mojo projects. It provides seamless integration with Pixi's package magement workflow. + +!!! warning + `pixi-build` is a preview feature, and will change untill 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 Mojo projects, and can install those into local environments. + +## Basic Usage + +To use the Mojo backend in your `pixi.toml`, add it to your package's build configuration: + + +```txt +# Example project layout for combined binary/library. +. +├── greetings +│   ├── __init__.mojo +│   └── lib.mojo +├── main.mojo +├── pixi.lock +├── pixi.toml +├── README.md +├── src +└── structure.txt +``` + +```toml +[workspace] +authors = ["Seth Stadick "] +platforms = ["linux-64"] +preview = ["pixi-build"] +channels = [ + "conda-forge", + "https://conda.modular.com/max-nightly", + "https://prefix.dev/pixi-build-backends", + "https://repo.prefix.dev/modular-community" +] + +[package] +name = "greetings" +version = "0.1.0" + +[package.build] +backend = { name = "pixi-build-mojo", version = "0.1.*" } + +[tasks] + +[package.build.configuration] +# This is entirey optional. pixi install is recommended +# dist-dir = "./target" + +[[package.build.configuration.bins]] +name = "greet" +path = "./main.mojo" +#extra-args = ["-I", "special-thing"] +#extra-input_globs = ["**/.c"] + +[package.build.configuration.pkg] +name = "greetings" +path = "greetings" +#extra-args = ["-I", "special-thing"] +#extra-input_globs = ["**/.c"] + +[package.host-dependencies] +max = "=25.4.0" + +[package.build-dependencies] +max = "=25.4.0" +small_time = ">=25.4.1,<26" +extramojo = ">=0.16.0,<0.17" + +[package.run-dependencies] +max = "=25.4.0" + +[dependencies] +# For running `mojo test` while developing you'll probably want +# everything under "build-dependencies" as well +greetings = { path = "." } +``` + +### Required Dependencies + +- `max` package for both the compiler and linked runtime + +## Configuration Options + +- **env** key-value pairs of environment variables to pass in + - **note** not yet implemented +- **dist-dir** a directory to create relative to the manifest pixi.toml to copy build artifacts into. Prefer `pixi install` to using this for better caching. +- **debug-dir** directory to place internal pixi debug information into. +- **extra-input-globs** additional globs to pass to pixi for including more than just mojo files in the build +- **bins** list of binary configurations to build, see [bins](#bins). +- **pkg** pkg configuration for creating a `.mojopkg`, see [pkg](#pkg). + +### `bins` + +- **name** the binary name +- **path** the path to the file that contains a `main` function +- **extra-args** list of extra arguments to pass to the compiler +- **env** env vars to set + - **note** not yet implemented + +The created binary will be placed in the `$PREFIX/bin` dir and will be in the path after running `pixi install` assuming the package is listed as a dependency as in the example above. `pixi build` will create a conda package that includes the binary. + +### `pkg` + +- **name** the binary name +- **path** the path to the file that contains a `main` function +- **extra-args** list of extra arguments to pass to the compiler +- **env** env vars to set + - **note** not yet implemented + +The created mojopkg will be placed in the `$PREFIX/lib/mojo` dir, which will make it discoverable to anything that depends on the package. From 6db6dee2015e3c83941a9244618e9a6821557278 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Sat, 26 Jul 2025 17:56:52 +0000 Subject: [PATCH 11/22] chore: update docs --- docs/backends/pixi-build-mojo.md | 209 ++++++++++++++++++++++++++----- 1 file changed, 178 insertions(+), 31 deletions(-) diff --git a/docs/backends/pixi-build-mojo.md b/docs/backends/pixi-build-mojo.md index d4e5fbcb..7c0f9145 100644 --- a/docs/backends/pixi-build-mojo.md +++ b/docs/backends/pixi-build-mojo.md @@ -1,19 +1,21 @@ # pixi-build-mojo -The `pixi-build-mojo` backend is designed for building Mojo projects. It provides seamless integration with Pixi's package magement workflow. +The `pixi-build-mojo` backend is designed for building Mojo projects. It provides seamless integration with Pixi's package management workflow. !!! warning - `pixi-build` is a preview feature, and will change untill it is stabilized. + `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'] + preview = ["pixi-build"] ``` ## Overview -This backend automatically generates conda packages from Mojo projects, and can install those into local environments. +This backend automatically generates conda packages from Mojo projects. + +The generated packages can be installed into local envs for devlopment, or packaged for distribution. ## Basic Usage @@ -34,9 +36,11 @@ To use the Mojo backend in your `pixi.toml`, add it to your package's build conf └── structure.txt ``` +Commented out sections represent optional params that may be useful. + ```toml [workspace] -authors = ["Seth Stadick "] +authors = ["J. Doe "] platforms = ["linux-64"] preview = ["pixi-build"] channels = [ @@ -56,20 +60,19 @@ backend = { name = "pixi-build-mojo", version = "0.1.*" } [tasks] [package.build.configuration] -# This is entirey optional. pixi install is recommended # dist-dir = "./target" [[package.build.configuration.bins]] name = "greet" path = "./main.mojo" -#extra-args = ["-I", "special-thing"] -#extra-input_globs = ["**/.c"] +# extra-args = ["-I", "special-thing"] +# extra-input-globs = ["**/.c"] [package.build.configuration.pkg] name = "greetings" path = "greetings" -#extra-args = ["-I", "special-thing"] -#extra-input_globs = ["**/.c"] +# extra-args = ["-I", "special-thing"] +# extra-input-globs = ["**/.c"] [package.host-dependencies] max = "=25.4.0" @@ -83,8 +86,8 @@ extramojo = ">=0.16.0,<0.17" max = "=25.4.0" [dependencies] -# For running `mojo test` while developing you'll probably want -# everything under "build-dependencies" as well +# For running `mojo test` while developing add all dependencies under +# `[package.build-dependencies]` here as well. greetings = { path = "." } ``` @@ -94,30 +97,174 @@ greetings = { path = "." } ## Configuration Options -- **env** key-value pairs of environment variables to pass in - - **note** not yet implemented -- **dist-dir** a directory to create relative to the manifest pixi.toml to copy build artifacts into. Prefer `pixi install` to using this for better caching. -- **debug-dir** directory to place internal pixi debug information into. -- **extra-input-globs** additional globs to pass to pixi for including more than just mojo files in the build -- **bins** list of binary configurations to build, see [bins](#bins). -- **pkg** pkg configuration for creating a `.mojopkg`, see [pkg](#pkg). +You can customize the Mojo backend behavior using the `[package.build.configuration]` section in your `pixi.toml`. The backend supports the following configuration options: + +#### `env` + +- **Type**: `Map` +- **Default**: `{}` +- **Note**: Not yet implemented + +Environment variables to set during the build process. + +```toml +[package.build.configuration] +env = { ASSERT = "all" } +``` + +#### `dist-dir` + +- **Type**: `String` (path) +- **Default**: Not set + +A directory to create relative to the manifest pixi.toml to copy build artifacts into. `pixi install` will install the artifacts into your local enviroment and as such the `dist-dir` is mostly for debugging. + +```toml +[package.build.configuration] +dist-dir = "./target" +``` + +#### `debug-dir` + +- **Type**: `String` (path) +- **Default**: Not set + +Directory to place internal pixi debug information into. + +```toml +[package.build.configuration] +debug-dir = ".build-debug" +``` + +#### `extra-input-globs` + +- **Type**: `Array` +- **Default**: `[]` + +Additional globs to pass to pixi for including more than just mojo files in the build. + +```toml +[package.build.configuration] +extra-input-globs = ["**/*.c", "assets/**/*", "*.md"] +``` ### `bins` -- **name** the binary name -- **path** the path to the file that contains a `main` function -- **extra-args** list of extra arguments to pass to the compiler -- **env** env vars to set - - **note** not yet implemented +- **Type**: `Array` +- **Default**: Not set + +List of binary configurations to build. The created binary will be placed in the `$PREFIX/bin` dir and will be in the path after running `pixi install` assuming the package is listed as a dependency as in the example above. `pixi build` will create a conda package that includes the binary. + +#### `bins[].name` + +- **Type**: `String` +- **Default**: Required field (no default) + +The name of the binary executable to create. + +```toml +[[package.build.configuration.bins]] +name = "greet" +``` + +#### `bins[].path` + +- **Type**: `String` (path) +- **Default**: Required field (no default) + +The path to the Mojo file that contains a `main` function. + +```toml +[[package.build.configuration.bins]] +path = "./main.mojo" +``` + +#### `bins[].extra-args` + +- **Type**: `Array` +- **Default**: `[]` + +Additional command-line arguments to pass to the Mojo compiler when building this binary. + +```toml +[[package.build.configuration.bins]] +extra-args = ["-I", "special-thing"] +``` + +#### `bins[].env` + +- **Type**: `Map` +- **Default**: `{}` +- **Note**: Not yet implemented -The created binary will be placed in the `$PREFIX/bin` dir and will be in the path after running `pixi install` assuming the package is listed as a dependency as in the example above. `pixi build` will create a conda package that includes the binary. +Environment variables to set when building this binary. + +```toml +[[package.build.configuration.bins]] +env = { ASSERT = "all" } +``` ### `pkg` -- **name** the binary name -- **path** the path to the file that contains a `main` function -- **extra-args** list of extra arguments to pass to the compiler -- **env** env vars to set - - **note** not yet implemented +- **Type**: `PkgConfig` +- **Default**: Not set + +Package configuration for creating Mojo package. The created Mojo package will be placed in the `$PREFIX/lib/mojo` dir, which will make it discoverable to anything that depends on the package. + +#### `pkg.name` + +- **Type**: `String` +- **Default**: Required field (no default) + +The name to give the Mojo package. The `.mojopkg` suffix will be added automatically. + +```toml +[package.build.configuration.pkg] +name = "greetings" +``` + +#### `pkg.path` + +- **Type**: `String` (path) +- **Default**: Required field (no default) + +The path to the directory that constitutes the package. + +```toml +[package.build.configuration.pkg] +path = "greetings" +``` + +#### `pkg.extra-args` + +- **Type**: `Array` +- **Default**: `[]` + +Additional command-line arguments to pass to the Mojo compiler when building this package. + +```toml +[package.build.configuration.pkg] +extra-args = ["-I", "special-thing"] +``` + +#### `pkg.env` + +- **Type**: `Map` +- **Default**: `{}` +- **Note**: Not yet implemented + +Environment variables to set when building this package. + +```toml +[package.build.configuration.pkg] +env = { ASSERT = "all" } +``` + +## Limitations + +- Env var pass through not yet implemented + +## See Also -The created mojopkg will be placed in the `$PREFIX/lib/mojo` dir, which will make it discoverable to anything that depends on the package. +- [Mojo Pixi Basic](https://docs.modular.com/pixi/) +- [Modular Community Packages](https://github.com/modular/modular-community) From c2294fe0ca4c231c40c2393ffb792e7dfb37dafa Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Sat, 26 Jul 2025 23:12:14 +0000 Subject: [PATCH 12/22] fix: env var handling --- crates/pixi-build-mojo/src/config.rs | 6 ------ docs/backends/pixi-build-mojo.md | 31 ---------------------------- 2 files changed, 37 deletions(-) diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 2fa30083..e712d7f8 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -47,9 +47,6 @@ pub struct MojoBinConfig { /// Extra args to pass to the compiler. #[serde(default, rename(serialize = "extra_args"))] pub extra_args: Option>, - /// Env vars to set. - #[serde(default)] - pub env: IndexMap, } /// Config object for a Mojo package. @@ -63,9 +60,6 @@ pub struct MojoPkgConfig { /// Extra args to pass to the compiler. #[serde(default, rename(serialize = "extra_args"))] pub extra_args: Option>, - /// Env vars to set. - #[serde(default)] - pub env: IndexMap, } #[cfg(test)] diff --git a/docs/backends/pixi-build-mojo.md b/docs/backends/pixi-build-mojo.md index 7c0f9145..d912d6ac 100644 --- a/docs/backends/pixi-build-mojo.md +++ b/docs/backends/pixi-build-mojo.md @@ -103,7 +103,6 @@ You can customize the Mojo backend behavior using the `[package.build.configurat - **Type**: `Map` - **Default**: `{}` -- **Note**: Not yet implemented Environment variables to set during the build process. @@ -191,19 +190,6 @@ Additional command-line arguments to pass to the Mojo compiler when building thi extra-args = ["-I", "special-thing"] ``` -#### `bins[].env` - -- **Type**: `Map` -- **Default**: `{}` -- **Note**: Not yet implemented - -Environment variables to set when building this binary. - -```toml -[[package.build.configuration.bins]] -env = { ASSERT = "all" } -``` - ### `pkg` - **Type**: `PkgConfig` @@ -247,23 +233,6 @@ Additional command-line arguments to pass to the Mojo compiler when building thi extra-args = ["-I", "special-thing"] ``` -#### `pkg.env` - -- **Type**: `Map` -- **Default**: `{}` -- **Note**: Not yet implemented - -Environment variables to set when building this package. - -```toml -[package.build.configuration.pkg] -env = { ASSERT = "all" } -``` - -## Limitations - -- Env var pass through not yet implemented - ## See Also - [Mojo Pixi Basic](https://docs.modular.com/pixi/) From 28391b6a8b3f6ffb7a8c0f719313ad1938b07c78 Mon Sep 17 00:00:00 2001 From: Seth Date: Sat, 26 Jul 2025 20:20:04 -0400 Subject: [PATCH 13/22] chore: tidy up from review --- crates/pixi-build-mojo/src/main.rs | 3 +-- docs/backends/pixi-build-mojo.md | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 1e87849f..7477581b 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -34,7 +34,7 @@ impl GenerateRecipe for MojoGenerator { let mut generated_recipe = GeneratedRecipe::from_model(model.clone(), manifest_root.clone()); - // we need to add compilers + // Add compiler let requirements = &mut generated_recipe.recipe.requirements; let resolved_requirements = requirements.resolve(Some(host_platform)); @@ -71,7 +71,6 @@ impl GenerateRecipe for MojoGenerator { ..Default::default() }; - // How do I set globs on the build?? generated_recipe.build_input_globs = Self::globs().collect::>(); Ok(generated_recipe) diff --git a/docs/backends/pixi-build-mojo.md b/docs/backends/pixi-build-mojo.md index d912d6ac..79de590c 100644 --- a/docs/backends/pixi-build-mojo.md +++ b/docs/backends/pixi-build-mojo.md @@ -32,15 +32,14 @@ To use the Mojo backend in your `pixi.toml`, add it to your package's build conf ├── pixi.lock ├── pixi.toml ├── README.md -├── src -└── structure.txt +└── src ``` Commented out sections represent optional params that may be useful. ```toml [workspace] -authors = ["J. Doe "] +authors = ["J. Doe "] platforms = ["linux-64"] preview = ["pixi-build"] channels = [ From a72c166805f32bbc173dd75be28c50b66efed001 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Sun, 27 Jul 2025 19:13:16 +0000 Subject: [PATCH 14/22] feat: add deafults for package and bin specs --- Cargo.lock | 17 ++ crates/pixi-build-mojo/Cargo.toml | 1 + crates/pixi-build-mojo/src/build_script.rs | 6 - crates/pixi-build-mojo/src/config.rs | 179 ++++++++++++++++++++- crates/pixi-build-mojo/src/main.rs | 99 ++++-------- 5 files changed, 223 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5ce056c..0cef748c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1545,6 +1545,12 @@ dependencies = [ "syn", ] +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "diffy" version = "0.4.2" @@ -3999,6 +4005,7 @@ dependencies = [ "recipe-stage0", "serde", "serde_json", + "slug", "tempfile", "tokio", ] @@ -6187,6 +6194,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.15.0" diff --git a/crates/pixi-build-mojo/Cargo.toml b/crates/pixi-build-mojo/Cargo.toml index 2dd5449c..6984d04d 100644 --- a/crates/pixi-build-mojo/Cargo.toml +++ b/crates/pixi-build-mojo/Cargo.toml @@ -28,6 +28,7 @@ pixi_manifest = { workspace = true } pixi_build_type_conversions = { workspace = true } recipe-stage0 = { workspace = true } +slug = "0.1.6" [dev-dependencies] insta = { version = "1.42.1", features = ["yaml", "redactions", "filters"] } diff --git a/crates/pixi-build-mojo/src/build_script.rs b/crates/pixi-build-mojo/src/build_script.rs index d832dc84..6892bf40 100644 --- a/crates/pixi-build-mojo/src/build_script.rs +++ b/crates/pixi-build-mojo/src/build_script.rs @@ -12,12 +12,6 @@ pub struct BuildScriptContext { pub bins: Option>, /// Any packages to create. pub pkg: Option, - - /// Not currenlty used - /// The package has a host dependency on Python. - /// This is used to determine if the build script - /// should include Python-related logic. - pub has_host_python: bool, } impl BuildScriptContext { diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index e712d7f8..4524ea6b 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -1,8 +1,13 @@ -use std::path::{Path, PathBuf}; +use std::{ + collections::HashSet, + path::{Path, PathBuf}, +}; use indexmap::IndexMap; +use miette::Error; use pixi_build_backend::generated_recipe::BackendConfig; use serde::{Deserialize, Serialize}; +use slug::slugify; #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -41,27 +46,191 @@ impl BackendConfig for MojoBackendConfig { #[serde(rename_all = "kebab-case")] pub struct MojoBinConfig { /// Name of the binary. - pub name: String, + /// + /// This will default to the slugified name of the project for the first + /// binary selected. + pub name: Option, /// Path to file that has the `main` method. - pub path: String, + /// + /// This will deafault to looking for a `main.mojo` file in: + /// - `/main.mojo` + /// - `//main.mojo` + /// - `/src/main.mojo` + pub path: Option, /// Extra args to pass to the compiler. #[serde(default, rename(serialize = "extra_args"))] pub extra_args: Option>, } +impl MojoBinConfig { + /// Fill in any missing info and or try to find our default options. + /// + /// - If None, try to find a `main.mojo` file in manfiest_root + /// - If any, for the first one, see if name or path need to be filled in + /// - If any, verify that there are no name collisions + pub fn fill_defaults( + conf: Option<&Vec>, + manifest_root: &PathBuf, + slug_name: &str, + ) -> miette::Result<(Option>, bool)> { + let main = Self::find_main(manifest_root).map(|p| p.display().to_string()); + + // No configuration specified + if conf.is_none() { + if let Some(main) = main { + return Ok(( + Some(vec![Self { + name: Some(slug_name.to_owned()), + path: Some(main), + ..Default::default() + }]), + true, + )); + } else { + return Ok((None, false)); + } + } + + // Some configuration specified + let mut conf = conf.unwrap().clone(); // checked above + if conf.is_empty() { + return Ok((None, false)); + } + + if conf[0].name.is_none() { + conf[0].name = Some(slug_name.to_owned()); + } + if conf[0].path.is_none() { + if main.is_none() { + return Err(Error::msg("Could not find main.mojo for configured binary")); + } + conf[0].path = main; + } + + // Verify no name collisions and that the rest of the binaries have a name and path + let mut names = HashSet::new(); + for (i, c) in conf.iter().enumerate() { + if c.name.is_none() { + return Err(Error::msg(format!( + "Binary configuration {} is missing a name.", + i + ))); + } + if c.path.is_none() { + return Err(Error::msg(format!( + "Binary configuration {} is missing a path.", + c.name.as_ref().unwrap(), + ))); + } + if names.contains(c.name.as_deref().unwrap()) { + return Err(Error::msg(format!( + "Binary name has been used twice: {}", + c.name.as_ref().unwrap() + ))); + } + + names.insert(c.name.clone().unwrap()); + } + + Ok((Some(conf), false)) + } + + fn find_main(root: &PathBuf) -> Option { + let mut path = root.join("main"); + for ext in ["mojo", "🔥"] { + path.set_extension(ext); + if path.exists() { + return Some(path); + } + } + None + } +} + /// Config object for a Mojo package. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct MojoPkgConfig { /// Name to give the mojo package (.mojopkg suffix will be added). - pub name: String, + /// + /// This will default to the slugified name of the project. + pub name: Option, /// Path to the directory that constitutes the package. - pub path: String, + /// + /// This will default to lookingo for a folder with an `__init__.mojo` in + /// in the following order: + /// - `//__init__.mojo` + /// - `/src/__init__.mojo` + pub path: Option, /// Extra args to pass to the compiler. #[serde(default, rename(serialize = "extra_args"))] pub extra_args: Option>, } +impl MojoPkgConfig { + /// Fill in any missing info anod or try to find our default options. + /// + /// - If None, try to find a `src` or `` dir with an `__init__.mojo` file in it. + /// - If Some, see if name or path need to be filled in. + pub fn fill_defaults( + conf: Option<&Self>, + manifest_root: &PathBuf, + slug_name: &str, + ) -> miette::Result<(Option, bool)> { + if let Some(conf) = conf { + // A conf was given, make sure it has a name and path + let mut conf = conf.clone(); + if conf.name.is_none() { + conf.name = Some(slug_name.to_owned()); + } + + let path = Self::find_init_parent(manifest_root, slug_name); + if conf.path.is_none() { + if path.is_none() { + return Err(Error::msg(format!( + "Could not find valid package path for {}", + conf.name.unwrap() + ))); + } + conf.path = path.map(|p| p.display().to_string()); + } + Ok((Some(conf), false)) + } else { + // No conf given check if we can find a valid package + let path = Self::find_init_parent(manifest_root, slug_name); + if path.is_none() { + return Ok((None, false)); + } + Ok(( + Some(Self { + name: Some(slug_name.to_owned()), + path: path.map(|p| p.display().to_string()), + ..Default::default() + }), + true, + )) + } + } + + fn find_init_parent(root: &PathBuf, project_name: &str) -> Option { + for dir in [project_name, "src"] { + let mut path = root.join(dir).join("__init__"); + for ext in ["mojo", "🔥"] { + path.set_extension(ext); + if path.exists() { + return Some(root.join(dir)); + } + } + } + None + } +} + +/// Slugify a name for use in [`MojoPkgConfig`] and [`MojoBinconfig`]. +pub fn slugify_name>(s: S) -> String { + slugify(s) +} + #[cfg(test)] mod tests { use serde_json::json; diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 7477581b..5ccb6342 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -7,8 +7,8 @@ use std::{ }; use build_script::BuildScriptContext; -use config::MojoBackendConfig; -use miette::IntoDiagnostic; +use config::{slugify_name, MojoBackendConfig, MojoBinConfig, MojoPkgConfig}; +use miette::{Error, IntoDiagnostic}; use pixi_build_backend::{ generated_recipe::{GenerateRecipe, GeneratedRecipe, PythonParams}, intermediate_backend::IntermediateBackendInstantiator, @@ -34,6 +34,33 @@ impl GenerateRecipe for MojoGenerator { let mut generated_recipe = GeneratedRecipe::from_model(model.clone(), manifest_root.clone()); + let slug_name = slugify_name( + generated_recipe + .recipe + .package + .name + .concrete() + .ok_or(Error::msg("Package is missing a name"))?, + ); + + // Update bins configs + let (bins, bin_autodetected) = + MojoBinConfig::fill_defaults(config.bins.as_ref(), &manifest_root, &slug_name)?; + + // Update pkg config + let (mut pkg, pkg_autodetected) = + MojoPkgConfig::fill_defaults(config.pkg.as_ref(), &manifest_root, &slug_name)?; + + // Make sure we have at least one of the two + if bins.is_none() && pkg.is_none() { + return Err(Error::msg("No bin or pkg configuration detected.")); + } + + // If we are auto-generating both, keep only the bin? + if bin_autodetected && pkg_autodetected { + pkg = None; + } + // Add compiler let requirements = &mut generated_recipe.recipe.requirements; let resolved_requirements = requirements.resolve(Some(host_platform)); @@ -51,17 +78,11 @@ impl GenerateRecipe for MojoGenerator { .push(mojo_compiler_pkg.parse().into_diagnostic()?); } - // Check if the host platform has a host python dependency - // TODO: surely this will be needed for compiling bindings or something? or maybe those - // will be handled by uv? - let has_host_python = resolved_requirements.contains(&PackageName::new_unchecked("python")); - let build_script = BuildScriptContext { source_dir: manifest_root.display().to_string(), dist: config.dist_dir.clone().map(|d| d.display().to_string()), - bins: config.bins.clone(), - pkg: config.pkg.clone(), - has_host_python, + bins, + pkg, } .render(); @@ -96,9 +117,6 @@ impl MojoGenerator { [ // Source files "**/*.{mojo,🔥}", - "**/pixi.toml", - "**/pixi.lock", - "**/recipe.yaml", ] .iter() .map(|s: &&str| s.to_string()) @@ -308,61 +326,6 @@ mod tests { }); } - // I think we'll want this back at some point - // #[test] - // fn test_has_python_is_set_in_build_script() { - // let project_model = project_fixture!({ - // "name": "foobar", - // "version": "0.1.0", - // "targets": { - // "defaultTarget": { - // "runDependencies": { - // "boltons": { - // "binary": { - // "version": "*" - // } - // } - // }, - // "hostDependencies": { - // "python": { - // "binary": { - // "version": "*" - // } - // } - // } - // }, - // } - // }); - // - // let generated_recipe = MojoGenerator::default() - // .generate_recipe( - // &project_model, - // &MojoBackendConfig::default(), - // PathBuf::from("."), - // Platform::Linux64, - // None, - // ) - // .expect("Failed to generate recipe"); - // - // // we want to check that - // // -DPython_EXECUTABLE=$PYTHON is set in the build script - // insta::assert_yaml_snapshot!(generated_recipe.recipe.build, - // - // { - // ".script.content" => insta::dynamic_redaction(|value, _path| { - // dbg!(&value); - // // assert that the value looks like a uuid here - // assert!(value - // .as_slice() - // .unwrap() - // .iter() - // .any(|c| c.as_str().unwrap().contains("-DPython_EXECUTABLE")) - // ); - // "[content]" - // }) - // }); - // } - #[test] fn test_max_is_not_added_if_max_is_already_present() { let project_model = project_fixture!({ From 92e2b4aff78bba30d4a66f25c2c8eb17b822ffcd Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Sun, 27 Jul 2025 23:43:22 +0000 Subject: [PATCH 15/22] feat: add defaults for finding bins and pkg --- Cargo.lock | 54 ++-- crates/pixi-build-mojo/Cargo.toml | 2 +- crates/pixi-build-mojo/src/config.rs | 259 ++++++++++++++++-- crates/pixi-build-mojo/src/main.rs | 37 ++- ...sts__input_globs_includes_extra_globs.snap | 3 - docs/backends/pixi-build-mojo.md | 124 ++++++--- 6 files changed, 387 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cef748c..beebe085 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1545,12 +1545,6 @@ dependencies = [ "syn", ] -[[package]] -name = "deunicode" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" - [[package]] name = "diffy" version = "0.4.2" @@ -3978,7 +3972,7 @@ dependencies = [ "rattler-build", "rattler_conda_types", "recipe-stage0", - "rstest", + "rstest 0.25.0", "serde", "serde_json", "strum", @@ -4003,9 +3997,9 @@ dependencies = [ "rattler_conda_types", "rattler_package_streaming", "recipe-stage0", + "rstest 0.23.0", "serde", "serde_json", - "slug", "tempfile", "tokio", ] @@ -4064,7 +4058,7 @@ dependencies = [ "pixi_build_types", "rattler_conda_types", "recipe-stage0", - "rstest", + "rstest 0.25.0", "serde", "serde_json", "temp-env", @@ -5508,6 +5502,18 @@ dependencies = [ "serde", ] +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros 0.23.0", + "rustc_version", +] + [[package]] name = "rstest" version = "0.25.0" @@ -5516,10 +5522,28 @@ checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" dependencies = [ "futures-timer", "futures-util", - "rstest_macros", + "rstest_macros 0.25.0", "rustc_version", ] +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if 1.0.0", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rstest_macros" version = "0.25.0" @@ -6194,16 +6218,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "slug" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" -dependencies = [ - "deunicode", - "wasm-bindgen", -] - [[package]] name = "smallvec" version = "1.15.0" diff --git a/crates/pixi-build-mojo/Cargo.toml b/crates/pixi-build-mojo/Cargo.toml index 6984d04d..561d084e 100644 --- a/crates/pixi-build-mojo/Cargo.toml +++ b/crates/pixi-build-mojo/Cargo.toml @@ -28,7 +28,7 @@ pixi_manifest = { workspace = true } pixi_build_type_conversions = { workspace = true } recipe-stage0 = { workspace = true } -slug = "0.1.6" [dev-dependencies] insta = { version = "1.42.1", features = ["yaml", "redactions", "filters"] } +rstest = "0.23" diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 4524ea6b..20a8a88c 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -7,7 +7,6 @@ use indexmap::IndexMap; use miette::Error; use pixi_build_backend::generated_recipe::BackendConfig; use serde::{Deserialize, Serialize}; -use slug::slugify; #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -42,20 +41,18 @@ impl BackendConfig for MojoBackendConfig { } /// Config object for a Mojo binary. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct MojoBinConfig { /// Name of the binary. /// - /// This will default to the slugified name of the project for the first - /// binary selected. + /// This will default to the name of the project for the first + /// binary selected, any dashes will be replaced with `_`. pub name: Option, /// Path to file that has the `main` method. /// - /// This will deafault to looking for a `main.mojo` file in: + /// This will default to looking for a `main.mojo` file in: /// - `/main.mojo` - /// - `//main.mojo` - /// - `/src/main.mojo` pub path: Option, /// Extra args to pass to the compiler. #[serde(default, rename(serialize = "extra_args"))] @@ -71,7 +68,7 @@ impl MojoBinConfig { pub fn fill_defaults( conf: Option<&Vec>, manifest_root: &PathBuf, - slug_name: &str, + project_name: &str, ) -> miette::Result<(Option>, bool)> { let main = Self::find_main(manifest_root).map(|p| p.display().to_string()); @@ -80,7 +77,7 @@ impl MojoBinConfig { if let Some(main) = main { return Ok(( Some(vec![Self { - name: Some(slug_name.to_owned()), + name: Some(project_name.to_owned()), path: Some(main), ..Default::default() }]), @@ -98,7 +95,7 @@ impl MojoBinConfig { } if conf[0].name.is_none() { - conf[0].name = Some(slug_name.to_owned()); + conf[0].name = Some(project_name.to_owned()); } if conf[0].path.is_none() { if main.is_none() { @@ -113,7 +110,7 @@ impl MojoBinConfig { if c.name.is_none() { return Err(Error::msg(format!( "Binary configuration {} is missing a name.", - i + i + 1 ))); } if c.path.is_none() { @@ -136,6 +133,8 @@ impl MojoBinConfig { } fn find_main(root: &PathBuf) -> Option { + // Try to find main.mojo in: + // - /main.mojo let mut path = root.join("main"); for ext in ["mojo", "🔥"] { path.set_extension(ext); @@ -148,18 +147,19 @@ impl MojoBinConfig { } /// Config object for a Mojo package. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct MojoPkgConfig { /// Name to give the mojo package (.mojopkg suffix will be added). /// - /// This will default to the slugified name of the project. + /// This will default to the name of the project, any dashes will + /// be replaced with `_`. pub name: Option, /// Path to the directory that constitutes the package. /// /// This will default to lookingo for a folder with an `__init__.mojo` in /// in the following order: - /// - `//__init__.mojo` + /// - `//__init__.mojo` /// - `/src/__init__.mojo` pub path: Option, /// Extra args to pass to the compiler. @@ -175,16 +175,16 @@ impl MojoPkgConfig { pub fn fill_defaults( conf: Option<&Self>, manifest_root: &PathBuf, - slug_name: &str, + package_name: &str, ) -> miette::Result<(Option, bool)> { if let Some(conf) = conf { // A conf was given, make sure it has a name and path let mut conf = conf.clone(); if conf.name.is_none() { - conf.name = Some(slug_name.to_owned()); + conf.name = Some(package_name.to_owned()); } - let path = Self::find_init_parent(manifest_root, slug_name); + let path = Self::find_init_parent(manifest_root, package_name); if conf.path.is_none() { if path.is_none() { return Err(Error::msg(format!( @@ -197,13 +197,13 @@ impl MojoPkgConfig { Ok((Some(conf), false)) } else { // No conf given check if we can find a valid package - let path = Self::find_init_parent(manifest_root, slug_name); + let path = Self::find_init_parent(manifest_root, package_name); if path.is_none() { return Ok((None, false)); } Ok(( Some(Self { - name: Some(slug_name.to_owned()), + name: Some(package_name.to_owned()), path: path.map(|p| p.display().to_string()), ..Default::default() }), @@ -226,20 +226,233 @@ impl MojoPkgConfig { } } -/// Slugify a name for use in [`MojoPkgConfig`] and [`MojoBinconfig`]. -pub fn slugify_name>(s: S) -> String { - slugify(s) +/// Clean the package name for use in [`MojoPkgConfig`] and [`MojoBinconfig`]. +/// +/// This just entails converting - to _. +pub fn clean_project_name(s: &str) -> String { + s.to_owned().replace("-", "_") } #[cfg(test)] mod tests { + use rstest::rstest; use serde_json::json; + use tempfile::TempDir; - use super::MojoBackendConfig; + use super::*; #[test] fn test_ensure_deseralize_from_empty() { let json_data = json!({}); serde_json::from_value::(json_data).unwrap(); } + + #[derive(Debug)] + enum ExpectedBinResult { + Success { name: Option<&'static str>, autodetected: bool }, + Error(&'static str), + } + + struct BinTestCase { + config: Option>, + main_file: Option<&'static str>, + expected: ExpectedBinResult, + } + + #[rstest] + #[case::no_config_no_main(BinTestCase { + config: None, + main_file: None, + expected: ExpectedBinResult::Success { name: None, autodetected: false } + })] + #[case::no_config_with_main_mojo(BinTestCase { + config: None, + main_file: Some("main.mojo"), + expected: ExpectedBinResult::Success { name: Some("test_project"), autodetected: true } + })] + #[case::no_config_with_main_fire(BinTestCase { + config: None, + main_file: Some("main.🔥"), + expected: ExpectedBinResult::Success { name: Some("test_project"), autodetected: true } + })] + #[case::empty_config(BinTestCase { + config: Some(vec![]), + main_file: None, + expected: ExpectedBinResult::Success { name: None, autodetected: false } + })] + #[case::config_missing_name_and_path(BinTestCase { + config: Some(vec![MojoBinConfig::default()]), + main_file: Some("main.mojo"), + expected: ExpectedBinResult::Success { name: Some("test_project"), autodetected: false } + })] + #[case::config_missing_path_no_main(BinTestCase { + config: Some(vec![MojoBinConfig::default()]), + main_file: None, + expected: ExpectedBinResult::Error("Could not find main.mojo for configured binary") + })] + #[case::multiple_bins_missing_name(BinTestCase { + config: Some(vec![ + MojoBinConfig { name: Some("bin1".to_string()), path: Some("main1.mojo".to_string()), ..Default::default() }, + MojoBinConfig { path: Some("main2.mojo".to_string()), ..Default::default() }, + ]), + main_file: None, + expected: ExpectedBinResult::Error("Binary configuration 2 is missing a name.") + })] + #[case::multiple_bins_missing_path(BinTestCase { + config: Some(vec![ + MojoBinConfig { name: Some("bin1".to_string()), path: Some("main1.mojo".to_string()), ..Default::default() }, + MojoBinConfig { name: Some("bin2".to_string()), ..Default::default() }, + ]), + main_file: None, + expected: ExpectedBinResult::Error("Binary configuration bin2 is missing a path.") + })] + #[case::duplicate_names(BinTestCase { + config: Some(vec![ + MojoBinConfig { name: Some("mybin".to_string()), path: Some("main1.mojo".to_string()), ..Default::default() }, + MojoBinConfig { name: Some("mybin".to_string()), path: Some("main2.mojo".to_string()), ..Default::default() }, + ]), + main_file: None, + expected: ExpectedBinResult::Error("Binary name has been used twice: mybin") + })] + fn test_mojo_bin_config_fill_defaults(#[case] test_case: BinTestCase) { + let temp = TempDir::new().unwrap(); + let manifest_root = temp.path().to_path_buf(); + + if let Some(filename) = test_case.main_file { + std::fs::write(manifest_root.join(filename), "def main():\n pass").unwrap(); + } + + let result = MojoBinConfig::fill_defaults(test_case.config.as_ref(), &manifest_root, "test_project"); + + match test_case.expected { + ExpectedBinResult::Success { name: expected_name, autodetected: expected_autodetected } => { + let (bins, autodetected) = result.unwrap(); + assert_eq!(autodetected, expected_autodetected); + + if let Some(expected_name) = expected_name { + assert!(bins.is_some()); + let bins = bins.unwrap(); + assert_eq!(bins.len(), 1); + assert_eq!(bins[0].name, Some(expected_name.to_string())); + if let Some(filename) = test_case.main_file { + assert_eq!( + bins[0].path, + Some(manifest_root.join(filename).display().to_string()) + ); + } + } else { + assert_eq!(bins, None); + } + } + ExpectedBinResult::Error(expected_error) => { + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), expected_error); + } + } + } + + #[derive(Debug)] + enum ExpectedPkgResult { + Success { name: Option<&'static str>, autodetected: bool }, + Error(&'static str), + } + + struct PkgTestCase { + config: Option, + init_file: Option<(&'static str, &'static str)>, // (directory, filename) + expected: ExpectedPkgResult, + } + + #[rstest] + #[case::no_config_no_init(PkgTestCase { + config: None, + init_file: None, + expected: ExpectedPkgResult::Success { name: None, autodetected: false } + })] + #[case::no_config_with_init_in_project_dir(PkgTestCase { + config: None, + init_file: Some(("test_project", "__init__.mojo")), + expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: true } + })] + #[case::no_config_with_init_in_src(PkgTestCase { + config: None, + init_file: Some(("src", "__init__.mojo")), + expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: true } + })] + #[case::no_config_with_init_fire_emoji(PkgTestCase { + config: None, + init_file: Some(("src", "__init__.🔥")), + expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: true } + })] + #[case::config_missing_name_and_path(PkgTestCase { + config: Some(MojoPkgConfig::default()), + init_file: Some(("src", "__init__.mojo")), + expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: false } + })] + #[case::config_with_all_fields(PkgTestCase { + config: Some(MojoPkgConfig { + name: Some("mypackage".to_string()), + path: Some("custom/path".to_string()), + extra_args: Some(vec!["-O3".to_string()]), + }), + init_file: None, + expected: ExpectedPkgResult::Success { name: Some("mypackage"), autodetected: false } + })] + #[case::config_missing_path_no_init(PkgTestCase { + config: Some(MojoPkgConfig::default()), + init_file: None, + expected: ExpectedPkgResult::Error("Could not find valid package path for test_project") + })] + fn test_mojo_pkg_config_fill_defaults(#[case] test_case: PkgTestCase) { + let temp = TempDir::new().unwrap(); + let manifest_root = temp.path().to_path_buf(); + + if let Some((dir, filename)) = test_case.init_file { + let init_dir = manifest_root.join(dir); + std::fs::create_dir_all(&init_dir).unwrap(); + std::fs::write(init_dir.join(filename), "").unwrap(); + } + + let result = MojoPkgConfig::fill_defaults(test_case.config.as_ref(), &manifest_root, "test_project"); + + match test_case.expected { + ExpectedPkgResult::Success { name: expected_name, autodetected: expected_autodetected } => { + let (pkg, autodetected) = result.unwrap(); + assert_eq!(autodetected, expected_autodetected); + + if let Some(expected_name) = expected_name { + assert!(pkg.is_some()); + let pkg = pkg.unwrap(); + assert_eq!(pkg.name, Some(expected_name.to_string())); + + // For the custom config case, check the custom path and args + if expected_name == "mypackage" { + assert_eq!(pkg.path, Some("custom/path".to_string())); + assert_eq!(pkg.extra_args, Some(vec!["-O3".to_string()])); + } else if let Some((dir, _)) = test_case.init_file { + assert_eq!( + pkg.path, + Some(manifest_root.join(dir).display().to_string()) + ); + } + } else { + assert_eq!(pkg, None); + } + } + ExpectedPkgResult::Error(expected_error) => { + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), expected_error); + } + } + } + + #[rstest] + #[case("my-project", "my_project")] + #[case("test_project", "test_project")] + #[case("some-complex-name", "some_complex_name")] + #[case("nodashes", "nodashes")] + #[case("multiple-dashes-here", "multiple_dashes_here")] + fn test_clean_project_name(#[case] input: &str, #[case] expected: &str) { + assert_eq!(clean_project_name(input), expected); + } } diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 5ccb6342..89234f9c 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -7,7 +7,7 @@ use std::{ }; use build_script::BuildScriptContext; -use config::{slugify_name, MojoBackendConfig, MojoBinConfig, MojoPkgConfig}; +use config::{clean_project_name, MojoBackendConfig, MojoBinConfig, MojoPkgConfig}; use miette::{Error, IntoDiagnostic}; use pixi_build_backend::{ generated_recipe::{GenerateRecipe, GeneratedRecipe, PythonParams}, @@ -34,7 +34,7 @@ impl GenerateRecipe for MojoGenerator { let mut generated_recipe = GeneratedRecipe::from_model(model.clone(), manifest_root.clone()); - let slug_name = slugify_name( + let slug_name = clean_project_name( generated_recipe .recipe .package @@ -188,10 +188,9 @@ mod tests { &project_model, &MojoBackendConfig { bins: Some(vec![MojoBinConfig { - name: String::from("example"), - path: String::from("./main.mojo"), + name: Some(String::from("example")), + path: Some(String::from("./main.mojo")), extra_args: Some(vec![String::from("-I"), String::from(".")]), - env: IndexMap::new(), }]), ..Default::default() }, @@ -229,16 +228,14 @@ mod tests { &project_model, &MojoBackendConfig { bins: Some(vec![MojoBinConfig { - name: String::from("example"), - path: String::from("./main.mojo"), + name: Some(String::from("example")), + path: Some(String::from("./main.mojo")), extra_args: Some(vec![String::from("-i"), String::from(".")]), - env: IndexMap::new(), }]), pkg: Some(MojoPkgConfig { - name: String::from("lib"), - path: String::from("mylib"), + name: Some(String::from("lib")), + path: Some(String::from("mylib")), extra_args: Some(vec![String::from("-i"), String::from(".")]), - env: IndexMap::new(), }), ..Default::default() }, @@ -271,11 +268,15 @@ mod tests { } }); + // Create a temporary directory with a main.mojo file so the test has something to build + let temp = tempfile::TempDir::new().unwrap(); + std::fs::write(temp.path().join("main.mojo"), "def main():\n pass").unwrap(); + let generated_recipe = MojoGenerator::default() .generate_recipe( &project_model, &MojoBackendConfig::default(), - PathBuf::from("."), + temp.path().to_path_buf(), Platform::Linux64, None, ) @@ -307,6 +308,10 @@ mod tests { let env = IndexMap::from([("foo".to_string(), "bar".to_string())]); + // Create a temporary directory with a main.mojo file so the test has something to build + let temp = tempfile::TempDir::new().unwrap(); + std::fs::write(temp.path().join("main.mojo"), "def main():\n pass").unwrap(); + let generated_recipe = MojoGenerator::default() .generate_recipe( &project_model, @@ -314,7 +319,7 @@ mod tests { env: env.clone(), ..Default::default() }, - PathBuf::from("."), + temp.path().to_path_buf(), Platform::Linux64, None, ) @@ -351,11 +356,15 @@ mod tests { } }); + // Create a temporary directory with a main.mojo file so the test has something to build + let temp = tempfile::TempDir::new().unwrap(); + std::fs::write(temp.path().join("main.mojo"), "def main():\n pass").unwrap(); + let generated_recipe = MojoGenerator::default() .generate_recipe( &project_model, &MojoBackendConfig::default(), - PathBuf::from("."), + temp.path().to_path_buf(), Platform::Linux64, None, ) diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap index 7dd9a067..ead8397c 100644 --- a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__input_globs_includes_extra_globs.snap @@ -5,7 +5,4 @@ expression: result { "**/*.{mojo,🔥}", "**/.c", - "**/pixi.lock", - "**/pixi.toml", - "**/recipe.yaml", } diff --git a/docs/backends/pixi-build-mojo.md b/docs/backends/pixi-build-mojo.md index 79de590c..25b0f109 100644 --- a/docs/backends/pixi-build-mojo.md +++ b/docs/backends/pixi-build-mojo.md @@ -17,9 +17,22 @@ This backend automatically generates conda packages from Mojo projects. The generated packages can be installed into local envs for devlopment, or packaged for distribution. +### Auto-discovery + +The Mojo backend includes auto-discovery of your project structure: + +- **Binaries**: Automatically searches for `main.mojo` or `main.🔥` in: + - `/main.mojo` +- **Packages**: Automatically searches for directories with `__init__.mojo` or `__init__.🔥` in: + - `//` + - `/src/` + +This means in most cases, you don't need to explicitly configure the `bins` or `pkg` fields. + + ## Basic Usage -To use the Mojo backend in your `pixi.toml`, add it to your package's build configuration: +To use the Mojo backend in your `pixi.toml`, add it to your package's build configuration. The backend will automatically discover your project structure: ```txt @@ -31,11 +44,16 @@ To use the Mojo backend in your `pixi.toml`, add it to your package's build conf ├── main.mojo ├── pixi.lock ├── pixi.toml -├── README.md -└── src +└── README.md ``` -Commented out sections represent optional params that may be useful. +With the project structure above, pixi-build-mojo will automatically discover: +- The binary from `main.mojo` +- The package from `greetings/__init__.mojo` + +**Note** When both a binary and lib are auto-discovered, only the bin will be built as a project artifact. To create a package as well, add a `[package.build.configuration.pkg]` section to manualy confiruge the package. + +Here's a minimal configuration that leverages auto-discovery: ```toml [workspace] @@ -58,21 +76,6 @@ backend = { name = "pixi-build-mojo", version = "0.1.*" } [tasks] -[package.build.configuration] -# dist-dir = "./target" - -[[package.build.configuration.bins]] -name = "greet" -path = "./main.mojo" -# extra-args = ["-I", "special-thing"] -# extra-input-globs = ["**/.c"] - -[package.build.configuration.pkg] -name = "greetings" -path = "greetings" -# extra-args = ["-I", "special-thing"] -# extra-input-globs = ["**/.c"] - [package.host-dependencies] max = "=25.4.0" @@ -90,6 +93,49 @@ max = "=25.4.0" greetings = { path = "." } ``` +### Project Structure Examples + +The auto-discovery feature supports various common project layouts: + +#### Binary-only project +```txt +. +├── main.mojo # Auto-discovered as binary +├── pixi.toml +└── README.md +``` + +#### Package-only project +```txt +. +├── mypackage/ # Auto-discovered if matches project name +│ ├── __init__.mojo +│ └── utils.mojo +├── pixi.toml +└── README.md +``` + +#### Source directory layout +```txt +. +├── src/ +│ ├── __init__.mojo # Auto-discovered as package +│ └── lib.mojo +├── pixi.toml +└── README.md +``` + +#### Combined project (shown earlier) +```txt +. +├── greetings/ +│ ├── __init__.mojo # NOT auto-discovered as package +│ └── lib.mojo +├── main.mojo # Auto-discovered as binary +├── pixi.toml +└── README.md +``` + ### Required Dependencies - `max` package for both the compiler and linked runtime @@ -149,32 +195,40 @@ extra-input-globs = ["**/*.c", "assets/**/*", "*.md"] ### `bins` - **Type**: `Array` -- **Default**: Not set +- **Default**: Auto-discovered if not specified List of binary configurations to build. The created binary will be placed in the `$PREFIX/bin` dir and will be in the path after running `pixi install` assuming the package is listed as a dependency as in the example above. `pixi build` will create a conda package that includes the binary. +**Auto-discovery behavior:** +- If `bins` is not specified, pixi-build-mojo will search for a `main.mojo` or `main.🔥` file in the project root +- If found, it creates a binary with the name set to the project name + #### `bins[].name` - **Type**: `String` -- **Default**: Required field (no default) +- **Default**: Project name (with dashes converted to underscores) for the first binary -The name of the binary executable to create. +The name of the binary executable to create. If not specified: +- For the first binary in the list, defaults to the project name +- For additional binaries, this field is required ```toml [[package.build.configuration.bins]] -name = "greet" +# name = "greet" # Optional for first binary, defaults to project name ``` #### `bins[].path` - **Type**: `String` (path) -- **Default**: Required field (no default) +- **Default**: Auto-discovered for the first binary -The path to the Mojo file that contains a `main` function. +The path to the Mojo file that contains a `main` function. If not specified: +- For the first binary, searches for `main.mojo` or `main.🔥` in the project root +- For additional binaries, this field is required ```toml [[package.build.configuration.bins]] -path = "./main.mojo" +# path = "./main.mojo" # Optional if main.mojo exists in project root ``` #### `bins[].extra-args` @@ -192,16 +246,24 @@ extra-args = ["-I", "special-thing"] ### `pkg` - **Type**: `PkgConfig` -- **Default**: Not set +- **Default**: Auto-discovered if not specified Package configuration for creating Mojo package. The created Mojo package will be placed in the `$PREFIX/lib/mojo` dir, which will make it discoverable to anything that depends on the package. +**Auto-discovery behavior:** +- If `pkg` is not specified, pixi-build-mojo will search for a directory containing `__init__.mojo` or `__init__.🔥` in the following order: + 1. `//` + 2. `/src/` +- If found, it creates a package with the name set to the project name +- If no valid package directory is found, no package is built +- If a binary is also auto-discovered, a pkg will not be generateda and must be manually specified + #### `pkg.name` - **Type**: `String` -- **Default**: Required field (no default) +- **Default**: Project name (with dashes converted to underscores) -The name to give the Mojo package. The `.mojopkg` suffix will be added automatically. +The name to give the Mojo package. The `.mojopkg` suffix will be added automatically. If not specified, defaults to the project name. ```toml [package.build.configuration.pkg] @@ -211,9 +273,9 @@ name = "greetings" #### `pkg.path` - **Type**: `String` (path) -- **Default**: Required field (no default) +- **Default**: Auto-discovered -The path to the directory that constitutes the package. +The path to the directory that constitutes the package. If not specified, searches for a directory with `__init__.mojo` or `__init__.🔥` as described above. ```toml [package.build.configuration.pkg] From 09791d452760bb40c79c791308a651ad21ca1305 Mon Sep 17 00:00:00 2001 From: Seth Date: Sun, 27 Jul 2025 19:49:42 -0400 Subject: [PATCH 16/22] fix: update ordering of checked dirs for pkg --- crates/pixi-build-mojo/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 20a8a88c..53a7ca0e 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -170,7 +170,7 @@ pub struct MojoPkgConfig { impl MojoPkgConfig { /// Fill in any missing info anod or try to find our default options. /// - /// - If None, try to find a `src` or `` dir with an `__init__.mojo` file in it. + /// - If None, try to find a `` or `src` dir with an `__init__.mojo` file in it. /// - If Some, see if name or path need to be filled in. pub fn fill_defaults( conf: Option<&Self>, From 9c40882bffdfe2530421fbad6e1cd178311600ac Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Mon, 28 Jul 2025 00:40:07 +0000 Subject: [PATCH 17/22] fix: clean up logic around auto-derive --- crates/pixi-build-mojo/src/main.rs | 10 +++++++- docs/backends/pixi-build-mojo.md | 41 +++++++++++++++++------------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 89234f9c..1ab7f4e4 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -44,7 +44,7 @@ impl GenerateRecipe for MojoGenerator { ); // Update bins configs - let (bins, bin_autodetected) = + let (mut bins, bin_autodetected) = MojoBinConfig::fill_defaults(config.bins.as_ref(), &manifest_root, &slug_name)?; // Update pkg config @@ -60,6 +60,14 @@ impl GenerateRecipe for MojoGenerator { if bin_autodetected && pkg_autodetected { pkg = None; } + // If either wasn't auto-detected, disable auto-detection of the other + else if bin_autodetected && (!pkg_autodetected && pkg.is_some()) { + // If I'm publishing a pkg, I may not want to also publish a bin + bins = None + } else if (!bin_autodetected && bins.is_some()) && pkg_autodetected { + // If I'm publishing a bin, I may not want to publish the associated pkg + pkg = None + } // Add compiler let requirements = &mut generated_recipe.recipe.requirements; diff --git a/docs/backends/pixi-build-mojo.md b/docs/backends/pixi-build-mojo.md index 25b0f109..9ddffe83 100644 --- a/docs/backends/pixi-build-mojo.md +++ b/docs/backends/pixi-build-mojo.md @@ -17,9 +17,9 @@ This backend automatically generates conda packages from Mojo projects. The generated packages can be installed into local envs for devlopment, or packaged for distribution. -### Auto-discovery +### Auto-derive of pkg and bin -The Mojo backend includes auto-discovery of your project structure: +The Mojo backend includes auto-discovery of your project structure and will derive the following: - **Binaries**: Automatically searches for `main.mojo` or `main.🔥` in: - `/main.mojo` @@ -29,6 +29,11 @@ The Mojo backend includes auto-discovery of your project structure: This means in most cases, you don't need to explicitly configure the `bins` or `pkg` fields. +**Caveats**: +- If both a `bin` and a `pkg` are auto-derived, only the `bin` will be created, you must manually specify the pkg. +- If the user specifies a `pkg` a `bin` will not be auto-derived. +- If the user specifies a `bin` a `pkg` will not be auto-derived. + ## Basic Usage @@ -51,9 +56,7 @@ With the project structure above, pixi-build-mojo will automatically discover: - The binary from `main.mojo` - The package from `greetings/__init__.mojo` -**Note** When both a binary and lib are auto-discovered, only the bin will be built as a project artifact. To create a package as well, add a `[package.build.configuration.pkg]` section to manualy confiruge the package. - -Here's a minimal configuration that leverages auto-discovery: +Here's a minimal configuration that leverages auto-derive: ```toml [workspace] @@ -95,12 +98,12 @@ greetings = { path = "." } ### Project Structure Examples -The auto-discovery feature supports various common project layouts: +The auto-derive feature supports various common project layouts: #### Binary-only project ```txt . -├── main.mojo # Auto-discovered as binary +├── main.mojo # Auto-derive as binary ├── pixi.toml └── README.md ``` @@ -108,7 +111,7 @@ The auto-discovery feature supports various common project layouts: #### Package-only project ```txt . -├── mypackage/ # Auto-discovered if matches project name +├── mypackage/ # Auto-derive if matches project name │ ├── __init__.mojo │ └── utils.mojo ├── pixi.toml @@ -119,7 +122,7 @@ The auto-discovery feature supports various common project layouts: ```txt . ├── src/ -│ ├── __init__.mojo # Auto-discovered as package +│ ├── __init__.mojo # Auto-derive as package │ └── lib.mojo ├── pixi.toml └── README.md @@ -129,9 +132,9 @@ The auto-discovery feature supports various common project layouts: ```txt . ├── greetings/ -│ ├── __init__.mojo # NOT auto-discovered as package +│ ├── __init__.mojo # NOT auto-derived as package │ └── lib.mojo -├── main.mojo # Auto-discovered as binary +├── main.mojo # Auto-derived as binary ├── pixi.toml └── README.md ``` @@ -195,13 +198,14 @@ extra-input-globs = ["**/*.c", "assets/**/*", "*.md"] ### `bins` - **Type**: `Array` -- **Default**: Auto-discovered if not specified +- **Default**: Auto-derived if not specified List of binary configurations to build. The created binary will be placed in the `$PREFIX/bin` dir and will be in the path after running `pixi install` assuming the package is listed as a dependency as in the example above. `pixi build` will create a conda package that includes the binary. -**Auto-discovery behavior:** +**Auto-derive behavior:** - If `bins` is not specified, pixi-build-mojo will search for a `main.mojo` or `main.🔥` file in the project root - If found, it creates a binary with the name set to the project name +- If a pkg has been manually configured, a bin will not be auto-derived and must be manually configured. #### `bins[].name` @@ -220,7 +224,7 @@ The name of the binary executable to create. If not specified: #### `bins[].path` - **Type**: `String` (path) -- **Default**: Auto-discovered for the first binary +- **Default**: Auto-derived for the first binary The path to the Mojo file that contains a `main` function. If not specified: - For the first binary, searches for `main.mojo` or `main.🔥` in the project root @@ -246,17 +250,18 @@ extra-args = ["-I", "special-thing"] ### `pkg` - **Type**: `PkgConfig` -- **Default**: Auto-discovered if not specified +- **Default**: Auto-derived if not specified Package configuration for creating Mojo package. The created Mojo package will be placed in the `$PREFIX/lib/mojo` dir, which will make it discoverable to anything that depends on the package. -**Auto-discovery behavior:** +**Auto-derive behavior:** - If `pkg` is not specified, pixi-build-mojo will search for a directory containing `__init__.mojo` or `__init__.🔥` in the following order: 1. `//` 2. `/src/` - If found, it creates a package with the name set to the project name - If no valid package directory is found, no package is built -- If a binary is also auto-discovered, a pkg will not be generateda and must be manually specified +- If a binary is manually configured, a pkg will not be auto-derived and must be manually specified. +- If a binary is also auto-derive, a pkg will not be generated and must be manually specified #### `pkg.name` @@ -273,7 +278,7 @@ name = "greetings" #### `pkg.path` - **Type**: `String` (path) -- **Default**: Auto-discovered +- **Default**: Auto-derive The path to the directory that constitutes the package. If not specified, searches for a directory with `__init__.mojo` or `__init__.🔥` as described above. From 7d6f849d88c766922489a1c3bcf8be0b65950bc1 Mon Sep 17 00:00:00 2001 From: Seth Date: Sun, 27 Jul 2025 20:41:10 -0400 Subject: [PATCH 18/22] fix: update comment --- crates/pixi-build-mojo/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 1ab7f4e4..54b2bef7 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -56,7 +56,7 @@ impl GenerateRecipe for MojoGenerator { return Err(Error::msg("No bin or pkg configuration detected.")); } - // If we are auto-generating both, keep only the bin? + // If we are auto-generating both, keep only the bin if bin_autodetected && pkg_autodetected { pkg = None; } From fdedb4a1e9c5d8fd275274dfc489edf94dc7c788 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Mon, 28 Jul 2025 14:31:23 +0000 Subject: [PATCH 19/22] chore: cleanup code more --- crates/pixi-build-mojo/src/config.rs | 232 ++++++++++++++++++--------- crates/pixi-build-mojo/src/main.rs | 33 +--- 2 files changed, 163 insertions(+), 102 deletions(-) diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 53a7ca0e..1559b658 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -8,6 +8,7 @@ use miette::Error; use pixi_build_backend::generated_recipe::BackendConfig; use serde::{Deserialize, Serialize}; +/// Top level config struct for the Mojo backend. #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct MojoBackendConfig { @@ -40,6 +41,51 @@ impl BackendConfig for MojoBackendConfig { } } +impl MojoBackendConfig { + /// Auto-derive the bins and pkg config if they have not been specified by the user, + /// or if they have only been partially specified. + /// + /// See [`MojoPkgConfig`] and [`MojoBinConfig`] for details on how they are derived. + /// The following rules are applied for choosing when to use derived configs: + /// + /// - If a `pkg` has been specified by user, don't derive `bins` + /// - If a `bin` has been specified by user, don't derive `pkg` + /// - If both a `pkg` and `bin` have been auto-derived, only keep the `bin` + pub fn auto_derive( + &self, + manifest_root: &PathBuf, + project_name: &str, + ) -> miette::Result<(Option>, Option)> { + // Update bins configs + let (mut bins, bin_autodetected) = + MojoBinConfig::auto_derive(self.bins.as_ref(), &manifest_root, &project_name)?; + + // Update pkg config + let (mut pkg, pkg_autodetected) = + MojoPkgConfig::auto_derive(self.pkg.as_ref(), &manifest_root, &project_name)?; + + // Make sure we have at least one of the two + if bins.is_none() && pkg.is_none() { + return Err(Error::msg("No bin or pkg configuration detected.")); + } + + // If we are auto-generating both, keep only the bin + if bin_autodetected && pkg_autodetected { + pkg = None; + } + // If either wasn't auto-detected, disable auto-detection of the other + else if bin_autodetected && (!pkg_autodetected && pkg.is_some()) { + // If I'm publishing a pkg, I may not want to also publish a bin + bins = None + } else if (!bin_autodetected && bins.is_some()) && pkg_autodetected { + // If I'm publishing a bin, I may not want to publish the associated pkg + pkg = None + } + + Ok((bins, pkg)) + } +} + /// Config object for a Mojo binary. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -49,11 +95,13 @@ pub struct MojoBinConfig { /// This will default to the name of the project for the first /// binary selected, any dashes will be replaced with `_`. pub name: Option, + /// Path to file that has the `main` method. /// /// This will default to looking for a `main.mojo` file in: /// - `/main.mojo` pub path: Option, + /// Extra args to pass to the compiler. #[serde(default, rename(serialize = "extra_args"))] pub extra_args: Option>, @@ -65,7 +113,7 @@ impl MojoBinConfig { /// - If None, try to find a `main.mojo` file in manfiest_root /// - If any, for the first one, see if name or path need to be filled in /// - If any, verify that there are no name collisions - pub fn fill_defaults( + pub fn auto_derive( conf: Option<&Vec>, manifest_root: &PathBuf, project_name: &str, @@ -132,9 +180,9 @@ impl MojoBinConfig { Ok((Some(conf), false)) } + /// Try to find main.mojo in: + /// - /main.mojo fn find_main(root: &PathBuf) -> Option { - // Try to find main.mojo in: - // - /main.mojo let mut path = root.join("main"); for ext in ["mojo", "🔥"] { path.set_extension(ext); @@ -155,6 +203,7 @@ pub struct MojoPkgConfig { /// This will default to the name of the project, any dashes will /// be replaced with `_`. pub name: Option, + /// Path to the directory that constitutes the package. /// /// This will default to lookingo for a folder with an `__init__.mojo` in @@ -162,6 +211,7 @@ pub struct MojoPkgConfig { /// - `//__init__.mojo` /// - `/src/__init__.mojo` pub path: Option, + /// Extra args to pass to the compiler. #[serde(default, rename(serialize = "extra_args"))] pub extra_args: Option>, @@ -172,7 +222,7 @@ impl MojoPkgConfig { /// /// - If None, try to find a `` or `src` dir with an `__init__.mojo` file in it. /// - If Some, see if name or path need to be filled in. - pub fn fill_defaults( + pub fn auto_derive( conf: Option<&Self>, manifest_root: &PathBuf, package_name: &str, @@ -212,6 +262,13 @@ impl MojoPkgConfig { } } + /// Find the parent directory of a possible package `__init__.mojo` file. + /// + /// This checks (in this order): + /// - `` + /// - src + /// + /// and returns the first one found. fn find_init_parent(root: &PathBuf, project_name: &str) -> Option { for dir in [project_name, "src"] { let mut path = root.join(dir).join("__init__"); @@ -249,83 +306,97 @@ mod tests { #[derive(Debug)] enum ExpectedBinResult { - Success { name: Option<&'static str>, autodetected: bool }, + /// A possible binary name that would be found, as well as whether or not + /// it was auto-detected, or whether it was found via user configuration. + Success { + binary_name: Option<&'static str>, + autodetected: bool, + }, + /// There was some misconfiguration resulting in an error to show the user. Error(&'static str), } struct BinTestCase { + /// User specified config. config: Option>, + /// Path to a project `main.mojo` file to be created in a temp dir. main_file: Option<&'static str>, + /// The expected result of the test. expected: ExpectedBinResult, } #[rstest] - #[case::no_config_no_main(BinTestCase { - config: None, - main_file: None, - expected: ExpectedBinResult::Success { name: None, autodetected: false } + #[case::no_config_no_main(BinTestCase { + config: None, + main_file: None, + expected: ExpectedBinResult::Success { binary_name: None, autodetected: false } })] - #[case::no_config_with_main_mojo(BinTestCase { - config: None, - main_file: Some("main.mojo"), - expected: ExpectedBinResult::Success { name: Some("test_project"), autodetected: true } + #[case::no_config_with_main_mojo(BinTestCase { + config: None, + main_file: Some("main.mojo"), + expected: ExpectedBinResult::Success { binary_name: Some("test_project"), autodetected: true } })] - #[case::no_config_with_main_fire(BinTestCase { - config: None, - main_file: Some("main.🔥"), - expected: ExpectedBinResult::Success { name: Some("test_project"), autodetected: true } + #[case::no_config_with_main_fire(BinTestCase { + config: None, + main_file: Some("main.🔥"), + expected: ExpectedBinResult::Success { binary_name: Some("test_project"), autodetected: true } })] - #[case::empty_config(BinTestCase { - config: Some(vec![]), - main_file: None, - expected: ExpectedBinResult::Success { name: None, autodetected: false } + #[case::empty_config(BinTestCase { + config: Some(vec![]), + main_file: None, + expected: ExpectedBinResult::Success { binary_name: None, autodetected: false } })] - #[case::config_missing_name_and_path(BinTestCase { - config: Some(vec![MojoBinConfig::default()]), - main_file: Some("main.mojo"), - expected: ExpectedBinResult::Success { name: Some("test_project"), autodetected: false } + #[case::config_missing_name_and_path(BinTestCase { + config: Some(vec![MojoBinConfig::default()]), + main_file: Some("main.mojo"), + expected: ExpectedBinResult::Success { binary_name: Some("test_project"), autodetected: false } })] - #[case::config_missing_path_no_main(BinTestCase { - config: Some(vec![MojoBinConfig::default()]), - main_file: None, - expected: ExpectedBinResult::Error("Could not find main.mojo for configured binary") + #[case::config_missing_path_no_main(BinTestCase { + config: Some(vec![MojoBinConfig::default()]), + main_file: None, + expected: ExpectedBinResult::Error("Could not find main.mojo for configured binary") })] - #[case::multiple_bins_missing_name(BinTestCase { + #[case::multiple_bins_missing_name(BinTestCase { config: Some(vec![ MojoBinConfig { name: Some("bin1".to_string()), path: Some("main1.mojo".to_string()), ..Default::default() }, MojoBinConfig { path: Some("main2.mojo".to_string()), ..Default::default() }, - ]), - main_file: None, - expected: ExpectedBinResult::Error("Binary configuration 2 is missing a name.") + ]), + main_file: None, + expected: ExpectedBinResult::Error("Binary configuration 2 is missing a name.") })] - #[case::multiple_bins_missing_path(BinTestCase { + #[case::multiple_bins_missing_path(BinTestCase { config: Some(vec![ MojoBinConfig { name: Some("bin1".to_string()), path: Some("main1.mojo".to_string()), ..Default::default() }, MojoBinConfig { name: Some("bin2".to_string()), ..Default::default() }, - ]), - main_file: None, - expected: ExpectedBinResult::Error("Binary configuration bin2 is missing a path.") + ]), + main_file: None, + expected: ExpectedBinResult::Error("Binary configuration bin2 is missing a path.") })] - #[case::duplicate_names(BinTestCase { + #[case::duplicate_names(BinTestCase { config: Some(vec![ MojoBinConfig { name: Some("mybin".to_string()), path: Some("main1.mojo".to_string()), ..Default::default() }, MojoBinConfig { name: Some("mybin".to_string()), path: Some("main2.mojo".to_string()), ..Default::default() }, - ]), - main_file: None, - expected: ExpectedBinResult::Error("Binary name has been used twice: mybin") + ]), + main_file: None, + expected: ExpectedBinResult::Error("Binary name has been used twice: mybin") })] fn test_mojo_bin_config_fill_defaults(#[case] test_case: BinTestCase) { let temp = TempDir::new().unwrap(); let manifest_root = temp.path().to_path_buf(); + // Write the main.mojo file specified by test case, if present if let Some(filename) = test_case.main_file { std::fs::write(manifest_root.join(filename), "def main():\n pass").unwrap(); } - let result = MojoBinConfig::fill_defaults(test_case.config.as_ref(), &manifest_root, "test_project"); + let result = + MojoBinConfig::auto_derive(test_case.config.as_ref(), &manifest_root, "test_project"); match test_case.expected { - ExpectedBinResult::Success { name: expected_name, autodetected: expected_autodetected } => { + ExpectedBinResult::Success { + binary_name: expected_name, + autodetected: expected_autodetected, + } => { let (bins, autodetected) = result.unwrap(); assert_eq!(autodetected, expected_autodetected); @@ -353,55 +424,64 @@ mod tests { #[derive(Debug)] enum ExpectedPkgResult { - Success { name: Option<&'static str>, autodetected: bool }, + /// A possible pkg name that would be found, as well as whether or not + /// it was auto-detected, or whether it was found via user configuration. + Success { + name: Option<&'static str>, + autodetected: bool, + }, + /// An expected error message that the user would be shown. Error(&'static str), } struct PkgTestCase { + /// User defined config for the pkg config: Option, + /// Path to a `__init__.mojo` file to be created in a temp dir. init_file: Option<(&'static str, &'static str)>, // (directory, filename) + /// Expected result of the test. expected: ExpectedPkgResult, } #[rstest] - #[case::no_config_no_init(PkgTestCase { - config: None, - init_file: None, - expected: ExpectedPkgResult::Success { name: None, autodetected: false } + #[case::no_config_no_init(PkgTestCase { + config: None, + init_file: None, + expected: ExpectedPkgResult::Success { name: None, autodetected: false } })] - #[case::no_config_with_init_in_project_dir(PkgTestCase { - config: None, - init_file: Some(("test_project", "__init__.mojo")), - expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: true } + #[case::no_config_with_init_in_project_dir(PkgTestCase { + config: None, + init_file: Some(("test_project", "__init__.mojo")), + expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: true } })] - #[case::no_config_with_init_in_src(PkgTestCase { - config: None, - init_file: Some(("src", "__init__.mojo")), - expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: true } + #[case::no_config_with_init_in_src(PkgTestCase { + config: None, + init_file: Some(("src", "__init__.mojo")), + expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: true } })] - #[case::no_config_with_init_fire_emoji(PkgTestCase { - config: None, - init_file: Some(("src", "__init__.🔥")), - expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: true } + #[case::no_config_with_init_fire_emoji(PkgTestCase { + config: None, + init_file: Some(("src", "__init__.🔥")), + expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: true } })] - #[case::config_missing_name_and_path(PkgTestCase { - config: Some(MojoPkgConfig::default()), - init_file: Some(("src", "__init__.mojo")), - expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: false } + #[case::config_missing_name_and_path(PkgTestCase { + config: Some(MojoPkgConfig::default()), + init_file: Some(("src", "__init__.mojo")), + expected: ExpectedPkgResult::Success { name: Some("test_project"), autodetected: false } })] - #[case::config_with_all_fields(PkgTestCase { + #[case::config_with_all_fields(PkgTestCase { config: Some(MojoPkgConfig { name: Some("mypackage".to_string()), path: Some("custom/path".to_string()), extra_args: Some(vec!["-O3".to_string()]), - }), - init_file: None, - expected: ExpectedPkgResult::Success { name: Some("mypackage"), autodetected: false } + }), + init_file: None, + expected: ExpectedPkgResult::Success { name: Some("mypackage"), autodetected: false } })] - #[case::config_missing_path_no_init(PkgTestCase { - config: Some(MojoPkgConfig::default()), - init_file: None, - expected: ExpectedPkgResult::Error("Could not find valid package path for test_project") + #[case::config_missing_path_no_init(PkgTestCase { + config: Some(MojoPkgConfig::default()), + init_file: None, + expected: ExpectedPkgResult::Error("Could not find valid package path for test_project") })] fn test_mojo_pkg_config_fill_defaults(#[case] test_case: PkgTestCase) { let temp = TempDir::new().unwrap(); @@ -413,10 +493,14 @@ mod tests { std::fs::write(init_dir.join(filename), "").unwrap(); } - let result = MojoPkgConfig::fill_defaults(test_case.config.as_ref(), &manifest_root, "test_project"); + let result = + MojoPkgConfig::auto_derive(test_case.config.as_ref(), &manifest_root, "test_project"); match test_case.expected { - ExpectedPkgResult::Success { name: expected_name, autodetected: expected_autodetected } => { + ExpectedPkgResult::Success { + name: expected_name, + autodetected: expected_autodetected, + } => { let (pkg, autodetected) = result.unwrap(); assert_eq!(autodetected, expected_autodetected); diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 54b2bef7..fd76d6df 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -7,13 +7,13 @@ use std::{ }; use build_script::BuildScriptContext; -use config::{clean_project_name, MojoBackendConfig, MojoBinConfig, MojoPkgConfig}; +use config::{MojoBackendConfig, clean_project_name}; use miette::{Error, IntoDiagnostic}; use pixi_build_backend::{ generated_recipe::{GenerateRecipe, GeneratedRecipe, PythonParams}, intermediate_backend::IntermediateBackendInstantiator, }; -use rattler_build::{recipe::variable::Variable, NormalizedKey}; +use rattler_build::{NormalizedKey, recipe::variable::Variable}; use rattler_conda_types::{PackageName, Platform}; use recipe_stage0::recipe::Script; @@ -34,7 +34,7 @@ impl GenerateRecipe for MojoGenerator { let mut generated_recipe = GeneratedRecipe::from_model(model.clone(), manifest_root.clone()); - let slug_name = clean_project_name( + let cleaned_project_name = clean_project_name( generated_recipe .recipe .package @@ -43,31 +43,8 @@ impl GenerateRecipe for MojoGenerator { .ok_or(Error::msg("Package is missing a name"))?, ); - // Update bins configs - let (mut bins, bin_autodetected) = - MojoBinConfig::fill_defaults(config.bins.as_ref(), &manifest_root, &slug_name)?; - - // Update pkg config - let (mut pkg, pkg_autodetected) = - MojoPkgConfig::fill_defaults(config.pkg.as_ref(), &manifest_root, &slug_name)?; - - // Make sure we have at least one of the two - if bins.is_none() && pkg.is_none() { - return Err(Error::msg("No bin or pkg configuration detected.")); - } - - // If we are auto-generating both, keep only the bin - if bin_autodetected && pkg_autodetected { - pkg = None; - } - // If either wasn't auto-detected, disable auto-detection of the other - else if bin_autodetected && (!pkg_autodetected && pkg.is_some()) { - // If I'm publishing a pkg, I may not want to also publish a bin - bins = None - } else if (!bin_autodetected && bins.is_some()) && pkg_autodetected { - // If I'm publishing a bin, I may not want to publish the associated pkg - pkg = None - } + // Auto-derive bins and pkg fields/configs if needed + let (bins, pkg) = config.auto_derive(&manifest_root, &cleaned_project_name)?; // Add compiler let requirements = &mut generated_recipe.recipe.requirements; From 3905f85effff3320f6347d1d98098a34e99574f8 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Tue, 29 Jul 2025 14:45:06 +0000 Subject: [PATCH 20/22] fix: remove dist_dir and update docs on extra-input-globs --- crates/pixi-build-mojo/src/build_script.j2 | 9 --------- crates/pixi-build-mojo/src/build_script.rs | 2 -- crates/pixi-build-mojo/src/config.rs | 5 ----- crates/pixi-build-mojo/src/main.rs | 5 ++--- .../pixi_build_mojo__tests__mojo_bin_is_set.snap | 3 --- .../pixi_build_mojo__tests__mojo_pkg_is_set.snap | 4 ---- docs/backends/pixi-build-mojo.md | 14 +------------- 7 files changed, 3 insertions(+), 39 deletions(-) diff --git a/crates/pixi-build-mojo/src/build_script.j2 b/crates/pixi-build-mojo/src/build_script.j2 index 0762782d..b52c850c 100644 --- a/crates/pixi-build-mojo/src/build_script.j2 +++ b/crates/pixi-build-mojo/src/build_script.j2 @@ -8,24 +8,15 @@ mojo --version -{% if dist %} - mkdir -p {{ source_dir }}/{{ dist }} -{% endif %} {#- Build any binaries -#} {% if bins %} {% for bin in bins %} mojo build {{ bin.extra_args | join(" ") }} {{ bin.path }} -o {{ library_prefix }}/bin/{{ bin.name }} - {% if dist %} - cp {{ library_prefix }}/bin/{{ bin.name }} {{ source_dir }}/{{ dist }} - {% endif %} {% endfor %} {% endif %} {#- Build pkg -#} {% if pkg %} mojo package {{ pkg.extra_args | join(" ") }} {{ pkg.path }} -o {{ library_prefix }}/lib/mojo/{{ pkg.name}}.mojopkg - {% if dist %} - cp {{ library_prefix }}/lib/mojo/{{ pkg.name }}.mojopkg {{ source_dir }}/{{ dist }} - {% endif %} {% endif %} diff --git a/crates/pixi-build-mojo/src/build_script.rs b/crates/pixi-build-mojo/src/build_script.rs index 6892bf40..1cd4f871 100644 --- a/crates/pixi-build-mojo/src/build_script.rs +++ b/crates/pixi-build-mojo/src/build_script.rs @@ -6,8 +6,6 @@ use serde::Serialize; pub struct BuildScriptContext { /// The directory where the source code is located, the manifest root. pub source_dir: String, - /// The directory name to place output artifacts, will be created in `source_dir`. - pub dist: Option, /// Any executable artifacts to create. pub bins: Option>, /// Any packages to create. diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 1559b658..5669390c 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -16,11 +16,6 @@ pub struct MojoBackendConfig { #[serde(default)] pub env: IndexMap, - /// Directory that will be created to place output artifacts. - /// - /// This is releative to the manifest dir. - pub dist_dir: Option, - /// Dir that can be specified for outputting pixi debug state. pub debug_dir: Option, diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index fd76d6df..9227b072 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -7,13 +7,13 @@ use std::{ }; use build_script::BuildScriptContext; -use config::{MojoBackendConfig, clean_project_name}; +use config::{clean_project_name, MojoBackendConfig}; use miette::{Error, IntoDiagnostic}; use pixi_build_backend::{ generated_recipe::{GenerateRecipe, GeneratedRecipe, PythonParams}, intermediate_backend::IntermediateBackendInstantiator, }; -use rattler_build::{NormalizedKey, recipe::variable::Variable}; +use rattler_build::{recipe::variable::Variable, NormalizedKey}; use rattler_conda_types::{PackageName, Platform}; use recipe_stage0::recipe::Script; @@ -65,7 +65,6 @@ impl GenerateRecipe for MojoGenerator { let build_script = BuildScriptContext { source_dir: manifest_root.display().to_string(), - dist: config.dist_dir.clone().map(|d| d.display().to_string()), bins, pkg, } diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_bin_is_set.snap b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_bin_is_set.snap index 7c04020a..899172ea 100644 --- a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_bin_is_set.snap +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_bin_is_set.snap @@ -14,11 +14,8 @@ build: script: content: - mojo --version - - "" - - "" - "\t" - "\t\tmojo build -I . ./main.mojo -o $PREFIX/bin/example" - - "\t\t" - "\t" env: {} secrets: [] diff --git a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_pkg_is_set.snap b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_pkg_is_set.snap index 8640c2de..87c276d0 100644 --- a/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_pkg_is_set.snap +++ b/crates/pixi-build-mojo/src/snapshots/pixi_build_mojo__tests__mojo_pkg_is_set.snap @@ -14,15 +14,11 @@ build: script: content: - mojo --version - - "" - - "" - "\t" - "\t\tmojo build -i . ./main.mojo -o $PREFIX/bin/example" - - "\t\t" - "\t" - "" - "\tmojo package -i . mylib -o $PREFIX/lib/mojo/lib.mojopkg" - - "\t" env: {} secrets: [] requirements: diff --git a/docs/backends/pixi-build-mojo.md b/docs/backends/pixi-build-mojo.md index 9ddffe83..38646d34 100644 --- a/docs/backends/pixi-build-mojo.md +++ b/docs/backends/pixi-build-mojo.md @@ -159,18 +159,6 @@ Environment variables to set during the build process. env = { ASSERT = "all" } ``` -#### `dist-dir` - -- **Type**: `String` (path) -- **Default**: Not set - -A directory to create relative to the manifest pixi.toml to copy build artifacts into. `pixi install` will install the artifacts into your local enviroment and as such the `dist-dir` is mostly for debugging. - -```toml -[package.build.configuration] -dist-dir = "./target" -``` - #### `debug-dir` - **Type**: `String` (path) @@ -188,7 +176,7 @@ debug-dir = ".build-debug" - **Type**: `Array` - **Default**: `[]` -Additional globs to pass to pixi for including more than just mojo files in the build. +Additional globs to pass to pixi to discover if the package should be rebuilt. ```toml [package.build.configuration] From 72ff2ad396cbf614beb715cfdd0441f0b91333a5 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Tue, 29 Jul 2025 18:55:25 +0000 Subject: [PATCH 21/22] fix: add merge for specific target and small updates post rebase --- crates/pixi-build-mojo/Cargo.toml | 2 +- crates/pixi-build-mojo/src/config.rs | 153 ++++++++++++++++++++++++++- crates/pixi-build-mojo/src/main.rs | 11 +- 3 files changed, 159 insertions(+), 7 deletions(-) diff --git a/crates/pixi-build-mojo/Cargo.toml b/crates/pixi-build-mojo/Cargo.toml index 561d084e..5ba8e93b 100644 --- a/crates/pixi-build-mojo/Cargo.toml +++ b/crates/pixi-build-mojo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pixi-build-mojo" -version = "0.2.0" +version = "0.1.0" edition.workspace = true [profile.dev.package] diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index 5669390c..b5ff993e 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, path::{Path, PathBuf}, }; @@ -9,7 +9,7 @@ use pixi_build_backend::generated_recipe::BackendConfig; use serde::{Deserialize, Serialize}; /// Top level config struct for the Mojo backend. -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct MojoBackendConfig { /// Environment Variables @@ -34,6 +34,93 @@ impl BackendConfig for MojoBackendConfig { 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: + /// + /// - 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 + /// - bins: Any bins with matching not-None names will be merged, + /// Any set-settings on the platform specific pkg override base + /// Any bins found only in target_config will be kept + /// - pkg: Any set-settings on the platform specific pkg override 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"); + } + + let pkg = if target_config.pkg.is_some() { + if self.pkg.is_some() { + Some( + self.pkg + .as_ref() + .unwrap() + .merge_with_target_config(target_config.pkg.as_ref().unwrap())?, + ) + } else { + target_config.pkg.clone() + } + } else { + self.pkg.clone() + }; + + let bins = if target_config.bins.is_some() { + if self.bins.is_some() { + // Both base and target have binaries configured + // Override base with anything found in both target and base. + // If something is found only in base, drop it. + // If somethign is found only in target, drop it. + let base_bins: HashMap<_, _> = self + .bins + .as_ref() + .unwrap() + .iter() + .filter(|p| p.name.is_some()) + .map(|p| (p.name.clone().unwrap(), p.clone())) + .collect(); + + Some( + target_config + .bins + .as_ref() + .unwrap() + .iter() + .map(|p| match p.name.as_ref() { + Some(name) => { + if let Some(base_bin) = base_bins.get(name) { + base_bin.merge_with_target_config(p) + } else { + Ok(p.clone()) + } + } + None => Ok(p.clone()), + }) + .collect::>()?, + ) + } else { + target_config.bins.clone() + } + } else { + self.bins.clone() + }; + + 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() + }, + bins, + pkg, + }) + } } impl MojoBackendConfig { @@ -187,6 +274,39 @@ impl MojoBinConfig { } None } + + /// Merge with a target-specific configuration. + /// + /// All target-settings that are not None will override base. + /// + /// **Note** bins must have the same name to be merged. + fn merge_with_target_config(&self, target_config: &Self) -> miette::Result { + if self.name.is_some() && target_config.name.is_some() { + if self.name.as_ref().unwrap() != target_config.name.as_ref().unwrap() { + miette::bail!("Both bins must have a set name to be merged"); + } + } else { + miette::bail!("Both bins must have a set name to be merged"); + } + + let path = if target_config.path.is_some() { + target_config.path.clone() + } else { + self.path.clone() + }; + + let extra_args = if target_config.extra_args.is_some() { + target_config.extra_args.clone() + } else { + self.extra_args.clone() + }; + + Ok(Self { + name: self.name.clone(), + path, + extra_args, + }) + } } /// Config object for a Mojo package. @@ -276,6 +396,35 @@ impl MojoPkgConfig { } None } + + /// Merge with a target-specific configuration. + /// + /// All target-settings that are not None will override base. + fn merge_with_target_config(&self, target_config: &Self) -> miette::Result { + let name = if target_config.name.is_some() { + target_config.name.clone() + } else { + self.name.clone() + }; + + let path = if target_config.path.is_some() { + target_config.path.clone() + } else { + self.path.clone() + }; + + let extra_args = if target_config.extra_args.is_some() { + target_config.extra_args.clone() + } else { + self.extra_args.clone() + }; + + Ok(Self { + name, + path, + extra_args, + }) + } } /// Clean the package name for use in [`MojoPkgConfig`] and [`MojoBinconfig`]. diff --git a/crates/pixi-build-mojo/src/main.rs b/crates/pixi-build-mojo/src/main.rs index 9227b072..b9673190 100644 --- a/crates/pixi-build-mojo/src/main.rs +++ b/crates/pixi-build-mojo/src/main.rs @@ -4,16 +4,17 @@ mod config; use std::{ collections::{BTreeMap, BTreeSet}, path::Path, + sync::Arc, }; use build_script::BuildScriptContext; -use config::{clean_project_name, MojoBackendConfig}; +use config::{MojoBackendConfig, clean_project_name}; use miette::{Error, IntoDiagnostic}; use pixi_build_backend::{ generated_recipe::{GenerateRecipe, GeneratedRecipe, PythonParams}, intermediate_backend::IntermediateBackendInstantiator, }; -use rattler_build::{recipe::variable::Variable, NormalizedKey}; +use rattler_build::{NormalizedKey, recipe::variable::Variable}; use rattler_conda_types::{PackageName, Platform}; use recipe_stage0::recipe::Script; @@ -109,8 +110,10 @@ impl MojoGenerator { #[tokio::main] pub async fn main() { - if let Err(err) = - pixi_build_backend::cli::main(IntermediateBackendInstantiator::::new).await + if let Err(err) = pixi_build_backend::cli::main(|log| { + IntermediateBackendInstantiator::::new(log, Arc::default()) + }) + .await { eprintln!("{err:?}"); std::process::exit(1); From 0cfc1795d9de9060a55c2cc066a1810bd2b653a3 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Tue, 29 Jul 2025 19:25:45 +0000 Subject: [PATCH 22/22] fix: failing ci --- Cargo.lock | 2 +- .../src/generated_recipe.rs | 4 ++-- crates/pixi-build-mojo/src/config.rs | 22 +++++++++---------- docs/backends/pixi-build-mojo.md | 2 +- docs/index.md | 1 + mkdocs.yml | 1 + 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index beebe085..15a96e6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3981,7 +3981,7 @@ dependencies = [ [[package]] name = "pixi-build-mojo" -version = "0.2.0" +version = "0.1.0" dependencies = [ "async-trait", "chrono", diff --git a/crates/pixi-build-backend/src/generated_recipe.rs b/crates/pixi-build-backend/src/generated_recipe.rs index 211cb2d2..fa89a14e 100644 --- a/crates/pixi-build-backend/src/generated_recipe.rs +++ b/crates/pixi-build-backend/src/generated_recipe.rs @@ -5,7 +5,7 @@ use std::{ }; use pixi_build_types::ProjectModelV1; -use rattler_build::{recipe::variable::Variable, NormalizedKey}; +use rattler_build::{NormalizedKey, recipe::variable::Variable}; use rattler_conda_types::Platform; use recipe_stage0::recipe::{ConditionalList, IntermediateRecipe, Item, Package, Source, Value}; use serde::de::DeserializeOwned; @@ -82,7 +82,7 @@ pub trait BackendConfig: DeserializeOwned + Clone { fn merge_with_target_config(&self, target_config: &Self) -> miette::Result; } -#[derive(Default)] +#[derive(Default, Clone)] pub struct GeneratedRecipe { pub recipe: IntermediateRecipe, pub metadata_input_globs: BTreeSet, diff --git a/crates/pixi-build-mojo/src/config.rs b/crates/pixi-build-mojo/src/config.rs index b5ff993e..b77407b4 100644 --- a/crates/pixi-build-mojo/src/config.rs +++ b/crates/pixi-build-mojo/src/config.rs @@ -42,8 +42,8 @@ impl BackendConfig for MojoBackendConfig { /// - debug_dir: Not allowed to have target specific value /// - extra_input_globs: Platform-specific completely replaces base /// - bins: Any bins with matching not-None names will be merged, - /// Any set-settings on the platform specific pkg override base - /// Any bins found only in target_config will be kept + /// Any set-settings on the platform specific pkg override base + /// Any bins found only in target_config will be kept /// - pkg: Any set-settings on the platform specific pkg override base fn merge_with_target_config(&self, target_config: &Self) -> miette::Result { if target_config.debug_dir.is_some() { @@ -70,7 +70,7 @@ impl BackendConfig for MojoBackendConfig { // Both base and target have binaries configured // Override base with anything found in both target and base. // If something is found only in base, drop it. - // If somethign is found only in target, drop it. + // If something is found only in target, drop it. let base_bins: HashMap<_, _> = self .bins .as_ref() @@ -135,16 +135,16 @@ impl MojoBackendConfig { /// - If both a `pkg` and `bin` have been auto-derived, only keep the `bin` pub fn auto_derive( &self, - manifest_root: &PathBuf, + manifest_root: &Path, project_name: &str, ) -> miette::Result<(Option>, Option)> { // Update bins configs let (mut bins, bin_autodetected) = - MojoBinConfig::auto_derive(self.bins.as_ref(), &manifest_root, &project_name)?; + MojoBinConfig::auto_derive(self.bins.as_ref(), manifest_root, project_name)?; // Update pkg config let (mut pkg, pkg_autodetected) = - MojoPkgConfig::auto_derive(self.pkg.as_ref(), &manifest_root, &project_name)?; + MojoPkgConfig::auto_derive(self.pkg.as_ref(), manifest_root, project_name)?; // Make sure we have at least one of the two if bins.is_none() && pkg.is_none() { @@ -192,12 +192,12 @@ pub struct MojoBinConfig { impl MojoBinConfig { /// Fill in any missing info and or try to find our default options. /// - /// - If None, try to find a `main.mojo` file in manfiest_root + /// - If None, try to find a `main.mojo` file in manifest_root /// - If any, for the first one, see if name or path need to be filled in /// - If any, verify that there are no name collisions pub fn auto_derive( conf: Option<&Vec>, - manifest_root: &PathBuf, + manifest_root: &Path, project_name: &str, ) -> miette::Result<(Option>, bool)> { let main = Self::find_main(manifest_root).map(|p| p.display().to_string()); @@ -264,7 +264,7 @@ impl MojoBinConfig { /// Try to find main.mojo in: /// - /main.mojo - fn find_main(root: &PathBuf) -> Option { + fn find_main(root: &Path) -> Option { let mut path = root.join("main"); for ext in ["mojo", "🔥"] { path.set_extension(ext); @@ -339,7 +339,7 @@ impl MojoPkgConfig { /// - If Some, see if name or path need to be filled in. pub fn auto_derive( conf: Option<&Self>, - manifest_root: &PathBuf, + manifest_root: &Path, package_name: &str, ) -> miette::Result<(Option, bool)> { if let Some(conf) = conf { @@ -384,7 +384,7 @@ impl MojoPkgConfig { /// - src /// /// and returns the first one found. - fn find_init_parent(root: &PathBuf, project_name: &str) -> Option { + fn find_init_parent(root: &Path, project_name: &str) -> Option { for dir in [project_name, "src"] { let mut path = root.join(dir).join("__init__"); for ext in ["mojo", "🔥"] { diff --git a/docs/backends/pixi-build-mojo.md b/docs/backends/pixi-build-mojo.md index 38646d34..f6b17a38 100644 --- a/docs/backends/pixi-build-mojo.md +++ b/docs/backends/pixi-build-mojo.md @@ -15,7 +15,7 @@ The `pixi-build-mojo` backend is designed for building Mojo projects. It provide This backend automatically generates conda packages from Mojo projects. -The generated packages can be installed into local envs for devlopment, or packaged for distribution. +The generated packages can be installed into local envs for development, or packaged for distribution. ### Auto-derive of pkg and bin diff --git a/docs/index.md b/docs/index.md index 6f1decb4..1ff6f33f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,6 +31,7 @@ The repository currently provides four specialized build backends: | [**`pixi-build-python`**](./backends/pixi-build-python.md) | Building Python packages | | [**`pixi-build-rattler-build`**](./backends/pixi-build-rattler-build.md) | Direct `recipe.yaml` builds with full control | | [**`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 | All backends are available through the [prefix.dev/pixi-build-backends](https://prefix.dev/channels/pixi-build-backends) conda channel and work across multiple platforms (Linux, macOS, Windows). diff --git a/mkdocs.yml b/mkdocs.yml index bb70d03a..aa095c0e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -106,6 +106,7 @@ nav: - pixi-build-python: backends/pixi-build-python.md - pixi-build-rattler-build: backends/pixi-build-rattler-build.md - pixi-build-rust: backends/pixi-build-rust.md + - pixi-build-mojo: backends/pixi-build-mojo.md validation: omitted_files: warn