diff --git a/Cargo.toml b/Cargo.toml index 1d65446..c8bf244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ tempfile = "3.10.1" clap-verbosity-flag = "2.2.1" tracing-subscriber = "0.3.18" serde_yaml = "0.9.33" +serde = "1.0" +minijinja = "2.3.0" parking_lot = "0.12.3" diff --git a/crates/pixi-build-python/Cargo.toml b/crates/pixi-build/Cargo.toml similarity index 89% rename from crates/pixi-build-python/Cargo.toml rename to crates/pixi-build/Cargo.toml index ea22f8f..c413d31 100644 --- a/crates/pixi-build-python/Cargo.toml +++ b/crates/pixi-build/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "pixi-build-python" +name = "pixi-build-backend" version = "0.1.0" edition.workspace = true @@ -24,6 +24,8 @@ tempfile = { workspace = true } clap-verbosity-flag = { workspace = true } tracing-subscriber = { workspace = true } serde_yaml = { workspace = true } +serde = { workspace = true, features = ["derive"] } +minijinja = { workspace = true } parking_lot = { workspace = true } diff --git a/crates/pixi-build/src/bin/pixi-build-python/build_script.j2 b/crates/pixi-build/src/bin/pixi-build-python/build_script.j2 new file mode 100644 index 0000000..ca20d3f --- /dev/null +++ b/crates/pixi-build/src/bin/pixi-build-python/build_script.j2 @@ -0,0 +1,12 @@ +{% set PYTHON="%PYTHON%" if build_platform == "windows" else "$PYTHON" -%} +{% set SRC_DIR="%SRC_DIR%" if build_platform == "windows" else "$SRC_DIR" -%} + +{% if installer == "uv" -%} +uv pip install --python {{ PYTHON }} -vv --no-deps --no-build-isolation {{ SRC_DIR }} +{% else %} +{{ PYTHON }} -m pip install -vv --ignore-installed --no-deps --no-build-isolation {{ SRC_DIR }} +{% endif -%} + +{% if build_platform == "windows" -%} +if errorlevel 1 exit 1 +{% endif %} diff --git a/crates/pixi-build/src/bin/pixi-build-python/build_script.rs b/crates/pixi-build/src/bin/pixi-build-python/build_script.rs new file mode 100644 index 0000000..3cda057 --- /dev/null +++ b/crates/pixi-build/src/bin/pixi-build-python/build_script.rs @@ -0,0 +1,43 @@ +use minijinja::Environment; +use serde::Serialize; + +#[derive(Serialize)] +pub struct BuildScriptContext { + pub installer: Installer, + pub build_platform: BuildPlatform, +} + +#[derive(Default, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum Installer { + Uv, + #[default] + Pip, +} + +impl Installer { + pub fn package_name(&self) -> &str { + match self { + Installer::Uv => "uv", + Installer::Pip => "pip", + } + } +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum BuildPlatform { + Windows, + 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.split("\n").map(|s| s.to_string()).collect() + } +} diff --git a/crates/pixi-build/src/bin/pixi-build-python/main.rs b/crates/pixi-build/src/bin/pixi-build-python/main.rs new file mode 100644 index 0000000..eefb586 --- /dev/null +++ b/crates/pixi-build/src/bin/pixi-build-python/main.rs @@ -0,0 +1,12 @@ +mod build_script; +mod python; + +use python::PythonBuildBackend; + +#[tokio::main] +pub async fn main() { + if let Err(err) = pixi_build_backend::cli::main(PythonBuildBackend::factory).await { + eprintln!("{err:?}"); + std::process::exit(1); + } +} diff --git a/crates/pixi-build-python/src/python.rs b/crates/pixi-build/src/bin/pixi-build-python/python.rs similarity index 92% rename from crates/pixi-build-python/src/python.rs rename to crates/pixi-build/src/bin/pixi-build-python/python.rs index b01a0de..ba75bc5 100644 --- a/crates/pixi-build-python/src/python.rs +++ b/crates/pixi-build/src/bin/pixi-build-python/python.rs @@ -2,18 +2,20 @@ use std::{collections::BTreeMap, path::Path, str::FromStr, sync::Arc}; use chrono::Utc; use miette::{Context, IntoDiagnostic}; -use pixi_build_types::procedures::conda_build::CondaBuildParams; -use pixi_build_types::procedures::conda_metadata::{CondaMetadataParams, CondaMetadataResult}; +use pixi_build_backend::{ + protocol::{Protocol, ProtocolFactory}, + utils::TemporaryRenderedRecipe, +}; use pixi_build_types::{ procedures::{ - conda_build::CondaBuildResult, + conda_build::{CondaBuildParams, CondaBuildResult}, + conda_metadata::{CondaMetadataParams, CondaMetadataResult}, initialize::{InitializeParams, InitializeResult}, }, BackendCapabilities, CondaPackageMetadata, FrontendCapabilities, }; use pixi_manifest::{Dependencies, Manifest, SpecType}; use pixi_spec::PixiSpec; -use rattler_build::render::resolved_dependencies::DependencyInfo; use rattler_build::{ build::run_build, console_utils::LoggingOutputHandler, @@ -23,6 +25,7 @@ use rattler_build::{ parser::{Build, Dependency, Package, PathSource, Requirements, ScriptContent, Source}, Recipe, }, + render::resolved_dependencies::DependencyInfo, tool_configuration::Configuration, }; use rattler_conda_types::{ @@ -32,10 +35,7 @@ use rattler_package_streaming::write::CompressionLevel; use reqwest::Url; use tempfile::tempdir; -use crate::{ - protocol::{Protocol, ProtocolFactory}, - temporary_recipe::TemporaryRenderedRecipe, -}; +use crate::build_script::{BuildPlatform, BuildScriptContext, Installer}; pub struct PythonBuildBackend { logging_output_handler: LoggingOutputHandler, @@ -103,7 +103,7 @@ impl PythonBuildBackend { /// Returns the requirements of the project that should be used for a /// recipe. - fn requirements(&self, channel_config: &ChannelConfig) -> Requirements { + fn requirements(&self, channel_config: &ChannelConfig) -> (Requirements, Installer) { fn dependencies_into_matchspecs( deps: Dependencies, channel_config: &ChannelConfig, @@ -137,8 +137,18 @@ impl PythonBuildBackend { .filter_map(|f| f.dependencies(Some(SpecType::Build), None)), ); + // Determine the installer to use + let installer = if host_dependencies.contains_key("uv") + || run_dependencies.contains_key("uv") + || build_dependencies.contains_key("uv") + { + Installer::Uv + } else { + Installer::Pip + }; + // Ensure python and pip are available in the host dependencies section. - for pkg_name in ["pip", "python"] { + for pkg_name in [installer.package_name(), "python"] { if host_dependencies.contains_key(pkg_name) { // If the host dependencies already contain the package, we don't need to add it // again. @@ -171,7 +181,7 @@ impl PythonBuildBackend { .map(Dependency::Spec) .collect(); - requirements + (requirements, installer) } /// Constructs a [`Recipe`] from the current manifest. @@ -202,10 +212,20 @@ impl PythonBuildBackend { let noarch_type = NoArchType::python(); // TODO: Read from config / project. - let requirements = self.requirements(channel_config); + let (requirements, installer) = self.requirements(channel_config); let build_platform = Platform::current(); let build_number = 0; + let build_script = BuildScriptContext { + installer, + build_platform: if build_platform.is_windows() { + BuildPlatform::Windows + } else { + BuildPlatform::Unix + }, + } + .render(); + Ok(Recipe { schema_version: 1, package: Package { @@ -228,15 +248,7 @@ impl PythonBuildBackend { string: Default::default(), // skip: Default::default(), - script: ScriptContent::Commands( - if build_platform.is_windows() { - vec![ - "%PYTHON% -m pip install --ignore-installed --no-deps --no-build-isolation . -vv".to_string(), - "if errorlevel 1 exit 1".to_string()] - } else { - vec!["$PYTHON -m pip install --ignore-installed --no-deps --no-build-isolation . -vv".to_string()] - }) - .into(), + script: ScriptContent::Commands(build_script).into(), noarch: noarch_type, // TODO: Python is not exposed properly diff --git a/crates/pixi-build-python/src/main.rs b/crates/pixi-build/src/cli.rs similarity index 75% rename from crates/pixi-build-python/src/main.rs rename to crates/pixi-build/src/cli.rs index 76d9494..b9c5766 100644 --- a/crates/pixi-build-python/src/main.rs +++ b/crates/pixi-build/src/cli.rs @@ -1,10 +1,3 @@ -mod consts; -mod logging; -mod protocol; -mod python; -mod server; -mod temporary_recipe; - use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; @@ -14,16 +7,17 @@ use pixi_build_types::{ procedures::{ conda_build::{CondaBuildParams, CondaOutputIdentifier}, conda_metadata::{CondaMetadataParams, CondaMetadataResult}, + initialize::InitializeParams, }, - ChannelConfiguration, + ChannelConfiguration, FrontendCapabilities, }; use rattler_build::console_utils::{get_default_env_filter, LoggingOutputHandler}; use rattler_conda_types::ChannelConfig; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use crate::{ + consts, protocol::{Protocol, ProtocolFactory}, - python::PythonBuildBackend, server::Server, }; @@ -50,20 +44,12 @@ pub enum Commands { #[clap(env, long, env = "PIXI_PROJECT_MANIFEST", default_value = consts::PROJECT_MANIFEST)] manifest_path: PathBuf, }, - Build { + CondaBuild { #[clap(env, long, env = "PIXI_PROJECT_MANIFEST", default_value = consts::PROJECT_MANIFEST)] manifest_path: PathBuf, }, } -#[tokio::main] -pub async fn main() { - if let Err(err) = actual_main().await { - eprintln!("{err:?}"); - std::process::exit(1); - } -} - async fn run_server(port: Option, protocol: T) -> miette::Result<()> { let server = Server::new(protocol); if let Some(port) = port { @@ -73,7 +59,9 @@ async fn run_server(port: Option, protocol: T) -> miett } } -async fn actual_main() -> miette::Result<()> { +pub async fn main T>( + factory: F, +) -> miette::Result<()> { let args = App::parse(); // Setup logging @@ -82,11 +70,13 @@ async fn actual_main() -> miette::Result<()> { .with(get_default_env_filter(args.verbose.log_level_filter()).into_diagnostic()?); registry.with(log_handler.clone()).init(); + let factory = factory(log_handler); + match args.command { - None => run_server(args.http_port, PythonBuildBackend::factory(log_handler)).await, - Some(Commands::Build { manifest_path }) => build(log_handler, &manifest_path).await, + None => run_server(args.http_port, factory).await, + Some(Commands::CondaBuild { manifest_path }) => build(factory, &manifest_path).await, Some(Commands::GetCondaMetadata { manifest_path }) => { - let metadata = get_conda_metadata(log_handler, &manifest_path).await?; + let metadata = get_conda_metadata(factory, &manifest_path).await?; println!("{}", serde_yaml::to_string(&metadata).unwrap()); Ok(()) } @@ -94,7 +84,7 @@ async fn actual_main() -> miette::Result<()> { } async fn get_conda_metadata( - logging_output_handler: LoggingOutputHandler, + factory: impl ProtocolFactory, manifest_path: &Path, ) -> miette::Result { let channel_config = ChannelConfig::default_with_root_dir( @@ -104,8 +94,14 @@ async fn get_conda_metadata( .to_path_buf(), ); - let backend = PythonBuildBackend::new(manifest_path, logging_output_handler)?; - backend + let (protocol, _initialize_result) = factory + .initialize(InitializeParams { + manifest_path: manifest_path.to_path_buf(), + capabilities: FrontendCapabilities {}, + }) + .await?; + + protocol .get_conda_metadata(CondaMetadataParams { target_platform: None, channel_base_urls: None, @@ -116,10 +112,7 @@ async fn get_conda_metadata( .await } -async fn build( - logging_output_handler: LoggingOutputHandler, - manifest_path: &Path, -) -> miette::Result<()> { +async fn build(factory: impl ProtocolFactory, manifest_path: &Path) -> miette::Result<()> { let channel_config = ChannelConfig::default_with_root_dir( manifest_path .parent() @@ -127,8 +120,14 @@ async fn build( .to_path_buf(), ); - let backend = PythonBuildBackend::new(manifest_path, logging_output_handler)?; - let result = backend + let (protocol, _initialize_result) = factory + .initialize(InitializeParams { + manifest_path: manifest_path.to_path_buf(), + capabilities: FrontendCapabilities {}, + }) + .await?; + + let result = protocol .build_conda(CondaBuildParams { target_platform: None, channel_base_urls: None, diff --git a/crates/pixi-build-python/src/consts.rs b/crates/pixi-build/src/consts.rs similarity index 100% rename from crates/pixi-build-python/src/consts.rs rename to crates/pixi-build/src/consts.rs diff --git a/crates/pixi-build/src/lib.rs b/crates/pixi-build/src/lib.rs new file mode 100644 index 0000000..fa36025 --- /dev/null +++ b/crates/pixi-build/src/lib.rs @@ -0,0 +1,6 @@ +pub mod cli; +pub mod protocol; +pub mod server; + +mod consts; +pub mod utils; diff --git a/crates/pixi-build-python/src/protocol.rs b/crates/pixi-build/src/protocol.rs similarity index 100% rename from crates/pixi-build-python/src/protocol.rs rename to crates/pixi-build/src/protocol.rs diff --git a/crates/pixi-build-python/src/server.rs b/crates/pixi-build/src/server.rs similarity index 100% rename from crates/pixi-build-python/src/server.rs rename to crates/pixi-build/src/server.rs diff --git a/crates/pixi-build/src/utils/mod.rs b/crates/pixi-build/src/utils/mod.rs new file mode 100644 index 0000000..9253d11 --- /dev/null +++ b/crates/pixi-build/src/utils/mod.rs @@ -0,0 +1,3 @@ +mod temporary_recipe; + +pub use temporary_recipe::TemporaryRenderedRecipe; diff --git a/crates/pixi-build-python/src/temporary_recipe.rs b/crates/pixi-build/src/utils/temporary_recipe.rs similarity index 100% rename from crates/pixi-build-python/src/temporary_recipe.rs rename to crates/pixi-build/src/utils/temporary_recipe.rs