From 22f2ce59a3e64fb6aaa85d906f5badc0f0af4e95 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Sun, 7 Dec 2025 14:42:25 +0100 Subject: [PATCH 01/13] wip --- crates/pixi_api/src/workspace/add/mod.rs | 7 +- .../pixi_build_discovery/src/backend_spec.rs | 24 +-- .../src/build_backend_metadata/mod.rs | 21 +- .../solve_pixi/source_metadata_collector.rs | 34 ++- .../src/source_build/mod.rs | 8 +- .../src/source_metadata/mod.rs | 36 ++-- crates/pixi_record/src/pinned_source.rs | 201 +++++++----------- crates/pixi_record/src/source_record.rs | 8 +- crates/pixi_spec/src/lib.rs | 163 ++++++++++---- 9 files changed, 263 insertions(+), 239 deletions(-) diff --git a/crates/pixi_api/src/workspace/add/mod.rs b/crates/pixi_api/src/workspace/add/mod.rs index 22b46a0b2d..3a26f6c286 100644 --- a/crates/pixi_api/src/workspace/add/mod.rs +++ b/crates/pixi_api/src/workspace/add/mod.rs @@ -62,12 +62,7 @@ pub async fn add_conda_dep( }; ( name.clone(), - ( - SourceSpec { - location: SourceLocationSpec::Git(git_spec), - }, - *spec_type, - ), + (SourceLocationSpec::Git(git_spec).into(), *spec_type), ) }) .collect(); diff --git a/crates/pixi_build_discovery/src/backend_spec.rs b/crates/pixi_build_discovery/src/backend_spec.rs index 6bcffd4969..e01de71b02 100644 --- a/crates/pixi_build_discovery/src/backend_spec.rs +++ b/crates/pixi_build_discovery/src/backend_spec.rs @@ -1,4 +1,4 @@ -use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor, SourceSpec}; +use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor}; use pixi_spec_containers::DependencyMap; use rattler_conda_types::ChannelUrl; /// Describes how a backend should be instantiated. @@ -41,19 +41,8 @@ impl JsonRpcBackendSpec { name: self.name, command: { match self.command { - CommandSpec::EnvironmentSpec(mut env_spec) => { - let maybe_source_spec = env_spec.requirement.1.try_into_source_spec(); - let pixi_spec = match maybe_source_spec { - Ok(source_spec) => { - let resolved_spec = source_anchor.resolve(source_spec.location); - PixiSpec::from(SourceSpec { - location: resolved_spec, - }) - } - Err(pixi_spec) => pixi_spec, - }; - env_spec.requirement.1 = pixi_spec; - CommandSpec::EnvironmentSpec(env_spec) + CommandSpec::EnvironmentSpec(env_spec) => { + CommandSpec::EnvironmentSpec(Box::new(env_spec.resolve(source_anchor))) } CommandSpec::System(system_spec) => CommandSpec::System(system_spec), } @@ -134,12 +123,7 @@ impl EnvironmentSpec { pub fn resolve(mut self, source_anchor: SourceAnchor) -> Self { let maybe_source_spec = self.requirement.1.try_into_source_spec(); let pixi_spec = match maybe_source_spec { - Ok(source_spec) => { - let resolved_spec = source_anchor.resolve(source_spec.location); - PixiSpec::from(SourceSpec { - location: resolved_spec, - }) - } + Ok(source_spec) => PixiSpec::from(source_spec.resolve(source_anchor)), Err(pixi_spec) => pixi_spec, }; self.requirement.1 = pixi_spec; diff --git a/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs b/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs index 5e11ec42a9..edc1c7b131 100644 --- a/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs +++ b/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs @@ -14,7 +14,7 @@ use pixi_build_frontend::Backend; use pixi_build_types::{ProjectModelV1, procedures::conda_outputs::CondaOutputsParams}; use pixi_glob::GlobHashKey; use pixi_record::{InputHash, PinnedSourceSpec, VariantValue}; -use pixi_spec::{SourceAnchor, SourceSpec}; +use pixi_spec::{SourceAnchor, SourceLocationSpec}; use rand::random; use rattler_conda_types::{ChannelConfig, ChannelUrl}; use thiserror::Error; @@ -134,27 +134,22 @@ impl BuildBackendMetadataSpec { // Determine the location of the source to build from. let manifest_source_anchor = - SourceAnchor::from(SourceSpec::from(self.manifest_source.clone())); + SourceAnchor::from(SourceLocationSpec::from(self.manifest_source.clone())); // `build_source` is still relative to the `manifest_source` let build_source_checkout = match &discovered_backend.init_params.build_source { None => None, Some(build_source) => { // An out of tree source is provided. Resolve it against the manifest source. let resolved_location = manifest_source_anchor.resolve(build_source.clone()); - let resolved_source_build_spec = SourceSpec { - location: resolved_location.clone(), - }; // Check if we have a preferred build source that matches this same location match &self.preferred_build_source { - Some(pinned) if pinned.matches_source_spec(&resolved_source_build_spec) => { - Some( - command_dispatcher - .checkout_pinned_source(pinned.clone()) - .await - .map_err_with(BuildBackendMetadataError::SourceCheckout)?, - ) - } + Some(pinned) if pinned.matches_source_spec(&resolved_location) => Some( + command_dispatcher + .checkout_pinned_source(pinned.clone()) + .await + .map_err_with(BuildBackendMetadataError::SourceCheckout)?, + ), _ => Some( command_dispatcher .pin_and_checkout(resolved_location) diff --git a/crates/pixi_command_dispatcher/src/solve_pixi/source_metadata_collector.rs b/crates/pixi_command_dispatcher/src/solve_pixi/source_metadata_collector.rs index ab95bccdbe..a5d95a3d5b 100644 --- a/crates/pixi_command_dispatcher/src/solve_pixi/source_metadata_collector.rs +++ b/crates/pixi_command_dispatcher/src/solve_pixi/source_metadata_collector.rs @@ -8,8 +8,10 @@ use futures::{FutureExt, StreamExt}; use miette::Diagnostic; use pixi_build_discovery::EnabledProtocols; use pixi_record::{PinnedSourceSpec, VariantValue}; -use pixi_spec::{SourceAnchor, SourceSpec}; -use rattler_conda_types::{ChannelConfig, ChannelUrl, MatchSpec, ParseStrictness}; +use pixi_spec::{SourceAnchor, SourceLocationSpec, SourceSpec}; +use rattler_conda_types::{ + ChannelConfig, ChannelUrl, MatchSpec, PackageNameMatcher, ParseStrictness, +}; use thiserror::Error; use crate::{ @@ -127,27 +129,23 @@ impl SourceMetadataCollector { // Process transitive dependencies for record in &source_metadata.records { chain.push(record.package_record.name.clone()); - let anchor = SourceAnchor::from(SourceSpec::from(record.manifest_source.clone())); + let anchor = + SourceAnchor::from(SourceLocationSpec::from(record.manifest_source.clone())); for depend in &record.package_record.depends { if let Ok(spec) = MatchSpec::from_str(depend, ParseStrictness::Lenient) { - if let Some((name, source_spec)) = - spec.name.as_ref().and_then(|name_matcher| { - let name = name_matcher - .as_exact() - .expect("depends can only contain exact package names"); - record - .sources - .get(name.as_normalized()) - .map(|source_spec| (name.clone(), source_spec.clone())) - }) - { + let (Some(PackageNameMatcher::Exact(name)), nameless_spec) = + spec.clone().into_nameless() + else { + unimplemented!( + "non exact packages names are not supported in {depend}" + ); + }; + if let Some(source_location) = record.sources.get(name.as_normalized()) { // We encountered a transitive source dependency. - let resolved_location = anchor.resolve(source_spec.location); + let resolved_location = anchor.resolve(source_location.clone()); specs.push(( name, - SourceSpec { - location: resolved_location, - }, + SourceSpec::new(resolved_location, nameless_spec), chain.clone(), )); } else { diff --git a/crates/pixi_command_dispatcher/src/source_build/mod.rs b/crates/pixi_command_dispatcher/src/source_build/mod.rs index dd0d98985d..6b710c8d65 100644 --- a/crates/pixi_command_dispatcher/src/source_build/mod.rs +++ b/crates/pixi_command_dispatcher/src/source_build/mod.rs @@ -10,7 +10,7 @@ use pixi_build_discovery::EnabledProtocols; use pixi_build_frontend::Backend; use pixi_build_types::procedures::conda_outputs::CondaOutputsParams; use pixi_record::{PinnedSourceSpec, PixiRecord, VariantValue}; -use pixi_spec::{SourceAnchor, SourceLocationSpec, SourceSpec}; +use pixi_spec::{SourceAnchor, SourceLocationSpec}; use rattler_conda_types::{ ChannelConfig, ChannelUrl, ConvertSubdirError, InvalidPackageNameError, PackageRecord, Platform, RepoDataRecord, prefix::Prefix, @@ -273,7 +273,7 @@ impl SourceBuildSpec { discovered_backend.init_params.build_source.clone() { let manifest_source_anchor = - SourceAnchor::from(SourceSpec::from(manifest_source.clone())); + SourceAnchor::from(SourceLocationSpec::from(manifest_source.clone())); let resolved_build_source = manifest_source_anchor.resolve(manifest_build_source); &command_dispatcher .pin_and_checkout(resolved_build_source) @@ -289,7 +289,7 @@ impl SourceBuildSpec { backend_spec: discovered_backend .backend_spec .clone() - .resolve(SourceAnchor::from(SourceSpec::from( + .resolve(SourceAnchor::from(SourceLocationSpec::from( manifest_source.clone(), ))), build_source_dir: build_source_checkout.path.clone(), @@ -510,7 +510,7 @@ impl SourceBuildSpec { ) -> Result> { let manifest_source = self.source.manifest_source().clone(); - let source_anchor = SourceAnchor::from(SourceSpec::from(manifest_source.clone())); + let source_anchor = SourceAnchor::from(SourceLocationSpec::from(manifest_source.clone())); let host_platform = self.build_environment.host_platform; let build_platform = self.build_environment.build_platform; let editable = self.editable(); diff --git a/crates/pixi_command_dispatcher/src/source_metadata/mod.rs b/crates/pixi_command_dispatcher/src/source_metadata/mod.rs index 7fa01eac7d..c8265978cf 100644 --- a/crates/pixi_command_dispatcher/src/source_metadata/mod.rs +++ b/crates/pixi_command_dispatcher/src/source_metadata/mod.rs @@ -11,7 +11,7 @@ use itertools::{Either, Itertools}; use miette::Diagnostic; use pixi_build_types::procedures::conda_outputs::CondaOutput; use pixi_record::{InputHash, PixiRecord, SourceRecord}; -use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor, SourceSpec, SpecConversionError}; +use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor, SourceLocationSpec, SpecConversionError}; use pixi_spec_containers::DependencyMap; use rattler_conda_types::{ ChannelConfig, InvalidPackageNameError, MatchSpec, PackageName, PackageRecord, @@ -134,7 +134,7 @@ impl SourceMetadataSpec { ) -> Result> { let manifest_source = source.manifest_source().clone(); let build_source = source.build_source().cloned(); - let source_anchor = SourceAnchor::from(SourceSpec::from(manifest_source.clone())); + let source_anchor = SourceAnchor::from(SourceLocationSpec::from(manifest_source.clone())); // Solve the build environment for the output. let build_dependencies = output @@ -226,23 +226,25 @@ impl SourceMetadataSpec { let pixi_spec_to_match_spec = |name: &PackageName, spec: &PixiSpec, - sources: &mut HashMap| + sources: &mut HashMap| -> Result { match spec.clone().into_source_or_binary() { Either::Left(source) => { - let source = match sources.entry(name.clone()) { + match sources.entry(name.clone()) { std::collections::hash_map::Entry::Occupied(entry) => { // If the entry already exists, check if it points to the same source. - if entry.get() == &source { + if entry.get() == &source.location { return Err(SourceMetadataError::DuplicateSourceDependency { package: name.clone(), source1: Box::new(entry.get().clone()), - source2: Box::new(source.clone()), + source2: Box::new(source.location.clone()), }); } entry.into_mut() } - std::collections::hash_map::Entry::Vacant(entry) => entry.insert(source), + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(source.location.clone()) + } }; Ok(MatchSpec::from_nameless( source.to_nameless_match_spec(), @@ -259,7 +261,7 @@ impl SourceMetadataSpec { }; let pixi_specs_to_match_spec = |specs: DependencyMap, - sources: &mut HashMap| + sources: &mut HashMap| -> Result< Vec, CommandDispatcherError, @@ -439,7 +441,7 @@ impl SourceMetadataSpec { struct PackageRecordDependencies { pub depends: Vec, pub constrains: Vec, - pub sources: HashMap, + pub sources: HashMap, } impl PackageRecordDependencies { @@ -463,14 +465,12 @@ impl PackageRecordDependencies { for (name, spec) in dependencies.dependencies.into_specs() { match spec.value.into_source_or_binary() { Either::Left(source) => { - depends.push( - MatchSpec { - name: Some(name.clone().into()), - ..MatchSpec::default() - } - .to_string(), + let spec = MatchSpec::from_nameless( + source.to_nameless_match_spec(), + Some(name.clone().into()), ); - sources.insert(name, source); + depends.push(spec.to_string()); + sources.insert(name, source.location); } Either::Right(binary) => { if let Ok(spec) = binary.try_into_nameless_match_spec(channel_config) { @@ -520,8 +520,8 @@ pub enum SourceMetadataError { )] DuplicateSourceDependency { package: PackageName, - source1: Box, - source2: Box, + source1: Box, + source2: Box, }, #[error("the dependencies of some packages in the environment form a cycle")] diff --git a/crates/pixi_record/src/pinned_source.rs b/crates/pixi_record/src/pinned_source.rs index f0cb922591..a56e2d40c2 100644 --- a/crates/pixi_record/src/pinned_source.rs +++ b/crates/pixi_record/src/pinned_source.rs @@ -177,8 +177,8 @@ impl PinnedSourceSpec { /// # Ok(()) /// # } /// ``` - pub fn matches_source_spec(&self, source_spec: &SourceSpec) -> bool { - match (self, &source_spec.location) { + pub fn matches_source_spec(&self, source_spec: &SourceLocationSpec) -> bool { + match (self, &source_spec) { // Path sources: paths must be exactly equal (PinnedSourceSpec::Path(pinned_path), SourceLocationSpec::Path(source_path)) => { pinned_path.path == source_path.path @@ -992,18 +992,13 @@ impl Display for PinnedGitSpec { } } -impl From for SourceSpec { +impl From for SourceLocationSpec { fn from(value: PinnedSourceSpec) -> Self { match value { - PinnedSourceSpec::Url(url) => SourceSpec { - location: SourceLocationSpec::Url(url.into()), - }, - PinnedSourceSpec::Git(git) => SourceSpec { - location: SourceLocationSpec::Git(git.into()), - }, - PinnedSourceSpec::Path(path) => SourceSpec { - location: SourceLocationSpec::Path(path.into()), - }, + PinnedSourceSpec::Url(url) => SourceLocationSpec::Url(url.into()), + PinnedSourceSpec::Git(git) => SourceLocationSpec::Git(git.into()), + + PinnedSourceSpec::Path(path) => SourceLocationSpec::Path(path.into()), } } } @@ -1262,7 +1257,7 @@ mod tests { )); } - use pixi_spec::{PathSourceSpec, SourceLocationSpec, SourceSpec, UrlSourceSpec}; + use pixi_spec::{PathSourceSpec, SourceLocationSpec, UrlSourceSpec}; use typed_path::Utf8TypedPathBuf; #[test] @@ -1271,11 +1266,9 @@ mod tests { path: Utf8TypedPathBuf::from("/path/to/source"), }); - let spec = SourceSpec { - location: SourceLocationSpec::Path(PathSourceSpec { - path: Utf8TypedPathBuf::from("/path/to/source"), - }), - }; + let spec = SourceLocationSpec::Path(PathSourceSpec { + path: Utf8TypedPathBuf::from("/path/to/source"), + }); assert!(pinned.matches_source_spec(&spec)); } @@ -1286,11 +1279,9 @@ mod tests { path: Utf8TypedPathBuf::from("/path/to/source"), }); - let spec = SourceSpec { - location: SourceLocationSpec::Path(PathSourceSpec { - path: Utf8TypedPathBuf::from("/different/path"), - }), - }; + let spec = SourceLocationSpec::Path(PathSourceSpec { + path: Utf8TypedPathBuf::from("/different/path"), + }); assert!(!pinned.matches_source_spec(&spec)); } @@ -1306,13 +1297,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo.git").unwrap(), - rev: None, - subdirectory: None, - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo.git").unwrap(), + rev: None, + subdirectory: None, + }); // Should match despite .git suffix difference assert!(pinned.matches_source_spec(&spec)); @@ -1329,13 +1318,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo").unwrap(), - rev: None, - subdirectory: None, - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + rev: None, + subdirectory: None, + }); // Should match despite .git suffix difference assert!(pinned.matches_source_spec(&spec)); @@ -1352,13 +1339,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo2").unwrap(), - rev: None, - subdirectory: None, - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo2").unwrap(), + rev: None, + subdirectory: None, + }); assert!(!pinned.matches_source_spec(&spec)); } @@ -1374,13 +1359,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo").unwrap(), - rev: None, - subdirectory: None, - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + rev: None, + subdirectory: None, + }); // Should match - spec doesn't care about subdirectory assert!(pinned.matches_source_spec(&spec)); @@ -1397,13 +1380,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo").unwrap(), - rev: None, - subdirectory: Some("subdir".to_string()), - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + rev: None, + subdirectory: Some("subdir".to_string()), + }); assert!(pinned.matches_source_spec(&spec)); } @@ -1419,13 +1400,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo").unwrap(), - rev: None, - subdirectory: Some("subdir2".to_string()), - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + rev: None, + subdirectory: Some("subdir2".to_string()), + }); assert!(!pinned.matches_source_spec(&spec)); } @@ -1441,13 +1420,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo").unwrap(), - rev: None, - subdirectory: Some("subdir".to_string()), - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + rev: None, + subdirectory: Some("subdir".to_string()), + }); // Should not match - spec requires a subdirectory that pinned doesn't have assert!(!pinned.matches_source_spec(&spec)); @@ -1464,13 +1441,11 @@ mod tests { md5: None, }); - let spec = SourceSpec { - location: SourceLocationSpec::Url(UrlSourceSpec { - url: Url::parse("https://example.com/archive.tar.gz").unwrap(), - sha256: None, - md5: None, - }), - }; + let spec = SourceLocationSpec::Url(UrlSourceSpec { + url: Url::parse("https://example.com/archive.tar.gz").unwrap(), + sha256: None, + md5: None, + }); assert!(pinned.matches_source_spec(&spec)); } @@ -1486,13 +1461,11 @@ mod tests { md5: None, }); - let spec = SourceSpec { - location: SourceLocationSpec::Url(UrlSourceSpec { - url: Url::parse("https://example.com/different.tar.gz").unwrap(), - sha256: None, - md5: None, - }), - }; + let spec = SourceLocationSpec::Url(UrlSourceSpec { + url: Url::parse("https://example.com/different.tar.gz").unwrap(), + sha256: None, + md5: None, + }); assert!(!pinned.matches_source_spec(&spec)); } @@ -1503,13 +1476,11 @@ mod tests { path: Utf8TypedPathBuf::from("/path/to/source"), }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo").unwrap(), - rev: None, - subdirectory: None, - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + rev: None, + subdirectory: None, + }); assert!(!pinned.matches_source_spec(&spec)); } @@ -1525,13 +1496,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Url(UrlSourceSpec { - url: Url::parse("https://example.com/archive.tar.gz").unwrap(), - sha256: None, - md5: None, - }), - }; + let spec = SourceLocationSpec::Url(UrlSourceSpec { + url: Url::parse("https://example.com/archive.tar.gz").unwrap(), + sha256: None, + md5: None, + }); assert!(!pinned.matches_source_spec(&spec)); } @@ -1547,11 +1516,9 @@ mod tests { md5: None, }); - let spec = SourceSpec { - location: SourceLocationSpec::Path(PathSourceSpec { - path: Utf8TypedPathBuf::from("/path/to/source"), - }), - }; + let spec = SourceLocationSpec::Path(PathSourceSpec { + path: Utf8TypedPathBuf::from("/path/to/source"), + }); assert!(!pinned.matches_source_spec(&spec)); } @@ -1567,13 +1534,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo").unwrap(), - rev: Some(GitReference::Rev("v2.0.0".to_string())), - subdirectory: None, - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + rev: Some(GitReference::Rev("v2.0.0".to_string())), + subdirectory: None, + }); // Should match - we only compare repository and subdirectory, not the commit/rev assert!(pinned.matches_source_spec(&spec)); @@ -1590,13 +1555,11 @@ mod tests { }, }); - let spec = SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: Url::parse("https://github.com/user/repo").unwrap(), - rev: None, - subdirectory: None, - }), - }; + let spec = SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + rev: None, + subdirectory: None, + }); // Should match - GitHub URLs are case-insensitive assert!(pinned.matches_source_spec(&spec)); diff --git a/crates/pixi_record/src/source_record.rs b/crates/pixi_record/src/source_record.rs index 3c630a59ed..345aaa8ce4 100644 --- a/crates/pixi_record/src/source_record.rs +++ b/crates/pixi_record/src/source_record.rs @@ -5,7 +5,7 @@ use std::{ }; use pixi_git::{sha::GitSha, url::RepositoryUrl}; -use pixi_spec::{GitReference, SourceSpec}; +use pixi_spec::{GitReference, SourceLocationSpec}; use rattler_conda_types::{MatchSpec, Matches, NamelessMatchSpec, PackageRecord}; use rattler_digest::{Sha256, Sha256Hash}; use rattler_lock::{CondaSourceData, GitShallowSpec, PackageBuildSource}; @@ -16,7 +16,7 @@ use url::Url; use crate::{ParseLockFileError, PinnedGitCheckout, PinnedSourceSpec, VariantValue}; /// A record of a conda package that still requires building. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize)] pub struct SourceRecord { /// Information about the conda package. This is metadata of the package /// after it has been build. @@ -42,7 +42,7 @@ pub struct SourceRecord { /// Specifies which packages are expected to be installed as source packages /// and from which location. - pub sources: HashMap, + pub sources: HashMap, } /// Defines the hash of the input files that were used to build the metadata of @@ -208,7 +208,7 @@ impl SourceRecord { sources: data .sources .into_iter() - .map(|(k, v)| (k, SourceSpec::from(v))) + .map(|(k, v)| (k, SourceLocationSpec::from(v))) .collect(), variants: data.variants.map(|variants| { variants diff --git a/crates/pixi_spec/src/lib.rs b/crates/pixi_spec/src/lib.rs index 7f0ca5aec0..dd5fb8b512 100644 --- a/crates/pixi_spec/src/lib.rs +++ b/crates/pixi_spec/src/lib.rs @@ -22,7 +22,8 @@ pub use git::{GitReference, GitReferenceError, GitSpec}; use itertools::Either; pub use path::{PathBinarySpec, PathSourceSpec, PathSpec}; use rattler_conda_types::{ - ChannelConfig, NamedChannelOrUrl, NamelessMatchSpec, ParseChannelError, VersionSpec, + BuildNumberSpec, ChannelConfig, MatchSpecCondition, NamedChannelOrUrl, NamelessMatchSpec, + ParseChannelError, StringMatcher, VersionSpec, }; pub use source_anchor::SourceAnchor; use thiserror::Error; @@ -287,18 +288,12 @@ impl PixiSpec { } PixiSpec::Url(url) => url .into_source_or_binary() - .map_left(|url| SourceSpec { - location: SourceLocationSpec::Url(url), - }) + .map_left(|url| SourceLocationSpec::Url(url).into()) .map_right(BinarySpec::Url), - PixiSpec::Git(git) => Either::Left(SourceSpec { - location: SourceLocationSpec::Git(git), - }), + PixiSpec::Git(git) => Either::Left(SourceLocationSpec::Git(git).into()), PixiSpec::Path(path) => path .into_source_or_binary() - .map_left(|path| SourceSpec { - location: SourceLocationSpec::Path(path), - }) + .map_left(|path| SourceLocationSpec::Path(path).into()) .map_right(BinarySpec::Path), } } @@ -312,9 +307,7 @@ impl PixiSpec { .try_into_source_url() .map(SourceSpec::from) .map_err(PixiSpec::from), - PixiSpec::Git(git) => Ok(SourceSpec { - location: SourceLocationSpec::Git(git), - }), + PixiSpec::Git(git) => Ok(SourceLocationSpec::Git(git).into()), PixiSpec::Path(path) => path .try_into_source_path() .map(SourceSpec::from) @@ -362,14 +355,55 @@ impl PixiSpec { /// /// This type only represents source packages. Use [`PixiSpec`] to represent /// both binary and source packages. -#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)] pub struct SourceSpec { /// The location of the source. + #[serde(flatten)] pub location: SourceLocationSpec, + + /// The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`) + pub version: Option, + + /// The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`) + pub build: Option, + + /// The build number of the package + pub build_number: Option, + + /// Optional extra dependencies to select for the package + pub extras: Option>, + + /// The subdir of the channel + pub subdir: Option, + + /// The namespace of the package (currently not used) + pub namespace: Option, + + /// The license of the package + pub license: Option, + + /// The condition under which this match spec applies. + pub condition: Option, +} + +impl From for SourceSpec { + fn from(value: SourceLocationSpec) -> Self { + Self { + location: value, + version: None, + build: None, + build_number: None, + extras: None, + subdir: None, + namespace: None, + license: None, + condition: None, + } + } } /// A specification for a source location. -#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)] #[serde(untagged)] pub enum SourceLocationSpec { /// The spec is represented as an archive that can be downloaded from the @@ -383,9 +417,9 @@ pub enum SourceLocationSpec { Path(PathSourceSpec), } -impl Display for SourceSpec { +impl Display for SourceLocationSpec { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.location { + match &self { SourceLocationSpec::Url(url) => write!(f, "{url}"), SourceLocationSpec::Git(git) => write!(f, "{git}"), SourceLocationSpec::Path(path) => write!(f, "{path}"), @@ -393,10 +427,67 @@ impl Display for SourceSpec { } } +impl Display for SourceSpec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.location.fmt(f) + } +} + impl SourceSpec { + /// Constructs a new instance from a location and a nameless match spec. + pub fn new(location: SourceLocationSpec, spec: NamelessMatchSpec) -> Self { + let NamelessMatchSpec { + version, + build, + build_number, + file_name: _, + extras, + channel: _, + subdir, + namespace, + md5: _, + sha256: _, + url: _, + license, + condition, + } = spec; + Self { + location, + version, + build, + build_number, + extras, + subdir, + namespace, + license, + condition, + } + } + /// Convert this instance into a nameless match spec. pub fn to_nameless_match_spec(&self) -> NamelessMatchSpec { - NamelessMatchSpec::default() + let Self { + location: _, + version, + build, + build_number, + extras, + subdir, + namespace, + license, + condition, + } = self; + NamelessMatchSpec { + version: version.clone(), + build: build.clone(), + build_number: build_number.clone(), + extras: extras.clone(), + subdir: subdir.clone(), + namespace: namespace.clone(), + license: license.clone(), + condition: condition.clone(), + ..NamelessMatchSpec::default() + } } /// Converts this instance into a [`toml_edit::Value`]. @@ -404,6 +495,14 @@ impl SourceSpec { ::serde::Serialize::serialize(&self.location, toml_edit::ser::ValueSerializer::new()) .expect("conversion to toml cannot fail") } + + /// Resolves the source location using the provided source anchor. + pub fn resolve(self, source_anchor: SourceAnchor) -> Self { + Self { + location: source_anchor.resolve(self.location), + ..self + } + } } impl SourceLocationSpec { @@ -437,9 +536,7 @@ impl From for PixiSpec { impl From for SourceSpec { fn from(value: UrlSourceSpec) -> Self { - Self { - location: SourceLocationSpec::Url(value), - } + SourceLocationSpec::Url(value).into() } } @@ -457,9 +554,7 @@ impl From for PixiSpec { impl From for SourceSpec { fn from(value: PathSourceSpec) -> Self { - Self { - location: SourceLocationSpec::Path(value), - } + SourceLocationSpec::Path(value).into() } } @@ -541,26 +636,20 @@ impl From for BinarySpec { } #[cfg(feature = "rattler_lock")] -impl From for SourceSpec { +impl From for SourceLocationSpec { fn from(value: rattler_lock::source::SourceLocation) -> Self { match value { - rattler_lock::source::SourceLocation::Url(url) => Self { - location: SourceLocationSpec::Url(url.into()), - }, - rattler_lock::source::SourceLocation::Git(git) => Self { - location: SourceLocationSpec::Git(git.into()), - }, - rattler_lock::source::SourceLocation::Path(path) => Self { - location: SourceLocationSpec::Path(path.into()), - }, + rattler_lock::source::SourceLocation::Url(url) => Self::Url(url.into()), + rattler_lock::source::SourceLocation::Git(git) => Self::Git(git.into()), + rattler_lock::source::SourceLocation::Path(path) => Self::Path(path.into()), } } } #[cfg(feature = "rattler_lock")] -impl From for rattler_lock::source::SourceLocation { - fn from(value: SourceSpec) -> Self { - match value.location { +impl From for rattler_lock::source::SourceLocation { + fn from(value: SourceLocationSpec) -> Self { + match value { SourceLocationSpec::Url(url) => Self::Url(url.into()), SourceLocationSpec::Git(git) => Self::Git(git.into()), SourceLocationSpec::Path(path) => Self::Path(path.into()), From 4be744ac91e5da1e35a7534d70f5cd371a7b13fa Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:32:48 +0100 Subject: [PATCH 02/13] wip --- crates/pixi_api/src/workspace/add/mod.rs | 2 +- .../pixi_build_discovery/src/backend_spec.rs | 2 +- .../src/project_model.rs | 39 ++++-- ..._tests__conversions_v1_docs@workspace.snap | 7 +- ...onversions_v1_docs@workspace_variants.snap | 7 +- .../src/conda_package_metadata.rs | 4 +- crates/pixi_build_types/src/lib.rs | 4 +- crates/pixi_build_types/src/project_model.rs | 120 +++++++++++++++--- .../src/build/conversion.rs | 71 ++++++++--- .../src/build/dependencies.rs | 10 +- .../solve_pixi/source_metadata_collector.rs | 2 +- .../src/lock_file/satisfiability/mod.rs | 41 +++--- crates/pixi_record/src/pinned_source.rs | 8 +- crates/pixi_spec/src/lib.rs | 2 +- 14 files changed, 235 insertions(+), 84 deletions(-) diff --git a/crates/pixi_api/src/workspace/add/mod.rs b/crates/pixi_api/src/workspace/add/mod.rs index 3a26f6c286..8de5f5d5cc 100644 --- a/crates/pixi_api/src/workspace/add/mod.rs +++ b/crates/pixi_api/src/workspace/add/mod.rs @@ -5,7 +5,7 @@ use pixi_core::{ workspace::{PypiDeps, UpdateDeps, WorkspaceMut}, }; use pixi_manifest::{FeatureName, KnownPreviewFeature, SpecType}; -use pixi_spec::{GitSpec, SourceLocationSpec, SourceSpec}; +use pixi_spec::{GitSpec, SourceLocationSpec}; use rattler_conda_types::{MatchSpec, PackageName}; mod options; diff --git a/crates/pixi_build_discovery/src/backend_spec.rs b/crates/pixi_build_discovery/src/backend_spec.rs index e01de71b02..f67b02840f 100644 --- a/crates/pixi_build_discovery/src/backend_spec.rs +++ b/crates/pixi_build_discovery/src/backend_spec.rs @@ -123,7 +123,7 @@ impl EnvironmentSpec { pub fn resolve(mut self, source_anchor: SourceAnchor) -> Self { let maybe_source_spec = self.requirement.1.try_into_source_spec(); let pixi_spec = match maybe_source_spec { - Ok(source_spec) => PixiSpec::from(source_spec.resolve(source_anchor)), + Ok(source_spec) => PixiSpec::from(source_spec.resolve(&source_anchor)), Err(pixi_spec) => pixi_spec, }; self.requirement.1 = pixi_spec; diff --git a/crates/pixi_build_type_conversions/src/project_model.rs b/crates/pixi_build_type_conversions/src/project_model.rs index 5979d1e015..a0526b723a 100644 --- a/crates/pixi_build_type_conversions/src/project_model.rs +++ b/crates/pixi_build_type_conversions/src/project_model.rs @@ -12,7 +12,7 @@ use ordermap::OrderMap; use pixi_build_types::{self as pbt}; use pixi_manifest::{PackageManifest, PackageTarget, TargetSelector, Targets}; -use pixi_spec::{GitReference, PixiSpec, SpecConversionError}; +use pixi_spec::{GitReference, PixiSpec, SourceSpec, SpecConversionError}; use rattler_conda_types::{ChannelConfig, NamelessMatchSpec, PackageName}; /// Conversion from a `PixiSpec` to a `pbt::PixiSpecV1`. @@ -25,10 +25,26 @@ fn to_pixi_spec_v1( // Convert into correct type for pixi let pbt_spec = match source_or_binary { itertools::Either::Left(source) => { - let source = match source.location { + let SourceSpec { + location, + version, + build, + build_number, + extras: None, + subdir, + namespace: None, + license, + condition: None, + } = source + else { + unimplemented!( + "a particular field is not implemented in the pixi to pbt conversion" + ); + }; + let location = match location { pixi_spec::SourceLocationSpec::Url(url_source_spec) => { let pixi_spec::UrlSourceSpec { url, md5, sha256 } = url_source_spec; - pbt::SourcePackageSpecV1::Url(pbt::UrlSpecV1 { url, md5, sha256 }) + pbt::SourcePackageLocationSpec::Url(pbt::UrlSpecV1 { url, md5, sha256 }) } pixi_spec::SourceLocationSpec::Git(git_spec) => { let pixi_spec::GitSpec { @@ -36,7 +52,7 @@ fn to_pixi_spec_v1( rev, subdirectory, } = git_spec; - pbt::SourcePackageSpecV1::Git(pbt::GitSpecV1 { + pbt::SourcePackageLocationSpec::Git(pbt::GitSpecV1 { git, rev: rev.map(|r| match r { GitReference::Branch(b) => pbt::GitReferenceV1::Branch(b), @@ -48,12 +64,19 @@ fn to_pixi_spec_v1( }) } pixi_spec::SourceLocationSpec::Path(path_source_spec) => { - pbt::SourcePackageSpecV1::Path(pbt::PathSpecV1 { + pbt::SourcePackageLocationSpec::Path(pbt::PathSpecV1 { path: path_source_spec.path.to_string(), }) } }; - pbt::PackageSpecV1::Source(source) + pbt::PackageSpecV1::Source(pbt::SourcePackageSpec { + location, + version, + build, + build_number, + subdir, + license, + }) } itertools::Either::Right(binary) => { let NamelessMatchSpec { @@ -72,7 +95,7 @@ fn to_pixi_spec_v1( extras: _, condition: _, } = binary.try_into_nameless_match_spec(channel_config)?; - pbt::PackageSpecV1::Binary(Box::new(pbt::BinaryPackageSpecV1 { + pbt::PackageSpecV1::Binary(pbt::BinaryPackageSpecV1 { version, build, build_number, @@ -83,7 +106,7 @@ fn to_pixi_spec_v1( sha256, url, license, - })) + }) } }; Ok(pbt_spec) diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap index 5139394a89..d92ab829fd 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap @@ -39,7 +39,12 @@ expression: project_model "source": { "Path": { "path": "packages/cpp_math" - } + }, + "version": null, + "build": null, + "build_number": null, + "subdir": null, + "license": null } }, "rich": { diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap index b37526617c..4ed182d997 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap @@ -53,7 +53,12 @@ expression: project_model "source": { "Path": { "path": "packages/cpp_math" - } + }, + "version": null, + "build": null, + "build_number": null, + "subdir": null, + "license": null } }, "rich": { diff --git a/crates/pixi_build_types/src/conda_package_metadata.rs b/crates/pixi_build_types/src/conda_package_metadata.rs index 7594c9655f..3b3c1045b9 100644 --- a/crates/pixi_build_types/src/conda_package_metadata.rs +++ b/crates/pixi_build_types/src/conda_package_metadata.rs @@ -4,7 +4,7 @@ use rattler_conda_types::{NoArchType, PackageName, Platform, VersionWithSource}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::SourcePackageSpecV1; +use crate::project_model::SourcePackageLocationSpec; #[serde_as] #[derive(Debug, Serialize, Deserialize, Clone)] @@ -45,5 +45,5 @@ pub struct CondaPackageMetadata { /// Describes any packages that should be built from a particular /// source. #[serde(default)] - pub sources: HashMap, + pub sources: HashMap, } diff --git a/crates/pixi_build_types/src/lib.rs b/crates/pixi_build_types/src/lib.rs index d2c88505e5..e1d957b91d 100644 --- a/crates/pixi_build_types/src/lib.rs +++ b/crates/pixi_build_types/src/lib.rs @@ -13,8 +13,8 @@ pub use channel_configuration::ChannelConfiguration; pub use conda_package_metadata::CondaPackageMetadata; pub use project_model::{ BinaryPackageSpecV1, GitReferenceV1, GitSpecV1, NamedSpecV1, PackageSpecV1, PathSpecV1, - ProjectModelV1, SourcePackageName, SourcePackageSpecV1, TargetSelectorV1, TargetV1, TargetsV1, - UrlSpecV1, VersionedProjectModel, + ProjectModelV1, SourcePackageLocationSpec, SourcePackageName, SourcePackageSpec, + TargetSelectorV1, TargetV1, TargetsV1, UrlSpecV1, VersionedProjectModel, }; use rattler_conda_types::{ GenericVirtualPackage, PackageName, Platform, Version, VersionSpec, diff --git a/crates/pixi_build_types/src/project_model.rs b/crates/pixi_build_types/src/project_model.rs index 5a9c1d19c1..72d5878da4 100644 --- a/crates/pixi_build_types/src/project_model.rs +++ b/crates/pixi_build_types/src/project_model.rs @@ -230,9 +230,9 @@ impl IsDefault for TargetV1 { #[serde(rename_all = "camelCase")] pub enum PackageSpecV1 { /// This is a binary dependency - Binary(Box), + Binary(BinaryPackageSpecV1), /// This is a dependency on a source package - Source(SourcePackageSpecV1), + Source(SourcePackageSpec), } #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] @@ -244,8 +244,67 @@ pub struct NamedSpecV1 { pub spec: T, } +#[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub enum SourcePackageSpecV1 { +pub struct SourcePackageSpec { + #[serde(flatten)] + pub location: SourcePackageLocationSpec, + /// The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`) + #[serde_as(as = "Option")] + pub version: Option, + /// The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`) + #[serde_as(as = "Option")] + pub build: Option, + /// The build number of the package + pub build_number: Option, + /// The subdir of the channel + pub subdir: Option, + /// The md5 hash of the package + /// The license of the package + pub license: Option, +} + +impl From for SourcePackageSpec { + fn from(value: PathSpecV1) -> Self { + Self { + location: SourcePackageLocationSpec::Path(value), + version: None, + build: None, + build_number: None, + subdir: None, + license: None, + } + } +} + +impl From for SourcePackageSpec { + fn from(value: UrlSpecV1) -> Self { + Self { + location: SourcePackageLocationSpec::Url(value), + version: None, + build: None, + build_number: None, + subdir: None, + license: None, + } + } +} + +impl From for SourcePackageSpec { + fn from(value: GitSpecV1) -> Self { + Self { + location: SourcePackageLocationSpec::Git(value), + version: None, + build: None, + build_number: None, + subdir: None, + license: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub enum SourcePackageLocationSpec { /// The spec is represented as an archive that can be downloaded from the /// specified URL. The package should be retrieved from the URL and can /// either represent a source or binary package depending on the archive @@ -520,20 +579,45 @@ impl Hash for PackageSpecV1 { } } -impl Hash for SourcePackageSpecV1 { +impl Hash for SourcePackageSpec { + fn hash(&self, state: &mut H) { + let Self { + location, + version, + build, + build_number, + subdir, + license, + } = self; + + // Hash the location first, to ensure compatibility with older versions. + location.hash(state); + + // Add the new fields using StableHashBuilder for forward/backward compatibility. + StableHashBuilder::::new() + .field("build", build) + .field("build_number", build_number) + .field("license", license) + .field("subdir", subdir) + .field("version", version) + .finish(state); + } +} + +impl Hash for SourcePackageLocationSpec { /// Custom hash implementation that uses discriminant values to keep the /// hash as stable as possible when adding new enum variants. fn hash(&self, state: &mut H) { match self { - SourcePackageSpecV1::Url(spec) => { + Self::Url(spec) => { 0u8.hash(state); spec.hash(state); } - SourcePackageSpecV1::Git(spec) => { + Self::Git(spec) => { 1u8.hash(state); spec.hash(state); } - SourcePackageSpecV1::Path(spec) => { + Self::Path(spec) => { 2u8.hash(state); spec.hash(state); } @@ -723,7 +807,10 @@ mod tests { // Add a real dependency (should change hash) let mut deps = OrderMap::new(); - deps.insert("python".to_string(), PackageSpecV1::Binary(Box::default())); + deps.insert( + "python".to_string(), + PackageSpecV1::Binary(BinaryPackageSpecV1::default()), + ); let target_with_deps = TargetV1 { host_dependencies: Some(deps), @@ -790,8 +877,8 @@ mod tests { #[test] fn test_enum_variant_hash_stability() { // Test PackageSpecV1 enum variants - let binary_spec = PackageSpecV1::Binary(Box::default()); - let source_spec = PackageSpecV1::Source(SourcePackageSpecV1::Path(PathSpecV1 { + let binary_spec = PackageSpecV1::Binary(BinaryPackageSpecV1::default()); + let source_spec = PackageSpecV1::Source(SourcePackageSpec::from(PathSpecV1 { path: "test".to_string(), })); @@ -805,7 +892,7 @@ mod tests { ); // Same variant with same content should have same hash - let binary_spec2 = PackageSpecV1::Binary(Box::default()); + let binary_spec2 = PackageSpecV1::Binary(BinaryPackageSpecV1::default()); let hash3 = calculate_hash(&binary_spec2); assert_eq!( @@ -818,15 +905,15 @@ mod tests { TargetV1 { host_dependencies: Some(OrderMap::from([( "host_dep1".to_string(), - PackageSpecV1::Binary(Box::default()), + PackageSpecV1::Binary(BinaryPackageSpecV1::default()), )])), build_dependencies: Some(OrderMap::from([( "build_dep1".to_string(), - PackageSpecV1::Binary(Box::default()), + PackageSpecV1::Binary(BinaryPackageSpecV1::default()), )])), run_dependencies: Some(OrderMap::from([( "run_dep1".to_string(), - PackageSpecV1::Binary(Box::default()), + PackageSpecV1::Binary(BinaryPackageSpecV1::default()), )])), } } @@ -933,7 +1020,10 @@ mod tests { // different hashes let mut deps = OrderMap::new(); - deps.insert("python".to_string(), PackageSpecV1::Binary(Box::default())); + deps.insert( + "python".to_string(), + PackageSpecV1::Binary(BinaryPackageSpecV1::default()), + ); // Same dependency in host_dependencies let target1 = TargetV1 { diff --git a/crates/pixi_command_dispatcher/src/build/conversion.rs b/crates/pixi_command_dispatcher/src/build/conversion.rs index 8f3e421551..ab10e97f5e 100644 --- a/crates/pixi_command_dispatcher/src/build/conversion.rs +++ b/crates/pixi_command_dispatcher/src/build/conversion.rs @@ -1,22 +1,49 @@ use pixi_build_types::{ - BinaryPackageSpecV1, CondaPackageMetadata, PackageSpecV1, SourcePackageSpecV1, + BinaryPackageSpecV1, CondaPackageMetadata, PackageSpecV1, SourcePackageLocationSpec, + SourcePackageSpec, }; use pixi_record::{InputHash, PinnedSourceSpec, SourceRecord}; -use pixi_spec::{BinarySpec, DetailedSpec, SourceLocationSpec, UrlBinarySpec}; +use pixi_spec::{BinarySpec, DetailedSpec, UrlBinarySpec}; use rattler_conda_types::{NamedChannelOrUrl, PackageName, PackageRecord}; -/// Converts a [`SourcePackageSpecV1`] to a [`pixi_spec::SourceSpec`]. -pub fn from_source_spec_v1(source: SourcePackageSpecV1) -> pixi_spec::SourceSpec { - match source { - SourcePackageSpecV1::Url(url) => pixi_spec::SourceSpec { - location: SourceLocationSpec::Url(pixi_spec::UrlSourceSpec { +/// Converts a [`SourcePackageSpec`] to a [`pixi_spec::SourceSpec`]. +pub fn from_source_spec_v1(source: SourcePackageSpec) -> pixi_spec::SourceSpec { + let SourcePackageSpec { + location, + version, + build, + build_number, + subdir, + license, + } = source; + let location = from_source_package_location_spec(location); + pixi_spec::SourceSpec { + location, + version, + build, + build_number, + subdir, + license, + extras: None, + namespace: None, + condition: None, + } +} + +pub fn from_source_package_location_spec( + spec: SourcePackageLocationSpec, +) -> pixi_spec::SourceLocationSpec { + match spec { + SourcePackageLocationSpec::Url(url) => { + pixi_spec::SourceLocationSpec::Url(pixi_spec::UrlSourceSpec { url: url.url, md5: url.md5, sha256: url.sha256, - }), - }, - SourcePackageSpecV1::Git(git) => pixi_spec::SourceSpec { - location: SourceLocationSpec::Git(pixi_spec::GitSpec { + }) + } + + SourcePackageLocationSpec::Git(git) => { + pixi_spec::SourceLocationSpec::Git(pixi_spec::GitSpec { git: git.git, rev: git.rev.map(|r| match r { pixi_build_frontend::types::GitReferenceV1::Branch(b) => { @@ -33,13 +60,14 @@ pub fn from_source_spec_v1(source: SourcePackageSpecV1) -> pixi_spec::SourceSpec } }), subdirectory: git.subdirectory, - }), - }, - SourcePackageSpecV1::Path(path) => pixi_spec::SourceSpec { - location: SourceLocationSpec::Path(pixi_spec::PathSourceSpec { + }) + } + + SourcePackageLocationSpec::Path(path) => { + pixi_spec::SourceLocationSpec::Path(pixi_spec::PathSourceSpec { path: path.path.into(), - }), - }, + }) + } } } @@ -93,7 +121,7 @@ pub fn from_binary_spec_v1(spec: BinaryPackageSpecV1) -> pixi_spec::BinarySpec { pub fn from_package_spec_v1(source: PackageSpecV1) -> pixi_spec::PixiSpec { match source { PackageSpecV1::Source(source) => from_source_spec_v1(source).into(), - PackageSpecV1::Binary(binary) => from_binary_spec_v1(*binary).into(), + PackageSpecV1::Binary(binary) => from_binary_spec_v1(binary).into(), } } @@ -117,7 +145,12 @@ pub(crate) fn package_metadata_to_source_records( sources: p .sources .iter() - .map(|(name, source)| (name.clone(), from_source_spec_v1(source.clone()))) + .map(|(name, source)| { + ( + name.clone(), + from_source_package_location_spec(source.clone()), + ) + }) .collect(), package_record: PackageRecord { // We cannot now these values from the metadata because no actual package diff --git a/crates/pixi_command_dispatcher/src/build/dependencies.rs b/crates/pixi_command_dispatcher/src/build/dependencies.rs index 458689b712..8a7466924f 100644 --- a/crates/pixi_command_dispatcher/src/build/dependencies.rs +++ b/crates/pixi_command_dispatcher/src/build/dependencies.rs @@ -8,7 +8,7 @@ use pixi_build_types::{ }, }; use pixi_record::PixiRecord; -use pixi_spec::{BinarySpec, DetailedSpec, PixiSpec, SourceAnchor, SourceSpec, UrlBinarySpec}; +use pixi_spec::{BinarySpec, DetailedSpec, PixiSpec, SourceAnchor, UrlBinarySpec}; use pixi_spec_containers::DependencyMap; use rattler_conda_types::{ InvalidPackageNameError, MatchSpec, NamedChannelOrUrl, NamelessMatchSpec, PackageName, @@ -95,12 +95,12 @@ impl Dependencies { })?; match conversion::from_package_spec_v1(depend.spec.clone()).into_source_or_binary() { Either::Left(source) => { - let location = if let Some(anchor) = &source_anchor { - anchor.resolve(source.location) + let source = if let Some(anchor) = &source_anchor { + source.resolve(anchor) } else { - source.location + source }; - dependencies.insert(name, PixiSpec::from(SourceSpec { location }).into()); + dependencies.insert(name, PixiSpec::from(source).into()); } Either::Right(binary) => { dependencies.insert(name, PixiSpec::from(binary).into()); diff --git a/crates/pixi_command_dispatcher/src/solve_pixi/source_metadata_collector.rs b/crates/pixi_command_dispatcher/src/solve_pixi/source_metadata_collector.rs index a5d95a3d5b..e544e56572 100644 --- a/crates/pixi_command_dispatcher/src/solve_pixi/source_metadata_collector.rs +++ b/crates/pixi_command_dispatcher/src/solve_pixi/source_metadata_collector.rs @@ -109,7 +109,7 @@ impl SourceMetadataCollector { loop { // Create futures for all encountered specs. for (name, spec, chain) in specs.drain(..) { - if already_encountered_specs.insert(spec.clone()) { + if already_encountered_specs.insert((name.clone(), spec.location.clone())) { source_futures.push( self.collect_source_metadata(name, spec, chain) .boxed_local(), diff --git a/crates/pixi_core/src/lock_file/satisfiability/mod.rs b/crates/pixi_core/src/lock_file/satisfiability/mod.rs index 035522f6c6..e1c97efd6d 100644 --- a/crates/pixi_core/src/lock_file/satisfiability/mod.rs +++ b/crates/pixi_core/src/lock_file/satisfiability/mod.rs @@ -774,7 +774,7 @@ pub async fn verify_platform_satisfiability( enum Dependency { Input(PackageName, PixiSpec, Cow<'static, str>), Conda(MatchSpec, Cow<'static, str>), - CondaSource(PackageName, MatchSpec, SourceSpec, Cow<'static, str>), + CondaSource(PackageName, SourceSpec, Cow<'static, str>), PyPi(uv_distribution_types::Requirement, Cow<'static, str>), } @@ -1153,7 +1153,6 @@ pub(crate) async fn verify_package_platform_satisfiability( name, source_spec, source, - None, )? } Either::Right(binary_spec) => { @@ -1207,14 +1206,13 @@ pub(crate) async fn verify_package_platform_satisfiability( None => continue, } } - Dependency::CondaSource(name, spec, source_spec, source) => { + Dependency::CondaSource(name, source_spec, source) => { expected_conda_source_dependencies.insert(name.clone()); FoundPackage::Conda(find_matching_source_package( locked_pixi_records, name, source_spec, source, - Some(spec), )?) } Dependency::PyPi(requirement, source) => { @@ -1339,6 +1337,7 @@ pub(crate) async fn verify_package_platform_satisfiability( for depends in &record.package_record().depends { let spec = MatchSpec::from_str(depends.as_str(), Lenient) .map_err(|e| PlatformUnsat::FailedToParseMatchSpec(depends.clone(), e))?; + let (name, spec) = spec.into_nameless(); let (origin, anchor) = match record { PixiRecord::Binary(record) => ( @@ -1351,13 +1350,13 @@ pub(crate) async fn verify_package_platform_satisfiability( record.package_record.name.as_source(), &record.manifest_source )), - SourceSpec::from(record.manifest_source.clone()).into(), + SourceLocationSpec::from(record.manifest_source.clone()).into(), ), }; if let Some((source, package_name)) = record .as_source() - .and_then(|record| Some((record, spec.name.as_ref()?))) + .and_then(|record| Some((record, name.as_ref()?))) .and_then(|(record, package_name_matcher)| { let package_name = package_name_matcher .as_exact() @@ -1368,18 +1367,18 @@ pub(crate) async fn verify_package_platform_satisfiability( )) }) { - let anchored_location = anchor.resolve(source.location.clone()); - let anchored_source = SourceSpec { - location: anchored_location, - }; + let anchored_location = anchor.resolve(source.clone()); + let source_spec = SourceSpec::new(anchored_location, spec); conda_queue.push(Dependency::CondaSource( package_name.clone(), - spec, - anchored_source, + source_spec, origin, )); } else { - conda_queue.push(Dependency::Conda(spec, origin)); + conda_queue.push(Dependency::Conda( + MatchSpec::from_nameless(spec, name), + origin, + )); } } } @@ -1762,7 +1761,6 @@ fn find_matching_source_package( name: PackageName, source_spec: SourceSpec, source: Cow, - match_spec: Option, ) -> Result> { // Find the package that matches the source spec. let Some((idx, package)) = locked_pixi_records @@ -1786,16 +1784,15 @@ fn find_matching_source_package( source_package .manifest_source - .satisfies(&source_spec) + .satisfies(&source_spec.location) .map_err(|e| PlatformUnsat::SourcePackageMismatch(name.as_source().to_string(), e))?; - if let Some(match_spec) = match_spec { - if !match_spec.matches(package) { - return Err(Box::new(PlatformUnsat::UnsatisfiableMatchSpec( - Box::new(match_spec), - source.into_owned(), - ))); - } + let match_spec = source_spec.to_nameless_match_spec(); + if !match_spec.matches(package) { + return Err(Box::new(PlatformUnsat::UnsatisfiableMatchSpec( + Box::new(MatchSpec::from_nameless(match_spec, Some(name.into()))), + source.into_owned(), + ))); } Ok(CondaPackageIdx(idx)) diff --git a/crates/pixi_record/src/pinned_source.rs b/crates/pixi_record/src/pinned_source.rs index a56e2d40c2..715c5d2c5d 100644 --- a/crates/pixi_record/src/pinned_source.rs +++ b/crates/pixi_record/src/pinned_source.rs @@ -12,9 +12,7 @@ use pixi_git::{ sha::GitSha, url::{RepositoryUrl, redact_credentials}, }; -use pixi_spec::{ - GitReference, GitSpec, PathSourceSpec, SourceLocationSpec, SourceSpec, UrlSourceSpec, -}; +use pixi_spec::{GitReference, GitSpec, PathSourceSpec, SourceLocationSpec, UrlSourceSpec}; use rattler_digest::{Md5Hash, Sha256Hash}; use rattler_lock::UrlOrPath; use serde::{Deserialize, Serialize}; @@ -948,8 +946,8 @@ impl PinnedGitSpec { impl PinnedSourceSpec { #[allow(clippy::result_large_err)] /// Verifies if the locked source satisfies the requested source. - pub fn satisfies(&self, spec: &SourceSpec) -> Result<(), SourceMismatchError> { - match (self, &spec.location) { + pub fn satisfies(&self, spec: &SourceLocationSpec) -> Result<(), SourceMismatchError> { + match (self, &spec) { (PinnedSourceSpec::Path(locked), SourceLocationSpec::Path(spec)) => { locked.satisfies(spec) } diff --git a/crates/pixi_spec/src/lib.rs b/crates/pixi_spec/src/lib.rs index dd5fb8b512..1e657a7990 100644 --- a/crates/pixi_spec/src/lib.rs +++ b/crates/pixi_spec/src/lib.rs @@ -497,7 +497,7 @@ impl SourceSpec { } /// Resolves the source location using the provided source anchor. - pub fn resolve(self, source_anchor: SourceAnchor) -> Self { + pub fn resolve(self, source_anchor: &SourceAnchor) -> Self { Self { location: source_anchor.resolve(self.location), ..self From 13df0563972f2fab508967192632bac31ad3d3b5 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:43:52 +0100 Subject: [PATCH 03/13] use correct casing in api types --- ...ns__project_model__tests__conversions_v1_docs@workspace.snap | 2 +- ...ct_model__tests__conversions_v1_docs@workspace_variants.snap | 2 +- crates/pixi_build_types/src/project_model.rs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap index d92ab829fd..37cb11c795 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap @@ -42,7 +42,7 @@ expression: project_model }, "version": null, "build": null, - "build_number": null, + "buildNumber": null, "subdir": null, "license": null } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap index 4ed182d997..bc2dd7e2c8 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap @@ -56,7 +56,7 @@ expression: project_model }, "version": null, "build": null, - "build_number": null, + "buildNumber": null, "subdir": null, "license": null } diff --git a/crates/pixi_build_types/src/project_model.rs b/crates/pixi_build_types/src/project_model.rs index 72d5878da4..a18b2befb7 100644 --- a/crates/pixi_build_types/src/project_model.rs +++ b/crates/pixi_build_types/src/project_model.rs @@ -246,6 +246,7 @@ pub struct NamedSpecV1 { #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct SourcePackageSpec { #[serde(flatten)] pub location: SourcePackageLocationSpec, From 4b47d7548f35482cb9250c5928a6c29ffb37640d Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:46:30 +0100 Subject: [PATCH 04/13] fix: rustdoc --- .../src/build_backend_metadata/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs b/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs index edc1c7b131..dc9eb067e4 100644 --- a/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs +++ b/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs @@ -55,9 +55,9 @@ pub struct BuildBackendMetadataSpec { /// The optional pinned location of the source code. If not provide the /// location in the manifest is resolved. /// - /// This is passed as a hint. If the [`SourceSpec`] in the discovered - /// manifest does not match with the pinned source provided here, the one - /// in the manifest takes precedence and it is reresolved. + /// This is passed as a hint. If the [`pixi_spec::SourceSpec`] in the + /// discovered manifest does not match with the pinned source provided + /// here, the one in the manifest takes precedence and it is reresolved. /// /// See [`PinnedSourceSpec::matches_source_spec`] how the matching is done. pub preferred_build_source: Option, From 989dd942f294bc9d67dc171823bceaef48e5e6d8 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:31:35 +0100 Subject: [PATCH 05/13] feat: streamline schema --- .cargo/config.toml | 3 + Cargo.lock | 28 + Cargo.toml | 1 + .../pixi_build_backend_passthrough/src/lib.rs | 59 +-- crates/pixi_build_discovery/src/discovery.rs | 8 +- crates/pixi_build_frontend/Cargo.toml | 1 + .../src/backend/json_rpc.rs | 17 +- .../src/project_model.rs | 72 +-- ...sts__conversions_v1_docs@advanced_cpp.snap | 37 +- ...__conversions_v1_docs@advanced_python.snap | 27 - ...model__tests__conversions_v1_docs@cpp.snap | 117 ++--- ...model__tests__conversions_v1_docs@dev.snap | 119 ++--- ...__conversions_v1_docs@getting_started.snap | 92 ++-- ...el__tests__conversions_v1_docs@python.snap | 91 ++-- ..._tests__conversions_v1_docs@workspace.snap | 113 ++-- ...onversions_v1_docs@workspace_variants.snap | 139 +++-- ...nversions_v1_examples@array-api-extra.snap | 37 +- ...onversions_v1_examples@cpp-git-source.snap | 63 ++- ...ests__conversions_v1_examples@cpp-sdl.snap | 67 ++- ...l__tests__conversions_v1_examples@dev.snap | 143 +++-- crates/pixi_build_types/Cargo.toml | 1 + crates/pixi_build_types/src/capabilities.rs | 12 - crates/pixi_build_types/src/lib.rs | 12 +- .../src/procedures/conda_outputs.rs | 38 +- .../src/procedures/initialize.rs | 10 +- crates/pixi_build_types/src/project_model.rs | 350 +++++++------ .../pixi_build_types/src/protocol_version.rs | 72 --- .../src/build/build_cache.rs | 6 +- .../src/build/conversion.rs | 29 +- .../src/build/dependencies.rs | 6 +- .../command_dispatcher/instantiate_backend.rs | 12 +- .../src/command_dispatcher/mod.rs | 1 + .../src/command_dispatcher/url.rs | 13 +- .../pixi_command_dispatcher/src/input_hash.rs | 6 +- .../src/instantiate_tool_env/mod.rs | 4 +- .../tests/integration/main.rs | 5 + crates/pixi_record/src/pinned_source.rs | 80 ++- crates/pixi_record/src/source_record.rs | 5 +- crates/pixi_spec/src/lib.rs | 15 +- crates/pixi_spec/src/toml.rs | 3 + crates/pixi_spec/src/url.rs | 16 + crates/pixi_stable_hash/src/lib.rs | 8 + crates/pixi_url/src/source.rs | 4 + crates/pixi_url/tests/main.rs | 6 + crates/xtask/Cargo.toml | 15 + crates/xtask/src/main.rs | 75 +++ schema/pixi_build_api.json | 495 ++++++++++++++++++ 47 files changed, 1558 insertions(+), 975 deletions(-) delete mode 100644 crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_python.snap delete mode 100644 crates/pixi_build_types/src/protocol_version.rs create mode 100644 crates/xtask/Cargo.toml create mode 100644 crates/xtask/src/main.rs create mode 100644 schema/pixi_build_api.json diff --git a/.cargo/config.toml b/.cargo/config.toml index 755c7b8015..79d92ed2c2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,6 @@ +[alias] +xtask = "run -p xtask --" + [target.x86_64-pc-windows-msvc] linker = "rust-lld" diff --git a/Cargo.lock b/Cargo.lock index a4d965101e..38551e959a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5087,6 +5087,7 @@ dependencies = [ "pixi_build_discovery", "pixi_build_types", "rattler_conda_types", + "schemars 1.1.0", "serde", "serde_json", "thiserror 2.0.17", @@ -5117,6 +5118,7 @@ dependencies = [ "pixi_stable_hash", "rattler_conda_types", "rattler_digest", + "schemars 1.1.0", "serde", "serde_json", "serde_with", @@ -8318,6 +8320,20 @@ name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b441962c817e33508847a22bd82f03a30cff43642dc2fae8b050566121eb9a" +dependencies = [ + "console 0.15.11", + "similar", +] [[package]] name = "simple_spawn_blocking" @@ -11439,6 +11455,18 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "clap", + "fs-err", + "pixi_build_types", + "schemars 1.1.0", + "serde_json", + "similar-asserts", +] + [[package]] name = "xxhash-rust" version = "0.8.15" diff --git a/Cargo.toml b/Cargo.toml index 5a06f8c1f1..6ddf11bc44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,6 +150,7 @@ reqwest-retry = { git = "https://github.com/astral-sh/reqwest-middleware", rev = rlimit = "0.10.2" rstest = "0.26.0" same-file = "1.0.6" +schemars = "1.1.0" self-replace = "1.5.0" serde = "1.0.218" serde-untagged = "0.1.6" diff --git a/crates/pixi_build_backend_passthrough/src/lib.rs b/crates/pixi_build_backend_passthrough/src/lib.rs index 9868a92acf..b76dd46dac 100644 --- a/crates/pixi_build_backend_passthrough/src/lib.rs +++ b/crates/pixi_build_backend_passthrough/src/lib.rs @@ -18,8 +18,8 @@ use pixi_build_frontend::{ json_rpc::CommunicationError, }; use pixi_build_types::{ - BackendCapabilities, BinaryPackageSpecV1, NamedSpecV1, PackageSpecV1, ProjectModelV1, - SourcePackageName, TargetSelectorV1, TargetV1, TargetsV1, VariantValue, VersionedProjectModel, + BackendCapabilities, BinaryPackageSpec, NamedSpec, PackageSpec, ProjectModel, + SourcePackageName, Target, TargetSelector, Targets, VariantValue, procedures::{ conda_build_v1::{CondaBuildV1Params, CondaBuildV1Result}, conda_outputs::{ @@ -48,7 +48,7 @@ pub enum BackendEvent { /// backend is useful for testing and debugging purposes, as it does not perform /// any actual building or processing of the project model. pub struct PassthroughBackend { - project_model: ProjectModelV1, + project_model: ProjectModel, config: PassthroughBackendConfig, source_dir: PathBuf, index_json: Option, @@ -67,7 +67,6 @@ impl InMemoryBackend for PassthroughBackend { BackendCapabilities { provides_conda_outputs: Some(true), provides_conda_build_v1: Some(true), - ..BackendCapabilities::default() } } @@ -129,7 +128,7 @@ impl InMemoryBackend for PassthroughBackend { /// configuration for that package, multiple outputs will be generated - one for /// each variant combination. fn generate_variant_outputs( - project_model: &ProjectModelV1, + project_model: &ProjectModel, index_json: &Option, params: &CondaOutputsParams, ) -> Vec { @@ -180,7 +179,7 @@ fn generate_variant_outputs( /// Finds all dependency names that have "*" requirements and have variant /// configurations. -fn find_variant_keys(project_model: &ProjectModelV1, params: &CondaOutputsParams) -> Vec { +fn find_variant_keys(project_model: &ProjectModel, params: &CondaOutputsParams) -> Vec { let Some(targets) = &project_model.targets else { return Vec::new(); }; @@ -192,7 +191,7 @@ fn find_variant_keys(project_model: &ProjectModelV1, params: &CondaOutputsParams let mut variant_keys = BTreeSet::new(); // Helper to check dependencies in a target - let mut check_deps = |deps: Option<&OrderMap>| { + let mut check_deps = |deps: Option<&OrderMap>| { if let Some(deps) = deps { for (name, spec) in deps { // Check if this dependency has a "*" requirement @@ -229,13 +228,13 @@ fn find_variant_keys(project_model: &ProjectModelV1, params: &CondaOutputsParams } /// Checks if a package spec has a "*" version requirement. -fn is_star_requirement(spec: &PackageSpecV1) -> bool { - let PackageSpecV1::Binary(boxed) = spec else { +fn is_star_requirement(spec: &PackageSpec) -> bool { + let PackageSpec::Binary(boxed) = spec else { return false; }; match boxed { - BinaryPackageSpecV1 { + BinaryPackageSpec { version, build: None, build_number: None, @@ -297,7 +296,7 @@ fn generate_variant_combinations( /// Creates a single output with the given variant configuration. fn create_output( - project_model: &ProjectModelV1, + project_model: &ProjectModel, index_json: &Option, params: &CondaOutputsParams, mut variant: BTreeMap, @@ -374,8 +373,8 @@ fn create_output( } } -fn extract_dependencies Option<&OrderMap>>( - targets: &Option, +fn extract_dependencies Option<&OrderMap>>( + targets: &Option, extract: F, platform: Platform, variant: &BTreeMap, @@ -401,7 +400,7 @@ fn extract_dependencies Option<&OrderMap Option<&OrderMap Option<&OrderMap bool { +fn matches_target_selector(selector: &TargetSelector, platform: Platform) -> bool { match selector { - TargetSelectorV1::Unix => platform.is_unix(), - TargetSelectorV1::Linux => platform.is_linux(), - TargetSelectorV1::Win => platform.is_windows(), - TargetSelectorV1::MacOs => platform.is_osx(), - TargetSelectorV1::Platform(target_platform) => target_platform == platform.as_str(), + TargetSelector::Unix => platform.is_unix(), + TargetSelector::Linux => platform.is_linux(), + TargetSelector::Win => platform.is_windows(), + TargetSelector::MacOs => platform.is_osx(), + TargetSelector::Platform(target_platform) => target_platform == platform.as_str(), } } @@ -456,10 +455,10 @@ impl InMemoryBackendInstantiator for PassthroughBackendInstantiator { params: InitializeParams, ) -> Result> { let project_model = match params.project_model { - Some(VersionedProjectModel::V1(project_model)) => project_model, - _ => { + Some(project_model) => project_model, + None => { return Err(Box::new(CommunicationError::BackendError( - BackendError::new("Passthrough backend only supports project model v1"), + BackendError::new("Passthrough backend requires a project model"), ))); } }; @@ -470,7 +469,7 @@ impl InMemoryBackendInstantiator for PassthroughBackendInstantiator { }; // Read the package file if it is specified - let source_dir = params.source_dir.expect("Missing source directory"); + let source_dir = params.source_directory.expect("Missing source directory"); let index_json = match &config.package { Some(path) => { let path = source_dir.join(path); @@ -637,14 +636,14 @@ where #[cfg(test)] mod tests { - use pixi_build_types::{BinaryPackageSpecV1, PackageSpecV1}; + use pixi_build_types::{BinaryPackageSpec, PackageSpec}; use rattler_conda_types::{ParseStrictness, VersionSpec}; use super::*; #[test] fn test_is_star_requirement_with_star() { - let spec = PackageSpecV1::Binary(BinaryPackageSpecV1 { + let spec = PackageSpec::Binary(BinaryPackageSpec { version: Some(VersionSpec::from_str("*", ParseStrictness::Lenient).unwrap()), ..Default::default() }); @@ -654,7 +653,7 @@ mod tests { #[test] fn test_is_star_requirement_with_version() { - let spec = PackageSpecV1::Binary(BinaryPackageSpecV1 { + let spec = PackageSpec::Binary(BinaryPackageSpec { version: Some(VersionSpec::from_str(">=1.0", ParseStrictness::Lenient).unwrap()), ..Default::default() }); @@ -664,7 +663,7 @@ mod tests { #[test] fn test_is_star_requirement_with_no_version() { - let spec = PackageSpecV1::Binary(BinaryPackageSpecV1::default()); + let spec = PackageSpec::Binary(BinaryPackageSpec::default()); assert!(is_star_requirement(&spec)); } diff --git a/crates/pixi_build_discovery/src/discovery.rs b/crates/pixi_build_discovery/src/discovery.rs index 8c86c220aa..18fe5f40f4 100644 --- a/crates/pixi_build_discovery/src/discovery.rs +++ b/crates/pixi_build_discovery/src/discovery.rs @@ -7,7 +7,7 @@ use itertools::Itertools; use miette::Diagnostic; use ordermap::OrderMap; use pixi_build_type_conversions::{to_project_model_v1, to_target_selector_v1}; -use pixi_build_types::{ProjectModelV1, TargetSelectorV1}; +use pixi_build_types::{ProjectModel, TargetSelector}; use pixi_config::Config; use pixi_consts::consts::{RATTLER_BUILD_DIRS, RATTLER_BUILD_FILE_NAMES, ROS_BACKEND_FILE_NAMES}; use pixi_manifest::{ @@ -55,13 +55,13 @@ pub struct BackendInitializationParams { pub manifest_path: PathBuf, /// Optionally, the manifest of the discovered package. - pub project_model: Option, + pub project_model: Option, /// Additional configuration that applies to the backend. pub configuration: Option, /// Targets that apply to the backend. - pub target_configuration: Option>, + pub target_configuration: Option>, } /// Configuration to enable or disable certain protocols discovery. @@ -377,7 +377,7 @@ impl DiscoveredBackend { package_xml_absolute_path.clone(), ))? .to_path_buf(), - project_model: Some(ProjectModelV1::default()), + project_model: Some(ProjectModel::default()), configuration: None, target_configuration: None, }, diff --git a/crates/pixi_build_frontend/Cargo.toml b/crates/pixi_build_frontend/Cargo.toml index 8874f1ef03..daa0f5d9f6 100644 --- a/crates/pixi_build_frontend/Cargo.toml +++ b/crates/pixi_build_frontend/Cargo.toml @@ -24,6 +24,7 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["process", "io-std"] } tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } +schemars = { workspace = true, optional = true } [dev-dependencies] diff --git a/crates/pixi_build_frontend/src/backend/json_rpc.rs b/crates/pixi_build_frontend/src/backend/json_rpc.rs index 78c617acaf..9ae2c07539 100644 --- a/crates/pixi_build_frontend/src/backend/json_rpc.rs +++ b/crates/pixi_build_frontend/src/backend/json_rpc.rs @@ -14,8 +14,7 @@ use jsonrpsee::{ use miette::Diagnostic; use ordermap::OrderMap; use pixi_build_types::{ - BackendCapabilities, FrontendCapabilities, ProjectModelV1, TargetSelectorV1, - VersionedProjectModel, + BackendCapabilities, FrontendCapabilities, ProjectModel, TargetSelector, procedures::{ self, conda_build_v1::{CondaBuildV1Params, CondaBuildV1Result}, @@ -146,9 +145,9 @@ impl JsonRpcBackend { source_dir: PathBuf, manifest_path: PathBuf, workspace_root: PathBuf, - package_manifest: Option, + package_manifest: Option, configuration: Option, - target_configuration: Option>, + target_configuration: Option>, cache_dir: Option, tool: Tool, ) -> Result { @@ -215,9 +214,9 @@ impl JsonRpcBackend { source_dir: PathBuf, manifest_path: PathBuf, workspace_root: PathBuf, - project_model: Option, + project_model: Option, configuration: Option, - target_configuration: Option>, + target_configuration: Option>, cache_dir: Option, sender: impl TransportSenderT + Send, receiver: impl TransportReceiverT + Send, @@ -252,12 +251,12 @@ impl JsonRpcBackend { .request( procedures::initialize::METHOD_NAME, RpcParams::from(InitializeParams { - project_model: project_model.map(VersionedProjectModel::V1), + project_model, configuration, target_configuration, manifest_path: manifest_path.clone(), - source_dir: Some(source_dir), - workspace_root: Some(workspace_root), + source_directory: Some(source_dir), + workspace_directory: Some(workspace_root), cache_directory: cache_dir, }), ) diff --git a/crates/pixi_build_type_conversions/src/project_model.rs b/crates/pixi_build_type_conversions/src/project_model.rs index a0526b723a..c8d5ad2eaa 100644 --- a/crates/pixi_build_type_conversions/src/project_model.rs +++ b/crates/pixi_build_type_conversions/src/project_model.rs @@ -19,7 +19,7 @@ use rattler_conda_types::{ChannelConfig, NamelessMatchSpec, PackageName}; fn to_pixi_spec_v1( spec: &PixiSpec, channel_config: &ChannelConfig, -) -> Result { +) -> Result { // Convert into source or binary let source_or_binary = spec.clone().into_source_or_binary(); // Convert into correct type for pixi @@ -43,8 +43,18 @@ fn to_pixi_spec_v1( }; let location = match location { pixi_spec::SourceLocationSpec::Url(url_source_spec) => { - let pixi_spec::UrlSourceSpec { url, md5, sha256 } = url_source_spec; - pbt::SourcePackageLocationSpec::Url(pbt::UrlSpecV1 { url, md5, sha256 }) + let pixi_spec::UrlSourceSpec { + url, + md5, + sha256, + subdirectory, + } = url_source_spec; + pbt::SourcePackageLocationSpec::Url(pbt::UrlSpec { + url, + md5, + sha256, + subdirectory, + }) } pixi_spec::SourceLocationSpec::Git(git_spec) => { let pixi_spec::GitSpec { @@ -52,24 +62,24 @@ fn to_pixi_spec_v1( rev, subdirectory, } = git_spec; - pbt::SourcePackageLocationSpec::Git(pbt::GitSpecV1 { + pbt::SourcePackageLocationSpec::Git(pbt::GitSpec { git, rev: rev.map(|r| match r { - GitReference::Branch(b) => pbt::GitReferenceV1::Branch(b), - GitReference::Tag(t) => pbt::GitReferenceV1::Tag(t), - GitReference::Rev(rev) => pbt::GitReferenceV1::Rev(rev), - GitReference::DefaultBranch => pbt::GitReferenceV1::DefaultBranch, + GitReference::Branch(b) => pbt::GitReference::Branch(b), + GitReference::Tag(t) => pbt::GitReference::Tag(t), + GitReference::Rev(rev) => pbt::GitReference::Rev(rev), + GitReference::DefaultBranch => pbt::GitReference::DefaultBranch, }), subdirectory, }) } pixi_spec::SourceLocationSpec::Path(path_source_spec) => { - pbt::SourcePackageLocationSpec::Path(pbt::PathSpecV1 { + pbt::SourcePackageLocationSpec::Path(pbt::PathSpec { path: path_source_spec.path.to_string(), }) } }; - pbt::PackageSpecV1::Source(pbt::SourcePackageSpec { + pbt::PackageSpec::Source(pbt::SourcePackageSpec { location, version, build, @@ -95,7 +105,7 @@ fn to_pixi_spec_v1( extras: _, condition: _, } = binary.try_into_nameless_match_spec(channel_config)?; - pbt::PackageSpecV1::Binary(pbt::BinaryPackageSpecV1 { + pbt::PackageSpec::Binary(pbt::BinaryPackageSpec { version, build, build_number, @@ -117,7 +127,7 @@ fn to_pixi_spec_v1( fn to_pbt_dependencies<'a>( iter: impl Iterator, channel_config: &ChannelConfig, -) -> Result, SpecConversionError> { +) -> Result, SpecConversionError> { iter.map(|(name, spec)| { let converted = to_pixi_spec_v1(spec, channel_config)?; Ok((name.as_normalized().to_string(), converted)) @@ -125,14 +135,14 @@ fn to_pbt_dependencies<'a>( .collect() } -/// Converts a [`PackageTarget`] to a [`pbt::TargetV1`]. +/// Converts a [`PackageTarget`] to a [`pbt::Target`]. fn to_target_v1( target: &PackageTarget, channel_config: &ChannelConfig, -) -> Result { +) -> Result { // Difference for us is that [`pbt::TargetV1`] has split the host, run and build // dependencies into separate fields, so we need to split them up here - Ok(pbt::TargetV1 { + Ok(pbt::Target { host_dependencies: Some( target .host_dependencies() @@ -157,20 +167,20 @@ fn to_target_v1( }) } -pub fn to_target_selector_v1(selector: &TargetSelector) -> pbt::TargetSelectorV1 { +pub fn to_target_selector_v1(selector: &TargetSelector) -> pbt::TargetSelector { match selector { - TargetSelector::Platform(platform) => pbt::TargetSelectorV1::Platform(platform.to_string()), - TargetSelector::Unix => pbt::TargetSelectorV1::Unix, - TargetSelector::Linux => pbt::TargetSelectorV1::Linux, - TargetSelector::Win => pbt::TargetSelectorV1::Win, - TargetSelector::MacOs => pbt::TargetSelectorV1::MacOs, + TargetSelector::Platform(platform) => pbt::TargetSelector::Platform(platform.to_string()), + TargetSelector::Unix => pbt::TargetSelector::Unix, + TargetSelector::Linux => pbt::TargetSelector::Linux, + TargetSelector::Win => pbt::TargetSelector::Win, + TargetSelector::MacOs => pbt::TargetSelector::MacOs, } } fn to_targets_v1( targets: &Targets, channel_config: &ChannelConfig, -) -> Result { +) -> Result { let selected_targets = targets .iter() .filter_map(|(k, v)| { @@ -179,21 +189,23 @@ fn to_targets_v1( .map(|target| (to_target_selector_v1(selector), target)) }) }) - .collect::, _>>()?; + .collect::, _>>()?; - Ok(pbt::TargetsV1 { + Ok(pbt::Targets { default_target: Some(to_target_v1(targets.default(), channel_config)?), targets: Some(selected_targets), }) } -/// Converts a [`PackageManifest`] to a [`pbt::ProjectModelV1`]. +/// Converts a [`PackageManifest`] to a [`pbt::ProjectModel`]. pub fn to_project_model_v1( manifest: &PackageManifest, channel_config: &ChannelConfig, -) -> Result { - let project = pbt::ProjectModelV1 { +) -> Result { + let project = pbt::ProjectModel { name: manifest.package.name.clone(), + build_string: None, + build_number: None, version: manifest.package.version.clone(), description: manifest.package.description.clone(), authors: manifest.package.authors.clone(), @@ -212,7 +224,6 @@ pub fn to_project_model_v1( mod tests { use std::path::PathBuf; - use pixi_build_types::VersionedProjectModel; use rattler_conda_types::ChannelConfig; use rstest::rstest; @@ -245,10 +256,9 @@ mod tests { .unwrap(); // Convert the manifest to the project model - let project_model: VersionedProjectModel = + let project_model = super::to_project_model_v1(&package_manifest.value, &some_channel_config()) - .unwrap() - .into(); + .unwrap(); let mut settings = insta::Settings::clone_current(); settings.set_snapshot_suffix(name); settings.bind(|| { diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_cpp.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_cpp.snap index 42c45fb6b0..9b49aff6c8 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_cpp.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_cpp.snap @@ -3,25 +3,22 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": "cpp_math", - "version": "0.1.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": {}, - "buildDependencies": {}, - "runDependencies": {} - }, - "targets": {} - } + "name": "cpp_math", + "version": "0.1.0", + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": {}, + "buildDependencies": {}, + "runDependencies": {} + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_python.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_python.snap deleted file mode 100644 index cab0bb13f8..0000000000 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_python.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: crates/pixi_build_type_conversions/src/project_model.rs -expression: project_model ---- -{ - "version": "1", - "data": { - "name": "rich_example", - "version": "0.1.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": {}, - "buildDependencies": {}, - "runDependencies": {} - }, - "targets": {} - } - } -} diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@cpp.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@cpp.snap index b5e3947b63..638ef28236 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@cpp.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@cpp.snap @@ -3,68 +3,65 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": "cpp_math", - "version": "0.1.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": { - "cmake": { - "binary": { - "version": "3.20.*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } - }, - "nanobind": { - "binary": { - "version": "2.4.*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } - }, - "python": { - "binary": { - "version": "3.12.*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "name": "cpp_math", + "version": "0.1.0", + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": { + "cmake": { + "binary": { + "version": "3.20.*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } }, - "buildDependencies": {}, - "runDependencies": {} + "nanobind": { + "binary": { + "version": "2.4.*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null + } + }, + "python": { + "binary": { + "version": "3.12.*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null + } + } }, - "targets": {} - } + "buildDependencies": {}, + "runDependencies": {} + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@dev.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@dev.snap index 9a92cbe8c1..862610bf50 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@dev.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@dev.snap @@ -3,70 +3,67 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": null, - "version": null, - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": { - "python": { - "binary": { - "version": "*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "name": null, + "version": null, + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": { + "python": { + "binary": { + "version": "*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } - }, - "buildDependencies": { - "cmake": { - "binary": { - "version": "*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } - } - }, - "runDependencies": { - "bat": { - "binary": { - "version": "*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + } + }, + "buildDependencies": { + "cmake": { + "binary": { + "version": "*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } } }, - "targets": {} - } + "runDependencies": { + "bat": { + "binary": { + "version": "*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null + } + } + } + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@getting_started.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@getting_started.snap index 9379b82def..b0c3bd58f5 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@getting_started.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@getting_started.snap @@ -1,58 +1,54 @@ --- source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model -snapshot_kind: text --- { - "version": "1", - "data": { - "name": "python_rich", - "version": "0.1.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": { - "hatchling": { - "binary": { - "version": "*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } - } - }, - "buildDependencies": {}, - "runDependencies": { - "rich": { - "binary": { - "version": "13.9.*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "name": "python_rich", + "version": "0.1.0", + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": { + "hatchling": { + "binary": { + "version": "*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } } }, - "targets": {} - } + "buildDependencies": {}, + "runDependencies": { + "rich": { + "binary": { + "version": "13.9.*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null + } + } + } + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@python.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@python.snap index 31ac10f757..7b21750e19 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@python.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@python.snap @@ -3,55 +3,52 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": "python_rich", - "version": "0.1.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": { - "hatchling": { - "binary": { - "version": "==1.26.3", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } - } - }, - "buildDependencies": {}, - "runDependencies": { - "rich": { - "binary": { - "version": "13.9.*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "name": "python_rich", + "version": "0.1.0", + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": { + "hatchling": { + "binary": { + "version": "==1.26.3", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } } }, - "targets": {} - } + "buildDependencies": {}, + "runDependencies": { + "rich": { + "binary": { + "version": "13.9.*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null + } + } + } + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap index 37cb11c795..12f1c21fd5 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap @@ -3,67 +3,64 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": "python_rich", - "version": "0.1.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": { - "hatchling": { - "binary": { - "version": "==1.26.3", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "name": "python_rich", + "version": "0.1.0", + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": { + "hatchling": { + "binary": { + "version": "==1.26.3", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null + } + } + }, + "buildDependencies": {}, + "runDependencies": { + "cpp_math": { + "source": { + "path": { + "path": "packages/cpp_math" + }, + "version": null, + "build": null, + "buildNumber": null, + "subdir": null, + "license": null } }, - "buildDependencies": {}, - "runDependencies": { - "cpp_math": { - "source": { - "Path": { - "path": "packages/cpp_math" - }, - "version": null, - "build": null, - "buildNumber": null, - "subdir": null, - "license": null - } - }, - "rich": { - "binary": { - "version": "13.9.*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "rich": { + "binary": { + "version": "13.9.*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } } - }, - "targets": {} - } + } + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap index bc2dd7e2c8..51bade3171 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap @@ -3,81 +3,78 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": "python_rich", - "version": "0.1.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": { - "hatchling": { - "binary": { - "version": "==1.26.3", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } - }, - "python": { - "binary": { - "version": "*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "name": "python_rich", + "version": "0.1.0", + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": { + "hatchling": { + "binary": { + "version": "==1.26.3", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } }, - "buildDependencies": {}, - "runDependencies": { - "cpp_math": { - "source": { - "Path": { - "path": "packages/cpp_math" - }, - "version": null, - "build": null, - "buildNumber": null, - "subdir": null, - "license": null - } - }, - "rich": { - "binary": { - "version": ">=13.9.4,<14", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "python": { + "binary": { + "version": "*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } } }, - "targets": {} - } + "buildDependencies": {}, + "runDependencies": { + "cpp_math": { + "source": { + "path": { + "path": "packages/cpp_math" + }, + "version": null, + "build": null, + "buildNumber": null, + "subdir": null, + "license": null + } + }, + "rich": { + "binary": { + "version": ">=13.9.4,<14", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null + } + } + } + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@array-api-extra.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@array-api-extra.snap index 55e7aa78be..425530e53e 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@array-api-extra.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@array-api-extra.snap @@ -3,25 +3,22 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": "array-api-extra", - "version": "0.8.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": {}, - "buildDependencies": {}, - "runDependencies": {} - }, - "targets": {} - } + "name": "array-api-extra", + "version": "0.8.0", + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": {}, + "buildDependencies": {}, + "runDependencies": {} + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-git-source.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-git-source.snap index c372522d6b..c34da6f24a 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-git-source.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-git-source.snap @@ -3,40 +3,37 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": "sdl_example", - "version": "0.1.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": { - "sdl2": { - "binary": { - "version": ">=2.26.5,<3.0", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "name": "sdl_example", + "version": "0.1.0", + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": { + "sdl2": { + "binary": { + "version": ">=2.26.5,<3.0", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } - }, - "buildDependencies": {}, - "runDependencies": {} + } }, - "targets": {} - } + "buildDependencies": {}, + "runDependencies": {} + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-sdl.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-sdl.snap index 9881fa4641..2ea5b90d5b 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-sdl.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-sdl.snap @@ -3,42 +3,39 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": "sdl_example", - "version": "0.1.0", - "description": "Showcases how to create a simple C++ executable with Pixi", - "authors": [ - "Bas Zalmstra " - ], - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": { - "sdl2": { - "binary": { - "version": ">=2.26.5,<3.0", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "name": "sdl_example", + "version": "0.1.0", + "description": "Showcases how to create a simple C++ executable with Pixi", + "authors": [ + "Bas Zalmstra " + ], + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": { + "sdl2": { + "binary": { + "version": ">=2.26.5,<3.0", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } - }, - "buildDependencies": {}, - "runDependencies": {} + } }, - "targets": {} - } + "buildDependencies": {}, + "runDependencies": {} + }, + "targets": {} } } diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@dev.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@dev.snap index 950d9f3cab..80965a8a8f 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@dev.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@dev.snap @@ -3,84 +3,81 @@ source: crates/pixi_build_type_conversions/src/project_model.rs expression: project_model --- { - "version": "1", - "data": { - "name": "minimal-example", - "version": "0.1.0", - "description": null, - "authors": null, - "license": null, - "licenseFile": null, - "readme": null, - "homepage": null, - "repository": null, - "documentation": null, - "targets": { - "defaultTarget": { - "hostDependencies": { - "python": { - "binary": { - "version": ">=3.12.4,<4", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } - }, - "sdl2": { - "binary": { - "version": ">=2.26.5,<3.0", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "name": "minimal-example", + "version": "0.1.0", + "description": null, + "authors": null, + "license": null, + "licenseFile": null, + "readme": null, + "homepage": null, + "repository": null, + "documentation": null, + "targets": { + "defaultTarget": { + "hostDependencies": { + "python": { + "binary": { + "version": ">=3.12.4,<4", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } }, - "buildDependencies": { - "cmake": { - "binary": { - "version": ">=3.27.8,<4.0", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + "sdl2": { + "binary": { + "version": ">=2.26.5,<3.0", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } - }, - "runDependencies": { - "bat": { - "binary": { - "version": "*", - "build": null, - "buildNumber": null, - "fileName": null, - "channel": null, - "subdir": null, - "md5": null, - "sha256": null, - "url": null, - "license": null - } + } + }, + "buildDependencies": { + "cmake": { + "binary": { + "version": ">=3.27.8,<4.0", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null } } }, - "targets": {} - } + "runDependencies": { + "bat": { + "binary": { + "version": "*", + "build": null, + "buildNumber": null, + "fileName": null, + "channel": null, + "subdir": null, + "md5": null, + "sha256": null, + "url": null, + "license": null + } + } + } + }, + "targets": {} } } diff --git a/crates/pixi_build_types/Cargo.toml b/crates/pixi_build_types/Cargo.toml index ab76031cb1..93e37733f9 100644 --- a/crates/pixi_build_types/Cargo.toml +++ b/crates/pixi_build_types/Cargo.toml @@ -18,6 +18,7 @@ pixi_stable_hash = { workspace = true, features = [ ] } rattler_conda_types = { workspace = true } rattler_digest = { workspace = true, features = ["serde"] } +schemars = { workspace = true, optional = true, features = ["url2"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_with = { workspace = true } diff --git a/crates/pixi_build_types/src/capabilities.rs b/crates/pixi_build_types/src/capabilities.rs index b3a5889a32..253dcd7ef4 100644 --- a/crates/pixi_build_types/src/capabilities.rs +++ b/crates/pixi_build_types/src/capabilities.rs @@ -7,9 +7,6 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] /// Capabilities that the backend provides. pub struct BackendCapabilities { - /// The highest supported project model version. - pub highest_supported_project_model: Option, - /// Whether the backend provides the `conda/outputs` API. pub provides_conda_outputs: Option, @@ -22,10 +19,6 @@ impl BackendCapabilities { pub fn mask_with_api_version(&self, version: &PixiBuildApiVersion) -> Self { let expected = version.expected_backend_capabilities(); Self { - highest_supported_project_model: Some( - self.highest_supported_project_model() - .min(expected.highest_supported_project_model()), - ), provides_conda_outputs: Some( self.provides_conda_outputs() && expected.provides_conda_outputs(), ), @@ -35,11 +28,6 @@ impl BackendCapabilities { } } - /// The highest supported project model version. - pub fn highest_supported_project_model(&self) -> u32 { - self.highest_supported_project_model.unwrap_or(0) - } - /// Whether the backend provides the `conda/outputs` API. pub fn provides_conda_outputs(&self) -> bool { self.provides_conda_outputs.unwrap_or(false) diff --git a/crates/pixi_build_types/src/lib.rs b/crates/pixi_build_types/src/lib.rs index e1d957b91d..d344399684 100644 --- a/crates/pixi_build_types/src/lib.rs +++ b/crates/pixi_build_types/src/lib.rs @@ -12,9 +12,9 @@ pub use capabilities::{BackendCapabilities, FrontendCapabilities}; pub use channel_configuration::ChannelConfiguration; pub use conda_package_metadata::CondaPackageMetadata; pub use project_model::{ - BinaryPackageSpecV1, GitReferenceV1, GitSpecV1, NamedSpecV1, PackageSpecV1, PathSpecV1, - ProjectModelV1, SourcePackageLocationSpec, SourcePackageName, SourcePackageSpec, - TargetSelectorV1, TargetV1, TargetsV1, UrlSpecV1, VersionedProjectModel, + BinaryPackageSpec, GitReference, GitSpec, NamedSpec, PackageSpec, PathSpec, ProjectModel, + SourcePackageLocationSpec, SourcePackageName, SourcePackageSpec, Target, TargetSelector, + Targets, UrlSpec, }; use rattler_conda_types::{ GenericVirtualPackage, PackageName, Platform, Version, VersionSpec, @@ -27,14 +27,15 @@ pub use variant::VariantValue; // Version 1: Added conda/outputs and conda/build_v1 // Version 2: Name in project models can be `None`. // Version 3: Outputs with the same name must have unique variants. +// Version 4: (BREAKING) Add matchspec fields to source record, cleanup types, remove version from project model and streamline use of directory vs dir. /// The constraint for the pixi build api version package /// Adding this constraint when solving a pixi build backend environment ensures /// that a backend is selected that uses the same interface version as Pixi does pub static PIXI_BUILD_API_VERSION_NAME: LazyLock = LazyLock::new(|| PackageName::new_unchecked("pixi-build-api-version")); -pub const PIXI_BUILD_API_VERSION_LOWER: u64 = 1; -pub const PIXI_BUILD_API_VERSION_CURRENT: u64 = 3; +pub const PIXI_BUILD_API_VERSION_LOWER: u64 = 4; +pub const PIXI_BUILD_API_VERSION_CURRENT: u64 = 4; pub const PIXI_BUILD_API_VERSION_UPPER: u64 = PIXI_BUILD_API_VERSION_CURRENT + 1; pub static PIXI_BUILD_API_VERSION_SPEC: LazyLock = LazyLock::new(|| { VersionSpec::Group( @@ -82,7 +83,6 @@ impl PixiBuildApiVersion { 1 => BackendCapabilities { provides_conda_outputs: Some(true), provides_conda_build_v1: Some(true), - highest_supported_project_model: Some(1), }, 2 => BackendCapabilities { ..Self(1).expected_backend_capabilities() diff --git a/crates/pixi_build_types/src/procedures/conda_outputs.rs b/crates/pixi_build_types/src/procedures/conda_outputs.rs index 2ce1dcd71b..d977459a9d 100644 --- a/crates/pixi_build_types/src/procedures/conda_outputs.rs +++ b/crates/pixi_build_types/src/procedures/conda_outputs.rs @@ -16,7 +16,7 @@ use rattler_conda_types::{ChannelUrl, NoArchType, PackageName, Platform, Version use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::{BinaryPackageSpecV1, PackageSpecV1, VariantValue, project_model::NamedSpecV1}; +use crate::{BinaryPackageSpec, PackageSpec, VariantValue, project_model::NamedSpec}; pub const METHOD_NAME: &str = "conda/outputs"; @@ -163,11 +163,11 @@ pub struct CondaOutputMetadata { pub struct CondaOutputDependencies { /// A list of matchspecs that describe the dependencies of a particular /// environment. - pub depends: Vec>, + pub depends: Vec>, /// Additional constraints that apply to the environment in which the /// dependencies are solved. Constraints are represented as matchspecs. - pub constraints: Vec>, + pub constraints: Vec>, } /// Describes which run-exports should be ignored for a particular output. @@ -187,23 +187,23 @@ pub struct CondaOutputIgnoreRunExports { #[serde(rename_all = "camelCase")] pub struct CondaOutputRunExports { /// weak run exports apply a dependency from host to run - pub weak: Vec>, + pub weak: Vec>, /// strong run exports apply a dependency from build to host and run - pub strong: Vec>, + pub strong: Vec>, /// noarch run exports apply a run export only to noarch packages (other run /// exports are ignored) for example, python uses this to apply a /// dependency on python to all noarch packages, but not to /// the python_abi package - pub noarch: Vec>, + pub noarch: Vec>, /// weak constrains apply a constrain dependency from host to run - pub weak_constrains: Vec>, + pub weak_constrains: Vec>, /// strong constrains apply a constrain dependency from build to host and /// run - pub strong_constrains: Vec>, + pub strong_constrains: Vec>, } // TODO: Multi-output caching is not yet supported. @@ -214,18 +214,18 @@ pub struct CondaOutputRunExports { // /// An optional name // pub name: Option, // -// /// The build dependencies of the package. These refer to the packages that -// /// should be installed in the "build" environment. The build environment -// /// contains packages for the current architecture that can be used to run -// /// tools on the current machine like compilers, code generators, etc. -// pub build_dependencies: Option, +// /// The build dependencies of the package. These refer to the packages +// that /// should be installed in the "build" environment. The build +// environment /// contains packages for the current architecture that can +// be used to run /// tools on the current machine like compilers, code +// generators, etc. pub build_dependencies: Option, // -// /// The "host" dependencies of the package. These refer to the package that -// /// should be installed to be able to refer to them from the build process -// /// but not run them. They are installed for the "target" architecture (see -// /// subdir) or for the current architecture if the target is `noarch`. -// /// For C++ packages these would be libraries to link against. -// pub host_dependencies: Option, +// /// The "host" dependencies of the package. These refer to the package +// that /// should be installed to be able to refer to them from the build +// process /// but not run them. They are installed for the "target" +// architecture (see /// subdir) or for the current architecture if the +// target is `noarch`. /// For C++ packages these would be libraries to link +// against. pub host_dependencies: Option, // // /// Describes which run-exports should be ignored for this package. // pub ignore_run_exports: CondaOutputIgnoreRunExports, diff --git a/crates/pixi_build_types/src/procedures/initialize.rs b/crates/pixi_build_types/src/procedures/initialize.rs index eb43fb4442..cfcd4f0cb5 100644 --- a/crates/pixi_build_types/src/procedures/initialize.rs +++ b/crates/pixi_build_types/src/procedures/initialize.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use ordermap::OrderMap; use serde::{Deserialize, Serialize}; -use crate::{TargetSelectorV1, VersionedProjectModel}; +use crate::{ProjectModel, TargetSelector}; pub const METHOD_NAME: &str = "initialize"; @@ -35,12 +35,12 @@ pub struct InitializeParams { /// `manifest_path` as the source directory. /// /// This is an absolute path. This is always a directory. - pub source_dir: Option, + pub source_directory: Option, /// The root directory of the workspace. /// /// This is an absolute path. - pub workspace_root: Option, + pub workspace_directory: Option, /// Optionally the cache directory to use for any caching activity. pub cache_directory: Option, @@ -48,13 +48,13 @@ pub struct InitializeParams { /// Project model that the backend should use even though it is an option /// it is highly recommended to use this field. Otherwise, it will be very /// easy to break backwards compatibility. - pub project_model: Option, + pub project_model: Option, /// Backend specific configuration passed from the frontend to the backend. pub configuration: Option, /// Targets that apply to the backend. - pub target_configuration: Option>, + pub target_configuration: Option>, } /// The result of the initialize request. diff --git a/crates/pixi_build_types/src/project_model.rs b/crates/pixi_build_types/src/project_model.rs index a18b2befb7..fce98a2920 100644 --- a/crates/pixi_build_types/src/project_model.rs +++ b/crates/pixi_build_types/src/project_model.rs @@ -8,74 +8,35 @@ //! and backwards compatibility. The idea for **backwards compatibility** is //! that we try not to break this in pixi as much as possible. So as long as //! older pixi TOMLs keep loading, we can send them to the backend. -//! -//! In regards to forwards compatibility, we want to be able to keep converting -//! to all versions of the `VersionedProjectModel` as much as possible. -//! -//! This is why we append a `V{version}` to the type names, to indicate the -//! version of the protocol. -//! -//! Only the whole ProjectModel is versioned explicitly in an enum. -//! When making a change to one of the types, be sure to add another enum -//! declaration if it is breaking. use std::{convert::Infallible, fmt::Display, hash::Hash, path::PathBuf, str::FromStr}; use ordermap::OrderMap; use pixi_stable_hash::{IsDefault, StableHashBuilder}; -use rattler_conda_types::{BuildNumberSpec, StringMatcher, Version, VersionSpec}; +use rattler_conda_types::{BuildNumber, BuildNumberSpec, StringMatcher, Version, VersionSpec}; use rattler_digest::{Md5, Md5Hash, Sha256, Sha256Hash, serde::SerializableHash}; use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, DisplayFromStr, SerializeDisplay, serde_as}; use url::Url; -/// Enum containing all versions of the project model. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "version", content = "data")] -#[serde(rename_all = "camelCase")] -pub enum VersionedProjectModel { - /// Version 1 of the project model. - #[serde(rename = "1")] - V1(ProjectModelV1), - // When adding don't forget to update the highest_version function -} - -impl VersionedProjectModel { - /// Highest version of the project model. - pub fn highest_version() -> u32 { - // increase this when adding a new version - 1 - } - - /// Move into the v1 type, returns None if the version is not v1. - pub fn into_v1(self) -> Option { - match self { - VersionedProjectModel::V1(v) => Some(v), - // Add this once we have more versions - //_ => None, - } - } - - /// Returns a reference to the v1 type, returns None if the version is not - /// v1. - pub fn as_v1(&self) -> Option<&ProjectModelV1> { - match self { - VersionedProjectModel::V1(v) => Some(v), - // Add this once we have more versions - //_ => None, - } - } -} - /// The source package name of a package. Not normalized per se. pub type SourcePackageName = String; #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub struct ProjectModelV1 { +pub struct ProjectModel { /// The name of the project pub name: Option, + /// A build string configured by the user. + pub build_string: Option, + + /// The build number configured by the user. + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] + pub build_number: Option, + /// The version of the project + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub version: Option, /// An optional project description @@ -104,10 +65,10 @@ pub struct ProjectModelV1 { /// The target of the project, this may contain /// platform specific configurations. - pub targets: Option, + pub targets: Option, } -impl IsDefault for ProjectModelV1 { +impl IsDefault for ProjectModel { type Item = Self; fn is_non_default(&self) -> Option<&Self::Item> { @@ -115,16 +76,11 @@ impl IsDefault for ProjectModelV1 { } } -impl From for VersionedProjectModel { - fn from(value: ProjectModelV1) -> Self { - VersionedProjectModel::V1(value) - } -} - /// Represents a target selector. Currently, we only support explicit platform /// selection. #[derive(Debug, Clone, DeserializeFromStr, SerializeDisplay, Eq, PartialEq)] -pub enum TargetSelectorV1 { +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum TargetSelector { // Platform specific configuration Unix, Linux, @@ -134,43 +90,48 @@ pub enum TargetSelectorV1 { // TODO: Add minijinja coolness here. } -impl Display for TargetSelectorV1 { +impl Display for TargetSelector { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - TargetSelectorV1::Unix => write!(f, "unix"), - TargetSelectorV1::Linux => write!(f, "linux"), - TargetSelectorV1::Win => write!(f, "win"), - TargetSelectorV1::MacOs => write!(f, "macos"), - TargetSelectorV1::Platform(p) => write!(f, "{p}"), + TargetSelector::Unix => write!(f, "unix"), + TargetSelector::Linux => write!(f, "linux"), + TargetSelector::Win => write!(f, "win"), + TargetSelector::MacOs => write!(f, "macos"), + TargetSelector::Platform(p) => write!(f, "{p}"), } } } -impl FromStr for TargetSelectorV1 { +impl FromStr for TargetSelector { type Err = Infallible; fn from_str(s: &str) -> Result { match s { - "unix" => Ok(TargetSelectorV1::Unix), - "linux" => Ok(TargetSelectorV1::Linux), - "win" => Ok(TargetSelectorV1::Win), - "macos" => Ok(TargetSelectorV1::MacOs), - _ => Ok(TargetSelectorV1::Platform(s.to_string())), + "unix" => Ok(TargetSelector::Unix), + "linux" => Ok(TargetSelector::Linux), + "win" => Ok(TargetSelector::Win), + "macos" => Ok(TargetSelector::MacOs), + _ => Ok(TargetSelector::Platform(s.to_string())), } } } /// A collect of targets including a default target. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub struct TargetsV1 { - pub default_target: Option, +pub struct Targets { + pub default_target: Option, /// We use an [`OrderMap`] to preserve the order in which the items where /// defined in the manifest. - pub targets: Option>, + #[cfg_attr( + feature = "schemars", + schemars(with = "Option>") + )] + pub targets: Option>, } -impl TargetsV1 { +impl Targets { /// Check if this targets struct is effectively empty (contains no /// meaningful data that should affect the hash). pub fn is_empty(&self) -> bool { @@ -182,7 +143,7 @@ impl TargetsV1 { } } -impl IsDefault for TargetsV1 { +impl IsDefault for Targets { type Item = Self; fn is_non_default(&self) -> Option<&Self::Item> { @@ -191,19 +152,32 @@ impl IsDefault for TargetsV1 { } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub struct TargetV1 { +pub struct Target { /// Host dependencies of the project - pub host_dependencies: Option>, + #[cfg_attr( + feature = "schemars", + schemars(with = "Option>") + )] + pub host_dependencies: Option>, /// Build dependencies of the project - pub build_dependencies: Option>, + #[cfg_attr( + feature = "schemars", + schemars(with = "Option>") + )] + pub build_dependencies: Option>, /// Run dependencies of the project - pub run_dependencies: Option>, + #[cfg_attr( + feature = "schemars", + schemars(with = "Option>") + )] + pub run_dependencies: Option>, } -impl TargetV1 { +impl Target { /// Check if this target is effectively empty (contains no meaningful data /// that should affect the hash). pub fn is_empty(&self) -> bool { @@ -218,7 +192,7 @@ impl TargetV1 { } } -impl IsDefault for TargetV1 { +impl IsDefault for Target { type Item = Self; fn is_non_default(&self) -> Option<&Self::Item> { @@ -227,17 +201,19 @@ impl IsDefault for TargetV1 { } #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub enum PackageSpecV1 { +pub enum PackageSpec { /// This is a binary dependency - Binary(BinaryPackageSpecV1), + Binary(BinaryPackageSpec), /// This is a dependency on a source package Source(SourcePackageSpec), } #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub struct NamedSpecV1 { +pub struct NamedSpec { pub name: SourcePackageName, #[serde(flatten)] @@ -246,17 +222,21 @@ pub struct NamedSpecV1 { #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub struct SourcePackageSpec { #[serde(flatten)] pub location: SourcePackageLocationSpec, /// The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`) #[serde_as(as = "Option")] + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub version: Option, /// The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`) #[serde_as(as = "Option")] + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub build: Option, /// The build number of the package + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub build_number: Option, /// The subdir of the channel pub subdir: Option, @@ -265,8 +245,8 @@ pub struct SourcePackageSpec { pub license: Option, } -impl From for SourcePackageSpec { - fn from(value: PathSpecV1) -> Self { +impl From for SourcePackageSpec { + fn from(value: PathSpec) -> Self { Self { location: SourcePackageLocationSpec::Path(value), version: None, @@ -278,8 +258,8 @@ impl From for SourcePackageSpec { } } -impl From for SourcePackageSpec { - fn from(value: UrlSpecV1) -> Self { +impl From for SourcePackageSpec { + fn from(value: UrlSpec) -> Self { Self { location: SourcePackageLocationSpec::Url(value), version: None, @@ -291,8 +271,8 @@ impl From for SourcePackageSpec { } } -impl From for SourcePackageSpec { - fn from(value: GitSpecV1) -> Self { +impl From for SourcePackageSpec { + fn from(value: GitSpec) -> Self { Self { location: SourcePackageLocationSpec::Git(value), version: None, @@ -305,42 +285,50 @@ impl From for SourcePackageSpec { } #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] pub enum SourcePackageLocationSpec { /// The spec is represented as an archive that can be downloaded from the /// specified URL. The package should be retrieved from the URL and can /// either represent a source or binary package depending on the archive /// type. - Url(UrlSpecV1), + Url(UrlSpec), /// The spec is represented as a git repository. The package represents a /// source distribution of some kind. - Git(GitSpecV1), + Git(GitSpec), /// The spec is represented as a local path. The package should be retrieved /// from the local filesystem. The package can be either a source or binary /// package. - Path(PathSpecV1), + Path(PathSpec), } #[serde_as] #[derive(Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub struct UrlSpecV1 { +pub struct UrlSpec { /// The URL of the package pub url: Url, /// The md5 hash of the package #[serde_as(as = "Option>")] + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub md5: Option, /// The sha256 hash of the package #[serde_as(as = "Option>")] + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub sha256: Option, + + /// The subdirectory of the package in the archive + pub subdirectory: Option, } -impl std::fmt::Debug for UrlSpecV1 { +impl std::fmt::Debug for UrlSpec { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut debug_struct = f.debug_struct("UrlSpecV1"); + let mut debug_struct = f.debug_struct("UrlSpec"); debug_struct.field("url", &self.url); if let Some(md5) = &self.md5 { @@ -355,13 +343,14 @@ impl std::fmt::Debug for UrlSpecV1 { /// A specification of a package from a git repository. #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub struct GitSpecV1 { +pub struct GitSpec { /// The git url of the package which can contain git+ prefixes. pub git: Url, /// The git revision of the package - pub rev: Option, + pub rev: Option, /// The git subdirectory of the package pub subdirectory: Option, @@ -369,16 +358,18 @@ pub struct GitSpecV1 { /// A specification of a package from a path #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub struct PathSpecV1 { +pub struct PathSpec { /// The path to the package pub path: String, } /// A reference to a specific commit in a git repository. #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub enum GitReferenceV1 { +pub enum GitReference { /// The HEAD commit of a branch. Branch(String), @@ -395,15 +386,19 @@ pub enum GitReferenceV1 { /// Similar to a [`rattler_conda_types::NamelessMatchSpec`] #[serde_as] #[derive(Clone, Serialize, Deserialize, Default, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] -pub struct BinaryPackageSpecV1 { +pub struct BinaryPackageSpec { /// The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`) #[serde_as(as = "Option")] + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub version: Option, /// The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`) #[serde_as(as = "Option")] + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub build: Option, /// The build number of the package + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub build_number: Option, /// Match the specific filename of the package pub file_name: Option, @@ -413,9 +408,11 @@ pub struct BinaryPackageSpecV1 { pub subdir: Option, /// The md5 hash of the package #[serde_as(as = "Option>")] + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub md5: Option, /// The sha256 hash of the package #[serde_as(as = "Option>")] + #[cfg_attr(feature = "schemars", schemars(with = "Option"))] pub sha256: Option, /// The URL of the package, if it is available pub url: Option, @@ -423,7 +420,7 @@ pub struct BinaryPackageSpecV1 { pub license: Option, } -impl From for BinaryPackageSpecV1 { +impl From for BinaryPackageSpec { fn from(value: VersionSpec) -> Self { Self { version: Some(value), @@ -432,7 +429,7 @@ impl From for BinaryPackageSpecV1 { } } -impl From<&VersionSpec> for BinaryPackageSpecV1 { +impl From<&VersionSpec> for BinaryPackageSpec { fn from(value: &VersionSpec) -> Self { Self { version: Some(value.clone()), @@ -441,7 +438,7 @@ impl From<&VersionSpec> for BinaryPackageSpecV1 { } } -impl std::fmt::Debug for BinaryPackageSpecV1 { +impl std::fmt::Debug for BinaryPackageSpec { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut debug_struct = f.debug_struct("NamelessMatchSpecV1"); @@ -475,13 +472,15 @@ impl std::fmt::Debug for BinaryPackageSpecV1 { } // Custom Hash implementations that skip default values for stability -impl Hash for ProjectModelV1 { +impl Hash for ProjectModel { /// Custom hash implementation using StableHashBuilder to ensure different /// field configurations produce different hashes while maintaining /// forward/backward compatibility. fn hash(&self, state: &mut H) { - let ProjectModelV1 { + let ProjectModel { name, + build_string, + build_number, version, description, authors, @@ -496,6 +495,8 @@ impl Hash for ProjectModelV1 { StableHashBuilder::::new() .field("authors", authors) + .field("build_string", build_string) + .field("build_number", build_number) .field("description", description) .field("documentation", documentation) .field("homepage", homepage) @@ -510,16 +511,16 @@ impl Hash for ProjectModelV1 { } } -impl Hash for TargetSelectorV1 { +impl Hash for TargetSelector { /// Custom hash implementation that uses discriminant values to keep the /// hash as stable as possible when adding new enum variants. fn hash(&self, state: &mut H) { match self { - TargetSelectorV1::Unix => 0u8.hash(state), - TargetSelectorV1::Linux => 1u8.hash(state), - TargetSelectorV1::Win => 2u8.hash(state), - TargetSelectorV1::MacOs => 3u8.hash(state), - TargetSelectorV1::Platform(p) => { + TargetSelector::Unix => 0u8.hash(state), + TargetSelector::Linux => 1u8.hash(state), + TargetSelector::Win => 2u8.hash(state), + TargetSelector::MacOs => 3u8.hash(state), + TargetSelector::Platform(p) => { 4u8.hash(state); p.hash(state); } @@ -527,12 +528,12 @@ impl Hash for TargetSelectorV1 { } } -impl Hash for TargetsV1 { +impl Hash for Targets { /// Custom hash implementation using StableHashBuilder to ensure different /// field configurations produce different hashes while maintaining /// forward/backward compatibility. fn hash(&self, state: &mut H) { - let TargetsV1 { + let Targets { default_target, targets, } = self; @@ -544,12 +545,12 @@ impl Hash for TargetsV1 { } } -impl Hash for TargetV1 { +impl Hash for Target { /// Custom hash implementation using StableHashBuilder to ensure different /// field configurations produce different hashes while maintaining /// forward/backward compatibility. fn hash(&self, state: &mut H) { - let TargetV1 { + let Target { build_dependencies, host_dependencies, run_dependencies, @@ -563,16 +564,16 @@ impl Hash for TargetV1 { } } -impl Hash for PackageSpecV1 { +impl Hash for PackageSpec { /// Custom hash implementation that uses discriminant values to keep the /// hash as stable as possible when adding new enum variants. fn hash(&self, state: &mut H) { match self { - PackageSpecV1::Binary(spec) => { + PackageSpec::Binary(spec) => { 0u8.hash(state); spec.hash(state); } - PackageSpecV1::Source(spec) => { + PackageSpec::Source(spec) => { 1u8.hash(state); spec.hash(state); } @@ -594,7 +595,8 @@ impl Hash for SourcePackageSpec { // Hash the location first, to ensure compatibility with older versions. location.hash(state); - // Add the new fields using StableHashBuilder for forward/backward compatibility. + // Add the new fields using StableHashBuilder for forward/backward + // compatibility. StableHashBuilder::::new() .field("build", build) .field("build_number", build_number) @@ -626,22 +628,28 @@ impl Hash for SourcePackageLocationSpec { } } -impl Hash for UrlSpecV1 { +impl Hash for UrlSpec { /// Custom hash implementation using StableHashBuilder to ensure different /// field configurations produce different hashes while maintaining /// forward/backward compatibility. fn hash(&self, state: &mut H) { - let UrlSpecV1 { url, md5, sha256 } = self; + let UrlSpec { + url, + md5, + sha256, + subdirectory, + } = self; StableHashBuilder::::new() .field("md5", md5) .field("sha256", sha256) .field("url", url) + .field("subdirectory", subdirectory) .finish(state); } } -impl Hash for GitSpecV1 { +impl Hash for GitSpec { /// Custom hash implementation using StableHashBuilder to ensure different /// field configurations produce different hashes while maintaining /// forward/backward compatibility. @@ -654,40 +662,40 @@ impl Hash for GitSpecV1 { } } -impl Hash for PathSpecV1 { +impl Hash for PathSpec { /// Custom hash implementation to keep the hash as stable as possible. fn hash(&self, state: &mut H) { - let PathSpecV1 { path } = self; + let PathSpec { path } = self; path.hash(state); } } -impl Hash for GitReferenceV1 { +impl Hash for GitReference { /// Custom hash implementation that uses discriminant values to keep the /// hash as stable as possible when adding new enum variants. fn hash(&self, state: &mut H) { match self { - GitReferenceV1::Branch(b) => { + GitReference::Branch(b) => { 0u8.hash(state); b.hash(state); } - GitReferenceV1::Tag(t) => { + GitReference::Tag(t) => { 1u8.hash(state); t.hash(state); } - GitReferenceV1::Rev(r) => { + GitReference::Rev(r) => { 2u8.hash(state); r.hash(state); } - GitReferenceV1::DefaultBranch => { + GitReference::DefaultBranch => { 3u8.hash(state); } } } } -impl IsDefault for GitReferenceV1 { +impl IsDefault for GitReference { type Item = Self; fn is_non_default(&self) -> Option<&Self::Item> { @@ -695,7 +703,7 @@ impl IsDefault for GitReferenceV1 { } } -impl Hash for BinaryPackageSpecV1 { +impl Hash for BinaryPackageSpec { /// Custom hash implementation using StableHashBuilder to ensure different /// field configurations produce different hashes while maintaining /// forward/backward compatibility. @@ -730,8 +738,10 @@ mod tests { #[test] fn test_hash_stability_with_default_values() { // Create a minimal ProjectModelV1 instance - let mut project_model = ProjectModelV1 { + let mut project_model = ProjectModel { name: Some("test-project".to_string()), + build_number: None, + build_string: None, version: None, description: None, authors: None, @@ -749,19 +759,19 @@ mod tests { // Add empty targets field - with corrected implementation, this should NOT // change hash because we only include discriminants for // non-default/non-empty values - project_model.targets = Some(TargetsV1 { + project_model.targets = Some(Targets { default_target: None, targets: Some(OrderMap::new()), }); let hash2 = calculate_hash(&project_model); // Add a target with empty dependencies - this should also NOT change hash - let empty_target = TargetV1 { + let empty_target = Target { host_dependencies: Some(OrderMap::new()), build_dependencies: Some(OrderMap::new()), run_dependencies: Some(OrderMap::new()), }; - project_model.targets = Some(TargetsV1 { + project_model.targets = Some(Targets { default_target: Some(empty_target), targets: Some(OrderMap::new()), }); @@ -786,8 +796,10 @@ mod tests { #[test] fn test_hash_changes_with_meaningful_values() { // Create a minimal ProjectModelV1 instance - let mut project_model = ProjectModelV1 { + let mut project_model = ProjectModel { name: Some("test-project".to_string()), + build_number: None, + build_string: None, version: None, description: None, authors: None, @@ -810,15 +822,15 @@ mod tests { let mut deps = OrderMap::new(); deps.insert( "python".to_string(), - PackageSpecV1::Binary(BinaryPackageSpecV1::default()), + PackageSpec::Binary(BinaryPackageSpec::default()), ); - let target_with_deps = TargetV1 { + let target_with_deps = Target { host_dependencies: Some(deps), build_dependencies: Some(OrderMap::new()), run_dependencies: Some(OrderMap::new()), }; - project_model.targets = Some(TargetsV1 { + project_model.targets = Some(Targets { default_target: Some(target_with_deps), targets: Some(OrderMap::new()), }); @@ -838,11 +850,11 @@ mod tests { #[test] fn test_binary_package_spec_hash_stability() { - let spec1 = BinaryPackageSpecV1::default(); + let spec1 = BinaryPackageSpec::default(); let hash1 = calculate_hash(&spec1); // Create another default spec with explicit None values - let spec2 = BinaryPackageSpecV1 { + let spec2 = BinaryPackageSpec { version: None, build: None, build_number: None, @@ -863,7 +875,7 @@ mod tests { ); // Add a meaningful value - let spec3 = BinaryPackageSpecV1 { + let spec3 = BinaryPackageSpec { file_name: Some("test.tar.bz2".to_string()), ..Default::default() }; @@ -878,8 +890,8 @@ mod tests { #[test] fn test_enum_variant_hash_stability() { // Test PackageSpecV1 enum variants - let binary_spec = PackageSpecV1::Binary(BinaryPackageSpecV1::default()); - let source_spec = PackageSpecV1::Source(SourcePackageSpec::from(PathSpecV1 { + let binary_spec = PackageSpec::Binary(BinaryPackageSpec::default()); + let source_spec = PackageSpec::Source(SourcePackageSpec::from(PathSpec { path: "test".to_string(), })); @@ -893,7 +905,7 @@ mod tests { ); // Same variant with same content should have same hash - let binary_spec2 = PackageSpecV1::Binary(BinaryPackageSpecV1::default()); + let binary_spec2 = PackageSpec::Binary(BinaryPackageSpec::default()); let hash3 = calculate_hash(&binary_spec2); assert_eq!( @@ -902,26 +914,26 @@ mod tests { ); } - fn create_sample_target_v1() -> TargetV1 { - TargetV1 { + fn create_sample_target_v1() -> Target { + Target { host_dependencies: Some(OrderMap::from([( "host_dep1".to_string(), - PackageSpecV1::Binary(BinaryPackageSpecV1::default()), + PackageSpec::Binary(BinaryPackageSpec::default()), )])), build_dependencies: Some(OrderMap::from([( "build_dep1".to_string(), - PackageSpecV1::Binary(BinaryPackageSpecV1::default()), + PackageSpec::Binary(BinaryPackageSpec::default()), )])), run_dependencies: Some(OrderMap::from([( "run_dep1".to_string(), - PackageSpecV1::Binary(BinaryPackageSpecV1::default()), + PackageSpec::Binary(BinaryPackageSpec::default()), )])), } } #[test] fn serialize_targets_v1_with_default_target() { - let targets = TargetsV1 { + let targets = Targets { default_target: Some(create_sample_target_v1()), targets: None, }; @@ -946,17 +958,17 @@ mod tests { "win-arm64", ]; - let targets = TargetsV1 { + let targets = Targets { default_target: None, targets: Some( platform_strs .iter() .map(|s| { let selector = match *s { - "unix" => TargetSelectorV1::Unix, - "win" => TargetSelectorV1::Win, - "macos" => TargetSelectorV1::MacOs, - other => TargetSelectorV1::Platform(other.to_string()), + "unix" => TargetSelector::Unix, + "win" => TargetSelector::Win, + "macos" => TargetSelector::MacOs, + other => TargetSelector::Platform(other.to_string()), }; (selector, create_sample_target_v1()) }) @@ -978,7 +990,7 @@ mod tests { "targets": null }"#; - let deserialized: TargetsV1 = serde_json::from_str(json).unwrap(); + let deserialized: Targets = serde_json::from_str(json).unwrap(); assert!(deserialized.default_target.is_none()); assert!(deserialized.targets.is_none()); } @@ -1004,14 +1016,14 @@ mod tests { } }"#; - let deserialized: TargetsV1 = serde_json::from_str(json).unwrap(); + let deserialized: Targets = serde_json::from_str(json).unwrap(); assert!(deserialized.default_target.is_some()); assert!(deserialized.targets.is_some()); assert!( deserialized .targets .unwrap() - .contains_key(&TargetSelectorV1::Unix) + .contains_key(&TargetSelector::Unix) ); } @@ -1023,25 +1035,25 @@ mod tests { let mut deps = OrderMap::new(); deps.insert( "python".to_string(), - PackageSpecV1::Binary(BinaryPackageSpecV1::default()), + PackageSpec::Binary(BinaryPackageSpec::default()), ); // Same dependency in host_dependencies - let target1 = TargetV1 { + let target1 = Target { host_dependencies: Some(deps.clone()), build_dependencies: None, run_dependencies: None, }; // Same dependency in run_dependencies - let target2 = TargetV1 { + let target2 = Target { host_dependencies: None, build_dependencies: None, run_dependencies: Some(deps.clone()), }; // Same dependency in build_dependencies - let target3 = TargetV1 { + let target3 = Target { host_dependencies: None, build_dependencies: Some(deps.clone()), run_dependencies: None, @@ -1065,12 +1077,12 @@ mod tests { ); // Test with TargetsV1 as well - let targets1 = TargetsV1 { + let targets1 = Targets { default_target: Some(target1), targets: None, }; - let targets2 = TargetsV1 { + let targets2 = Targets { default_target: Some(target2), targets: None, }; @@ -1087,14 +1099,14 @@ mod tests { #[test] fn test_hash_collision_bug_project_model() { // Test the same issue in ProjectModelV1 - let project1 = ProjectModelV1 { + let project1 = ProjectModel { name: Some("test".to_string()), description: Some("test description".to_string()), license: None, ..Default::default() }; - let project2 = ProjectModelV1 { + let project2 = ProjectModel { name: Some("test".to_string()), description: None, license: Some("test description".to_string()), diff --git a/crates/pixi_build_types/src/protocol_version.rs b/crates/pixi_build_types/src/protocol_version.rs deleted file mode 100644 index ac2880b4d6..0000000000 --- a/crates/pixi_build_types/src/protocol_version.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Protocol version abstraction for backwards compatibility. -//! -//! This module provides a trait-based abstraction over different versions of -//! the pixi build API protocol. This allows pixi to communicate with backends -//! using different protocol versions while maintaining type safety. -//! -//! # Protocol Versions -//! -//! - V1: Initial version with conda/outputs and conda/build_v1 -//! - V2: Name in project models can be `None` -//! - V3: Outputs with the same name must have unique variants -//! - V4: SourcePackageSpec extended with version, build, build_number, etc. - -use std::hash::Hash; - -use serde::{Serialize, de::DeserializeOwned}; - -use crate::project_model::{SourcePackageSpecV1, SourcePackageSpecV4}; - -/// Trait that defines the types used for a specific protocol version. -/// -/// Each protocol version can have different representations for certain types. -/// By implementing this trait for marker types (like `ApiV1`, `ApiV4`), we can -/// write generic code that works with any protocol version. -pub trait ProtocolVersion { - /// The type used to represent a source package specification. - type SourcePackageSpec: Serialize + DeserializeOwned + Clone + std::fmt::Debug + PartialEq + Eq + Hash; -} - -/// Marker type for API version 1. -/// -/// This version uses `SourcePackageSpecV1` which is a simple enum without -/// additional match spec fields. -#[derive(Debug, Clone, Copy)] -pub struct ApiV1; - -impl ProtocolVersion for ApiV1 { - type SourcePackageSpec = SourcePackageSpecV1; -} - -/// Marker type for API version 2. -/// -/// Same types as V1, but allows `None` for the name field in project models. -#[derive(Debug, Clone, Copy)] -pub struct ApiV2; - -impl ProtocolVersion for ApiV2 { - /// Unchanged from V1. - type SourcePackageSpec = ::SourcePackageSpec; -} - -/// Marker type for API version 3. -/// -/// Same types as V2, but guarantees unique variants for outputs with same name. -#[derive(Debug, Clone, Copy)] -pub struct ApiV3; - -impl ProtocolVersion for ApiV3 { - /// Unchanged from V2. - type SourcePackageSpec = ::SourcePackageSpec; -} - -/// Marker type for API version 4. -/// -/// This version uses `SourcePackageSpecV4` which is a struct with additional -/// match spec fields like `version`, `build`, `build_number`, etc. -#[derive(Debug, Clone, Copy)] -pub struct ApiV4; - -impl ProtocolVersion for ApiV4 { - type SourcePackageSpec = SourcePackageSpecV4; -} diff --git a/crates/pixi_command_dispatcher/src/build/build_cache.rs b/crates/pixi_command_dispatcher/src/build/build_cache.rs index 3cd8d5f1f1..8f44ca0aa3 100644 --- a/crates/pixi_command_dispatcher/src/build/build_cache.rs +++ b/crates/pixi_command_dispatcher/src/build/build_cache.rs @@ -10,7 +10,7 @@ use async_fd_lock::{LockWrite, RwLockWriteGuard}; use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use ordermap::OrderMap; use pixi_build_discovery::{BackendInitializationParams, DiscoveredBackend}; -use pixi_build_types::{ProjectModelV1, TargetSelectorV1}; +use pixi_build_types::{ProjectModel, TargetSelector}; use pixi_path::{AbsPathBuf, AbsPresumedDirPath, AbsPresumedDirPathBuf, AbsPresumedFilePathBuf}; use pixi_record::PinnedSourceSpec; use pixi_stable_hash::{StableHashBuilder, json::StableJson, map::StableMap}; @@ -333,13 +333,13 @@ impl BuildCacheEntry { /// warranted. pub struct PackageBuildInputHashBuilder<'a> { /// The project model itself. Contains dependencies and more. - pub project_model: Option<&'a ProjectModelV1>, + pub project_model: Option<&'a ProjectModel>, /// The backend specific configuration pub configuration: Option<&'a serde_json::Value>, /// Target specific backend configuration - pub target_configuration: Option<&'a OrderMap>, + pub target_configuration: Option<&'a OrderMap>, } impl PackageBuildInputHashBuilder<'_> { diff --git a/crates/pixi_command_dispatcher/src/build/conversion.rs b/crates/pixi_command_dispatcher/src/build/conversion.rs index b4677aa49d..6d740a487b 100644 --- a/crates/pixi_command_dispatcher/src/build/conversion.rs +++ b/crates/pixi_command_dispatcher/src/build/conversion.rs @@ -1,5 +1,5 @@ use pixi_build_types::{ - BinaryPackageSpecV1, PackageSpecV1, SourcePackageLocationSpec, SourcePackageSpec, + BinaryPackageSpec, PackageSpec, SourcePackageLocationSpec, SourcePackageSpec, }; use pixi_spec::{BinarySpec, DetailedSpec, UrlBinarySpec}; use rattler_conda_types::NamedChannelOrUrl; @@ -37,6 +37,7 @@ pub fn from_source_package_location_spec( url: url.url, md5: url.md5, sha256: url.sha256, + subdirectory: url.subdirectory, }) } @@ -44,16 +45,16 @@ pub fn from_source_package_location_spec( pixi_spec::SourceLocationSpec::Git(pixi_spec::GitSpec { git: git.git, rev: git.rev.map(|r| match r { - pixi_build_frontend::types::GitReferenceV1::Branch(b) => { + pixi_build_frontend::types::GitReference::Branch(b) => { pixi_spec::GitReference::Branch(b) } - pixi_build_frontend::types::GitReferenceV1::Tag(t) => { + pixi_build_frontend::types::GitReference::Tag(t) => { pixi_spec::GitReference::Tag(t) } - pixi_build_frontend::types::GitReferenceV1::Rev(rev) => { + pixi_build_frontend::types::GitReference::Rev(rev) => { pixi_spec::GitReference::Rev(rev) } - pixi_build_frontend::types::GitReferenceV1::DefaultBranch => { + pixi_build_frontend::types::GitReference::DefaultBranch => { pixi_spec::GitReference::DefaultBranch } }), @@ -69,16 +70,16 @@ pub fn from_source_package_location_spec( } } -/// Converts a [`BinaryPackageSpecV1`] to a [`pixi_spec::BinarySpec`]. -pub fn from_binary_spec_v1(spec: BinaryPackageSpecV1) -> pixi_spec::BinarySpec { +/// Converts a [`BinaryPackageSpec`] to a [`pixi_spec::BinarySpec`]. +pub fn from_binary_spec_v1(spec: BinaryPackageSpec) -> pixi_spec::BinarySpec { match spec { - BinaryPackageSpecV1 { + BinaryPackageSpec { url: Some(url), sha256, md5, .. } => BinarySpec::Url(UrlBinarySpec { url, md5, sha256 }), - BinaryPackageSpecV1 { + BinaryPackageSpec { version: Some(version), build: None, build_number: None, @@ -90,7 +91,7 @@ pub fn from_binary_spec_v1(spec: BinaryPackageSpecV1) -> pixi_spec::BinarySpec { license: None, url: _, } => BinarySpec::Version(version), - BinaryPackageSpecV1 { + BinaryPackageSpec { version, build, build_number, @@ -115,10 +116,10 @@ pub fn from_binary_spec_v1(spec: BinaryPackageSpecV1) -> pixi_spec::BinarySpec { } } -/// Converts a [`PackageSpecV1`] to a [`pixi_spec::PixiSpec`]. -pub fn from_package_spec_v1(source: PackageSpecV1) -> pixi_spec::PixiSpec { +/// Converts a [`PackageSpec`] to a [`pixi_spec::PixiSpec`]. +pub fn from_package_spec_v1(source: PackageSpec) -> pixi_spec::PixiSpec { match source { - PackageSpecV1::Source(source) => from_source_spec_v1(source).into(), - PackageSpecV1::Binary(binary) => from_binary_spec_v1(binary).into(), + PackageSpec::Source(source) => from_source_spec_v1(source).into(), + PackageSpec::Binary(binary) => from_binary_spec_v1(binary).into(), } } diff --git a/crates/pixi_command_dispatcher/src/build/dependencies.rs b/crates/pixi_command_dispatcher/src/build/dependencies.rs index 8a7466924f..ab44088ecc 100644 --- a/crates/pixi_command_dispatcher/src/build/dependencies.rs +++ b/crates/pixi_command_dispatcher/src/build/dependencies.rs @@ -2,7 +2,7 @@ use std::{fmt::Display, hash::Hash, str::FromStr, sync::Arc}; use itertools::Either; use pixi_build_types::{ - BinaryPackageSpecV1, NamedSpecV1, PackageSpecV1, + BinaryPackageSpec, NamedSpec, PackageSpec, procedures::conda_outputs::{ CondaOutputDependencies, CondaOutputIgnoreRunExports, CondaOutputRunExports, }, @@ -372,7 +372,7 @@ impl PixiRunExports { /// Converts a [`CondaOutputRunExports`] to a [`PixiRunExports`]. pub fn try_from_protocol(output: &CondaOutputRunExports) -> Result { fn convert_package_spec( - specs: &[NamedSpecV1], + specs: &[NamedSpec], ) -> Result, DependenciesError> { specs .iter() @@ -388,7 +388,7 @@ impl PixiRunExports { } fn convert_binary_spec( - specs: &[NamedSpecV1], + specs: &[NamedSpec], ) -> Result, DependenciesError> { specs .iter() diff --git a/crates/pixi_command_dispatcher/src/command_dispatcher/instantiate_backend.rs b/crates/pixi_command_dispatcher/src/command_dispatcher/instantiate_backend.rs index 9d822df9aa..f56b2e5a12 100644 --- a/crates/pixi_command_dispatcher/src/command_dispatcher/instantiate_backend.rs +++ b/crates/pixi_command_dispatcher/src/command_dispatcher/instantiate_backend.rs @@ -15,7 +15,7 @@ use pixi_build_frontend::{ tool::{IsolatedTool, SystemTool, Tool}, }; use pixi_build_types::{ - PixiBuildApiVersion, ProjectModelV1, TargetSelectorV1, procedures::initialize::InitializeParams, + PixiBuildApiVersion, ProjectModel, TargetSelector, procedures::initialize::InitializeParams, }; use pixi_path::{AbsPresumedDirPathBuf, AbsPresumedFilePathBuf}; use pixi_spec::SpecConversionError; @@ -39,13 +39,13 @@ pub struct InstantiateBackendSpec { pub manifest_path: AbsPresumedFilePathBuf, /// Optionally, the manifest of the discovered package. - pub project_model: Option, + pub project_model: Option, /// Additional configuration that applies to the backend. pub configuration: Option, /// Targets that apply to the backend. - pub target_configuration: Option>, + pub target_configuration: Option>, /// The source directory to use for the backend pub build_source_dir: AbsPresumedDirPathBuf, @@ -89,10 +89,10 @@ impl CommandDispatcher { let memory = in_mem .initialize(InitializeParams { manifest_path: spec.manifest_path.to_std_path_buf(), - source_dir: Some(source_dir), - workspace_root: Some(spec.workspace_root.to_std_path_buf()), + source_directory: Some(source_dir), + workspace_directory: Some(spec.workspace_root.to_std_path_buf()), cache_directory: Some(self.cache_dirs().root().to_owned().into()), - project_model: spec.project_model.map(Into::into), + project_model: spec.project_model, configuration: spec.configuration, target_configuration: spec.target_configuration, }) diff --git a/crates/pixi_command_dispatcher/src/command_dispatcher/mod.rs b/crates/pixi_command_dispatcher/src/command_dispatcher/mod.rs index e17fbec9d0..bc5788eff8 100644 --- a/crates/pixi_command_dispatcher/src/command_dispatcher/mod.rs +++ b/crates/pixi_command_dispatcher/src/command_dispatcher/mod.rs @@ -650,6 +650,7 @@ impl CommandDispatcher { url: url.url, md5: url.md5, sha256: url.sha256, + subdirectory: url.subdirectory, }) .await } diff --git a/crates/pixi_command_dispatcher/src/command_dispatcher/url.rs b/crates/pixi_command_dispatcher/src/command_dispatcher/url.rs index 30d8f3735d..69efbfe827 100644 --- a/crates/pixi_command_dispatcher/src/command_dispatcher/url.rs +++ b/crates/pixi_command_dispatcher/src/command_dispatcher/url.rs @@ -64,6 +64,7 @@ impl CommandDispatcher { url: pinned_url_spec.url.clone(), md5: pinned_url_spec.md5, sha256: Some(pinned_url_spec.sha256), + subdirectory: pinned_url_spec.subdirectory.clone(), }; // Fetch the url in the background let fetch = self @@ -71,13 +72,11 @@ impl CommandDispatcher { .await .map_err(|err| err.map(SourceCheckoutError::from))?; - // TODO: Similar to TODO above. - // let path = if let Some(subdir) = url_spec.source.subdirectory.as_ref() { - // fetch.path().join(subdir) - // } else { - // fetch.into_path() - // }; - let path = fetch.into_path(); + let path = if let Some(subdir) = &pinned_url_spec.subdirectory { + fetch.dir.join(subdir).into_assume_dir() + } else { + fetch.into_path() + }; Ok(SourceCheckout { path: path.into(), diff --git a/crates/pixi_command_dispatcher/src/input_hash.rs b/crates/pixi_command_dispatcher/src/input_hash.rs index 8948c3ee18..5d471e7a5e 100644 --- a/crates/pixi_command_dispatcher/src/input_hash.rs +++ b/crates/pixi_command_dispatcher/src/input_hash.rs @@ -1,4 +1,4 @@ -use pixi_build_types::ProjectModelV1; +use pixi_build_types::ProjectModel; use serde::{Deserialize, Serialize}; use std::hash::{Hash, Hasher}; use xxhash_rust::xxh3::Xxh3; @@ -7,8 +7,8 @@ use xxhash_rust::xxh3::Xxh3; #[serde(transparent)] pub struct ProjectModelHash(u64); -impl From<&'_ ProjectModelV1> for ProjectModelHash { - fn from(value: &'_ ProjectModelV1) -> Self { +impl From<&'_ ProjectModel> for ProjectModelHash { + fn from(value: &'_ ProjectModel) -> Self { let mut hasher = Xxh3::new(); value.hash(&mut hasher); Self(hasher.finish()) diff --git a/crates/pixi_command_dispatcher/src/instantiate_tool_env/mod.rs b/crates/pixi_command_dispatcher/src/instantiate_tool_env/mod.rs index 2f61f55a2a..5906e7bde0 100644 --- a/crates/pixi_command_dispatcher/src/instantiate_tool_env/mod.rs +++ b/crates/pixi_command_dispatcher/src/instantiate_tool_env/mod.rs @@ -233,7 +233,7 @@ impl InstantiateToolEnvironmentSpec { else { return Err(CommandDispatcherError::Failed( InstantiateToolEnvironmentError::NoMatchingBackends { - build_backend: self.requirement, + build_backend: Box::new(self.requirement), }, )); }; @@ -330,6 +330,6 @@ pub enum InstantiateToolEnvironmentError { "Modify the requirements on `{}` or contact the maintainers to ensure a dependency on `{}` is added.", .build_backend.0.as_normalized(), PIXI_BUILD_API_VERSION_NAME.as_normalized() ))] NoMatchingBackends { - build_backend: (PackageName, PixiSpec), + build_backend: Box<(PackageName, PixiSpec)>, }, } diff --git a/crates/pixi_command_dispatcher/tests/integration/main.rs b/crates/pixi_command_dispatcher/tests/integration/main.rs index a8f9a76841..40eb2af418 100644 --- a/crates/pixi_command_dispatcher/tests/integration/main.rs +++ b/crates/pixi_command_dispatcher/tests/integration/main.rs @@ -1149,6 +1149,7 @@ pub async fn pin_and_checkout_url_reuses_cached_checkout() { url: "https://example.com/archive.tar.gz".parse().unwrap(), md5: None, sha256: Some(sha), + subdirectory: None, }; let checkout = dispatcher @@ -1182,11 +1183,13 @@ pub async fn pin_and_checkout_url_reports_sha_mismatch_from_concurrent_request() url: url.clone(), md5: None, sha256: None, + subdirectory: None, }; let bad_spec = UrlSpec { url, md5: None, sha256: Some(Sha256::digest(b"pixi-url-bad-sha")), + subdirectory: None, }; let (good, bad) = tokio::join!( @@ -1219,6 +1222,7 @@ pub async fn pin_and_checkout_url_validates_cached_results() { url: url.clone(), md5: None, sha256: None, + subdirectory: None, }; dispatcher @@ -1230,6 +1234,7 @@ pub async fn pin_and_checkout_url_validates_cached_results() { url: url.clone(), md5: None, sha256: Some(Sha256::digest(b"pixi-url-bad-cache")), + subdirectory: None, }; let err = dispatcher.checkout_url(bad_spec).await.unwrap_err(); diff --git a/crates/pixi_record/src/pinned_source.rs b/crates/pixi_record/src/pinned_source.rs index 308a923914..c06076efb4 100644 --- a/crates/pixi_record/src/pinned_source.rs +++ b/crates/pixi_record/src/pinned_source.rs @@ -5,7 +5,6 @@ use std::{ str::FromStr, }; -use crate::path_utils::unixify_relative_path; use miette::IntoDiagnostic; use pixi_git::{ GitUrl, @@ -21,6 +20,8 @@ use thiserror::Error; use typed_path::{Utf8TypedPathBuf, Utf8UnixPathBuf}; use url::Url; +use crate::path_utils::unixify_relative_path; + /// Describes an exact revision of a source checkout. This is used to pin a /// particular source definition to a revision. A git source spec does not /// describe an exact commit. This struct describes an exact commit. @@ -197,8 +198,9 @@ impl PinnedSourceSpec { // If source spec specifies a subdirectory, it must match match (&source_git.subdirectory, &pinned_git.source.subdirectory) { (Some(source_subdir), Some(pinned_subdir)) => source_subdir == pinned_subdir, - (Some(_), None) => false, // Source expects subdirectory, but pinned doesn't have one - (None, _) => true, // Source doesn't care about subdirectory + (Some(_), None) => false, /* Source expects subdirectory, but pinned doesn't + * have one */ + (None, _) => true, // Source doesn't care about subdirectory } } @@ -212,12 +214,12 @@ impl PinnedSourceSpec { } } - /// Resolves a relative path from the lock file back into a full pinned source spec. - /// This is the inverse of `make_relative_to`. + /// Resolves a relative path from the lock file back into a full pinned + /// source spec. This is the inverse of `make_relative_to`. /// - /// Given a relative path (typically from a lock file's `build_source`) and a base - /// pinned source (typically the `manifest_source`), this reconstructs the full - /// pinned source spec. + /// Given a relative path (typically from a lock file's `build_source`) and + /// a base pinned source (typically the `manifest_source`), this + /// reconstructs the full pinned source spec. /// /// Returns `None` if: /// - The base is not a compatible type @@ -225,7 +227,8 @@ impl PinnedSourceSpec { /// /// # Arguments /// * `build_source_path` - The possibly relative path from the lock file - /// * `base` - The base pinned source to resolve against (typically the manifest_source) + /// * `base` - The base pinned source to resolve against (typically the + /// manifest_source) /// * `workspace_root` - The workspace root directory pub fn from_relative_to( build_source_path: Utf8UnixPathBuf, @@ -307,16 +310,18 @@ impl PinnedSourceSpec { } } - /// Makes this pinned source relative to another pinned source if both are path sources - /// or both are git sources pointing to the same repository. - /// This is useful for making `build_source` relative to `manifest_source` in lock files. + /// Makes this pinned source relative to another pinned source if both are + /// path sources or both are git sources pointing to the same + /// repository. This is useful for making `build_source` relative to + /// `manifest_source` in lock files. /// /// Returns `None` if: /// - Not a compatible combination (different types or different git repos) /// - The sources cannot be made relative to each other /// /// # Arguments - /// * `base` - The base pinned source to make this path relative to (typically the manifest_source) + /// * `base` - The base pinned source to make this path relative to + /// (typically the manifest_source) pub fn make_relative_to( &self, base: &PinnedSourceSpec, @@ -360,7 +365,8 @@ impl PinnedSourceSpec { let this_path = std::path::Path::new(this_subdir); let relative = pathdiff::diff_paths(this_path, base_path)?; - // Same here: ensure lock only contains `/` even when diff runs on Windows paths. + // Same here: ensure lock only contains `/` even when diff runs on Windows + // paths. let relative_str = unixify_relative_path(relative.as_path()); Some(Utf8UnixPathBuf::from(relative_str)) @@ -409,6 +415,9 @@ pub struct PinnedUrlSpec { /// The md5 hash of the archive. #[serde_as(as = "Option>")] pub md5: Option, + /// The subdirectory of the package inside the archive + #[serde(skip_serializing_if = "Option::is_none")] + pub subdirectory: Option, } impl PinnedUrlSpec { @@ -848,6 +857,19 @@ pub enum SourceMismatchError { requested: Option, }, + #[error( + "the locked url subdirectory '{locked}' for '{url}' does not match the requested git subdirectory '{requested}'" + )] + /// The locked git rev does not match the requested git rev. + UrlSubdirectoryMismatch { + /// The url. + url: Url, + /// The locked git subdirectory. + locked: String, + /// The requested git subdirectory. + requested: String, + }, + #[error("the locked source type does not match the requested type")] /// The locked source type does not match the requested type. SourceTypeMismatch, @@ -900,6 +922,13 @@ impl PinnedUrlSpec { requested: format!("{md5:x}"), }); } + if spec.subdirectory != self.subdirectory { + return Err(SourceMismatchError::UrlSubdirectoryMismatch { + url: self.url.clone(), + locked: self.subdirectory.as_deref().unwrap_or("None").to_string(), + requested: spec.subdirectory.as_deref().unwrap_or("None").to_string(), + }); + } Ok(()) } } @@ -1013,6 +1042,7 @@ impl From for UrlSourceSpec { url: value.url, sha256: Some(value.sha256), md5: value.md5, + subdirectory: value.subdirectory, } } } @@ -1029,16 +1059,16 @@ impl From for GitSpec { #[cfg(test)] mod tests { - use std::str::FromStr; + use std::{path::Path, str::FromStr}; use pixi_git::sha::GitSha; use pixi_spec::{GitReference, GitSpec}; use url::Url; - use crate::{PinnedGitCheckout, PinnedGitSpec, PinnedUrlSpec, SourceMismatchError}; - use std::path::Path; - - use crate::{PinnedPathSpec, PinnedSourceSpec}; + use crate::{ + PinnedGitCheckout, PinnedGitSpec, PinnedPathSpec, PinnedSourceSpec, PinnedUrlSpec, + SourceMismatchError, + }; #[test] fn test_spec_satisfies() { @@ -1437,12 +1467,14 @@ mod tests { ) .unwrap(), md5: None, + subdirectory: None, }); let spec = SourceLocationSpec::Url(UrlSourceSpec { url: Url::parse("https://example.com/archive.tar.gz").unwrap(), sha256: None, md5: None, + subdirectory: None, }); assert!(pinned.matches_source_spec(&spec)); @@ -1457,12 +1489,14 @@ mod tests { ) .unwrap(), md5: None, + subdirectory: None, }); let spec = SourceLocationSpec::Url(UrlSourceSpec { url: Url::parse("https://example.com/different.tar.gz").unwrap(), sha256: None, md5: None, + subdirectory: None, }); assert!(!pinned.matches_source_spec(&spec)); @@ -1498,6 +1532,7 @@ mod tests { url: Url::parse("https://example.com/archive.tar.gz").unwrap(), sha256: None, md5: None, + subdirectory: None, }); assert!(!pinned.matches_source_spec(&spec)); @@ -1512,6 +1547,7 @@ mod tests { ) .unwrap(), md5: None, + subdirectory: None, }); let spec = SourceLocationSpec::Path(PathSourceSpec { @@ -1538,7 +1574,8 @@ mod tests { subdirectory: None, }); - // Should match - we only compare repository and subdirectory, not the commit/rev + // Should match - we only compare repository and subdirectory, not the + // commit/rev assert!(pinned.matches_source_spec(&spec)); } @@ -1565,7 +1602,8 @@ mod tests { #[test] fn test_relative_to_relative() { - // Both paths are relative - after resolution they become absolute, then relative path is computed + // Both paths are relative - after resolution they become absolute, then + // relative path is computed let workspace_root = Path::new("/workspace"); let this_spec = PinnedSourceSpec::Path(PinnedPathSpec { diff --git a/crates/pixi_record/src/source_record.rs b/crates/pixi_record/src/source_record.rs index 894c5dda09..2da3ef404b 100644 --- a/crates/pixi_record/src/source_record.rs +++ b/crates/pixi_record/src/source_record.rs @@ -56,7 +56,7 @@ impl SourceRecord { PinnedSourceSpec::Url(pinned_url_spec) => Some(PackageBuildSource::Url { url: pinned_url_spec.url, sha256: pinned_url_spec.sha256, - subdir: None, + subdir: pinned_url_spec.subdirectory.map(Into::into), }), PinnedSourceSpec::Git(pinned_git_spec) => Some(PackageBuildSource::Git { url: pinned_git_spec.git, @@ -142,11 +142,12 @@ impl SourceRecord { PackageBuildSource::Url { url, sha256, - subdir: _, + subdir, } => PinnedSourceSpec::Url(crate::PinnedUrlSpec { url, sha256, md5: None, + subdirectory: subdir.map(|s| s.to_string()), }), PackageBuildSource::Path { path } => { // Convert path to Unix format for from_relative_to diff --git a/crates/pixi_spec/src/lib.rs b/crates/pixi_spec/src/lib.rs index 05eb35d534..ca031a1e0c 100644 --- a/crates/pixi_spec/src/lib.rs +++ b/crates/pixi_spec/src/lib.rs @@ -132,6 +132,9 @@ impl PixiSpec { url, md5: spec.md5, sha256: spec.sha256, + // A namelessmatchspec always describes a binary spec which cannot have a + // subdirectory + subdirectory: None, }) } else if spec.build.is_none() && spec.build_number.is_none() @@ -335,7 +338,8 @@ impl PixiSpec { } /// Returns true if this spec represents a mutable source. - /// A spec is mutable if it points to a local path-based source (non-binary). + /// A spec is mutable if it points to a local path-based source + /// (non-binary). pub fn is_mutable(&self) -> bool { match self { Self::Version(_) => false, @@ -674,10 +678,13 @@ impl From for rattler_lock::source::SourceLocation { #[cfg(feature = "rattler_lock")] impl From for UrlSourceSpec { fn from(value: rattler_lock::source::UrlSourceLocation) -> Self { + let rattler_lock::source::UrlSourceLocation { url, md5, sha256 } = value; + Self { - url: value.url, - md5: value.md5, - sha256: value.sha256, + url, + md5, + sha256, + subdirectory: None, } } } diff --git a/crates/pixi_spec/src/toml.rs b/crates/pixi_spec/src/toml.rs index 16082e9f7c..7d19006ed0 100644 --- a/crates/pixi_spec/src/toml.rs +++ b/crates/pixi_spec/src/toml.rs @@ -282,6 +282,7 @@ impl TomlSpec { url, md5: loc.md5, sha256: loc.sha256, + subdirectory: loc.subdirectory, }), (None, Some(path), None) => PixiSpec::Path(PathSpec { path: path.into() }), (None, None, Some(git)) => { @@ -369,6 +370,7 @@ impl TomlSpec { url, md5: loc.md5, sha256: loc.sha256, + subdirectory: loc.subdirectory, }; if let Either::Right(binary) = url_spec.into_source_or_binary() { BinarySpec::Url(binary) @@ -490,6 +492,7 @@ impl TomlLocationSpec { url, md5: self.md5, sha256: self.sha256, + subdirectory: self.subdirectory, }), (None, Some(path), None) => { SourceLocationSpec::Path(PathSourceSpec { path: path.into() }) diff --git a/crates/pixi_spec/src/url.rs b/crates/pixi_spec/src/url.rs index 96b7732dc4..bef3570e89 100644 --- a/crates/pixi_spec/src/url.rs +++ b/crates/pixi_spec/src/url.rs @@ -24,6 +24,10 @@ pub struct UrlSpec { #[serde(skip_serializing_if = "Option::is_none")] #[serde_as(as = "Option>")] pub sha256: Option, + + /// The subdirectory of the package inside the archive + #[serde(skip_serializing_if = "Option::is_none")] + pub subdirectory: Option, } impl UrlSpec { @@ -54,6 +58,7 @@ impl UrlSpec { url: self.url, md5: self.md5, sha256: self.sha256, + subdirectory: self.subdirectory, }) } } @@ -72,6 +77,7 @@ impl UrlSpec { url: self.url, md5: self.md5, sha256: self.sha256, + subdirectory: self.subdirectory, }) } } @@ -111,6 +117,10 @@ pub struct UrlSourceSpec { #[serde_as(as = "Option>")] #[serde(skip_serializing_if = "Option::is_none")] pub sha256: Option, + + /// The subdirectory of the package inside the archive + #[serde(skip_serializing_if = "Option::is_none")] + pub subdirectory: Option, } impl Display for UrlSourceSpec { @@ -122,6 +132,9 @@ impl Display for UrlSourceSpec { if let Some(sha256) = &self.sha256 { write!(f, " sha256={sha256:x}")?; } + if let Some(subdirectory) = &self.subdirectory { + write!(f, " subdirectory={subdirectory}")?; + } Ok(()) } } @@ -132,6 +145,7 @@ impl From for UrlSpec { url: value.url, md5: value.md5, sha256: value.sha256, + subdirectory: value.subdirectory, } } } @@ -155,6 +169,8 @@ impl From for UrlSpec { url: value.url, md5: value.md5, sha256: value.sha256, + // A binary url spec is already a conda package so it cannot have a subdirectory + subdirectory: None, } } } diff --git a/crates/pixi_stable_hash/src/lib.rs b/crates/pixi_stable_hash/src/lib.rs index 5021bb21d2..8d2c4680bf 100644 --- a/crates/pixi_stable_hash/src/lib.rs +++ b/crates/pixi_stable_hash/src/lib.rs @@ -127,6 +127,14 @@ impl IsDefault for i32 { } } +impl IsDefault for u64 { + type Item = Self; + + fn is_non_default(&self) -> Option<&Self::Item> { + Some(self) // Never skip numeric fields + } +} + impl IsDefault for std::path::PathBuf { type Item = Self; diff --git a/crates/pixi_url/src/source.rs b/crates/pixi_url/src/source.rs index a643b1d44a..fbe58fbc94 100644 --- a/crates/pixi_url/src/source.rs +++ b/crates/pixi_url/src/source.rs @@ -146,6 +146,7 @@ impl UrlSource { async_fs::create_dir_all(self.checkouts_dir()).await?; async_fs::create_dir_all(self.locks_dir()).await?; + let subdirectory = self.spec.subdirectory.clone(); let url = self.spec.url.clone(); let file_name = url_file_name(&url); if !extract::is_archive(&file_name) { @@ -169,6 +170,7 @@ impl UrlSource { url: url.clone(), sha256: sha, md5, + subdirectory, }; return Ok(Fetch { pinned, path }); } @@ -195,6 +197,7 @@ impl UrlSource { url, sha256: sha, md5, + subdirectory, }; return Ok(Fetch { pinned, path }); } @@ -247,6 +250,7 @@ impl UrlSource { url, sha256, md5: Some(md5), + subdirectory, }; Ok(Fetch { diff --git a/crates/pixi_url/tests/main.rs b/crates/pixi_url/tests/main.rs index cee800ca8f..74717c9f0d 100644 --- a/crates/pixi_url/tests/main.rs +++ b/crates/pixi_url/tests/main.rs @@ -107,6 +107,7 @@ async fn url_source_uses_existing_checkout_when_sha_and_files_present() { url: Url::parse("https://example.com/hello.zip").unwrap(), md5: None, sha256: Some(sha), + subdirectory: None, }; let fetch = UrlSource::new(spec, panic_client(), cache.path()) @@ -132,6 +133,7 @@ async fn resolver_reuses_cached_sha_without_downloading() { url, md5: None, sha256: None, + subdirectory: None, }; let fetch = resolver @@ -154,6 +156,7 @@ async fn url_source_downloads_and_reuses_checkout() { url: url.clone(), md5: None, sha256: None, + subdirectory: None, }; let first = UrlSource::new(spec.clone(), client.clone(), cache.path()) @@ -180,6 +183,7 @@ async fn url_source_errors_on_sha_mismatch() { url: file_url(&archive, "sha-mismatch.zip"), md5: None, sha256: Some(Sha256Hash::from([0u8; 32])), + subdirectory: None, }; let err = UrlSource::new(spec, LazyClient::default(), cache.path()) @@ -198,6 +202,7 @@ async fn url_source_errors_on_md5_mismatch() { url: file_url(&archive, "md5-mismatch.zip"), md5: Some(bogus_md5()), sha256: Some(archive_sha()), + subdirectory: None, }; let err = UrlSource::new(spec, LazyClient::default(), cache.path()) @@ -215,6 +220,7 @@ async fn url_source_downloads_over_http_and_extracts_contents() { url: server.url().clone(), md5: None, sha256: Some(archive_sha()), + subdirectory: None, }; let fetch = UrlSource::new(spec, LazyClient::default(), cache.path()) diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml new file mode 100644 index 0000000000..44c1a3571b --- /dev/null +++ b/crates/xtask/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "xtask" +version = "0.1.0" +edition.workspace = true +publish = false + +[dependencies] +clap = { workspace = true, features = ["derive", "std"] } +fs-err = { workspace = true } +pixi_build_types = { workspace = true, features = ["schemars"] } +schemars = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +similar-asserts = "1.6" diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs new file mode 100644 index 0000000000..3e54b3297a --- /dev/null +++ b/crates/xtask/src/main.rs @@ -0,0 +1,75 @@ +use clap::Parser; +use pixi_build_types::ProjectModel; +use schemars::schema_for; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(name = "xtask")] +#[command(about = "Development tasks for the pixi workspace")] +enum Cli { + /// Generate JSON Schema for pixi_build_types + GenerateSchema, +} + +fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + match cli { + Cli::GenerateSchema => generate_schema()?, + } + + Ok(()) +} + +fn generate_schema() -> Result<(), Box> { + let schema = schema_for!(ProjectModel); + let schema_json = serde_json::to_string_pretty(&schema)?; + + let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|p| p.parent()) + .map(ToOwned::to_owned) + .ok_or("Failed to find workspace root")?; + + let output_dir = workspace_root.join("schema"); + fs_err::create_dir_all(&output_dir)?; + + let output_path = output_dir.join("pixi_build_api.json"); + fs_err::write(&output_path, format!("{}\n", schema_json))?; + + println!("Schema written to {}", output_path.display()); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_schema_is_up_to_date() { + // Generate the schema programmatically + let schema = schema_for!(ProjectModel); + let generated_schema_json = + serde_json::to_string_pretty(&schema).expect("Failed to serialize schema to JSON"); + + // Find the workspace root + let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|p| p.parent()) + .map(ToOwned::to_owned) + .expect("Failed to find workspace root"); + + // Read the committed schema file + let schema_path = workspace_root.join("schema").join("pixi_build_api.json"); + let committed_schema_json = + fs_err::read_to_string(&schema_path).expect("Failed to read committed schema file"); + + // Compare the schemas using similar-asserts + // Note: We need to trim the committed schema to account for trailing newline + similar_asserts::assert_eq!( + committed_schema_json.trim(), + generated_schema_json, + "The committed schema does not match the generated schema.\nPlease run `cargo xtask generate-schema` to update the schema file." + ); + } +} diff --git a/schema/pixi_build_api.json b/schema/pixi_build_api.json new file mode 100644 index 0000000000..155a6e4334 --- /dev/null +++ b/schema/pixi_build_api.json @@ -0,0 +1,495 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ProjectModel", + "type": "object", + "properties": { + "authors": { + "description": "Optional authors", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "buildNumber": { + "description": "The build number configured by the user.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "buildString": { + "description": "A build string configured by the user.", + "type": [ + "string", + "null" + ] + }, + "description": { + "description": "An optional project description", + "type": [ + "string", + "null" + ] + }, + "documentation": { + "description": "URL of the project documentation", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "homepage": { + "description": "URL of the project homepage", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "license": { + "description": "The license as a valid SPDX string (e.g. MIT AND Apache-2.0)", + "type": [ + "string", + "null" + ] + }, + "licenseFile": { + "description": "The license file (relative to the project root)", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The name of the project", + "type": [ + "string", + "null" + ] + }, + "readme": { + "description": "Path to the README file of the project (relative to the project root)", + "type": [ + "string", + "null" + ] + }, + "repository": { + "description": "URL of the project source repository", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "targets": { + "description": "The target of the project, this may contain\nplatform specific configurations.", + "anyOf": [ + { + "$ref": "#/$defs/Targets" + }, + { + "type": "null" + } + ] + }, + "version": { + "description": "The version of the project", + "type": [ + "string", + "null" + ] + } + }, + "$defs": { + "BinaryPackageSpec": { + "description": "Similar to a [`rattler_conda_types::NamelessMatchSpec`]", + "type": "object", + "properties": { + "build": { + "description": "The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`)", + "type": [ + "string", + "null" + ], + "default": null + }, + "buildNumber": { + "description": "The build number of the package", + "type": [ + "string", + "null" + ] + }, + "channel": { + "description": "The channel of the package", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "fileName": { + "description": "Match the specific filename of the package", + "type": [ + "string", + "null" + ] + }, + "license": { + "description": "The license of the package", + "type": [ + "string", + "null" + ] + }, + "md5": { + "description": "The md5 hash of the package", + "type": [ + "string", + "null" + ], + "default": null + }, + "sha256": { + "description": "The sha256 hash of the package", + "type": [ + "string", + "null" + ], + "default": null + }, + "subdir": { + "description": "The subdir of the channel", + "type": [ + "string", + "null" + ] + }, + "url": { + "description": "The URL of the package, if it is available", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "version": { + "description": "The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`)", + "type": [ + "string", + "null" + ], + "default": null + } + } + }, + "GitReference": { + "description": "A reference to a specific commit in a git repository.", + "oneOf": [ + { + "description": "The HEAD commit of a branch.", + "type": "object", + "properties": { + "branch": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "branch" + ] + }, + { + "description": "A specific tag.", + "type": "object", + "properties": { + "tag": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "tag" + ] + }, + { + "description": "A specific commit.", + "type": "object", + "properties": { + "rev": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "rev" + ] + }, + { + "description": "A default branch.", + "type": "string", + "const": "defaultBranch" + } + ] + }, + "GitSpec": { + "description": "A specification of a package from a git repository.", + "type": "object", + "properties": { + "git": { + "description": "The git url of the package which can contain git+ prefixes.", + "type": "string", + "format": "uri" + }, + "rev": { + "description": "The git revision of the package", + "anyOf": [ + { + "$ref": "#/$defs/GitReference" + }, + { + "type": "null" + } + ] + }, + "subdirectory": { + "description": "The git subdirectory of the package", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "git" + ] + }, + "PackageSpec": { + "oneOf": [ + { + "description": "This is a binary dependency", + "type": "object", + "properties": { + "binary": { + "$ref": "#/$defs/BinaryPackageSpec" + } + }, + "additionalProperties": false, + "required": [ + "binary" + ] + }, + { + "description": "This is a dependency on a source package", + "type": "object", + "properties": { + "source": { + "$ref": "#/$defs/SourcePackageSpec" + } + }, + "additionalProperties": false, + "required": [ + "source" + ] + } + ] + }, + "PathSpec": { + "description": "A specification of a package from a path", + "type": "object", + "properties": { + "path": { + "description": "The path to the package", + "type": "string" + } + }, + "required": [ + "path" + ] + }, + "SourcePackageSpec": { + "type": "object", + "properties": { + "build": { + "description": "The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`)", + "type": [ + "string", + "null" + ], + "default": null + }, + "buildNumber": { + "description": "The build number of the package", + "type": [ + "string", + "null" + ] + }, + "license": { + "description": "The md5 hash of the package\nThe license of the package", + "type": [ + "string", + "null" + ] + }, + "subdir": { + "description": "The subdir of the channel", + "type": [ + "string", + "null" + ] + }, + "version": { + "description": "The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`)", + "type": [ + "string", + "null" + ], + "default": null + } + }, + "oneOf": [ + { + "description": "The spec is represented as an archive that can be downloaded from the\nspecified URL. The package should be retrieved from the URL and can\neither represent a source or binary package depending on the archive\ntype.", + "type": "object", + "properties": { + "url": { + "$ref": "#/$defs/UrlSpec" + } + }, + "required": [ + "url" + ] + }, + { + "description": "The spec is represented as a git repository. The package represents a\nsource distribution of some kind.", + "type": "object", + "properties": { + "git": { + "$ref": "#/$defs/GitSpec" + } + }, + "required": [ + "git" + ] + }, + { + "description": "The spec is represented as a local path. The package should be retrieved\nfrom the local filesystem. The package can be either a source or binary\npackage.", + "type": "object", + "properties": { + "path": { + "$ref": "#/$defs/PathSpec" + } + }, + "required": [ + "path" + ] + } + ] + }, + "Target": { + "type": "object", + "properties": { + "buildDependencies": { + "description": "Build dependencies of the project", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/PackageSpec" + } + }, + "hostDependencies": { + "description": "Host dependencies of the project", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/PackageSpec" + } + }, + "runDependencies": { + "description": "Run dependencies of the project", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/PackageSpec" + } + } + } + }, + "Targets": { + "description": "A collect of targets including a default target.", + "type": "object", + "properties": { + "defaultTarget": { + "anyOf": [ + { + "$ref": "#/$defs/Target" + }, + { + "type": "null" + } + ] + }, + "targets": { + "description": "We use an [`OrderMap`] to preserve the order in which the items where\ndefined in the manifest.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/Target" + } + } + } + }, + "UrlSpec": { + "type": "object", + "properties": { + "md5": { + "description": "The md5 hash of the package", + "type": [ + "string", + "null" + ], + "default": null + }, + "sha256": { + "description": "The sha256 hash of the package", + "type": [ + "string", + "null" + ], + "default": null + }, + "subdirectory": { + "description": "The subdirectory of the package in the archive", + "type": [ + "string", + "null" + ] + }, + "url": { + "description": "The URL of the package", + "type": "string", + "format": "uri" + } + }, + "required": [ + "url" + ] + } + } +} From e604ccc7bd1d850c8605d4b16a0710e02452ebc6 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:44:40 +0100 Subject: [PATCH 06/13] pin-compatible wip --- crates/pixi_build_types/src/lib.rs | 2 +- crates/pixi_build_types/src/project_model.rs | 85 ++++++++++++++++++-- crates/pixi_stable_hash/src/lib.rs | 8 ++ 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/crates/pixi_build_types/src/lib.rs b/crates/pixi_build_types/src/lib.rs index d344399684..11da0a989d 100644 --- a/crates/pixi_build_types/src/lib.rs +++ b/crates/pixi_build_types/src/lib.rs @@ -14,7 +14,7 @@ pub use conda_package_metadata::CondaPackageMetadata; pub use project_model::{ BinaryPackageSpec, GitReference, GitSpec, NamedSpec, PackageSpec, PathSpec, ProjectModel, SourcePackageLocationSpec, SourcePackageName, SourcePackageSpec, Target, TargetSelector, - Targets, UrlSpec, + Targets, UrlSpec, PinCompatibleSpec, PinBound }; use rattler_conda_types::{ GenericVirtualPackage, PackageName, Platform, Version, VersionSpec, diff --git a/crates/pixi_build_types/src/project_model.rs b/crates/pixi_build_types/src/project_model.rs index fce98a2920..f49d02b3f4 100644 --- a/crates/pixi_build_types/src/project_model.rs +++ b/crates/pixi_build_types/src/project_model.rs @@ -1,6 +1,6 @@ //! This module is a collection of types that represent a pixi package in a //! protocol format that can be sent over the wire. -//! We need to vendor a lot of the types, and simplify them in some cases, so +//! We need to vendor a lot of the types and simplify them in some cases so //! that we have a stable protocol that can be used to communicate in the build //! tasks. //! @@ -9,12 +9,12 @@ //! that we try not to break this in pixi as much as possible. So as long as //! older pixi TOMLs keep loading, we can send them to the backend. use std::{convert::Infallible, fmt::Display, hash::Hash, path::PathBuf, str::FromStr}; - +use std::hash::Hasher; use ordermap::OrderMap; use pixi_stable_hash::{IsDefault, StableHashBuilder}; use rattler_conda_types::{BuildNumber, BuildNumberSpec, StringMatcher, Version, VersionSpec}; use rattler_digest::{Md5, Md5Hash, Sha256, Sha256Hash, serde::SerializableHash}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer}; use serde_with::{DeserializeFromStr, DisplayFromStr, SerializeDisplay, serde_as}; use url::Url; @@ -208,6 +208,39 @@ pub enum PackageSpec { Binary(BinaryPackageSpec), /// This is a dependency on a source package Source(SourcePackageSpec), + /// Pin to a version that is compatible with a version from the "previous" environment + PinCompatible(PinCompatibleSpec), +} + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct PinCompatibleSpec { + /// A minimum pin to a version, using `x.x.x...` as syntax + #[serde(default, skip_serializing_if = "Option::is_none")] + pub lower_bound: Option, + + /// A pin to a version, using `x.x.x...` as syntax + #[serde(default, skip_serializing_if = "Option::is_none")] + pub upper_bound: Option, + + /// If an exact pin is given, we pin the exact version & hash + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub exact: bool, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub build: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub enum PinBound { + Expression(String), + Version( + #[cfg_attr(feature = "schemars", schemars(with = "String"))] + Version, + ), } #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] @@ -577,10 +610,50 @@ impl Hash for PackageSpec { 1u8.hash(state); spec.hash(state); } + PackageSpec::PinCompatible(spec) => { + 2u8.hash(state); + spec.hash(state); + } } } } +impl Hash for PinCompatibleSpec { + fn hash(&self, state: &mut H) { + let PinCompatibleSpec { lower_bound, upper_bound, exact, build } = self; + + StableHashBuilder::::new() + .field("lower_bound", lower_bound) + .field("upper_bound", upper_bound) + .field("exact", exact) + .field("build", build) + .finish(state); + } +} + +impl Hash for PinBound { + fn hash(&self, state: &mut H) { + match self { + PinBound::Expression(expr) => { + 0u8.hash(state); + expr.hash(state); + } + PinBound::Version(ver) => { + 1u8.hash(state); + ver.hash(state); + } + } + } +} + +impl IsDefault for PinBound { + type Item = Self; + + fn is_non_default(&self) -> Option<&Self::Item> { + Some(self) + } +} + impl Hash for SourcePackageSpec { fn hash(&self, state: &mut H) { let Self { @@ -592,7 +665,7 @@ impl Hash for SourcePackageSpec { license, } = self; - // Hash the location first, to ensure compatibility with older versions. + // Hash the location first to ensure compatibility with older versions. location.hash(state); // Add the new fields using StableHashBuilder for forward/backward @@ -756,7 +829,7 @@ mod tests { let hash1 = calculate_hash(&project_model); - // Add empty targets field - with corrected implementation, this should NOT + // Add an empty targets field - with corrected implementation, this should NOT // change hash because we only include discriminants for // non-default/non-empty values project_model.targets = Some(Targets { @@ -765,7 +838,7 @@ mod tests { }); let hash2 = calculate_hash(&project_model); - // Add a target with empty dependencies - this should also NOT change hash + // Add a target with empty dependencies - this should also NOT change the hash let empty_target = Target { host_dependencies: Some(OrderMap::new()), build_dependencies: Some(OrderMap::new()), diff --git a/crates/pixi_stable_hash/src/lib.rs b/crates/pixi_stable_hash/src/lib.rs index 8d2c4680bf..511e35facd 100644 --- a/crates/pixi_stable_hash/src/lib.rs +++ b/crates/pixi_stable_hash/src/lib.rs @@ -167,6 +167,14 @@ impl IsDefault for &T { } } +impl IsDefault for bool { + type Item = Self; + + fn is_non_default(&self) -> Option<&Self::Item> { + Some(self) + } +} + #[cfg(feature = "url")] impl IsDefault for url::Url { type Item = Self; From cf5e90c475c98c45e55ac733228b5a8d24a8d1af Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:17:22 +0100 Subject: [PATCH 07/13] feat: add pin_compatible support --- crates/pixi_build_types/src/lib.rs | 6 +- crates/pixi_build_types/src/project_model.rs | 18 +- .../src/build/conversion.rs | 12 +- .../src/build/dependencies.rs | 89 ++- .../pixi_command_dispatcher/src/build/mod.rs | 1 + .../src/build/pin_compatible.rs | 727 ++++++++++++++++++ .../src/dev_source_metadata/mod.rs | 25 +- .../src/source_build/mod.rs | 43 +- .../src/source_metadata/mod.rs | 55 +- 9 files changed, 886 insertions(+), 90 deletions(-) create mode 100644 crates/pixi_command_dispatcher/src/build/pin_compatible.rs diff --git a/crates/pixi_build_types/src/lib.rs b/crates/pixi_build_types/src/lib.rs index 11da0a989d..08f4db049f 100644 --- a/crates/pixi_build_types/src/lib.rs +++ b/crates/pixi_build_types/src/lib.rs @@ -12,9 +12,9 @@ pub use capabilities::{BackendCapabilities, FrontendCapabilities}; pub use channel_configuration::ChannelConfiguration; pub use conda_package_metadata::CondaPackageMetadata; pub use project_model::{ - BinaryPackageSpec, GitReference, GitSpec, NamedSpec, PackageSpec, PathSpec, ProjectModel, - SourcePackageLocationSpec, SourcePackageName, SourcePackageSpec, Target, TargetSelector, - Targets, UrlSpec, PinCompatibleSpec, PinBound + BinaryPackageSpec, GitReference, GitSpec, NamedSpec, PackageSpec, PathSpec, PinBound, + PinCompatibleSpec, ProjectModel, SourcePackageLocationSpec, SourcePackageName, + SourcePackageSpec, Target, TargetSelector, Targets, UrlSpec, }; use rattler_conda_types::{ GenericVirtualPackage, PackageName, Platform, Version, VersionSpec, diff --git a/crates/pixi_build_types/src/project_model.rs b/crates/pixi_build_types/src/project_model.rs index f49d02b3f4..523b3953dc 100644 --- a/crates/pixi_build_types/src/project_model.rs +++ b/crates/pixi_build_types/src/project_model.rs @@ -8,14 +8,14 @@ //! and backwards compatibility. The idea for **backwards compatibility** is //! that we try not to break this in pixi as much as possible. So as long as //! older pixi TOMLs keep loading, we can send them to the backend. -use std::{convert::Infallible, fmt::Display, hash::Hash, path::PathBuf, str::FromStr}; -use std::hash::Hasher; use ordermap::OrderMap; use pixi_stable_hash::{IsDefault, StableHashBuilder}; use rattler_conda_types::{BuildNumber, BuildNumberSpec, StringMatcher, Version, VersionSpec}; use rattler_digest::{Md5, Md5Hash, Sha256, Sha256Hash, serde::SerializableHash}; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, DisplayFromStr, SerializeDisplay, serde_as}; +use std::hash::Hasher; +use std::{convert::Infallible, fmt::Display, hash::Hash, path::PathBuf, str::FromStr}; use url::Url; /// The source package name of a package. Not normalized per se. @@ -237,10 +237,7 @@ pub struct PinCompatibleSpec { #[serde(rename_all = "camelCase")] pub enum PinBound { Expression(String), - Version( - #[cfg_attr(feature = "schemars", schemars(with = "String"))] - Version, - ), + Version(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Version), } #[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] @@ -620,7 +617,12 @@ impl Hash for PackageSpec { impl Hash for PinCompatibleSpec { fn hash(&self, state: &mut H) { - let PinCompatibleSpec { lower_bound, upper_bound, exact, build } = self; + let PinCompatibleSpec { + lower_bound, + upper_bound, + exact, + build, + } = self; StableHashBuilder::::new() .field("lower_bound", lower_bound) diff --git a/crates/pixi_command_dispatcher/src/build/conversion.rs b/crates/pixi_command_dispatcher/src/build/conversion.rs index 6d740a487b..e3fbf6cd25 100644 --- a/crates/pixi_command_dispatcher/src/build/conversion.rs +++ b/crates/pixi_command_dispatcher/src/build/conversion.rs @@ -1,6 +1,4 @@ -use pixi_build_types::{ - BinaryPackageSpec, PackageSpec, SourcePackageLocationSpec, SourcePackageSpec, -}; +use pixi_build_types::{BinaryPackageSpec, SourcePackageLocationSpec, SourcePackageSpec}; use pixi_spec::{BinarySpec, DetailedSpec, UrlBinarySpec}; use rattler_conda_types::NamedChannelOrUrl; @@ -115,11 +113,3 @@ pub fn from_binary_spec_v1(spec: BinaryPackageSpec) -> pixi_spec::BinarySpec { })), } } - -/// Converts a [`PackageSpec`] to a [`pixi_spec::PixiSpec`]. -pub fn from_package_spec_v1(source: PackageSpec) -> pixi_spec::PixiSpec { - match source { - PackageSpec::Source(source) => from_source_spec_v1(source).into(), - PackageSpec::Binary(binary) => from_binary_spec_v1(binary).into(), - } -} diff --git a/crates/pixi_command_dispatcher/src/build/dependencies.rs b/crates/pixi_command_dispatcher/src/build/dependencies.rs index ab44088ecc..4506598eca 100644 --- a/crates/pixi_command_dispatcher/src/build/dependencies.rs +++ b/crates/pixi_command_dispatcher/src/build/dependencies.rs @@ -1,6 +1,10 @@ use std::{fmt::Display, hash::Hash, str::FromStr, sync::Arc}; -use itertools::Either; +use super::conversion; +use crate::build::pin_compatible::{ + PinCompatibilityMap, PinCompatibleError, resolve_pin_compatible, +}; +use pixi_build_types as pbt; use pixi_build_types::{ BinaryPackageSpec, NamedSpec, PackageSpec, procedures::conda_outputs::{ @@ -17,11 +21,13 @@ use rattler_conda_types::{ use rattler_repodata_gateway::{Gateway, RunExportExtractorError, RunExportsReporter}; use serde::Serialize; -use super::conversion; - -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum DependenciesError { - InvalidPackageName(String, InvalidPackageNameError), + #[error(transparent)] + InvalidPackageName(#[from] InvalidPackageNameError), + + #[error(transparent)] + PinCompatibleError(#[from] PinCompatibleError), } #[derive(Debug, Clone, Default, Serialize)] @@ -82,37 +88,42 @@ impl WithSource { } impl Dependencies { - pub fn new( + pub fn new<'a>( output: &CondaOutputDependencies, source_anchor: Option, + compatibility_map: &PinCompatibilityMap<'a>, ) -> Result { let mut dependencies = DependencyMap::default(); let mut constraints = DependencyMap::default(); for depend in &output.depends { - let name = rattler_conda_types::PackageName::from_str(&depend.name).map_err(|err| { - DependenciesError::InvalidPackageName(depend.name.to_owned(), err) - })?; - match conversion::from_package_spec_v1(depend.spec.clone()).into_source_or_binary() { - Either::Left(source) => { - let source = if let Some(anchor) = &source_anchor { - source.resolve(anchor) + let name = rattler_conda_types::PackageName::from_str(&depend.name)?; + + // Match directly on PackageSpec + match &depend.spec { + pbt::PackageSpec::Binary(binary) => { + let spec = conversion::from_binary_spec_v1(binary.clone()); + dependencies.insert(name, PixiSpec::from(spec).into()); + } + pbt::PackageSpec::Source(source) => { + let spec = conversion::from_source_spec_v1(source.clone()); + let resolved = if let Some(anchor) = &source_anchor { + spec.resolve(anchor) } else { - source + spec }; - dependencies.insert(name, PixiSpec::from(source).into()); + dependencies.insert(name, PixiSpec::from(resolved).into()); } - Either::Right(binary) => { - dependencies.insert(name, PixiSpec::from(binary).into()); + pbt::PackageSpec::PinCompatible(pin) => { + // Resolve immediately with O(1) HashMap lookup + let resolved = resolve_pin_compatible(&name, pin, compatibility_map)?; + dependencies.insert(name, resolved.into()); } } } for constraint in &output.constraints { - let name = - rattler_conda_types::PackageName::from_str(&constraint.name).map_err(|err| { - DependenciesError::InvalidPackageName(constraint.name.to_owned(), err) - })?; + let name = rattler_conda_types::PackageName::from_str(&constraint.name)?; constraints.insert( name, conversion::from_binary_spec_v1(constraint.spec.clone()).into(), @@ -370,18 +381,32 @@ pub struct PixiRunExports { impl PixiRunExports { /// Converts a [`CondaOutputRunExports`] to a [`PixiRunExports`]. - pub fn try_from_protocol(output: &CondaOutputRunExports) -> Result { - fn convert_package_spec( + pub fn try_from_protocol<'a>( + output: &CondaOutputRunExports, + compatibility_map: &PinCompatibilityMap<'a>, + ) -> Result { + fn convert_package_spec<'a>( specs: &[NamedSpec], + compatibility_map: &PinCompatibilityMap<'a>, ) -> Result, DependenciesError> { specs .iter() .cloned() .map(|named_spec| { - let spec = conversion::from_package_spec_v1(named_spec.spec); - let name = PackageName::from_str(&named_spec.name).map_err(|err| { - DependenciesError::InvalidPackageName(named_spec.name.to_owned(), err) - })?; + let name = PackageName::from_str(&named_spec.name)?; + + let spec = match named_spec.spec { + pbt::PackageSpec::Binary(binary) => { + conversion::from_binary_spec_v1(binary).into() + } + pbt::PackageSpec::Source(source) => { + conversion::from_source_spec_v1(source).into() + } + pbt::PackageSpec::PinCompatible(pin) => { + resolve_pin_compatible(&name, &pin, compatibility_map)? + } + }; + Ok((name, spec)) }) .collect() @@ -395,18 +420,16 @@ impl PixiRunExports { .cloned() .map(|named_spec| { let spec = conversion::from_binary_spec_v1(named_spec.spec); - let name = PackageName::from_str(&named_spec.name).map_err(|err| { - DependenciesError::InvalidPackageName(named_spec.name.to_owned(), err) - })?; + let name = PackageName::from_str(&named_spec.name)?; Ok((name, spec)) }) .collect() } Ok(PixiRunExports { - weak: convert_package_spec(&output.weak)?, - strong: convert_package_spec(&output.strong)?, - noarch: convert_package_spec(&output.noarch)?, + weak: convert_package_spec(&output.weak, compatibility_map)?, + strong: convert_package_spec(&output.strong, compatibility_map)?, + noarch: convert_package_spec(&output.noarch, compatibility_map)?, weak_constrains: convert_binary_spec(&output.weak_constrains)?, strong_constrains: convert_binary_spec(&output.strong_constrains)?, }) diff --git a/crates/pixi_command_dispatcher/src/build/mod.rs b/crates/pixi_command_dispatcher/src/build/mod.rs index bec86a2dc8..5ec6b1b08c 100644 --- a/crates/pixi_command_dispatcher/src/build/mod.rs +++ b/crates/pixi_command_dispatcher/src/build/mod.rs @@ -5,6 +5,7 @@ mod build_environment; pub mod conversion; mod dependencies; mod move_file; +pub mod pin_compatible; mod work_dir_key; use std::hash::{DefaultHasher, Hash, Hasher}; diff --git a/crates/pixi_command_dispatcher/src/build/pin_compatible.rs b/crates/pixi_command_dispatcher/src/build/pin_compatible.rs new file mode 100644 index 0000000000..2b2630a8cb --- /dev/null +++ b/crates/pixi_command_dispatcher/src/build/pin_compatible.rs @@ -0,0 +1,727 @@ +//! Pin compatible resolution for build dependencies +//! +//! This module implements the `pin_compatible` functionality from rattler_build, +//! which allows runtime dependencies to be pinned based on resolved versions +//! from build/host environments. + +use itertools::Itertools; +use pixi_build_types::{PinBound, PinCompatibleSpec}; +use pixi_record::PixiRecord; +use pixi_spec::{DetailedSpec, PixiSpec}; +use rattler_conda_types::version_spec::{LogicalOperator, RangeOperator}; +use rattler_conda_types::{PackageName, Version, VersionBumpError, VersionBumpType, VersionSpec}; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +/// A map of resolved packages that can be referenced by pin_compatible +pub type PinCompatibilityMap<'a> = HashMap; + +/// A validated pin expression that can only contain 'x' and '.' +/// +/// Just stores the segment count - can reconstruct the string if needed. +/// Examples: segment_count=1 → "x", segment_count=3 → "x.x.x" +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PinExpression { + /// The number of 'x' segments in the expression + segment_count: usize, +} + +impl PinExpression { + /// Create a new pin expression with the given segment count + pub fn new(segment_count: usize) -> Result { + if segment_count == 0 { + return Err(PinCompatibleError::InvalidPinExpression( + "Pin expression must have at least one segment".to_string(), + )); + } + Ok(PinExpression { segment_count }) + } + + /// Get the number of segments (number of 'x' characters) + pub fn segment_count(&self) -> usize { + self.segment_count + } +} + +impl FromStr for PinExpression { + type Err = PinCompatibleError; + + fn from_str(s: &str) -> Result { + // Validate that string only contains 'x' and '.' + if s.chars().any(|c| c != 'x' && c != '.') { + return Err(PinCompatibleError::InvalidPinExpression(format!( + "Pin expression can only contain 'x' and '.', got: '{}'", + s + ))); + } + + let segment_count = s.chars().filter(|c| *c == 'x').count(); + + PinExpression::new(segment_count) + } +} + +impl Display for PinExpression { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + std::iter::repeat_n('x', self.segment_count).format(".") + ) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum PinCompatibleError { + #[error("Could not apply pin_compatible. Package '{}' is not in the compatibility environment", .0.as_normalized())] + PackageNotFound(PackageName), + + #[error("Could not parse pin expression: {0}")] + InvalidPinExpression(String), + + #[error("Could not increment version: {0}")] + VersionBump(String), + + #[error("Build specifier and exact=True are not supported together")] + BuildSpecifierWithExact, + + #[error("Failed to parse build string: {0}")] + BuildStringParse(String), +} + +/// Resolve a pin_compatible spec against solved environment records +/// +/// Mimics rattler_build's Pin::apply logic but returns a PixiSpec +pub fn resolve_pin_compatible( + package_name: &PackageName, + spec: &PinCompatibleSpec, + compatibility_map: &PinCompatibilityMap<'_>, +) -> Result { + // 1. Find the package in the compatibility map (O(1) lookup) + let record = compatibility_map + .get(package_name) + .ok_or_else(|| PinCompatibleError::PackageNotFound(package_name.clone()))?; + + let version = &record.package_record().version; + let build_string = &record.package_record().build; + + // 2. Check for conflicting args + if spec.build.is_some() && spec.exact { + return Err(PinCompatibleError::BuildSpecifierWithExact); + } + + // 3. Handle exact pin + if spec.exact { + let version_spec = VersionSpec::Exact( + rattler_conda_types::version_spec::EqualityOperator::Equals, + version.clone().into(), + ); + let build_matcher = build_string + .parse() + .map_err(|e| PinCompatibleError::BuildStringParse(format!("{}", e)))?; + + return Ok(PixiSpec::DetailedVersion(Box::new(DetailedSpec { + version: Some(version_spec), + build: Some(build_matcher), + ..Default::default() + }))); + } + + // 4. Build version constraints from bounds using VersionSpec types directly + let mut constraints = Vec::new(); + + // Lower bound: >=version + if let Some(lower_bound) = &spec.lower_bound { + let lower = apply_pin_bound(lower_bound, version, false)?; + constraints.push(VersionSpec::Range(RangeOperator::GreaterEquals, lower)); + } + + // Upper bound: VersionSpec::Any, + 1 => constraints.into_iter().next().unwrap(), + _ => VersionSpec::Group(LogicalOperator::And, constraints), + }; + + // 6. Add build matcher if specified + if let Some(build) = &spec.build { + let build_matcher = build + .parse() + .map_err(|e| PinCompatibleError::BuildStringParse(format!("{}", e)))?; + + return Ok(PixiSpec::DetailedVersion(Box::new(DetailedSpec { + version: Some(version_spec), + build: Some(build_matcher), + ..Default::default() + }))); + } + + // 7. Return simple version spec + Ok(PixiSpec::Version(version_spec)) +} + +/// Apply a pin bound to a version +/// +/// - For Expression: extract N segments from version or increment +/// - For Version: use as-is +/// - If increment=true: bump the last segment and add .0a0 +fn apply_pin_bound( + bound: &PinBound, + version: &Version, + increment: bool, +) -> Result { + match bound { + PinBound::Expression(expr_string) => { + // Parse and validate the expression string + let expr = PinExpression::from_str(expr_string)?; + + if increment { + // Increment version (like rattler_build's increment function) + increment_version(version, expr.segment_count()) + } else { + // Extract segments for lower bound + extract_version_segments(version, expr.segment_count()) + } + } + PinBound::Version(v) => Ok(v.clone()), + } +} + +/// Extract N segments from a version (for lower bound) +/// +/// Example: "1.2.3" with segment_count=2 → "1.2" +fn extract_version_segments( + version: &Version, + segment_count: usize, +) -> Result { + use std::cmp::min; + + // Extract only the first N segments + version + .clone() + .with_segments(..min(version.segment_count(), segment_count)) + .ok_or_else(|| { + PinCompatibleError::VersionBump(format!( + "Failed to extract {} segments from version {}", + segment_count, version + )) + }) +} + +/// Increment a version at the Nth segment (for upper bound) +/// +/// Example: "1.2.3" with segment_count=2 → "1.3.0a0" +/// Example: "1.2.3" with segment_count=3 → "1.2.4.0a0" +/// +/// This mimics rattler_build's increment() function +fn increment_version( + version: &Version, + segment_count: usize, +) -> Result { + use std::cmp::min; + + if segment_count == 0 { + return Err(PinCompatibleError::VersionBump( + "Segment count must be at least 1".to_string(), + )); + } + + // Extract first N segments + let truncated = version + .clone() + .with_segments(..min(version.segment_count(), segment_count)) + .ok_or_else(|| { + PinCompatibleError::VersionBump(format!( + "Failed to extract {} segments from version {}", + segment_count, version + )) + })?; + + // Bump the last segment (segment_count - 1) + let bumped = truncated + .bump(VersionBumpType::Segment((segment_count - 1) as i32)) + .map_err(|e: VersionBumpError| PinCompatibleError::VersionBump(e.to_string()))?; + + // Add .0a0 suffix and remove local version if present + Ok(bumped.with_alpha().remove_local().into_owned()) +} + +#[cfg(test)] +mod tests { + use super::*; + + // PinExpression tests + #[test] + fn test_pin_expression_valid() { + assert!(PinExpression::from_str("x").is_ok()); + assert!(PinExpression::from_str("x.x").is_ok()); + assert!(PinExpression::from_str("x.x.x").is_ok()); + assert!(PinExpression::from_str("x.x.x.x.x.x").is_ok()); + + let expr = PinExpression::from_str("x.x.x").unwrap(); + assert_eq!(expr.segment_count(), 3); + assert_eq!(expr.to_string(), "x.x.x"); + } + + #[test] + fn test_pin_expression_new() { + let expr = PinExpression::new(3).unwrap(); + assert_eq!(expr.segment_count(), 3); + assert_eq!(expr.to_string(), "x.x.x"); + + assert!(PinExpression::new(0).is_err()); + } + + #[test] + fn test_pin_expression_invalid() { + // Contains invalid characters + assert!(PinExpression::from_str("x.y").is_err()); + assert!(PinExpression::from_str("1.2.3").is_err()); + assert!(PinExpression::from_str("x.x.x.4").is_err()); + + // Empty or no 'x' + assert!(PinExpression::from_str("").is_err()); + assert!(PinExpression::from_str("...").is_err()); + } + + // Version manipulation tests + #[test] + fn test_extract_version_segments() { + let version = Version::from_str("1.2.3").unwrap(); + + assert_eq!( + extract_version_segments(&version, 1).unwrap().to_string(), + "1" + ); + assert_eq!( + extract_version_segments(&version, 2).unwrap().to_string(), + "1.2" + ); + assert_eq!( + extract_version_segments(&version, 3).unwrap().to_string(), + "1.2.3" + ); + // More segments than version has should just return full version + assert_eq!( + extract_version_segments(&version, 5).unwrap().to_string(), + "1.2.3" + ); + } + + #[test] + fn test_increment_version() { + // Test basic increment + let version = Version::from_str("1.2.3").unwrap(); + + // Increment at segment 1: 1 -> 2.0a0 + assert_eq!(increment_version(&version, 1).unwrap().to_string(), "2.0a0"); + + // Increment at segment 2: 1.2 -> 1.3.0a0 + assert_eq!( + increment_version(&version, 2).unwrap().to_string(), + "1.3.0a0" + ); + + // Increment at segment 3: 1.2.3 -> 1.2.4.0a0 + assert_eq!( + increment_version(&version, 3).unwrap().to_string(), + "1.2.4.0a0" + ); + + // Increment beyond version length: uses actual segments + pads + // 1.2.3 with 5 segments: truncate to 3, then bump segment 4 (5-1=4, but max is 2) + // This creates 1.2.3.0.1.0a0 which is the rattler behavior + assert_eq!( + increment_version(&version, 5).unwrap().to_string(), + "1.2.3.0.1.0a0" + ); + } + + #[test] + fn test_increment_version_with_local() { + // Version with local part should have it removed + let version = Version::from_str("1.2.3+local").unwrap(); + assert_eq!( + increment_version(&version, 2).unwrap().to_string(), + "1.3.0a0" + ); + } + + #[test] + fn test_increment_version_zero_segments() { + let version = Version::from_str("1.2.3").unwrap(); + assert!(increment_version(&version, 0).is_err()); + } + + // Helper to create a test PixiRecord + fn create_test_record(name: &str, version: &str, build: &str) -> PixiRecord { + use rattler_conda_types::{NoArchType, PackageRecord, Platform, RepoDataRecord}; + use std::collections::BTreeMap; + use url::Url; + + let package_record = PackageRecord { + arch: None, + build: build.to_string(), + build_number: 0, + constrains: vec![], + depends: vec![], + features: None, + legacy_bz2_md5: None, + legacy_bz2_size: None, + license: None, + license_family: None, + md5: None, + name: PackageName::new_unchecked(name), + noarch: NoArchType::default(), + platform: Some(Platform::Linux64.to_string()), + sha256: None, + size: None, + subdir: "linux-64".to_string(), + timestamp: None, + track_features: vec![], + version: Version::from_str(version).unwrap().into(), + purls: None, + run_exports: None, + experimental_extra_depends: BTreeMap::new(), + python_site_packages_path: None, + }; + + PixiRecord::Binary(RepoDataRecord { + package_record, + file_name: format!("{}-{}-{}.conda", name, version, build), + url: Url::parse("https://conda.anaconda.org/conda-forge/linux-64/test.conda").unwrap(), + channel: Some("conda-forge".to_string()), + }) + } + + // Pin compatible resolution tests + #[test] + fn test_pin_compatible_basic_bounds() { + // Setup: python 3.11.5 + let python_record = create_test_record("python", "3.11.5", "h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("python"), &python_record); + + // Test: pin_compatible("python", lower_bound="x.x", upper_bound="x.x") + // Expected: >=3.11,<3.12.0a0 + let spec = PinCompatibleSpec { + lower_bound: Some(PinBound::Expression("x.x".to_string())), + upper_bound: Some(PinBound::Expression("x.x".to_string())), + exact: false, + build: None, + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("python"), &spec, &map).unwrap(); + + // Verify it's a Version spec + if let PixiSpec::Version(version_spec) = result { + assert_eq!(version_spec.to_string(), ">=3.11,<3.12.0a0"); + } else { + panic!("Expected PixiSpec::Version"); + } + } + + #[test] + fn test_pin_compatible_three_segments() { + // Setup: numpy 1.23.4 + let numpy_record = create_test_record("numpy", "1.23.4", "py311h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("numpy"), &numpy_record); + + // Test: pin_compatible("numpy", lower_bound="x.x.x", upper_bound="x.x.x") + // Expected: >=1.23.4,<1.23.5.0a0 + let spec = PinCompatibleSpec { + lower_bound: Some(PinBound::Expression("x.x.x".to_string())), + upper_bound: Some(PinBound::Expression("x.x.x".to_string())), + exact: false, + build: None, + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("numpy"), &spec, &map).unwrap(); + + if let PixiSpec::Version(version_spec) = result { + assert_eq!(version_spec.to_string(), ">=1.23.4,<1.23.5.0a0"); + } else { + panic!("Expected PixiSpec::Version"); + } + } + + #[test] + fn test_pin_compatible_major_only() { + // Setup: python 3.11.5 + let python_record = create_test_record("python", "3.11.5", "h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("python"), &python_record); + + // Test: pin_compatible("python", lower_bound="x", upper_bound="x") + // Expected: >=3,<4.0a0 + let spec = PinCompatibleSpec { + lower_bound: Some(PinBound::Expression("x".to_string())), + upper_bound: Some(PinBound::Expression("x".to_string())), + exact: false, + build: None, + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("python"), &spec, &map).unwrap(); + + if let PixiSpec::Version(version_spec) = result { + assert_eq!(version_spec.to_string(), ">=3,<4.0a0"); + } else { + panic!("Expected PixiSpec::Version"); + } + } + + #[test] + fn test_pin_compatible_lower_bound_only() { + // Setup: openssl 1.1.1k + let openssl_record = create_test_record("openssl", "1.1.1", "h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("openssl"), &openssl_record); + + // Test: pin_compatible("openssl", lower_bound="x.x.x", upper_bound=None) + // Expected: >=1.1.1 + let spec = PinCompatibleSpec { + lower_bound: Some(PinBound::Expression("x.x.x".to_string())), + upper_bound: None, + exact: false, + build: None, + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("openssl"), &spec, &map).unwrap(); + + if let PixiSpec::Version(version_spec) = result { + assert_eq!(version_spec.to_string(), ">=1.1.1"); + } else { + panic!("Expected PixiSpec::Version"); + } + } + + #[test] + fn test_pin_compatible_upper_bound_only() { + // Setup: openssl 1.1.1k + let openssl_record = create_test_record("openssl", "1.1.1", "h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("openssl"), &openssl_record); + + // Test: pin_compatible("openssl", lower_bound=None, upper_bound="x.x") + // Expected: <1.2.0a0 + let spec = PinCompatibleSpec { + lower_bound: None, + upper_bound: Some(PinBound::Expression("x.x".to_string())), + exact: false, + build: None, + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("openssl"), &spec, &map).unwrap(); + + if let PixiSpec::Version(version_spec) = result { + assert_eq!(version_spec.to_string(), "<1.2.0a0"); + } else { + panic!("Expected PixiSpec::Version"); + } + } + + #[test] + fn test_pin_compatible_exact() { + // Setup: python 3.11.5 + let python_record = create_test_record("python", "3.11.5", "h12345_0"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("python"), &python_record); + + // Test: pin_compatible("python", exact=True) + // Expected: ==3.11.5 h12345_0 + let spec = PinCompatibleSpec { + lower_bound: None, + upper_bound: None, + exact: true, + build: None, + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("python"), &spec, &map).unwrap(); + + if let PixiSpec::DetailedVersion(detailed) = result { + assert_eq!(detailed.version.as_ref().unwrap().to_string(), "==3.11.5"); + assert_eq!(detailed.build.as_ref().unwrap().to_string(), "h12345_0"); + } else { + panic!("Expected PixiSpec::DetailedVersion"); + } + } + + #[test] + fn test_pin_compatible_with_build_string() { + // Setup: python 3.11.5 + let python_record = create_test_record("python", "3.11.5", "h12345_0"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("python"), &python_record); + + // Test: pin_compatible("python", lower_bound="x.x", upper_bound="x.x", build="h*") + // Expected: >=3.11,<3.12.0a0 h* + let spec = PinCompatibleSpec { + lower_bound: Some(PinBound::Expression("x.x".to_string())), + upper_bound: Some(PinBound::Expression("x.x".to_string())), + exact: false, + build: Some("h*".to_string()), + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("python"), &spec, &map).unwrap(); + + if let PixiSpec::DetailedVersion(detailed) = result { + assert_eq!( + detailed.version.as_ref().unwrap().to_string(), + ">=3.11,<3.12.0a0" + ); + assert_eq!(detailed.build.as_ref().unwrap().to_string(), "h*"); + } else { + panic!("Expected PixiSpec::DetailedVersion"); + } + } + + #[test] + fn test_pin_compatible_literal_version_bounds() { + // Setup: python 3.11.5 + let python_record = create_test_record("python", "3.11.5", "h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("python"), &python_record); + + // Test: pin_compatible("python", lower_bound="3.10", upper_bound="3.12") + // Expected: >=3.10,<3.12 + let spec = PinCompatibleSpec { + lower_bound: Some(PinBound::Version(Version::from_str("3.10").unwrap())), + upper_bound: Some(PinBound::Version(Version::from_str("3.12").unwrap())), + exact: false, + build: None, + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("python"), &spec, &map).unwrap(); + + if let PixiSpec::Version(version_spec) = result { + assert_eq!(version_spec.to_string(), ">=3.10,<3.12"); + } else { + panic!("Expected PixiSpec::Version"); + } + } + + #[test] + fn test_pin_compatible_no_bounds() { + // Setup: python 3.11.5 + let python_record = create_test_record("python", "3.11.5", "h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("python"), &python_record); + + // Test: pin_compatible("python") with no bounds + // Expected: * (any version) + let spec = PinCompatibleSpec { + lower_bound: None, + upper_bound: None, + exact: false, + build: None, + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("python"), &spec, &map).unwrap(); + + if let PixiSpec::Version(version_spec) = result { + assert_eq!(version_spec, VersionSpec::Any); + } else { + panic!("Expected PixiSpec::Version"); + } + } + + // Error cases + #[test] + fn test_pin_compatible_package_not_found() { + let map = PinCompatibilityMap::new(); + + let spec = PinCompatibleSpec { + lower_bound: Some(PinBound::Expression("x.x".to_string())), + upper_bound: Some(PinBound::Expression("x.x".to_string())), + exact: false, + build: None, + }; + + let result = + resolve_pin_compatible(&PackageName::new_unchecked("nonexistent"), &spec, &map); + assert!(matches!( + result, + Err(PinCompatibleError::PackageNotFound(_)) + )); + } + + #[test] + fn test_pin_compatible_exact_with_build_error() { + let python_record = create_test_record("python", "3.11.5", "h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("python"), &python_record); + + // Test: exact=true with build should error + let spec = PinCompatibleSpec { + lower_bound: None, + upper_bound: None, + exact: true, + build: Some("h*".to_string()), + }; + + let result = resolve_pin_compatible(&PackageName::new_unchecked("python"), &spec, &map); + assert!(matches!( + result, + Err(PinCompatibleError::BuildSpecifierWithExact) + )); + } + + #[test] + fn test_pin_compatible_invalid_expression() { + let python_record = create_test_record("python", "3.11.5", "h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("python"), &python_record); + + // Test: invalid expression + let spec = PinCompatibleSpec { + lower_bound: Some(PinBound::Expression("x.y.z".to_string())), + upper_bound: None, + exact: false, + build: None, + }; + + let result = resolve_pin_compatible(&PackageName::new_unchecked("python"), &spec, &map); + assert!(matches!( + result, + Err(PinCompatibleError::InvalidPinExpression(_)) + )); + } + + #[test] + fn test_pin_compatible_invalid_build_string() { + let python_record = create_test_record("python", "3.11.5", "h12345"); + let mut map = PinCompatibilityMap::new(); + map.insert(PackageName::new_unchecked("python"), &python_record); + + // Test: invalid build string (use a pattern that glob parsing will reject) + let spec = PinCompatibleSpec { + lower_bound: Some(PinBound::Expression("x.x".to_string())), + upper_bound: Some(PinBound::Expression("x.x".to_string())), + exact: false, + build: Some("**[".to_string()), // Invalid glob - unterminated character class + }; + + let result = resolve_pin_compatible(&PackageName::new_unchecked("python"), &spec, &map); + assert!(matches!( + result, + Err(PinCompatibleError::BuildStringParse(_)) + )); + } +} diff --git a/crates/pixi_command_dispatcher/src/dev_source_metadata/mod.rs b/crates/pixi_command_dispatcher/src/dev_source_metadata/mod.rs index 4d73f86d12..61d0d65873 100644 --- a/crates/pixi_command_dispatcher/src/dev_source_metadata/mod.rs +++ b/crates/pixi_command_dispatcher/src/dev_source_metadata/mod.rs @@ -2,6 +2,7 @@ use std::fmt::Display; use itertools::Itertools; use miette::Diagnostic; +use pixi_build_types::PackageSpec; use pixi_record::{DevSourceRecord, PinnedSourceSpec}; use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor, SourceLocationSpec}; use pixi_spec_containers::DependencyMap; @@ -210,15 +211,25 @@ impl DevSourceMetadataSpec { // Process depends for depend in &deps.depends { let name = PackageName::new_unchecked(&depend.name); - let spec = - crate::build::conversion::from_package_spec_v1(depend.spec.clone()); - // Resolve relative paths for source dependencies - let resolved_spec = match spec.into_source_or_binary() { - itertools::Either::Left(source_spec) => { - PixiSpec::from(source_spec.resolve(source_anchor)) + // Match directly on PackageSpec + let resolved_spec = match &depend.spec { + PackageSpec::Binary(binary) => { + let spec = + crate::build::conversion::from_binary_spec_v1(binary.clone()); + PixiSpec::from(spec) + } + PackageSpec::Source(source) => { + let spec = + crate::build::conversion::from_source_spec_v1(source.clone()); + PixiSpec::from(spec.resolve(source_anchor)) + } + PackageSpec::PinCompatible(_) => { + // Just ignore the pin compatible dependency. Since we are also adding + // the dependencies for build and host directly the pin_compatible + // wouldnt have any effect anyway. + continue; } - itertools::Either::Right(binary_spec) => PixiSpec::from(binary_spec), }; dependencies.insert(name, resolved_spec); } diff --git a/crates/pixi_command_dispatcher/src/source_build/mod.rs b/crates/pixi_command_dispatcher/src/source_build/mod.rs index 8472de8983..1a58f6bd7f 100644 --- a/crates/pixi_command_dispatcher/src/source_build/mod.rs +++ b/crates/pixi_command_dispatcher/src/source_build/mod.rs @@ -1,5 +1,5 @@ use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, path::{Path, PathBuf}, sync::Arc, }; @@ -31,6 +31,7 @@ use crate::{ InstantiateBackendError, InstantiateBackendSpec, PixiEnvironmentSpec, SolvePixiEnvironmentError, SourceBuildCacheStatusError, SourceBuildCacheStatusSpec, SourceCheckoutError, + build::pin_compatible::PinCompatibleError, build::{ BuildCacheError, BuildHostEnvironment, BuildHostPackage, CachedBuild, CachedBuildSourceInfo, Dependencies, DependenciesError, MoveError, PackageBuildInputHash, @@ -607,10 +608,11 @@ impl SourceBuildSpec { let directories = Directories::new(&work_directory, host_platform); // Solve the build environment. + let mut compatibility_map = HashMap::new(); let build_dependencies = output .build_dependencies .as_ref() - .map(|deps| Dependencies::new(deps, Some(source_anchor.clone()))) + .map(|deps| Dependencies::new(deps, Some(source_anchor.clone()), &compatibility_map)) .transpose() .map_err(SourceBuildError::from) .map_err(CommandDispatcherError::Failed)? @@ -638,11 +640,17 @@ impl SourceBuildSpec { .map_err(|err| SourceBuildError::RunExportsExtraction(String::from("build"), err)) .map_err(CommandDispatcherError::Failed)?; + compatibility_map.extend( + build_records + .iter() + .map(|record| (record.package_record().name.clone(), record)), + ); + // Solve the host environment for the output. let host_dependencies = output .host_dependencies .as_ref() - .map(|deps| Dependencies::new(deps, Some(source_anchor.clone()))) + .map(|deps| Dependencies::new(deps, Some(source_anchor.clone()), &compatibility_map)) .transpose() .map_err(SourceBuildError::from) .map_err(CommandDispatcherError::Failed)? @@ -734,8 +742,14 @@ impl SourceBuildSpec { CommandDispatcherError::Failed(SourceBuildError::CreateWorkDirectory(err)) })?; + compatibility_map.extend( + host_records + .iter() + .map(|record| (record.package_record().name.clone(), record)), + ); + // Gather the dependencies for the output. - let dependencies = Dependencies::new(&output.run_dependencies, None) + let dependencies = Dependencies::new(&output.run_dependencies, None, &compatibility_map) .map_err(SourceBuildError::from) .map_err(CommandDispatcherError::Failed)? .extend_with_run_exports_from_build_and_host( @@ -745,9 +759,10 @@ impl SourceBuildSpec { ); // Convert the run exports - let run_exports = PixiRunExports::try_from_protocol(&output.run_exports) - .map_err(SourceBuildError::from) - .map_err(CommandDispatcherError::Failed)?; + let run_exports = + PixiRunExports::try_from_protocol(&output.run_exports, &compatibility_map) + .map_err(SourceBuildError::from) + .map_err(CommandDispatcherError::Failed)?; // Extract the repodata records from the build and host environments. let build_records = Self::extract_prefix_repodata(build_records, build_prefix); @@ -963,8 +978,11 @@ pub enum SourceBuildError { )] MissingOutputFile(PathBuf), - #[error("backend returned a dependency on an invalid package name: {0}")] - InvalidPackageName(String, #[source] InvalidPackageNameError), + #[error("backend returned a dependency on an invalid package name")] + InvalidPackageName(#[from] InvalidPackageNameError), + + #[error(transparent)] + PinCompatibleError(#[from] PinCompatibleError), #[error(transparent)] #[diagnostic(transparent)] @@ -995,8 +1013,11 @@ pub enum SourceBuildError { impl From for SourceBuildError { fn from(value: DependenciesError) -> Self { match value { - DependenciesError::InvalidPackageName(name, error) => { - SourceBuildError::InvalidPackageName(name, error) + DependenciesError::InvalidPackageName(error) => { + SourceBuildError::InvalidPackageName(error) + } + DependenciesError::PinCompatibleError(error) => { + SourceBuildError::PinCompatibleError(error) } } } diff --git a/crates/pixi_command_dispatcher/src/source_metadata/mod.rs b/crates/pixi_command_dispatcher/src/source_metadata/mod.rs index 5173e2e527..9ba2e739c1 100644 --- a/crates/pixi_command_dispatcher/src/source_metadata/mod.rs +++ b/crates/pixi_command_dispatcher/src/source_metadata/mod.rs @@ -218,11 +218,12 @@ impl SourceMetadataSpec { let source_anchor = SourceAnchor::from(SourceLocationSpec::from(manifest_source.clone())); // Solve the build environment for the output. + let mut compatibility_map = HashMap::new(); let build_dependencies = output .build_dependencies .as_ref() // TODO(tim): we need to check if this works for out-of-tree builds with source dependencies in the out-of-tree, this might be incorrectly anchored - .map(|deps| Dependencies::new(deps, Some(source_anchor.clone()))) + .map(|deps| Dependencies::new(deps, Some(source_anchor.clone()), &compatibility_map)) .transpose() .map_err(SourceMetadataError::from) .map_err(CommandDispatcherError::Failed)? @@ -251,11 +252,17 @@ impl SourceMetadataSpec { .map_err(|err| SourceMetadataError::RunExportsExtraction(String::from("build"), err)) .map_err(CommandDispatcherError::Failed)?; + compatibility_map.extend( + build_records + .iter() + .map(|record| (record.package_record().name.clone(), record)), + ); + // Solve the host environment for the output. let host_dependencies = output .host_dependencies .as_ref() - .map(|deps| Dependencies::new(deps, Some(source_anchor.clone()))) + .map(|deps| Dependencies::new(deps, Some(source_anchor.clone()), &compatibility_map)) .transpose() .map_err(SourceMetadataError::from) .map_err(CommandDispatcherError::Failed)? @@ -282,15 +289,22 @@ impl SourceMetadataSpec { .map_err(|err| SourceMetadataError::RunExportsExtraction(String::from("host"), err)) .map_err(CommandDispatcherError::Failed)?; + compatibility_map.extend( + host_records + .iter() + .map(|record| (record.package_record().name.clone(), record)), + ); + // Gather the dependencies for the output. - let run_dependencies = Dependencies::new(&output.run_dependencies, None) - .map_err(SourceMetadataError::from) - .map_err(CommandDispatcherError::Failed)? - .extend_with_run_exports_from_build_and_host( - host_run_exports, - build_run_exports, - output.metadata.subdir, - ); + let run_dependencies = + Dependencies::new(&output.run_dependencies, None, &compatibility_map) + .map_err(SourceMetadataError::from) + .map_err(CommandDispatcherError::Failed)? + .extend_with_run_exports_from_build_and_host( + host_run_exports, + build_run_exports, + output.metadata.subdir, + ); let PackageRecordDependencies { depends, @@ -301,9 +315,10 @@ impl SourceMetadataSpec { .map_err(CommandDispatcherError::Failed)?; // Convert the run exports - let run_exports = PixiRunExports::try_from_protocol(&output.run_exports) - .map_err(SourceMetadataError::from) - .map_err(CommandDispatcherError::Failed)?; + let run_exports = + PixiRunExports::try_from_protocol(&output.run_exports, &compatibility_map) + .map_err(SourceMetadataError::from) + .map_err(CommandDispatcherError::Failed)?; let pixi_spec_to_match_spec = |name: &PackageName, spec: &PixiSpec, @@ -594,8 +609,11 @@ pub enum SourceMetadataError { #[error(transparent)] SpecConversionError(#[from] SpecConversionError), - #[error("backend returned a dependency on an invalid package name: {0}")] - InvalidPackageName(String, #[source] InvalidPackageNameError), + #[error(transparent)] + InvalidPackageName(#[from] InvalidPackageNameError), + + #[error(transparent)] + PinCompatibleError(#[from] crate::build::pin_compatible::PinCompatibleError), #[error("found two source dependencies for {} but for different sources ({source1} and {source2})", package.as_source() )] @@ -619,8 +637,11 @@ pub enum SourceMetadataError { impl From for SourceMetadataError { fn from(value: DependenciesError) -> Self { match value { - DependenciesError::InvalidPackageName(name, error) => { - SourceMetadataError::InvalidPackageName(name, error) + DependenciesError::InvalidPackageName(error) => { + SourceMetadataError::InvalidPackageName(error) + } + DependenciesError::PinCompatibleError(error) => { + SourceMetadataError::PinCompatibleError(error) } } } From e6235f25def39aa433a529cf766852b91a104ccd Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:40:28 +0100 Subject: [PATCH 08/13] feat: add type for constraintspec --- crates/pixi_build_types/src/lib.rs | 6 +- .../src/procedures/conda_outputs.rs | 8 +- crates/pixi_build_types/src/project_model.rs | 36 +++++++- .../src/build/dependencies.rs | 31 ++++--- .../src/build/pin_compatible.rs | 64 ++++++++++---- .../src/dev_source_metadata/mod.rs | 13 ++- schema/pixi_build_api.json | 83 +++++++++++++++++++ 7 files changed, 203 insertions(+), 38 deletions(-) diff --git a/crates/pixi_build_types/src/lib.rs b/crates/pixi_build_types/src/lib.rs index 08f4db049f..e3b8e785d9 100644 --- a/crates/pixi_build_types/src/lib.rs +++ b/crates/pixi_build_types/src/lib.rs @@ -12,9 +12,9 @@ pub use capabilities::{BackendCapabilities, FrontendCapabilities}; pub use channel_configuration::ChannelConfiguration; pub use conda_package_metadata::CondaPackageMetadata; pub use project_model::{ - BinaryPackageSpec, GitReference, GitSpec, NamedSpec, PackageSpec, PathSpec, PinBound, - PinCompatibleSpec, ProjectModel, SourcePackageLocationSpec, SourcePackageName, - SourcePackageSpec, Target, TargetSelector, Targets, UrlSpec, + BinaryPackageSpec, ConstraintSpec, GitReference, GitSpec, NamedSpec, PackageSpec, PathSpec, + PinBound, PinCompatibleSpec, PinExpression, ProjectModel, SourcePackageLocationSpec, + SourcePackageName, SourcePackageSpec, Target, TargetSelector, Targets, UrlSpec, }; use rattler_conda_types::{ GenericVirtualPackage, PackageName, Platform, Version, VersionSpec, diff --git a/crates/pixi_build_types/src/procedures/conda_outputs.rs b/crates/pixi_build_types/src/procedures/conda_outputs.rs index d977459a9d..4353d0515e 100644 --- a/crates/pixi_build_types/src/procedures/conda_outputs.rs +++ b/crates/pixi_build_types/src/procedures/conda_outputs.rs @@ -16,7 +16,7 @@ use rattler_conda_types::{ChannelUrl, NoArchType, PackageName, Platform, Version use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::{BinaryPackageSpec, PackageSpec, VariantValue, project_model::NamedSpec}; +use crate::{ConstraintSpec, PackageSpec, VariantValue, project_model::NamedSpec}; pub const METHOD_NAME: &str = "conda/outputs"; @@ -167,7 +167,7 @@ pub struct CondaOutputDependencies { /// Additional constraints that apply to the environment in which the /// dependencies are solved. Constraints are represented as matchspecs. - pub constraints: Vec>, + pub constraints: Vec>, } /// Describes which run-exports should be ignored for a particular output. @@ -199,11 +199,11 @@ pub struct CondaOutputRunExports { pub noarch: Vec>, /// weak constrains apply a constrain dependency from host to run - pub weak_constrains: Vec>, + pub weak_constrains: Vec>, /// strong constrains apply a constrain dependency from build to host and /// run - pub strong_constrains: Vec>, + pub strong_constrains: Vec>, } // TODO: Multi-output caching is not yet supported. diff --git a/crates/pixi_build_types/src/project_model.rs b/crates/pixi_build_types/src/project_model.rs index 523b3953dc..246c097430 100644 --- a/crates/pixi_build_types/src/project_model.rs +++ b/crates/pixi_build_types/src/project_model.rs @@ -212,6 +212,16 @@ pub enum PackageSpec { PinCompatible(PinCompatibleSpec), } +/// A package spec that can be used for constraints. +/// Constraints don't support source packages but may support pin_compatible in the future. +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub enum ConstraintSpec { + /// A binary package constraint (version spec) + Binary(BinaryPackageSpec), +} + #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] @@ -232,11 +242,22 @@ pub struct PinCompatibleSpec { pub build: Option, } +/// A pin expression string like "x", "x.x", "x.x.x", etc. +/// +/// This represents the number of version segments to pin. +/// For example, "x.x" means pin the major and minor version. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(transparent)] +pub struct PinExpression( + #[cfg_attr(feature = "schemars", schemars(regex(pattern = r"^x(\.x)*$")))] pub String, +); + #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub enum PinBound { - Expression(String), + Expression(PinExpression), Version(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Version), } @@ -615,6 +636,19 @@ impl Hash for PackageSpec { } } +impl Hash for ConstraintSpec { + /// Custom hash implementation that uses discriminant values to keep the + /// hash as stable as possible when adding new enum variants. + fn hash(&self, state: &mut H) { + match self { + Self::Binary(spec) => { + 0u8.hash(state); + spec.hash(state); + } + } + } +} + impl Hash for PinCompatibleSpec { fn hash(&self, state: &mut H) { let PinCompatibleSpec { diff --git a/crates/pixi_command_dispatcher/src/build/dependencies.rs b/crates/pixi_command_dispatcher/src/build/dependencies.rs index 4506598eca..6197e22ff0 100644 --- a/crates/pixi_command_dispatcher/src/build/dependencies.rs +++ b/crates/pixi_command_dispatcher/src/build/dependencies.rs @@ -6,7 +6,7 @@ use crate::build::pin_compatible::{ }; use pixi_build_types as pbt; use pixi_build_types::{ - BinaryPackageSpec, NamedSpec, PackageSpec, + NamedSpec, PackageSpec, procedures::conda_outputs::{ CondaOutputDependencies, CondaOutputIgnoreRunExports, CondaOutputRunExports, }, @@ -124,10 +124,14 @@ impl Dependencies { for constraint in &output.constraints { let name = rattler_conda_types::PackageName::from_str(&constraint.name)?; - constraints.insert( - name, - conversion::from_binary_spec_v1(constraint.spec.clone()).into(), - ); + + // Match on ConstraintSpec enum + match &constraint.spec { + pbt::ConstraintSpec::Binary(binary) => { + constraints + .insert(name, conversion::from_binary_spec_v1(binary.clone()).into()); + } + } } Ok(Self { @@ -412,15 +416,22 @@ impl PixiRunExports { .collect() } - fn convert_binary_spec( - specs: &[NamedSpec], + fn convert_constraint_spec( + specs: &[NamedSpec], ) -> Result, DependenciesError> { specs .iter() .cloned() .map(|named_spec| { - let spec = conversion::from_binary_spec_v1(named_spec.spec); let name = PackageName::from_str(&named_spec.name)?; + + // Match on ConstraintSpec enum + let spec = match named_spec.spec { + pbt::ConstraintSpec::Binary(binary) => { + conversion::from_binary_spec_v1(binary) + } + }; + Ok((name, spec)) }) .collect() @@ -430,8 +441,8 @@ impl PixiRunExports { weak: convert_package_spec(&output.weak, compatibility_map)?, strong: convert_package_spec(&output.strong, compatibility_map)?, noarch: convert_package_spec(&output.noarch, compatibility_map)?, - weak_constrains: convert_binary_spec(&output.weak_constrains)?, - strong_constrains: convert_binary_spec(&output.strong_constrains)?, + weak_constrains: convert_constraint_spec(&output.weak_constrains)?, + strong_constrains: convert_constraint_spec(&output.strong_constrains)?, }) } } diff --git a/crates/pixi_command_dispatcher/src/build/pin_compatible.rs b/crates/pixi_command_dispatcher/src/build/pin_compatible.rs index 2b2630a8cb..a26fdf1331 100644 --- a/crates/pixi_command_dispatcher/src/build/pin_compatible.rs +++ b/crates/pixi_command_dispatcher/src/build/pin_compatible.rs @@ -178,9 +178,9 @@ fn apply_pin_bound( increment: bool, ) -> Result { match bound { - PinBound::Expression(expr_string) => { + PinBound::Expression(pin_expr) => { // Parse and validate the expression string - let expr = PinExpression::from_str(expr_string)?; + let expr = PinExpression::from_str(&pin_expr.0)?; if increment { // Increment version (like rattler_build's increment function) @@ -412,8 +412,12 @@ mod tests { // Test: pin_compatible("python", lower_bound="x.x", upper_bound="x.x") // Expected: >=3.11,<3.12.0a0 let spec = PinCompatibleSpec { - lower_bound: Some(PinBound::Expression("x.x".to_string())), - upper_bound: Some(PinBound::Expression("x.x".to_string())), + lower_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x".to_string(), + ))), + upper_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x".to_string(), + ))), exact: false, build: None, }; @@ -439,8 +443,12 @@ mod tests { // Test: pin_compatible("numpy", lower_bound="x.x.x", upper_bound="x.x.x") // Expected: >=1.23.4,<1.23.5.0a0 let spec = PinCompatibleSpec { - lower_bound: Some(PinBound::Expression("x.x.x".to_string())), - upper_bound: Some(PinBound::Expression("x.x.x".to_string())), + lower_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x.x".to_string(), + ))), + upper_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x.x".to_string(), + ))), exact: false, build: None, }; @@ -465,8 +473,12 @@ mod tests { // Test: pin_compatible("python", lower_bound="x", upper_bound="x") // Expected: >=3,<4.0a0 let spec = PinCompatibleSpec { - lower_bound: Some(PinBound::Expression("x".to_string())), - upper_bound: Some(PinBound::Expression("x".to_string())), + lower_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x".to_string(), + ))), + upper_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x".to_string(), + ))), exact: false, build: None, }; @@ -491,7 +503,9 @@ mod tests { // Test: pin_compatible("openssl", lower_bound="x.x.x", upper_bound=None) // Expected: >=1.1.1 let spec = PinCompatibleSpec { - lower_bound: Some(PinBound::Expression("x.x.x".to_string())), + lower_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x.x".to_string(), + ))), upper_bound: None, exact: false, build: None, @@ -518,7 +532,9 @@ mod tests { // Expected: <1.2.0a0 let spec = PinCompatibleSpec { lower_bound: None, - upper_bound: Some(PinBound::Expression("x.x".to_string())), + upper_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x".to_string(), + ))), exact: false, build: None, }; @@ -570,8 +586,12 @@ mod tests { // Test: pin_compatible("python", lower_bound="x.x", upper_bound="x.x", build="h*") // Expected: >=3.11,<3.12.0a0 h* let spec = PinCompatibleSpec { - lower_bound: Some(PinBound::Expression("x.x".to_string())), - upper_bound: Some(PinBound::Expression("x.x".to_string())), + lower_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x".to_string(), + ))), + upper_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x".to_string(), + ))), exact: false, build: Some("h*".to_string()), }; @@ -648,8 +668,12 @@ mod tests { let map = PinCompatibilityMap::new(); let spec = PinCompatibleSpec { - lower_bound: Some(PinBound::Expression("x.x".to_string())), - upper_bound: Some(PinBound::Expression("x.x".to_string())), + lower_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x".to_string(), + ))), + upper_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x".to_string(), + ))), exact: false, build: None, }; @@ -691,7 +715,9 @@ mod tests { // Test: invalid expression let spec = PinCompatibleSpec { - lower_bound: Some(PinBound::Expression("x.y.z".to_string())), + lower_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.y.z".to_string(), + ))), upper_bound: None, exact: false, build: None, @@ -712,8 +738,12 @@ mod tests { // Test: invalid build string (use a pattern that glob parsing will reject) let spec = PinCompatibleSpec { - lower_bound: Some(PinBound::Expression("x.x".to_string())), - upper_bound: Some(PinBound::Expression("x.x".to_string())), + lower_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x".to_string(), + ))), + upper_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "x.x".to_string(), + ))), exact: false, build: Some("**[".to_string()), // Invalid glob - unterminated character class }; diff --git a/crates/pixi_command_dispatcher/src/dev_source_metadata/mod.rs b/crates/pixi_command_dispatcher/src/dev_source_metadata/mod.rs index 61d0d65873..3345c5c87a 100644 --- a/crates/pixi_command_dispatcher/src/dev_source_metadata/mod.rs +++ b/crates/pixi_command_dispatcher/src/dev_source_metadata/mod.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use itertools::Itertools; use miette::Diagnostic; -use pixi_build_types::PackageSpec; +use pixi_build_types::{ConstraintSpec, PackageSpec}; use pixi_record::{DevSourceRecord, PinnedSourceSpec}; use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor, SourceLocationSpec}; use pixi_spec_containers::DependencyMap; @@ -10,6 +10,7 @@ use rattler_conda_types::PackageName; use thiserror::Error; use tracing::instrument; +use crate::build::conversion; use crate::{ BuildBackendMetadataError, BuildBackendMetadataSpec, CommandDispatcher, CommandDispatcherError, CommandDispatcherErrorResultExt, @@ -237,8 +238,14 @@ impl DevSourceMetadataSpec { // Process constraints for constraint in &deps.constraints { let name = PackageName::new_unchecked(&constraint.name); - let spec = - crate::build::conversion::from_binary_spec_v1(constraint.spec.clone()); + + // Match on ConstraintSpec enum + let spec = match &constraint.spec { + ConstraintSpec::Binary(binary) => { + conversion::from_binary_spec_v1(binary.clone()) + } + }; + constraints.insert(name, spec); } } diff --git a/schema/pixi_build_api.json b/schema/pixi_build_api.json index 155a6e4334..b2372815a1 100644 --- a/schema/pixi_build_api.json +++ b/schema/pixi_build_api.json @@ -298,6 +298,19 @@ "required": [ "source" ] + }, + { + "description": "Pin to a version that is compatible with a version from the \"previous\" environment", + "type": "object", + "properties": { + "pinCompatible": { + "$ref": "#/$defs/PinCompatibleSpec" + } + }, + "additionalProperties": false, + "required": [ + "pinCompatible" + ] } ] }, @@ -314,6 +327,76 @@ "path" ] }, + "PinBound": { + "oneOf": [ + { + "type": "object", + "properties": { + "expression": { + "$ref": "#/$defs/PinExpression" + } + }, + "additionalProperties": false, + "required": [ + "expression" + ] + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "version" + ] + } + ] + }, + "PinCompatibleSpec": { + "type": "object", + "properties": { + "build": { + "type": [ + "string", + "null" + ] + }, + "exact": { + "description": "If an exact pin is given, we pin the exact version & hash", + "type": "boolean" + }, + "lowerBound": { + "description": "A minimum pin to a version, using `x.x.x...` as syntax", + "anyOf": [ + { + "$ref": "#/$defs/PinBound" + }, + { + "type": "null" + } + ] + }, + "upperBound": { + "description": "A pin to a version, using `x.x.x...` as syntax", + "anyOf": [ + { + "$ref": "#/$defs/PinBound" + }, + { + "type": "null" + } + ] + } + } + }, + "PinExpression": { + "description": "A pin expression string like \"x\", \"x.x\", \"x.x.x\", etc.\n\nThis represents the number of version segments to pin.\nFor example, \"x.x\" means pin the major and minor version.", + "type": "string", + "pattern": "^x(\\.x)*$" + }, "SourcePackageSpec": { "type": "object", "properties": { From 1b86784c6be094809fdc50168fb82598936969b9 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:07:01 +0100 Subject: [PATCH 09/13] fix: merge issue --- crates/pixi_build_backend_passthrough/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/pixi_build_backend_passthrough/src/lib.rs b/crates/pixi_build_backend_passthrough/src/lib.rs index e5d936a294..c02d7294ae 100644 --- a/crates/pixi_build_backend_passthrough/src/lib.rs +++ b/crates/pixi_build_backend_passthrough/src/lib.rs @@ -622,7 +622,7 @@ fn extract_dependencies Option<&OrderMap, -) -> Option> { +) -> Option> { // Parse the run_export string as a MatchSpec let match_spec = rattler_conda_types::MatchSpec::from_str( run_export_str, @@ -659,12 +659,12 @@ fn resolve_run_export_spec( match_spec.version.clone() }; - Some(NamedSpecV1 { + Some(NamedSpec { name: SourcePackageName::from(name), - spec: PackageSpecV1::Binary(Box::new(BinaryPackageSpecV1 { + spec: PackageSpec::Binary(BinaryPackageSpec { version: version_spec, ..Default::default() - })), + }), }) } From 7f2b5d7fbcb9c8da8f3ef8fcbcb0ef17d318bfa3 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:25:46 +0100 Subject: [PATCH 10/13] fix: matching line endings --- crates/xtask/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 3e54b3297a..ffe6aa5dd6 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -67,8 +67,8 @@ mod tests { // Compare the schemas using similar-asserts // Note: We need to trim the committed schema to account for trailing newline similar_asserts::assert_eq!( - committed_schema_json.trim(), - generated_schema_json, + committed_schema_json.trim().replace("\r\n", "\n"), + generated_schema_json.trim().replace("\r\n", "\n"), "The committed schema does not match the generated schema.\nPlease run `cargo xtask generate-schema` to update the schema file." ); } From 4e5e4049f9623c503f24b5c6a999ea55d6c41310 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:27:11 +0100 Subject: [PATCH 11/13] fix: backend capabilities --- crates/pixi_build_types/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/pixi_build_types/src/lib.rs b/crates/pixi_build_types/src/lib.rs index e3b8e785d9..1428890435 100644 --- a/crates/pixi_build_types/src/lib.rs +++ b/crates/pixi_build_types/src/lib.rs @@ -90,6 +90,9 @@ impl PixiBuildApiVersion { 3 => BackendCapabilities { ..Self(2).expected_backend_capabilities() }, + 4 => BackendCapabilities { + ..Self(3).expected_backend_capabilities() + }, _ => BackendCapabilities::default(), } } From 80cae1962f2219b0a288cb08fa8edc5b74afaa42 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:40:38 +0100 Subject: [PATCH 12/13] fix: preserve order --- crates/xtask/Cargo.toml | 2 +- schema/pixi_build_api.json | 578 ++++++++++++++++++------------------- 2 files changed, 290 insertions(+), 290 deletions(-) diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index 44c1a3571b..8e633f057e 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -8,7 +8,7 @@ publish = false clap = { workspace = true, features = ["derive", "std"] } fs-err = { workspace = true } pixi_build_types = { workspace = true, features = ["schemars"] } -schemars = { workspace = true } +schemars = { workspace = true, features = ["preserve_order"] } serde_json = { workspace = true } [dev-dependencies] diff --git a/schema/pixi_build_api.json b/schema/pixi_build_api.json index b2372815a1..14fd1e4603 100644 --- a/schema/pixi_build_api.json +++ b/schema/pixi_build_api.json @@ -3,15 +3,19 @@ "title": "ProjectModel", "type": "object", "properties": { - "authors": { - "description": "Optional authors", + "name": { + "description": "The name of the project", "type": [ - "array", + "string", "null" - ], - "items": { - "type": "string" - } + ] + }, + "buildString": { + "description": "A build string configured by the user.", + "type": [ + "string", + "null" + ] }, "buildNumber": { "description": "The build number configured by the user.", @@ -22,8 +26,8 @@ "format": "uint64", "minimum": 0 }, - "buildString": { - "description": "A build string configured by the user.", + "version": { + "description": "The version of the project", "type": [ "string", "null" @@ -36,21 +40,15 @@ "null" ] }, - "documentation": { - "description": "URL of the project documentation", - "type": [ - "string", - "null" - ], - "format": "uri" - }, - "homepage": { - "description": "URL of the project homepage", + "authors": { + "description": "Optional authors", "type": [ - "string", + "array", "null" ], - "format": "uri" + "items": { + "type": "string" + } }, "license": { "description": "The license as a valid SPDX string (e.g. MIT AND Apache-2.0)", @@ -66,19 +64,20 @@ "null" ] }, - "name": { - "description": "The name of the project", + "readme": { + "description": "Path to the README file of the project (relative to the project root)", "type": [ "string", "null" ] }, - "readme": { - "description": "Path to the README file of the project (relative to the project root)", + "homepage": { + "description": "URL of the project homepage", "type": [ "string", "null" - ] + ], + "format": "uri" }, "repository": { "description": "URL of the project source repository", @@ -88,6 +87,14 @@ ], "format": "uri" }, + "documentation": { + "description": "URL of the project documentation", + "type": [ + "string", + "null" + ], + "format": "uri" + }, "targets": { "description": "The target of the project, this may contain\nplatform specific configurations.", "anyOf": [ @@ -98,20 +105,125 @@ "type": "null" } ] - }, - "version": { - "description": "The version of the project", - "type": [ - "string", - "null" - ] } }, "$defs": { + "Targets": { + "description": "A collect of targets including a default target.", + "type": "object", + "properties": { + "defaultTarget": { + "anyOf": [ + { + "$ref": "#/$defs/Target" + }, + { + "type": "null" + } + ] + }, + "targets": { + "description": "We use an [`OrderMap`] to preserve the order in which the items where\ndefined in the manifest.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/Target" + } + } + } + }, + "Target": { + "type": "object", + "properties": { + "hostDependencies": { + "description": "Host dependencies of the project", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/PackageSpec" + } + }, + "buildDependencies": { + "description": "Build dependencies of the project", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/PackageSpec" + } + }, + "runDependencies": { + "description": "Run dependencies of the project", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/$defs/PackageSpec" + } + } + } + }, + "PackageSpec": { + "oneOf": [ + { + "description": "This is a binary dependency", + "type": "object", + "properties": { + "binary": { + "$ref": "#/$defs/BinaryPackageSpec" + } + }, + "required": [ + "binary" + ], + "additionalProperties": false + }, + { + "description": "This is a dependency on a source package", + "type": "object", + "properties": { + "source": { + "$ref": "#/$defs/SourcePackageSpec" + } + }, + "required": [ + "source" + ], + "additionalProperties": false + }, + { + "description": "Pin to a version that is compatible with a version from the \"previous\" environment", + "type": "object", + "properties": { + "pinCompatible": { + "$ref": "#/$defs/PinCompatibleSpec" + } + }, + "required": [ + "pinCompatible" + ], + "additionalProperties": false + } + ] + }, "BinaryPackageSpec": { "description": "Similar to a [`rattler_conda_types::NamelessMatchSpec`]", "type": "object", "properties": { + "version": { + "description": "The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`)", + "type": [ + "string", + "null" + ], + "default": null + }, "build": { "description": "The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`)", "type": [ @@ -127,23 +239,23 @@ "null" ] }, - "channel": { - "description": "The channel of the package", + "fileName": { + "description": "Match the specific filename of the package", "type": [ "string", "null" - ], - "format": "uri" + ] }, - "fileName": { - "description": "Match the specific filename of the package", + "channel": { + "description": "The channel of the package", "type": [ "string", "null" - ] + ], + "format": "uri" }, - "license": { - "description": "The license of the package", + "subdir": { + "description": "The subdir of the channel", "type": [ "string", "null" @@ -165,13 +277,6 @@ ], "default": null }, - "subdir": { - "description": "The subdir of the channel", - "type": [ - "string", - "null" - ] - }, "url": { "description": "The URL of the package, if it is available", "type": [ @@ -180,6 +285,18 @@ ], "format": "uri" }, + "license": { + "description": "The license of the package", + "type": [ + "string", + "null" + ] + } + } + }, + "SourcePackageSpec": { + "type": "object", + "properties": { "version": { "description": "The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`)", "type": [ @@ -187,56 +304,110 @@ "null" ], "default": null + }, + "build": { + "description": "The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`)", + "type": [ + "string", + "null" + ], + "default": null + }, + "buildNumber": { + "description": "The build number of the package", + "type": [ + "string", + "null" + ] + }, + "subdir": { + "description": "The subdir of the channel", + "type": [ + "string", + "null" + ] + }, + "license": { + "description": "The md5 hash of the package\nThe license of the package", + "type": [ + "string", + "null" + ] } - } - }, - "GitReference": { - "description": "A reference to a specific commit in a git repository.", + }, "oneOf": [ { - "description": "The HEAD commit of a branch.", + "description": "The spec is represented as an archive that can be downloaded from the\nspecified URL. The package should be retrieved from the URL and can\neither represent a source or binary package depending on the archive\ntype.", "type": "object", "properties": { - "branch": { - "type": "string" + "url": { + "$ref": "#/$defs/UrlSpec" } }, - "additionalProperties": false, "required": [ - "branch" + "url" ] }, { - "description": "A specific tag.", + "description": "The spec is represented as a git repository. The package represents a\nsource distribution of some kind.", "type": "object", "properties": { - "tag": { - "type": "string" + "git": { + "$ref": "#/$defs/GitSpec" } }, - "additionalProperties": false, "required": [ - "tag" + "git" ] }, { - "description": "A specific commit.", + "description": "The spec is represented as a local path. The package should be retrieved\nfrom the local filesystem. The package can be either a source or binary\npackage.", "type": "object", "properties": { - "rev": { - "type": "string" + "path": { + "$ref": "#/$defs/PathSpec" } }, - "additionalProperties": false, "required": [ - "rev" + "path" ] - }, - { - "description": "A default branch.", + } + ] + }, + "UrlSpec": { + "type": "object", + "properties": { + "url": { + "description": "The URL of the package", "type": "string", - "const": "defaultBranch" + "format": "uri" + }, + "md5": { + "description": "The md5 hash of the package", + "type": [ + "string", + "null" + ], + "default": null + }, + "sha256": { + "description": "The sha256 hash of the package", + "type": [ + "string", + "null" + ], + "default": null + }, + "subdirectory": { + "description": "The subdirectory of the package in the archive", + "type": [ + "string", + "null" + ] } + }, + "required": [ + "url" ] }, "GitSpec": { @@ -271,46 +442,52 @@ "git" ] }, - "PackageSpec": { + "GitReference": { + "description": "A reference to a specific commit in a git repository.", "oneOf": [ { - "description": "This is a binary dependency", + "description": "The HEAD commit of a branch.", "type": "object", "properties": { - "binary": { - "$ref": "#/$defs/BinaryPackageSpec" + "branch": { + "type": "string" } }, - "additionalProperties": false, "required": [ - "binary" - ] + "branch" + ], + "additionalProperties": false }, { - "description": "This is a dependency on a source package", + "description": "A specific tag.", "type": "object", "properties": { - "source": { - "$ref": "#/$defs/SourcePackageSpec" + "tag": { + "type": "string" } }, - "additionalProperties": false, "required": [ - "source" - ] + "tag" + ], + "additionalProperties": false }, { - "description": "Pin to a version that is compatible with a version from the \"previous\" environment", + "description": "A specific commit.", "type": "object", "properties": { - "pinCompatible": { - "$ref": "#/$defs/PinCompatibleSpec" + "rev": { + "type": "string" } }, - "additionalProperties": false, "required": [ - "pinCompatible" - ] + "rev" + ], + "additionalProperties": false + }, + { + "description": "A default branch.", + "type": "string", + "const": "defaultBranch" } ] }, @@ -327,47 +504,9 @@ "path" ] }, - "PinBound": { - "oneOf": [ - { - "type": "object", - "properties": { - "expression": { - "$ref": "#/$defs/PinExpression" - } - }, - "additionalProperties": false, - "required": [ - "expression" - ] - }, - { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "version" - ] - } - ] - }, "PinCompatibleSpec": { "type": "object", "properties": { - "build": { - "type": [ - "string", - "null" - ] - }, - "exact": { - "description": "If an exact pin is given, we pin the exact version & hash", - "type": "boolean" - }, "lowerBound": { "description": "A minimum pin to a version, using `x.x.x...` as syntax", "anyOf": [ @@ -389,190 +528,51 @@ "type": "null" } ] - } - } - }, - "PinExpression": { - "description": "A pin expression string like \"x\", \"x.x\", \"x.x.x\", etc.\n\nThis represents the number of version segments to pin.\nFor example, \"x.x\" means pin the major and minor version.", - "type": "string", - "pattern": "^x(\\.x)*$" - }, - "SourcePackageSpec": { - "type": "object", - "properties": { - "build": { - "description": "The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`)", - "type": [ - "string", - "null" - ], - "default": null - }, - "buildNumber": { - "description": "The build number of the package", - "type": [ - "string", - "null" - ] }, - "license": { - "description": "The md5 hash of the package\nThe license of the package", - "type": [ - "string", - "null" - ] + "exact": { + "description": "If an exact pin is given, we pin the exact version & hash", + "type": "boolean" }, - "subdir": { - "description": "The subdir of the channel", + "build": { "type": [ "string", "null" ] - }, - "version": { - "description": "The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`)", - "type": [ - "string", - "null" - ], - "default": null } - }, + } + }, + "PinBound": { "oneOf": [ { - "description": "The spec is represented as an archive that can be downloaded from the\nspecified URL. The package should be retrieved from the URL and can\neither represent a source or binary package depending on the archive\ntype.", "type": "object", "properties": { - "url": { - "$ref": "#/$defs/UrlSpec" - } - }, - "required": [ - "url" - ] - }, - { - "description": "The spec is represented as a git repository. The package represents a\nsource distribution of some kind.", - "type": "object", - "properties": { - "git": { - "$ref": "#/$defs/GitSpec" + "expression": { + "$ref": "#/$defs/PinExpression" } }, "required": [ - "git" - ] + "expression" + ], + "additionalProperties": false }, { - "description": "The spec is represented as a local path. The package should be retrieved\nfrom the local filesystem. The package can be either a source or binary\npackage.", "type": "object", "properties": { - "path": { - "$ref": "#/$defs/PathSpec" + "version": { + "type": "string" } }, "required": [ - "path" - ] - } - ] - }, - "Target": { - "type": "object", - "properties": { - "buildDependencies": { - "description": "Build dependencies of the project", - "type": [ - "object", - "null" - ], - "additionalProperties": { - "$ref": "#/$defs/PackageSpec" - } - }, - "hostDependencies": { - "description": "Host dependencies of the project", - "type": [ - "object", - "null" - ], - "additionalProperties": { - "$ref": "#/$defs/PackageSpec" - } - }, - "runDependencies": { - "description": "Run dependencies of the project", - "type": [ - "object", - "null" - ], - "additionalProperties": { - "$ref": "#/$defs/PackageSpec" - } - } - } - }, - "Targets": { - "description": "A collect of targets including a default target.", - "type": "object", - "properties": { - "defaultTarget": { - "anyOf": [ - { - "$ref": "#/$defs/Target" - }, - { - "type": "null" - } - ] - }, - "targets": { - "description": "We use an [`OrderMap`] to preserve the order in which the items where\ndefined in the manifest.", - "type": [ - "object", - "null" - ], - "additionalProperties": { - "$ref": "#/$defs/Target" - } - } - } - }, - "UrlSpec": { - "type": "object", - "properties": { - "md5": { - "description": "The md5 hash of the package", - "type": [ - "string", - "null" - ], - "default": null - }, - "sha256": { - "description": "The sha256 hash of the package", - "type": [ - "string", - "null" + "version" ], - "default": null - }, - "subdirectory": { - "description": "The subdirectory of the package in the archive", - "type": [ - "string", - "null" - ] - }, - "url": { - "description": "The URL of the package", - "type": "string", - "format": "uri" + "additionalProperties": false } - }, - "required": [ - "url" ] + }, + "PinExpression": { + "description": "A pin expression string like \"x\", \"x.x\", \"x.x.x\", etc.\n\nThis represents the number of version segments to pin.\nFor example, \"x.x\" means pin the major and minor version.", + "type": "string", + "pattern": "^x(\\.x)*$" } } } From 84c71408fdfe62c1601e0a0c65a73947268a4323 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:23:52 +0100 Subject: [PATCH 13/13] fix: more tests --- .../tests/snapshots/discovery__direct_package_xml.snap | 2 ++ .../tests/snapshots/discovery__discovery@inherit__nested.snap | 2 ++ .../tests/snapshots/discovery__discovery@nested__nested.snap | 2 ++ .../tests/snapshots/discovery__discovery@simple.snap | 2 ++ ..._project_model__tests__conversions_v1_docs@advanced_cpp.snap | 2 ++ ...versions__project_model__tests__conversions_v1_docs@cpp.snap | 2 ++ ...versions__project_model__tests__conversions_v1_docs@dev.snap | 2 ++ ...oject_model__tests__conversions_v1_docs@getting_started.snap | 2 ++ ...sions__project_model__tests__conversions_v1_docs@python.snap | 2 ++ ...ns__project_model__tests__conversions_v1_docs@workspace.snap | 2 ++ ...ct_model__tests__conversions_v1_docs@workspace_variants.snap | 2 ++ ...t_model__tests__conversions_v1_examples@array-api-extra.snap | 2 ++ ...ct_model__tests__conversions_v1_examples@cpp-git-source.snap | 2 ++ ...__project_model__tests__conversions_v1_examples@cpp-sdl.snap | 2 ++ ...ions__project_model__tests__conversions_v1_examples@dev.snap | 2 ++ 15 files changed, 30 insertions(+) diff --git a/crates/pixi_build_discovery/tests/snapshots/discovery__direct_package_xml.snap b/crates/pixi_build_discovery/tests/snapshots/discovery__direct_package_xml.snap index 57070a6923..6d52945a46 100644 --- a/crates/pixi_build_discovery/tests/snapshots/discovery__direct_package_xml.snap +++ b/crates/pixi_build_discovery/tests/snapshots/discovery__direct_package_xml.snap @@ -19,6 +19,8 @@ init-params: manifest-path: "file:///ros-package" project-model: name: ~ + buildString: ~ + buildNumber: ~ version: ~ description: ~ authors: ~ diff --git a/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@inherit__nested.snap b/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@inherit__nested.snap index a25bdee7fb..fbcb3c9450 100644 --- a/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@inherit__nested.snap +++ b/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@inherit__nested.snap @@ -20,6 +20,8 @@ init-params: manifest-path: "file:///inherit/nested/pixi.toml" project-model: name: simple + buildString: ~ + buildNumber: ~ version: 0.1.0 description: ~ authors: ~ diff --git a/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@nested__nested.snap b/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@nested__nested.snap index 9db3f8a517..93c3c23e1f 100644 --- a/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@nested__nested.snap +++ b/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@nested__nested.snap @@ -20,6 +20,8 @@ init-params: manifest-path: "file:///nested/nested/pixi.toml" project-model: name: simple + buildString: ~ + buildNumber: ~ version: 0.1.0 description: ~ authors: ~ diff --git a/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@simple.snap b/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@simple.snap index a8dd4a1798..cf592070a6 100644 --- a/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@simple.snap +++ b/crates/pixi_build_discovery/tests/snapshots/discovery__discovery@simple.snap @@ -20,6 +20,8 @@ init-params: manifest-path: "file:///simple/pixi.toml" project-model: name: simple + buildString: ~ + buildNumber: ~ version: 0.1.0 description: ~ authors: ~ diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_cpp.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_cpp.snap index 9b49aff6c8..c48a8e1a3a 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_cpp.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@advanced_cpp.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "cpp_math", + "buildString": null, + "buildNumber": null, "version": "0.1.0", "description": null, "authors": null, diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@cpp.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@cpp.snap index 638ef28236..d338169040 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@cpp.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@cpp.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "cpp_math", + "buildString": null, + "buildNumber": null, "version": "0.1.0", "description": null, "authors": null, diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@dev.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@dev.snap index 862610bf50..306a2eacdb 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@dev.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@dev.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": null, + "buildString": null, + "buildNumber": null, "version": null, "description": null, "authors": null, diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@getting_started.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@getting_started.snap index b0c3bd58f5..23100a49c5 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@getting_started.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@getting_started.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "python_rich", + "buildString": null, + "buildNumber": null, "version": "0.1.0", "description": null, "authors": null, diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@python.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@python.snap index 7b21750e19..41477db887 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@python.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@python.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "python_rich", + "buildString": null, + "buildNumber": null, "version": "0.1.0", "description": null, "authors": null, diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap index 12f1c21fd5..8d3b187208 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "python_rich", + "buildString": null, + "buildNumber": null, "version": "0.1.0", "description": null, "authors": null, diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap index 51bade3171..26e5b4ee92 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_docs@workspace_variants.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "python_rich", + "buildString": null, + "buildNumber": null, "version": "0.1.0", "description": null, "authors": null, diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@array-api-extra.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@array-api-extra.snap index 425530e53e..31045fb7c6 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@array-api-extra.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@array-api-extra.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "array-api-extra", + "buildString": null, + "buildNumber": null, "version": "0.8.0", "description": null, "authors": null, diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-git-source.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-git-source.snap index c34da6f24a..e483ac4da0 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-git-source.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-git-source.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "sdl_example", + "buildString": null, + "buildNumber": null, "version": "0.1.0", "description": null, "authors": null, diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-sdl.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-sdl.snap index 2ea5b90d5b..33797266ed 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-sdl.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@cpp-sdl.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "sdl_example", + "buildString": null, + "buildNumber": null, "version": "0.1.0", "description": "Showcases how to create a simple C++ executable with Pixi", "authors": [ diff --git a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@dev.snap b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@dev.snap index 80965a8a8f..7730cf1c59 100644 --- a/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@dev.snap +++ b/crates/pixi_build_type_conversions/src/snapshots/pixi_build_type_conversions__project_model__tests__conversions_v1_examples@dev.snap @@ -4,6 +4,8 @@ expression: project_model --- { "name": "minimal-example", + "buildString": null, + "buildNumber": null, "version": "0.1.0", "description": null, "authors": null,