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 34191580f8..727e3e3801 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5178,6 +5178,7 @@ dependencies = [ "pixi_build_discovery", "pixi_build_types", "rattler_conda_types", + "schemars 1.1.0", "serde", "serde_json", "thiserror 2.0.17", @@ -5208,6 +5209,7 @@ dependencies = [ "pixi_stable_hash", "rattler_conda_types", "rattler_digest", + "schemars 1.1.0", "serde", "serde_json", "serde_with", @@ -8412,6 +8414,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" @@ -11545,6 +11561,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_api/src/workspace/add/mod.rs b/crates/pixi_api/src/workspace/add/mod.rs index 22b46a0b2d..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; @@ -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_backend_passthrough/src/lib.rs b/crates/pixi_build_backend_passthrough/src/lib.rs index 17f2205357..c02d7294ae 100644 --- a/crates/pixi_build_backend_passthrough/src/lib.rs +++ b/crates/pixi_build_backend_passthrough/src/lib.rs @@ -19,8 +19,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::{ @@ -52,7 +52,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: IndexJson, @@ -74,7 +74,6 @@ impl InMemoryBackend for PassthroughBackend { BackendCapabilities { provides_conda_outputs: Some(true), provides_conda_build_v1: Some(true), - ..BackendCapabilities::default() } } @@ -250,12 +249,14 @@ fn create_conda_package_on_the_fly( Ok(()) } -/// Generates all variant outputs for a package based on the variant configuration. +/// Generates all variant outputs for a package based on the variant +/// configuration. /// -/// If any dependency has a "*" version requirement and there's a variant configuration -/// for that package, multiple outputs will be generated - one for each variant combination. +/// If any dependency has a "*" version requirement and there's a variant +/// 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: &IndexJson, params: &CondaOutputsParams, run_exports: &BTreeMap, @@ -307,8 +308,9 @@ fn generate_variant_outputs( .collect() } -/// Finds all dependency names that have "*" requirements and have variant configurations. -fn find_variant_keys(project_model: &ProjectModelV1, params: &CondaOutputsParams) -> Vec { +/// Finds all dependency names that have "*" requirements and have variant +/// configurations. +fn find_variant_keys(project_model: &ProjectModel, params: &CondaOutputsParams) -> Vec { let Some(targets) = &project_model.targets else { return Vec::new(); }; @@ -320,7 +322,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 @@ -357,13 +359,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.as_ref() { - BinaryPackageSpecV1 { + match boxed { + BinaryPackageSpec { version, build: None, build_number: None, @@ -462,7 +464,7 @@ fn compute_variant_hash(variant: &BTreeMap) -> String { /// Creates a single output with the given variant configuration. fn create_output( - project_model: &ProjectModelV1, + project_model: &ProjectModel, index_json: &IndexJson, params: &CondaOutputsParams, mut variant: BTreeMap, @@ -555,8 +557,8 @@ fn create_output( } } -fn extract_dependencies Option<&OrderMap>>( - targets: &Option, +fn extract_dependencies Option<&OrderMap>>( + targets: &Option, extract: F, platform: Platform, variant: &BTreeMap, @@ -582,7 +584,7 @@ fn extract_dependencies Option<&OrderMap Option<&OrderMap Option<&OrderMap Option<&OrderMap, -) -> Option> { +) -> Option> { // Parse the run_export string as a MatchSpec let match_spec = rattler_conda_types::MatchSpec::from_str( run_export_str, @@ -657,24 +659,24 @@ 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() - })), + }), }) } -/// Returns true if the given [`TargetSelectorV1`] matches the specified +/// Returns true if the given [`TargetSelector`] matches the specified /// `platform`. -fn matches_target_selector(selector: &TargetSelectorV1, platform: Platform) -> 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(), } } @@ -707,10 +709,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"), ))); } }; @@ -721,7 +723,7 @@ impl InMemoryBackendInstantiator for PassthroughBackendInstantiator { }; // Read the package file if it is specified, or create IndexJson for on_the_fly mode - 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); @@ -870,8 +872,9 @@ pub struct ObservableBackend { } impl ObservableBackend { - /// Creates a new instantiator for an ObservableBackend wrapping the given backend. - /// Returns both the instantiator and a BackendObserver for collecting events. + /// Creates a new instantiator for an ObservableBackend wrapping the given + /// backend. Returns both the instantiator and a BackendObserver for + /// collecting events. pub fn instantiator( inner_instantiator: I, ) -> ( @@ -961,33 +964,34 @@ where #[cfg(test)] mod tests { - use super::*; - 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(Box::new(BinaryPackageSpecV1 { + let spec = PackageSpec::Binary(BinaryPackageSpec { version: Some(VersionSpec::from_str("*", ParseStrictness::Lenient).unwrap()), ..Default::default() - })); + }); assert!(is_star_requirement(&spec)); } #[test] fn test_is_star_requirement_with_version() { - let spec = PackageSpecV1::Binary(Box::new(BinaryPackageSpecV1 { + let spec = PackageSpec::Binary(BinaryPackageSpec { version: Some(VersionSpec::from_str(">=1.0", ParseStrictness::Lenient).unwrap()), ..Default::default() - })); + }); assert!(!is_star_requirement(&spec)); } #[test] fn test_is_star_requirement_with_no_version() { - let spec = PackageSpecV1::Binary(Box::default()); + let spec = PackageSpec::Binary(BinaryPackageSpec::default()); assert!(is_star_requirement(&spec)); } diff --git a/crates/pixi_build_discovery/src/backend_spec.rs b/crates/pixi_build_discovery/src/backend_spec.rs index 6bcffd4969..f67b02840f 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_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_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_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 5979d1e015..c8d5ad2eaa 100644 --- a/crates/pixi_build_type_conversions/src/project_model.rs +++ b/crates/pixi_build_type_conversions/src/project_model.rs @@ -12,23 +12,49 @@ 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`. 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 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 }) + 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 { @@ -36,24 +62,31 @@ fn to_pixi_spec_v1( rev, subdirectory, } = git_spec; - pbt::SourcePackageSpecV1::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::SourcePackageSpecV1::Path(pbt::PathSpecV1 { + pbt::SourcePackageLocationSpec::Path(pbt::PathSpec { path: path_source_spec.path.to_string(), }) } }; - pbt::PackageSpecV1::Source(source) + pbt::PackageSpec::Source(pbt::SourcePackageSpec { + location, + version, + build, + build_number, + subdir, + license, + }) } itertools::Either::Right(binary) => { let NamelessMatchSpec { @@ -72,7 +105,7 @@ fn to_pixi_spec_v1( extras: _, condition: _, } = binary.try_into_nameless_match_spec(channel_config)?; - pbt::PackageSpecV1::Binary(Box::new(pbt::BinaryPackageSpecV1 { + pbt::PackageSpec::Binary(pbt::BinaryPackageSpec { version, build, build_number, @@ -83,7 +116,7 @@ fn to_pixi_spec_v1( sha256, url, license, - })) + }) } }; Ok(pbt_spec) @@ -94,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)) @@ -102,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() @@ -134,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)| { @@ -156,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(), @@ -189,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; @@ -222,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..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 @@ -3,25 +3,24 @@ 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", + "buildString": null, + "buildNumber": null, + "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..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 @@ -3,68 +3,67 @@ 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", + "buildString": null, + "buildNumber": null, + "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..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 @@ -3,70 +3,69 @@ 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, + "buildString": null, + "buildNumber": 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..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 @@ -1,58 +1,56 @@ --- 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", + "buildString": null, + "buildNumber": null, + "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..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 @@ -3,55 +3,54 @@ 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", + "buildString": null, + "buildNumber": null, + "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 5139394a89..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 @@ -3,62 +3,66 @@ 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", + "buildString": null, + "buildNumber": null, + "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" - } - } - }, - "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 b37526617c..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 @@ -3,76 +3,80 @@ 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", + "buildString": null, + "buildNumber": null, + "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" - } - } - }, - "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..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 @@ -3,25 +3,24 @@ 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", + "buildString": null, + "buildNumber": null, + "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..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 @@ -3,40 +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": 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", + "buildString": null, + "buildNumber": null, + "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..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 @@ -3,42 +3,41 @@ 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", + "buildString": null, + "buildNumber": null, + "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..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 @@ -3,84 +3,83 @@ 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", + "buildString": null, + "buildNumber": null, + "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/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..1428890435 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, SourcePackageName, SourcePackageSpecV1, TargetSelectorV1, TargetV1, TargetsV1, - UrlSpecV1, VersionedProjectModel, + 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, @@ -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() @@ -90,6 +90,9 @@ impl PixiBuildApiVersion { 3 => BackendCapabilities { ..Self(2).expected_backend_capabilities() }, + 4 => BackendCapabilities { + ..Self(3).expected_backend_capabilities() + }, _ => BackendCapabilities::default(), } } diff --git a/crates/pixi_build_types/src/procedures/conda_outputs.rs b/crates/pixi_build_types/src/procedures/conda_outputs.rs index 2ce1dcd71b..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::{BinaryPackageSpecV1, PackageSpecV1, VariantValue, project_model::NamedSpecV1}; +use crate::{ConstraintSpec, 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 5a9c1d19c1..246c097430 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. //! @@ -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 std::hash::Hasher; +use std::{convert::Infallible, fmt::Display, hash::Hash, path::PathBuf, str::FromStr}; 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,60 +201,185 @@ 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(Box), + Binary(BinaryPackageSpec), /// This is a dependency on a source package - Source(SourcePackageSpecV1), + Source(SourcePackageSpec), + /// Pin to a version that is compatible with a version from the "previous" environment + 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")] +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, +} + +/// 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(PinExpression), + Version(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Version), } #[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)] pub spec: T, } +#[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, + /// The md5 hash of the package + /// The license of the package + pub license: Option, +} + +impl From for SourcePackageSpec { + fn from(value: PathSpec) -> Self { + Self { + location: SourcePackageLocationSpec::Path(value), + version: None, + build: None, + build_number: None, + subdir: None, + license: None, + } + } +} + +impl From for SourcePackageSpec { + fn from(value: UrlSpec) -> Self { + Self { + location: SourcePackageLocationSpec::Url(value), + version: None, + build: None, + build_number: None, + subdir: None, + license: None, + } + } +} + +impl From for SourcePackageSpec { + fn from(value: GitSpec) -> 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 SourcePackageSpecV1 { +#[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 { @@ -295,13 +394,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, @@ -309,16 +409,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), @@ -335,15 +437,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, @@ -353,9 +459,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, @@ -363,7 +471,7 @@ pub struct BinaryPackageSpecV1 { pub license: Option, } -impl From for BinaryPackageSpecV1 { +impl From for BinaryPackageSpec { fn from(value: VersionSpec) -> Self { Self { version: Some(value), @@ -372,7 +480,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()), @@ -381,7 +489,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"); @@ -415,13 +523,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, @@ -436,6 +546,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) @@ -450,16 +562,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); } @@ -467,12 +579,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; @@ -484,12 +596,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, @@ -503,37 +615,121 @@ 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); } + PackageSpec::PinCompatible(spec) => { + 2u8.hash(state); + spec.hash(state); + } } } } -impl Hash for SourcePackageSpecV1 { +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 { - SourcePackageSpecV1::Url(spec) => { + Self::Binary(spec) => { 0u8.hash(state); spec.hash(state); } - SourcePackageSpecV1::Git(spec) => { + } + } +} + +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 { + 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 { + Self::Url(spec) => { + 0u8.hash(state); spec.hash(state); } - SourcePackageSpecV1::Path(spec) => { + Self::Git(spec) => { + 1u8.hash(state); + spec.hash(state); + } + Self::Path(spec) => { 2u8.hash(state); spec.hash(state); } @@ -541,22 +737,28 @@ impl Hash for SourcePackageSpecV1 { } } -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. @@ -569,40 +771,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> { @@ -610,7 +812,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. @@ -645,8 +847,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, @@ -661,22 +865,22 @@ 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(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 { + // 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()), run_dependencies: Some(OrderMap::new()), }; - project_model.targets = Some(TargetsV1 { + project_model.targets = Some(Targets { default_target: Some(empty_target), targets: Some(OrderMap::new()), }); @@ -701,8 +905,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, @@ -723,14 +929,17 @@ 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(), + 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()), }); @@ -750,11 +959,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, @@ -775,7 +984,7 @@ mod tests { ); // Add a meaningful value - let spec3 = BinaryPackageSpecV1 { + let spec3 = BinaryPackageSpec { file_name: Some("test.tar.bz2".to_string()), ..Default::default() }; @@ -790,8 +999,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 = PackageSpec::Binary(BinaryPackageSpec::default()); + let source_spec = PackageSpec::Source(SourcePackageSpec::from(PathSpec { path: "test".to_string(), })); @@ -805,7 +1014,7 @@ mod tests { ); // Same variant with same content should have same hash - let binary_spec2 = PackageSpecV1::Binary(Box::default()); + let binary_spec2 = PackageSpec::Binary(BinaryPackageSpec::default()); let hash3 = calculate_hash(&binary_spec2); assert_eq!( @@ -814,26 +1023,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(Box::default()), + PackageSpec::Binary(BinaryPackageSpec::default()), )])), build_dependencies: Some(OrderMap::from([( "build_dep1".to_string(), - PackageSpecV1::Binary(Box::default()), + PackageSpec::Binary(BinaryPackageSpec::default()), )])), run_dependencies: Some(OrderMap::from([( "run_dep1".to_string(), - PackageSpecV1::Binary(Box::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, }; @@ -858,17 +1067,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()) }) @@ -890,7 +1099,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()); } @@ -916,14 +1125,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) ); } @@ -933,24 +1142,27 @@ mod tests { // different hashes let mut deps = OrderMap::new(); - deps.insert("python".to_string(), PackageSpecV1::Binary(Box::default())); + deps.insert( + "python".to_string(), + 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, @@ -974,12 +1186,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, }; @@ -996,14 +1208,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 8ac17f7346..f0113cbb0c 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, VariantValue}; use pixi_stable_hash::{StableHashBuilder, json::StableJson, map::StableMap}; @@ -344,13 +344,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 b85d86e096..e3fbf6cd25 100644 --- a/crates/pixi_command_dispatcher/src/build/conversion.rs +++ b/crates/pixi_command_dispatcher/src/build/conversion.rs @@ -1,55 +1,83 @@ -use pixi_build_types::{BinaryPackageSpecV1, PackageSpecV1, SourcePackageSpecV1}; -use pixi_spec::{BinarySpec, DetailedSpec, SourceLocationSpec, UrlBinarySpec}; +use pixi_build_types::{BinaryPackageSpec, SourcePackageLocationSpec, SourcePackageSpec}; +use pixi_spec::{BinarySpec, DetailedSpec, UrlBinarySpec}; use rattler_conda_types::NamedChannelOrUrl; -/// 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 { + subdirectory: url.subdirectory, + }) + } + + 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) => { + 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 } }), 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(), - }), - }, + }) + } } } -/// 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, @@ -61,7 +89,7 @@ pub fn from_binary_spec_v1(spec: BinaryPackageSpecV1) -> pixi_spec::BinarySpec { license: None, url: _, } => BinarySpec::Version(version), - BinaryPackageSpecV1 { + BinaryPackageSpec { version, build, build_number, @@ -85,11 +113,3 @@ 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 { - match source { - PackageSpecV1::Source(source) => from_source_spec_v1(source).into(), - PackageSpecV1::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 458689b712..6197e22ff0 100644 --- a/crates/pixi_command_dispatcher/src/build/dependencies.rs +++ b/crates/pixi_command_dispatcher/src/build/dependencies.rs @@ -1,14 +1,18 @@ 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::{ - BinaryPackageSpecV1, NamedSpecV1, PackageSpecV1, + NamedSpec, PackageSpec, procedures::conda_outputs::{ CondaOutputDependencies, CondaOutputIgnoreRunExports, CondaOutputRunExports, }, }; 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, @@ -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,41 +88,50 @@ 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 location = if let Some(anchor) = &source_anchor { - anchor.resolve(source.location) + 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.location + spec }; - dependencies.insert(name, PixiSpec::from(SourceSpec { location }).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) - })?; - constraints.insert( - name, - conversion::from_binary_spec_v1(constraint.spec.clone()).into(), - ); + let name = rattler_conda_types::PackageName::from_str(&constraint.name)?; + + // Match on ConstraintSpec enum + match &constraint.spec { + pbt::ConstraintSpec::Binary(binary) => { + constraints + .insert(name, conversion::from_binary_spec_v1(binary.clone()).into()); + } + } } Ok(Self { @@ -370,45 +385,64 @@ pub struct PixiRunExports { impl PixiRunExports { /// Converts a [`CondaOutputRunExports`] to a [`PixiRunExports`]. - pub fn try_from_protocol(output: &CondaOutputRunExports) -> Result { - fn convert_package_spec( - specs: &[NamedSpecV1], + 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() } - fn convert_binary_spec( - specs: &[NamedSpecV1], + 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).map_err(|err| { - DependenciesError::InvalidPackageName(named_spec.name.to_owned(), err) - })?; + 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() } Ok(PixiRunExports { - weak: convert_package_spec(&output.weak)?, - strong: convert_package_spec(&output.strong)?, - noarch: convert_package_spec(&output.noarch)?, - weak_constrains: convert_binary_spec(&output.weak_constrains)?, - strong_constrains: convert_binary_spec(&output.strong_constrains)?, + 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_constraint_spec(&output.weak_constrains)?, + strong_constrains: convert_constraint_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..a26fdf1331 --- /dev/null +++ b/crates/pixi_command_dispatcher/src/build/pin_compatible.rs @@ -0,0 +1,757 @@ +//! 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(pin_expr) => { + // Parse and validate the expression string + let expr = PinExpression::from_str(&pin_expr.0)?; + + 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(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, + }; + + 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(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, + }; + + 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(pixi_build_types::PinExpression( + "x".to_string(), + ))), + upper_bound: Some(PinBound::Expression(pixi_build_types::PinExpression( + "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(pixi_build_types::PinExpression( + "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(pixi_build_types::PinExpression( + "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(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()), + }; + + 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(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, + }; + + 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(pixi_build_types::PinExpression( + "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(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 + }; + + 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/build_backend_metadata/mod.rs b/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs index 2362dad9e6..569010a27d 100644 --- a/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs +++ b/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs @@ -6,7 +6,7 @@ use pixi_build_frontend::Backend; use pixi_build_types::procedures::conda_outputs::CondaOutputsParams; use pixi_glob::GlobSet; use pixi_record::{PinnedSourceSpec, VariantValue}; -use pixi_spec::{SourceAnchor, SourceSpec}; +use pixi_spec::{SourceAnchor, SourceLocationSpec}; use rattler_conda_types::{ChannelConfig, ChannelUrl}; use std::time::SystemTime; use std::{ @@ -56,9 +56,9 @@ pub struct BuildBackendMetadataSpec { /// The optional pinned location of the source code. If not provided, 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, @@ -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/command_dispatcher/instantiate_backend.rs b/crates/pixi_command_dispatcher/src/command_dispatcher/instantiate_backend.rs index 4c2d18f387..d3a2b9138d 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/dev_source_metadata/mod.rs b/crates/pixi_command_dispatcher/src/dev_source_metadata/mod.rs index 8424220dcd..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,13 +2,15 @@ use std::fmt::Display; use itertools::Itertools; use miette::Diagnostic; +use pixi_build_types::{ConstraintSpec, PackageSpec}; use pixi_record::{DevSourceRecord, PinnedSourceSpec}; -use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor, SourceSpec}; +use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor, SourceLocationSpec}; use pixi_spec_containers::DependencyMap; use rattler_conda_types::PackageName; use thiserror::Error; use tracing::instrument; +use crate::build::conversion; use crate::{ BuildBackendMetadataError, BuildBackendMetadataSpec, CommandDispatcher, CommandDispatcherError, CommandDispatcherErrorResultExt, @@ -145,7 +147,7 @@ impl DevSourceMetadataSpec { .map_err_with(DevSourceMetadataError::BuildBackendMetadata)?; // Create a SourceAnchor for resolving relative paths in dependencies - let source_anchor = SourceAnchor::from(pixi_spec::SourceSpec::from( + let source_anchor = SourceAnchor::from(SourceLocationSpec::from( build_backend_metadata.source.manifest_source().clone(), )); @@ -210,15 +212,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(SourceSpec { - location: source_anchor.resolve(source_spec.location), - }), - itertools::Either::Right(binary_spec) => PixiSpec::from(binary_spec), + // 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; + } }; dependencies.insert(name, resolved_spec); } @@ -226,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/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/src/solve_pixi/source_metadata_collector.rs b/crates/pixi_command_dispatcher/src/solve_pixi/source_metadata_collector.rs index 3a881ce71e..279c96b20b 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::{ @@ -103,7 +105,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(), @@ -123,27 +125,23 @@ impl SourceMetadataCollector { // Process transitive dependencies for record in &source_metadata.cached_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 de5f368454..160bceb13f 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, }; @@ -11,7 +11,7 @@ use pixi_build_frontend::Backend; use pixi_build_types::procedures::conda_outputs::CondaOutputsParams; use pixi_path::AbsPath; 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, @@ -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, @@ -274,7 +275,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) @@ -290,7 +291,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 @@ -532,7 +533,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(); @@ -608,10 +609,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)? @@ -639,11 +641,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)? @@ -735,8 +743,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( @@ -746,9 +760,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); @@ -964,8 +979,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)] @@ -996,8 +1014,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 6415d945df..9ba2e739c1 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::{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, @@ -215,14 +215,15 @@ 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 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,29 +315,32 @@ 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, - 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(), @@ -340,7 +357,7 @@ impl SourceMetadataSpec { }; let pixi_specs_to_match_spec = |specs: DependencyMap, - sources: &mut HashMap| + sources: &mut HashMap| -> Result< Vec, CommandDispatcherError, @@ -520,7 +537,7 @@ impl SourceMetadataSpec { struct PackageRecordDependencies { pub depends: Vec, pub constrains: Vec, - pub sources: HashMap, + pub sources: HashMap, } impl PackageRecordDependencies { @@ -544,14 +561,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) { @@ -594,15 +609,18 @@ 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() )] DuplicateSourceDependency { package: PackageName, - source1: Box, - source2: Box, + source1: Box, + source2: Box, }, #[error("the dependencies of some packages in the environment form a cycle")] @@ -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) } } } diff --git a/crates/pixi_command_dispatcher/tests/integration/main.rs b/crates/pixi_command_dispatcher/tests/integration/main.rs index 87d6ef7582..f23b56689f 100644 --- a/crates/pixi_command_dispatcher/tests/integration/main.rs +++ b/crates/pixi_command_dispatcher/tests/integration/main.rs @@ -1150,6 +1150,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 @@ -1183,11 +1184,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!( @@ -1220,6 +1223,7 @@ pub async fn pin_and_checkout_url_validates_cached_results() { url: url.clone(), md5: None, sha256: None, + subdirectory: None, }; dispatcher @@ -1231,6 +1235,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_core/src/lock_file/satisfiability/mod.rs b/crates/pixi_core/src/lock_file/satisfiability/mod.rs index 6dcf2d4da5..b9afeb6774 100644 --- a/crates/pixi_core/src/lock_file/satisfiability/mod.rs +++ b/crates/pixi_core/src/lock_file/satisfiability/mod.rs @@ -815,7 +815,7 @@ pub async fn verify_platform_satisfiability( pub 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>), } @@ -826,7 +826,7 @@ impl Dependency { match self { Dependency::Input(name, _, _) => Some(name.clone()), Dependency::Conda(spec, _) => spec.name.as_ref().and_then(|m| m.as_exact().cloned()), - Dependency::CondaSource(name, _, _, _) => Some(name.clone()), + Dependency::CondaSource(name, _, _) => Some(name.clone()), Dependency::PyPi(_, _) => None, } } @@ -1423,19 +1423,13 @@ async fn resolve_single_dev_dependency( // Process source dependencies for (dev_name, dep) in dev_source.into_specs() { - let anchored_source = SourceAnchor::Workspace.resolve(dep.clone().location); - - let spec = MatchSpec::from_str(dev_name.as_source(), Lenient).map_err(|e| { - PlatformUnsat::FailedToParseMatchSpec(dev_name.as_source().to_string(), e) - })?; + let string = dep.to_string(); + let anchored_source = dep.resolve(&SourceAnchor::Workspace); dependencies.push(Dependency::CondaSource( dev_name.clone(), - spec, - SourceSpec { - location: anchored_source, - }, - Cow::Owned(format!("{} @ {}", dev_name.as_source(), dep)), + anchored_source, + Cow::Owned(format!("{} @ {}", dev_name.as_source(), string)), )); } @@ -1683,7 +1677,6 @@ pub(crate) async fn verify_package_platform_satisfiability( name, source_spec, source, - None, )? } Either::Right(binary_spec) => { @@ -1737,14 +1730,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) => { @@ -1864,6 +1856,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) => ( @@ -1876,13 +1869,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() @@ -1893,18 +1886,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, + )); } } } @@ -2175,7 +2168,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 @@ -2199,14 +2191,13 @@ 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 - && !match_spec.matches(package) - { + let match_spec = source_spec.to_nameless_match_spec(); + if !match_spec.matches(package) { return Err(Box::new(PlatformUnsat::UnsatisfiableMatchSpec( - Box::new(match_spec), + Box::new(MatchSpec::from_nameless(match_spec, Some(name.into()))), source.into_owned(), ))); } diff --git a/crates/pixi_manifest/src/toml/target.rs b/crates/pixi_manifest/src/toml/target.rs index 8bf955d575..8ebf8bc37e 100644 --- a/crates/pixi_manifest/src/toml/target.rs +++ b/crates/pixi_manifest/src/toml/target.rs @@ -79,7 +79,7 @@ impl TomlTarget { .map(|(name, toml_loc)| { toml_loc .into_source_location_spec() - .map(|location| (name, SourceSpec { location })) + .map(|location| (name, SourceSpec::from(location))) }) .collect::, _>>() }) diff --git a/crates/pixi_record/src/pinned_source.rs b/crates/pixi_record/src/pinned_source.rs index 76337f912d..2651c5ab58 100644 --- a/crates/pixi_record/src/pinned_source.rs +++ b/crates/pixi_record/src/pinned_source.rs @@ -5,16 +5,13 @@ use std::{ str::FromStr, }; -use crate::path_utils::unixify_relative_path; use miette::IntoDiagnostic; use pixi_git::{ GitUrl, 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}; @@ -23,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. @@ -177,8 +176,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 @@ -199,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 } } @@ -214,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 @@ -227,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, @@ -309,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, @@ -362,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)) @@ -411,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 { @@ -850,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, @@ -902,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(()) } } @@ -948,8 +975,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) } @@ -992,18 +1019,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()), } } } @@ -1020,6 +1042,7 @@ impl From for UrlSourceSpec { url: value.url, sha256: Some(value.sha256), md5: value.md5, + subdirectory: value.subdirectory, } } } @@ -1036,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() { @@ -1262,7 +1285,7 @@ mod tests { )); } - use pixi_spec::{PathSourceSpec, SourceLocationSpec, SourceSpec, UrlSourceSpec}; + use pixi_spec::{PathSourceSpec, SourceLocationSpec, UrlSourceSpec}; use typed_path::Utf8TypedPathBuf; #[test] @@ -1271,11 +1294,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 +1307,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 +1325,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 +1346,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 +1367,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 +1387,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 +1408,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 +1428,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 +1448,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)); @@ -1462,15 +1467,15 @@ mod tests { ) .unwrap(), md5: None, + subdirectory: 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, + subdirectory: None, + }); assert!(pinned.matches_source_spec(&spec)); } @@ -1484,15 +1489,15 @@ mod tests { ) .unwrap(), md5: None, + subdirectory: 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, + subdirectory: None, + }); assert!(!pinned.matches_source_spec(&spec)); } @@ -1503,13 +1508,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 +1528,12 @@ 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, + subdirectory: None, + }); assert!(!pinned.matches_source_spec(&spec)); } @@ -1545,13 +1547,12 @@ mod tests { ) .unwrap(), md5: None, + subdirectory: 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,15 +1568,14 @@ 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 + // Should match - we only compare repository and subdirectory, not the + // commit/rev assert!(pinned.matches_source_spec(&spec)); } @@ -1590,13 +1590,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)); @@ -1604,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 80af36e528..2da3ef404b 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_lock::{CondaSourceData, GitShallowSpec, PackageBuildSource}; use typed_path::{Utf8TypedPathBuf, Utf8UnixPathBuf}; @@ -33,7 +33,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, } impl SourceRecord { @@ -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 @@ -175,7 +176,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 8933b3287f..ca031a1e0c 100644 --- a/crates/pixi_spec/src/lib.rs +++ b/crates/pixi_spec/src/lib.rs @@ -24,7 +24,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; @@ -131,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() @@ -289,18 +293,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), } } @@ -314,9 +312,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) @@ -342,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, @@ -376,10 +373,51 @@ 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. @@ -397,9 +435,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}"), @@ -407,10 +445,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`]. @@ -418,6 +513,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 { @@ -451,9 +554,7 @@ impl From for PixiSpec { impl From for SourceSpec { fn from(value: UrlSourceSpec) -> Self { - Self { - location: SourceLocationSpec::Url(value), - } + SourceLocationSpec::Url(value).into() } } @@ -471,9 +572,7 @@ impl From for PixiSpec { impl From for SourceSpec { fn from(value: PathSourceSpec) -> Self { - Self { - location: SourceLocationSpec::Path(value), - } + SourceLocationSpec::Path(value).into() } } @@ -555,26 +654,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()), @@ -585,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..511e35facd 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; @@ -159,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; 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..8e633f057e --- /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, features = ["preserve_order"] } +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..ffe6aa5dd6 --- /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().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." + ); + } +} diff --git a/schema/pixi_build_api.json b/schema/pixi_build_api.json new file mode 100644 index 0000000000..14fd1e4603 --- /dev/null +++ b/schema/pixi_build_api.json @@ -0,0 +1,578 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ProjectModel", + "type": "object", + "properties": { + "name": { + "description": "The name of the project", + "type": [ + "string", + "null" + ] + }, + "buildString": { + "description": "A build string configured by the user.", + "type": [ + "string", + "null" + ] + }, + "buildNumber": { + "description": "The build number configured by the user.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "version": { + "description": "The version of the project", + "type": [ + "string", + "null" + ] + }, + "description": { + "description": "An optional project description", + "type": [ + "string", + "null" + ] + }, + "authors": { + "description": "Optional authors", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "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" + ] + }, + "readme": { + "description": "Path to the README file of the project (relative to the project root)", + "type": [ + "string", + "null" + ] + }, + "homepage": { + "description": "URL of the project homepage", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "repository": { + "description": "URL of the project source repository", + "type": [ + "string", + "null" + ], + "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": [ + { + "$ref": "#/$defs/Targets" + }, + { + "type": "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": [ + "string", + "null" + ], + "default": null + }, + "buildNumber": { + "description": "The build number of the package", + "type": [ + "string", + "null" + ] + }, + "fileName": { + "description": "Match the specific filename of the package", + "type": [ + "string", + "null" + ] + }, + "channel": { + "description": "The channel of the package", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "subdir": { + "description": "The subdir of the channel", + "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 + }, + "url": { + "description": "The URL of the package, if it is available", + "type": [ + "string", + "null" + ], + "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": [ + "string", + "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" + ] + } + }, + "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" + ] + } + ] + }, + "UrlSpec": { + "type": "object", + "properties": { + "url": { + "description": "The URL of the package", + "type": "string", + "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": { + "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" + ] + }, + "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" + } + }, + "required": [ + "branch" + ], + "additionalProperties": false + }, + { + "description": "A specific tag.", + "type": "object", + "properties": { + "tag": { + "type": "string" + } + }, + "required": [ + "tag" + ], + "additionalProperties": false + }, + { + "description": "A specific commit.", + "type": "object", + "properties": { + "rev": { + "type": "string" + } + }, + "required": [ + "rev" + ], + "additionalProperties": false + }, + { + "description": "A default branch.", + "type": "string", + "const": "defaultBranch" + } + ] + }, + "PathSpec": { + "description": "A specification of a package from a path", + "type": "object", + "properties": { + "path": { + "description": "The path to the package", + "type": "string" + } + }, + "required": [ + "path" + ] + }, + "PinCompatibleSpec": { + "type": "object", + "properties": { + "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" + } + ] + }, + "exact": { + "description": "If an exact pin is given, we pin the exact version & hash", + "type": "boolean" + }, + "build": { + "type": [ + "string", + "null" + ] + } + } + }, + "PinBound": { + "oneOf": [ + { + "type": "object", + "properties": { + "expression": { + "$ref": "#/$defs/PinExpression" + } + }, + "required": [ + "expression" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": [ + "version" + ], + "additionalProperties": false + } + ] + }, + "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)*$" + } + } +}