diff --git a/Cargo.lock b/Cargo.lock index a83f32359c..c25669399d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5317,8 +5317,11 @@ dependencies = [ name = "pixi_record" version = "0.1.0" dependencies = [ + "dunce", "file_url", + "insta", "miette 7.6.0", + "pathdiff", "pixi_git", "pixi_spec", "rattler_conda_types", @@ -5327,6 +5330,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "serde_yaml", "thiserror 2.0.17", "typed-path", "url", diff --git a/crates/pixi_build_discovery/src/backend_spec.rs b/crates/pixi_build_discovery/src/backend_spec.rs index e2889173a3..6bcffd4969 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}; +use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor, SourceSpec}; use pixi_spec_containers::DependencyMap; use rattler_conda_types::ChannelUrl; /// Describes how a backend should be instantiated. @@ -45,8 +45,10 @@ impl JsonRpcBackendSpec { 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); - PixiSpec::from(resolved_spec) + let resolved_spec = source_anchor.resolve(source_spec.location); + PixiSpec::from(SourceSpec { + location: resolved_spec, + }) } Err(pixi_spec) => pixi_spec, }; @@ -133,8 +135,10 @@ impl EnvironmentSpec { 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); - PixiSpec::from(resolved_spec) + let resolved_spec = source_anchor.resolve(source_spec.location); + PixiSpec::from(SourceSpec { + location: resolved_spec, + }) } Err(pixi_spec) => pixi_spec, }; diff --git a/crates/pixi_build_types/src/procedures/initialize.rs b/crates/pixi_build_types/src/procedures/initialize.rs index acaa8f244b..eb43fb4442 100644 --- a/crates/pixi_build_types/src/procedures/initialize.rs +++ b/crates/pixi_build_types/src/procedures/initialize.rs @@ -27,14 +27,14 @@ pub const METHOD_NAME: &str = "initialize"; pub struct InitializeParams { /// The manifest that the build backend should use. /// - /// This is an absolute path. + /// This is an absolute path to a manifest file. pub manifest_path: PathBuf, /// The root directory of the source code that the build backend should use. /// If this is `None`, the backend should use the directory of the /// `manifest_path` as the source directory. /// - /// This is an absolute path. + /// This is an absolute path. This is always a directory. pub source_dir: Option, /// The root directory of the workspace. diff --git a/crates/pixi_cli/src/build.rs b/crates/pixi_cli/src/build.rs index d3288bd821..1bb7d5dd77 100644 --- a/crates/pixi_cli/src/build.rs +++ b/crates/pixi_cli/src/build.rs @@ -7,6 +7,7 @@ use miette::{Context, IntoDiagnostic}; use pixi_build_frontend::BackendOverride; use pixi_command_dispatcher::{ BuildBackendMetadataSpec, BuildEnvironment, BuildProfile, CacheDirs, SourceBuildSpec, + build::SourceCodeLocation, }; use pixi_config::ConfigCli; use pixi_consts::consts::{ @@ -270,13 +271,13 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Create the build backend metadata specification. let backend_metadata_spec = BuildBackendMetadataSpec { manifest_source: manifest_source.clone(), + preferred_build_source: None, channels: channels.clone(), channel_config: channel_config.clone(), build_environment: build_environment.clone(), variants: Some(variants.clone()), variant_files: Some(variant_files.clone()), enabled_protocols: Default::default(), - pin_override: None, }; let backend_metadata = command_dispatcher .build_backend_metadata(backend_metadata_spec.clone()) @@ -307,8 +308,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { package, // Build into a temporary directory first output_directory: Some(temp_output_dir.path().to_path_buf()), - manifest_source: manifest_source.clone(), - build_source: None, + source: SourceCodeLocation::new(manifest_source.clone(), None), channels: channels.clone(), channel_config: channel_config.clone(), build_environment: build_environment.clone(), diff --git a/crates/pixi_command_dispatcher/src/build/build_cache.rs b/crates/pixi_command_dispatcher/src/build/build_cache.rs index c76324df6f..97a9988249 100644 --- a/crates/pixi_command_dispatcher/src/build/build_cache.rs +++ b/crates/pixi_command_dispatcher/src/build/build_cache.rs @@ -19,7 +19,7 @@ use thiserror::Error; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use xxhash_rust::xxh3::Xxh3; -use crate::build::source_checkout_cache_key; +use crate::build::{SourceCodeLocation, source_checkout_cache_key}; /// A cache for caching build artifacts of a source checkout. #[derive(Clone)] @@ -249,7 +249,7 @@ pub struct BuildHostPackage { pub repodata_record: RepoDataRecord, /// The source location from which the package was built. - pub source: Option, + pub source: Option, } /// A cache entry returned by [`BuildCache::entry`] which enables diff --git a/crates/pixi_command_dispatcher/src/build/dependencies.rs b/crates/pixi_command_dispatcher/src/build/dependencies.rs index abfea9c4d4..4c137987a2 100644 --- a/crates/pixi_command_dispatcher/src/build/dependencies.rs +++ b/crates/pixi_command_dispatcher/src/build/dependencies.rs @@ -8,7 +8,7 @@ use pixi_build_types::{ }, }; use pixi_record::PixiRecord; -use pixi_spec::{BinarySpec, DetailedSpec, PixiSpec, SourceAnchor, UrlBinarySpec}; +use pixi_spec::{BinarySpec, DetailedSpec, PixiSpec, SourceAnchor, SourceSpec, UrlBinarySpec}; use pixi_spec_containers::DependencyMap; use rattler_conda_types::{ InvalidPackageNameError, MatchSpec, NamedChannelOrUrl, NamelessMatchSpec, PackageName, @@ -95,12 +95,12 @@ impl Dependencies { })?; match conversion::from_package_spec_v1(depend.spec.clone()).into_source_or_binary() { Either::Left(source) => { - let source = if let Some(anchor) = &source_anchor { - anchor.resolve(source) + let location = if let Some(anchor) = &source_anchor { + anchor.resolve(source.location) } else { - source + source.location }; - dependencies.insert(name, PixiSpec::from(source).into()); + dependencies.insert(name, PixiSpec::from(SourceSpec { location }).into()); } Either::Right(binary) => { dependencies.insert(name, PixiSpec::from(binary).into()); diff --git a/crates/pixi_command_dispatcher/src/build/mod.rs b/crates/pixi_command_dispatcher/src/build/mod.rs index 38d5b68d79..0555467701 100644 --- a/crates/pixi_command_dispatcher/src/build/mod.rs +++ b/crates/pixi_command_dispatcher/src/build/mod.rs @@ -22,12 +22,79 @@ pub use dependencies::{ }; pub(crate) use move_file::{MoveError, move_file}; use pixi_record::PinnedSourceSpec; +use serde::{Deserialize, Serialize}; use url::Url; pub use work_dir_key::{SourceRecordOrCheckout, WorkDirKey}; use xxhash_rust::xxh3::Xxh3; const KNOWN_SUFFIXES: [&str; 3] = [".git", ".tar.gz", ".zip"]; +/// Stores the two possible locations for the source code, +/// in the case of an out-of-tree source build. +/// +/// Something which looks like: +/// ```toml +/// [package.build] +/// source = { path = "some-path" } +/// ``` +/// +/// We want to prefer that location for our cache checks +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct SourceCodeLocation { + /// The location of the manifest and the possible source code + manifest_source: PinnedSourceSpec, + /// The location of the source code that should be queried and build + build_source: Option, +} + +impl SourceCodeLocation { + pub fn new(manifest_source: PinnedSourceSpec, build_source: Option) -> Self { + Self { + manifest_source, + build_source, + } + } + + /// Get the reference to the manifest source + pub fn manifest_source(&self) -> &PinnedSourceSpec { + &self.manifest_source + } + + /// Get the pinned source spec to the actual source code + /// This is the normally the path to the manifest_source + /// but when set is the path to the build_source + pub fn source_code(&self) -> &PinnedSourceSpec { + self.build_source.as_ref().unwrap_or(&self.manifest_source) + } + + /// Get the optional explicit build source override. + pub fn build_source(&self) -> Option<&PinnedSourceSpec> { + self.build_source.as_ref() + } + + pub fn as_source_and_alternative_root(&self) -> (&PinnedSourceSpec, Option<&PinnedSourceSpec>) { + if let Some(build_source) = &self.build_source { + (build_source, Some(&self.manifest_source)) + } else { + (&self.manifest_source, None) + } + } +} + +impl std::fmt::Display for SourceCodeLocation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "(manifest-src: {}, build-src: {})", + self.manifest_source(), + self.build_source + .as_ref() + .map(|build| format!("{build}")) + .unwrap_or("undefined".to_string()) + ) + } +} + /// Try to deduce a name from a url. fn pretty_url_name(url: &Url) -> String { if let Some(last_segment) = url 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 48ba77d463..189e7af115 100644 --- a/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs +++ b/crates/pixi_command_dispatcher/src/build_backend_metadata/mod.rs @@ -25,7 +25,7 @@ use crate::{ BuildEnvironment, CommandDispatcher, CommandDispatcherError, CommandDispatcherErrorResultExt, InstantiateBackendError, InstantiateBackendSpec, SourceCheckout, SourceCheckoutError, build::{ - SourceRecordOrCheckout, WorkDirKey, + SourceCodeLocation, SourceRecordOrCheckout, WorkDirKey, source_metadata_cache::{self, CachedCondaMetadata, MetadataKind, SourceMetadataKey}, }, }; @@ -49,9 +49,19 @@ fn warn_once_per_backend(backend_name: &str) { /// particular source. #[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize)] pub struct BuildBackendMetadataSpec { - /// The source specification where manifest is located at. + /// The location that refers to where the manifest is stored. pub manifest_source: PinnedSourceSpec, + /// The optional pinned location of the source code. If not provide the + /// location in the manifest is resolved. + /// + /// This is passed as a hint. If the [`SourceSpec`] in the discovered + /// manifest does not match with the pinned source provided here, the one + /// in the manifest takes precedence and it is reresolved. + /// + /// See [`PinnedSourceSpec::matches_source_spec`] how the matching is done. + pub preferred_build_source: Option, + /// The channel configuration to use for the build backend. pub channel_config: ChannelConfig, @@ -71,21 +81,13 @@ pub struct BuildBackendMetadataSpec { /// The protocols that are enabled for this source #[serde(skip_serializing_if = "crate::is_default")] pub enabled_protocols: EnabledProtocols, - - /// Optional override for the pinned build source of the current package. - /// When set, this takes precedence over any discovered build_source. - #[serde(skip)] - pub pin_override: Option, } /// The metadata of a source checkout. #[derive(Debug)] pub struct BuildBackendMetadata { - /// The source checkout that the manifest was extracted from. - pub manifest_source: PinnedSourceSpec, - - /// The source checkout from which we want to build package. - pub build_source: Option, + /// The manifest and optional build source location for this metadata. + pub source: SourceCodeLocation, /// The cache entry that contains the metadata acquired from the build /// backend. @@ -103,7 +105,8 @@ impl BuildBackendMetadataSpec { skip_all, name="backend-metadata", fields( - source = %self.manifest_source, + manifest_source = ?self.manifest_source, + build_source = ?self.preferred_build_source, platform = %self.build_environment.host_platform, ) )] @@ -114,6 +117,7 @@ impl BuildBackendMetadataSpec { ) -> Result> { // Ensure that the source is checked out before proceeding. let manifest_source_checkout = command_dispatcher + // Never has an alternative root because we want to get the manifest .checkout_pinned_source(self.manifest_source.clone()) .await .map_err_with(BuildBackendMetadataError::SourceCheckout)?; @@ -128,37 +132,37 @@ impl BuildBackendMetadataSpec { .await .map_err_with(BuildBackendMetadataError::Discovery)?; - let build_source_checkout = if let Some(pin_override) = &self.pin_override { - Some( - command_dispatcher - .checkout_pinned_source(pin_override.clone()) - .await - .map_err_with(BuildBackendMetadataError::SourceCheckout)?, - ) - } else if let Some(build_source) = &discovered_backend.init_params.build_source { - Some( - command_dispatcher - .pin_and_checkout( - build_source.clone(), + // Determine the location of the source to build from. + let manifest_source_anchor = + SourceAnchor::from(SourceSpec::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( - discovered_backend - .init_params - .manifest_path - .parent() - .ok_or_else(|| { - SourceCheckoutError::ParentDir( - discovered_backend.init_params.manifest_path.clone(), - ) - }) - .map_err(BuildBackendMetadataError::SourceCheckout) - .map_err(CommandDispatcherError::Failed)?, - ), - ) - .await - .map_err_with(BuildBackendMetadataError::SourceCheckout)?, - ) - } else { - None + command_dispatcher + .checkout_pinned_source(pinned.clone()) + .await + .map_err_with(BuildBackendMetadataError::SourceCheckout)?, + ) + } + _ => Some( + command_dispatcher + .pin_and_checkout(resolved_location) + .await + .map_err_with(BuildBackendMetadataError::SourceCheckout)?, + ), + } + } }; let (build_source_checkout, build_source) = if let Some(checkout) = build_source_checkout { @@ -167,6 +171,10 @@ impl BuildBackendMetadataSpec { } else { (manifest_source_checkout.clone(), None) }; + let source_location = SourceCodeLocation::new( + manifest_source_checkout.pinned.clone(), + build_source.clone(), + ); // Calculate the hash of the project model let additional_glob_hash = calculate_additional_glob_hash( @@ -200,10 +208,9 @@ impl BuildBackendMetadataSpec { .await? { return Ok(BuildBackendMetadata { + source: source_location.clone(), metadata, cache_entry, - manifest_source: manifest_source_checkout.pinned, - build_source, }); } } else { @@ -215,22 +222,25 @@ impl BuildBackendMetadataSpec { let build_source_dir = build_source_checkout.path.clone(); // Instantiate the backend with the discovered information. - let backend = - command_dispatcher - .instantiate_backend(InstantiateBackendSpec { - backend_spec: discovered_backend.backend_spec.clone().resolve( - SourceAnchor::from(SourceSpec::from(self.manifest_source.clone())), - ), - init_params: discovered_backend.init_params.clone(), - build_source_dir, - channel_config: self.channel_config.clone(), - enabled_protocols: self.enabled_protocols.clone(), - }) - .await - .map_err_with(BuildBackendMetadataError::Initialize)?; + let backend = command_dispatcher + .instantiate_backend(InstantiateBackendSpec { + backend_spec: discovered_backend + .backend_spec + .clone() + .resolve(manifest_source_anchor), + build_source_dir, + channel_config: self.channel_config.clone(), + enabled_protocols: self.enabled_protocols.clone(), + workspace_root: discovered_backend.init_params.workspace_root.clone(), + manifest_path: discovered_backend.init_params.manifest_path.clone(), + project_model: discovered_backend.init_params.project_model.clone(), + configuration: discovered_backend.init_params.configuration.clone(), + target_configuration: discovered_backend.init_params.target_configuration.clone(), + }) + .await + .map_err_with(BuildBackendMetadataError::Initialize)?; // Call the conda_outputs method to get metadata. - let manifest_source = manifest_source_checkout.pinned.clone(); if !backend.capabilities().provides_conda_outputs() { return Err(CommandDispatcherError::Failed( BuildBackendMetadataError::BackendMissingCapabilities( @@ -261,8 +271,7 @@ impl BuildBackendMetadataSpec { .map_err(CommandDispatcherError::Failed)?; Ok(BuildBackendMetadata { - manifest_source, - build_source, + source: source_location, metadata, cache_entry, }) 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 07444fb4ba..c373921484 100644 --- a/crates/pixi_command_dispatcher/src/command_dispatcher/instantiate_backend.rs +++ b/crates/pixi_command_dispatcher/src/command_dispatcher/instantiate_backend.rs @@ -1,15 +1,16 @@ use std::path::PathBuf; use miette::Diagnostic; -use pixi_build_discovery::{ - BackendInitializationParams, BackendSpec, CommandSpec, EnabledProtocols, -}; +use ordermap::OrderMap; +use pixi_build_discovery::{BackendSpec, CommandSpec, EnabledProtocols}; use pixi_build_frontend::{ Backend, BackendOverride, json_rpc, json_rpc::{CommunicationError, JsonRpcBackend}, tool::{IsolatedTool, SystemTool, Tool}, }; -use pixi_build_types::{PixiBuildApiVersion, procedures::initialize::InitializeParams}; +use pixi_build_types::{ + PixiBuildApiVersion, ProjectModelV1, TargetSelectorV1, procedures::initialize::InitializeParams, +}; use pixi_spec::SpecConversionError; use rattler_conda_types::ChannelConfig; use rattler_shell::{ @@ -33,8 +34,20 @@ pub struct InstantiateBackendSpec { /// The backend specification pub backend_spec: BackendSpec, - /// The parameters to initialize the backend with - pub init_params: BackendInitializationParams, + /// The root directory of the workspace. + pub workspace_root: PathBuf, + + /// The absolute path of the discovered manifest + pub manifest_path: PathBuf, + + /// Optionally, the manifest of the discovered package. + pub project_model: Option, + + /// Additional configuration that applies to the backend. + pub configuration: Option, + + /// Targets that apply to the backend. + pub target_configuration: Option>, /// The source directory to use for the backend pub build_source_dir: PathBuf, @@ -55,15 +68,13 @@ impl CommandDispatcher { ) -> Result> { let BackendSpec::JsonRpc(backend_spec) = spec.backend_spec; - let source_dir = spec.build_source_dir; - // Canonicalize the source_dir to ensure it's a fully resolved absolute path // without any relative components like ".." or "." - let source_dir = dunce::canonicalize(&source_dir).map_err(|e| { + let source_dir = dunce::canonicalize(&spec.build_source_dir).map_err(|e| { CommandDispatcherError::Failed(InstantiateBackendError::SpecConversionError( SpecConversionError::InvalidPath(format!( "failed to canonicalize source directory '{}': {}", - source_dir.display(), + spec.build_source_dir.display(), e )), )) @@ -79,13 +90,13 @@ impl CommandDispatcher { if let Some(in_mem) = backend { let memory = in_mem .initialize(InitializeParams { - manifest_path: spec.init_params.manifest_path, + manifest_path: spec.manifest_path, source_dir: Some(source_dir), - workspace_root: Some(spec.init_params.workspace_root), + workspace_root: Some(spec.workspace_root), cache_directory: Some(self.cache_dirs().root().clone()), - project_model: spec.init_params.project_model.map(Into::into), - configuration: spec.init_params.configuration, - target_configuration: spec.init_params.target_configuration, + project_model: spec.project_model.map(Into::into), + configuration: spec.configuration, + target_configuration: spec.target_configuration, }) .map_err(InstantiateBackendError::InMemoryError) .map_err(CommandDispatcherError::Failed)?; @@ -165,7 +176,6 @@ impl CommandDispatcher { // Make sure that the project model is compatible with the API version. if !api_version.supports_name_none() && spec - .init_params .project_model .as_ref() .is_some_and(|p| p.name.is_none()) @@ -177,11 +187,11 @@ impl CommandDispatcher { JsonRpcBackend::setup( source_dir, - spec.init_params.manifest_path, - spec.init_params.workspace_root, - spec.init_params.project_model, - spec.init_params.configuration, - spec.init_params.target_configuration, + spec.manifest_path, + spec.workspace_root, + spec.project_model, + spec.configuration, + spec.target_configuration, Some(self.cache_dirs().root().clone()), tool, ) diff --git a/crates/pixi_command_dispatcher/src/command_dispatcher/mod.rs b/crates/pixi_command_dispatcher/src/command_dispatcher/mod.rs index 9bf06a3061..ac046347bf 100644 --- a/crates/pixi_command_dispatcher/src/command_dispatcher/mod.rs +++ b/crates/pixi_command_dispatcher/src/command_dispatcher/mod.rs @@ -597,7 +597,6 @@ impl CommandDispatcher { pub async fn pin_and_checkout( &self, source_location_spec: SourceLocationSpec, - alternative_root: Option<&Path>, ) -> Result> { match source_location_spec { SourceLocationSpec::Url(url) => { @@ -611,7 +610,7 @@ impl CommandDispatcher { SourceLocationSpec::Path(path) => { let source_path = self .data - .resolve_typed_path(path.path.to_path(), alternative_root) + .resolve_typed_path(path.path.to_path()) .map_err(SourceCheckoutError::from) .map_err(CommandDispatcherError::Failed)?; Ok(SourceCheckout { @@ -639,10 +638,10 @@ impl CommandDispatcher { pinned_spec: PinnedSourceSpec, ) -> Result> { match pinned_spec { - PinnedSourceSpec::Path(ref path) => { + PinnedSourceSpec::Path(ref path_spec) => { let source_path = self .data - .resolve_typed_path(path.path.to_path(), None) + .resolve_typed_path(path_spec.path.to_path()) .map_err(SourceCheckoutError::from) .map_err(CommandDispatcherError::Failed)?; Ok(SourceCheckout { @@ -675,11 +674,7 @@ impl CommandDispatcherData { /// /// This function does not check if the path exists and also does not follow /// symlinks. - fn resolve_typed_path( - &self, - path_spec: Utf8TypedPath, - alternative_root: Option<&Path>, - ) -> Result { + fn resolve_typed_path(&self, path_spec: Utf8TypedPath) -> Result { if path_spec.is_absolute() { Ok(Path::new(path_spec.as_str()).to_path_buf()) } else if let Ok(user_path) = path_spec.strip_prefix("~/") { @@ -689,20 +684,7 @@ impl CommandDispatcherData { debug_assert!(home_dir.is_absolute()); normalize_absolute_path(&home_dir.join(Path::new(user_path.as_str()))) } else { - let root_dir = match alternative_root { - Some(root_path) => { - debug_assert!( - root_path.is_absolute(), - "alternative_root must be absolute, got: {root_path:?}" - ); - debug_assert!( - !root_path.is_file(), - "alternative_root should be a directory, not a file: {root_path:?}" - ); - root_path - } - None => self.root_dir.as_path(), - }; + let root_dir = self.root_dir.as_path(); let native_path = Path::new(path_spec.as_str()); debug_assert!(root_dir.is_absolute()); normalize_absolute_path(&root_dir.join(native_path)) diff --git a/crates/pixi_command_dispatcher/src/install_pixi/mod.rs b/crates/pixi_command_dispatcher/src/install_pixi/mod.rs index 98b6b1de63..bd8b6bcb3a 100644 --- a/crates/pixi_command_dispatcher/src/install_pixi/mod.rs +++ b/crates/pixi_command_dispatcher/src/install_pixi/mod.rs @@ -23,8 +23,8 @@ use thiserror::Error; use crate::{ BuildEnvironment, BuildProfile, CommandDispatcher, CommandDispatcherError, - CommandDispatcherErrorResultExt, SourceBuildError, SourceBuildSpec, executor::ExecutorFutures, - install_pixi::reporter::WrappingInstallReporter, + CommandDispatcherErrorResultExt, SourceBuildError, SourceBuildSpec, build::SourceCodeLocation, + executor::ExecutorFutures, install_pixi::reporter::WrappingInstallReporter, }; #[derive(Debug, Clone, serde::Serialize)] @@ -215,7 +215,10 @@ impl InstallPixiEnvironmentSpec { .contains(&source_record.package_record.name); let built_source = command_dispatcher .source_build(SourceBuildSpec { - manifest_source: source_record.manifest_source.clone(), + source: SourceCodeLocation::new( + source_record.manifest_source.clone(), + source_record.build_source.clone(), + ), package: source_record.into(), channel_config: self.channel_config.clone(), channels: self.channels.clone(), @@ -230,7 +233,6 @@ impl InstallPixiEnvironmentSpec { force, // When we install a pixi environment we always build in development mode. build_profile: BuildProfile::Development, - build_source: None, }) .await?; 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 3e83bd31e8..0553e60428 100644 --- a/crates/pixi_command_dispatcher/src/instantiate_tool_env/mod.rs +++ b/crates/pixi_command_dispatcher/src/instantiate_tool_env/mod.rs @@ -216,7 +216,7 @@ impl InstantiateToolEnvironmentSpec { variants: self.variants.clone(), variant_files: self.variant_files.clone(), strategy: SolveStrategy::default(), - pin_overrides: BTreeMap::new(), + preferred_build_source: BTreeMap::new(), }) .await .map_err_with(Box::new) diff --git a/crates/pixi_command_dispatcher/src/solve_conda/mod.rs b/crates/pixi_command_dispatcher/src/solve_conda/mod.rs index 3617937f42..24fa98e753 100644 --- a/crates/pixi_command_dispatcher/src/solve_conda/mod.rs +++ b/crates/pixi_command_dispatcher/src/solve_conda/mod.rs @@ -156,7 +156,7 @@ impl SolveCondaEnvironmentSpec { channel: None, }; let mut record = record.clone(); - record.build_source = source_metadata.build_source.clone(); + record.build_source = source_metadata.source.build_source().cloned(); url_to_source_package.insert(url, (record, repodata_record)); } } diff --git a/crates/pixi_command_dispatcher/src/solve_pixi/mod.rs b/crates/pixi_command_dispatcher/src/solve_pixi/mod.rs index af8f645bd5..06a47339e8 100644 --- a/crates/pixi_command_dispatcher/src/solve_pixi/mod.rs +++ b/crates/pixi_command_dispatcher/src/solve_pixi/mod.rs @@ -89,9 +89,10 @@ pub struct PixiEnvironmentSpec { /// Optional override for a specific packages: use this pinned /// source for checkout and as the `package_build_source` instead - /// of pinning anew. + /// of recomputing the pinned location. #[serde(skip)] - pub pin_overrides: BTreeMap, + pub preferred_build_source: + BTreeMap, } impl Default for PixiEnvironmentSpec { @@ -110,7 +111,7 @@ impl Default for PixiEnvironmentSpec { variants: None, variant_files: None, enabled_protocols: EnabledProtocols::default(), - pin_overrides: BTreeMap::new(), + preferred_build_source: BTreeMap::new(), } } } @@ -148,7 +149,7 @@ impl PixiEnvironmentSpec { self.variants.clone(), self.variant_files.clone(), self.enabled_protocols.clone(), - self.pin_overrides.clone(), + self.preferred_build_source.clone(), ) .collect( source_specs 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 9438ad1b8d..6e40b768a1 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 @@ -29,7 +29,7 @@ pub struct SourceMetadataCollector { enabled_protocols: EnabledProtocols, variants: Option>>, variant_files: Option>, - pin_overrides: BTreeMap, + preferred_build_sources: BTreeMap, } #[derive(Default)] @@ -78,7 +78,7 @@ impl SourceMetadataCollector { variants: Option>>, variant_files: Option>, enabled_protocols: EnabledProtocols, - pin_overrides: BTreeMap, + preferred_build_sources: BTreeMap, ) -> Self { Self { command_queue, @@ -88,7 +88,7 @@ impl SourceMetadataCollector { channel_config, variants, variant_files, - pin_overrides, + preferred_build_sources, } } @@ -137,7 +137,14 @@ impl SourceMetadataCollector { .map(|source_spec| (name.clone(), source_spec.clone())) }) { // We encountered a transitive source dependency. - specs.push((name, anchor.resolve(source_spec), chain.clone())); + let resolved_location = anchor.resolve(source_spec.location); + specs.push(( + name, + SourceSpec { + location: resolved_location, + }, + chain.clone(), + )); } else { // We encountered a transitive dependency that is not a source result.transitive_dependencies.push(spec); @@ -165,13 +172,13 @@ impl SourceMetadataCollector { tracing::trace!("Collecting source metadata for {name:#?}"); // Determine if we should override the build_source pin for this package. - let override_pin = self.pin_overrides.get(&name).cloned(); + let preferred_build_source = self.preferred_build_sources.get(&name).cloned(); // Always checkout the manifest-defined source location (root), discovery - // will pick build_source; we only override the build pin later. - let source = self + // will pick build_source; we only pass preferred locations. + let manifest_source_checkout = self .command_queue - .pin_and_checkout(spec.location, None) + .pin_and_checkout(spec.location) .await .map_err(|err| CollectSourceMetadataError::SourceCheckoutError { name: name.as_source().to_string(), @@ -185,14 +192,14 @@ impl SourceMetadataCollector { .source_metadata(SourceMetadataSpec { package: name.clone(), backend_metadata: BuildBackendMetadataSpec { - manifest_source: source.pinned, + manifest_source: manifest_source_checkout.pinned, + preferred_build_source, channel_config: self.channel_config.clone(), channels: self.channels.clone(), build_environment: self.build_environment.clone(), variants: self.variants.clone(), variant_files: self.variant_files.clone(), enabled_protocols: self.enabled_protocols.clone(), - pin_override: override_pin, }, }) .await @@ -230,7 +237,7 @@ impl SourceMetadataCollector { source_metadata.skipped_packages.clone(), ), name, - pinned_source: Box::new(source_metadata.manifest_source.clone()), + pinned_source: Box::new(source_metadata.source.manifest_source().clone()), }, )); } diff --git a/crates/pixi_command_dispatcher/src/source_build/mod.rs b/crates/pixi_command_dispatcher/src/source_build/mod.rs index 49b7f818ca..bce03494be 100644 --- a/crates/pixi_command_dispatcher/src/source_build/mod.rs +++ b/crates/pixi_command_dispatcher/src/source_build/mod.rs @@ -33,7 +33,7 @@ use crate::{ build::{ BuildCacheError, BuildHostEnvironment, BuildHostPackage, CachedBuild, CachedBuildSourceInfo, Dependencies, DependenciesError, MoveError, PackageBuildInputHash, - PixiRunExports, SourceRecordOrCheckout, WorkDirKey, move_file, + PixiRunExports, SourceCodeLocation, SourceRecordOrCheckout, WorkDirKey, move_file, }, package_identifier::PackageIdentifier, }; @@ -51,12 +51,8 @@ pub struct SourceBuildSpec { /// The source to build pub package: PackageIdentifier, - /// The location of the source code to build. - pub manifest_source: PinnedSourceSpec, - - /// Optional source spec of sources which will be built. - #[serde(skip_serializing_if = "Option::is_none")] - pub build_source: Option, + /// The manifest and optional build source location. + pub source: SourceCodeLocation, /// The channel configuration to use when resolving metadata pub channel_config: ChannelConfig, @@ -124,7 +120,8 @@ impl SourceBuildSpec { skip_all, name = "source-build", fields( - source= %self.manifest_source, + manifest_source = %self.source.manifest_source(), + build_source = ?self.source.build_source(), package = %self.package, ) )] @@ -134,6 +131,8 @@ impl SourceBuildSpec { reporter: Option>, log_sink: UnboundedSender, ) -> Result> { + let manifest_source = self.source.manifest_source().clone(); + // If the output directory is not set, we want to use the build cache. Read the // build cache in that case. let (output_directory, build_cache) = if let Some(output_directory) = @@ -147,7 +146,7 @@ impl SourceBuildSpec { .source_build_cache_status(SourceBuildCacheStatusSpec { package: self.package.clone(), build_environment: self.build_environment.clone(), - source: self.manifest_source.clone(), + source: self.source.clone(), channels: self.channels.clone(), channel_config: self.channel_config.clone(), enabled_protocols: self.enabled_protocols.clone(), @@ -163,7 +162,7 @@ impl SourceBuildSpec { if !self.force { // If the build is up to date, we can return the cached build. tracing::debug!( - source = %self.manifest_source, + source = %self.source.manifest_source(), package = ?cached_build.record.package_record.name, build = %cached_build.record.package_record.build, output = %cached_build.record.file_name, @@ -185,7 +184,7 @@ impl SourceBuildSpec { self.package.name.as_normalized() ); tracing::debug!( - source = %self.manifest_source, + source = %self.source.manifest_source(), package = ?cached_build.record.package_record.name, build = %cached_build.record.package_record.build, output = %cached_build.record.file_name, @@ -201,7 +200,7 @@ impl SourceBuildSpec { match &*build_cache.cached_build.lock().await { CachedBuildStatus::Stale(existing) => { tracing::debug!( - source = %self.manifest_source, + source = %self.source.manifest_source(), package = ?existing.record.package_record.name, build = %existing.record.package_record.build, "rebuilding stale source build", @@ -209,7 +208,7 @@ impl SourceBuildSpec { } CachedBuildStatus::Missing => { tracing::debug!( - source = %self.manifest_source, + source = %self.source.manifest_source(), "no cached source build; starting fresh build", ); } @@ -221,7 +220,7 @@ impl SourceBuildSpec { // Check out the source code. let manifest_source_checkout = command_dispatcher - .checkout_pinned_source(self.manifest_source.clone()) + .checkout_pinned_source(manifest_source.clone()) .await .map_err_with(SourceBuildError::SourceCheckout)?; @@ -243,7 +242,7 @@ impl SourceBuildSpec { // Ensure legacy lock entries that missed the git subdirectory pick it up from the // manifest so we check out the correct directory. - let mut build_source = self.build_source.clone(); + let mut build_source = self.source.build_source().cloned(); if let (Some(PinnedSourceSpec::Git(pinned_git)), Some(SourceLocationSpec::Git(git_spec))) = ( build_source.as_mut(), discovered_backend.init_params.build_source.clone(), @@ -257,53 +256,45 @@ impl SourceBuildSpec { // 1. Lock file `package_build_source`. Since we're running lock file update before building package it should pin source in there. // 2. Manifest package build. This can happen if package isn't added to the dependencies of manifest, so no pinning happens in that case. // 3. Manifest source. Just assume that source is located at the same directory as the manifest. - let build_source_dir = if let Some(pinned_build_source) = build_source { - let build_source_checkout = command_dispatcher + let build_source_checkout = if let Some(pinned_build_source) = build_source { + &command_dispatcher .checkout_pinned_source(pinned_build_source) .await - .map_err_with(SourceBuildError::SourceCheckout)?; - build_source_checkout.path + .map_err_with(SourceBuildError::SourceCheckout)? } else if let Some(manifest_build_source) = discovered_backend.init_params.build_source.clone() { - let build_source_checkout = command_dispatcher - .pin_and_checkout( - manifest_build_source, - Some( - discovered_backend - .init_params - .manifest_path - .parent() - .ok_or_else(|| { - SourceCheckoutError::ParentDir( - discovered_backend.init_params.manifest_path.clone(), - ) - }) - .map_err(SourceBuildError::SourceCheckout) - .map_err(CommandDispatcherError::Failed)?, - ), - ) + let manifest_source_anchor = + SourceAnchor::from(SourceSpec::from(manifest_source.clone())); + let resolved_build_source = manifest_source_anchor.resolve(manifest_build_source); + &command_dispatcher + .pin_and_checkout(resolved_build_source) .await - .map_err_with(SourceBuildError::SourceCheckout)?; - build_source_checkout.path + .map_err_with(SourceBuildError::SourceCheckout)? } else { - manifest_source_checkout.path + &manifest_source_checkout }; // Instantiate the backend with the discovered information. - let backend = - command_dispatcher - .instantiate_backend(InstantiateBackendSpec { - backend_spec: discovered_backend.backend_spec.clone().resolve( - SourceAnchor::from(SourceSpec::from(self.manifest_source.clone())), - ), - init_params: discovered_backend.init_params.clone(), - build_source_dir, - channel_config: self.channel_config.clone(), - enabled_protocols: self.enabled_protocols.clone(), - }) - .await - .map_err_with(SourceBuildError::Initialize)?; + let backend = command_dispatcher + .instantiate_backend(InstantiateBackendSpec { + backend_spec: discovered_backend + .backend_spec + .clone() + .resolve(SourceAnchor::from(SourceSpec::from( + manifest_source.clone(), + ))), + build_source_dir: build_source_checkout.path.clone(), + channel_config: self.channel_config.clone(), + enabled_protocols: self.enabled_protocols.clone(), + workspace_root: discovered_backend.init_params.workspace_root.clone(), + manifest_path: discovered_backend.init_params.manifest_path.clone(), + project_model: discovered_backend.init_params.project_model.clone(), + configuration: discovered_backend.init_params.configuration.clone(), + target_configuration: discovered_backend.init_params.target_configuration.clone(), + }) + .await + .map_err_with(SourceBuildError::Initialize)?; // Determine the working directory for the build. let work_directory = match std::mem::take(&mut self.work_directory) { @@ -311,7 +302,7 @@ impl SourceBuildSpec { None => command_dispatcher.cache_dirs().working_dirs().join( WorkDirKey { source: SourceRecordOrCheckout::Record { - pinned: self.manifest_source.clone(), + pinned: manifest_source.clone(), package_name: self.package.name.clone(), }, host_platform: self.build_environment.host_platform, @@ -321,7 +312,7 @@ impl SourceBuildSpec { ), }; tracing::debug!( - source = %self.manifest_source, + source = %manifest_source, work_directory = %work_directory.display(), backend = backend.identifier(), "using work directory for source build", @@ -337,7 +328,7 @@ impl SourceBuildSpec { } // Build the package using the v1 build method. - let source_for_logging = self.manifest_source.clone(); + let source_for_logging = manifest_source.clone(); let mut built_source = self .build_v1( command_dispatcher, @@ -478,7 +469,10 @@ impl SourceBuildSpec { .expect("the source record should be present in the result sources"); BuildHostPackage { repodata_record, - source: Some(source.manifest_source), + source: Some(SourceCodeLocation::new( + source.manifest_source, + source.build_source, + )), } } }) @@ -487,7 +481,7 @@ impl SourceBuildSpec { /// Returns whether the package should be built in an editable mode. fn editable(&self) -> bool { - self.build_profile == BuildProfile::Development && self.manifest_source.is_mutable() + self.build_profile == BuildProfile::Development && self.source.source_code().is_mutable() } async fn build_v1( @@ -499,7 +493,9 @@ impl SourceBuildSpec { reporter: Option>, mut log_sink: UnboundedSender, ) -> Result> { - let source_anchor = SourceAnchor::from(SourceSpec::from(self.manifest_source.clone())); + let manifest_source = self.source.manifest_source().clone(); + + let source_anchor = SourceAnchor::from(SourceSpec::from(manifest_source.clone())); let host_platform = self.build_environment.host_platform; let build_platform = self.build_environment.build_platform; @@ -734,7 +730,7 @@ impl SourceBuildSpec { }), backend, package: self.package, - source: self.manifest_source, + source: manifest_source, work_directory, channels: self.channels, channel_config: self.channel_config, @@ -790,7 +786,7 @@ impl SourceBuildSpec { variants: self.variants.clone(), variant_files: self.variant_files.clone(), enabled_protocols: self.enabled_protocols.clone(), - pin_overrides: BTreeMap::new(), + preferred_build_source: BTreeMap::new(), }) .await } diff --git a/crates/pixi_command_dispatcher/src/source_build_cache_status/mod.rs b/crates/pixi_command_dispatcher/src/source_build_cache_status/mod.rs index 544d7f80ff..8b67630527 100644 --- a/crates/pixi_command_dispatcher/src/source_build_cache_status/mod.rs +++ b/crates/pixi_command_dispatcher/src/source_build_cache_status/mod.rs @@ -15,6 +15,7 @@ use crate::{ PackageIdentifier, SourceCheckoutError, build::{ BuildCacheEntry, BuildCacheError, BuildInput, CachedBuild, PackageBuildInputHashBuilder, + SourceCodeLocation, }, }; @@ -35,7 +36,7 @@ pub struct SourceBuildCacheStatusSpec { pub package: PackageIdentifier, /// Describes the source location of the package to query. - pub source: PinnedSourceSpec, + pub source: SourceCodeLocation, /// The channels to use when building source packages. pub channels: Vec, @@ -135,7 +136,7 @@ impl SourceBuildCacheStatusSpec { }; let (cached_build, build_cache_entry) = command_dispatcher .build_cache() - .entry(&self.source, &build_input) + .entry(self.source.manifest_source(), &build_input) .await .map_err(SourceBuildCacheStatusError::BuildCache) .map_err(CommandDispatcherError::Failed)?; @@ -176,13 +177,17 @@ impl SourceBuildCacheStatusSpec { let source = &self.source; // Immutable source records are always considered valid. - if source.is_immutable() { + if source.source_code().is_immutable() { return Ok(CachedBuildStatus::UpToDate(cached_build)); } // Check if the project configuration has changed. let cached_build = match self - .check_package_configuration_changed(command_dispatcher, cached_build, source) + .check_package_configuration_changed( + command_dispatcher, + cached_build, + source.manifest_source(), + ) .await? { CachedBuildStatus::UpToDate(cached_build) | CachedBuildStatus::New(cached_build) => { @@ -196,7 +201,7 @@ impl SourceBuildCacheStatusSpec { // Determine if the package is out of date by checking the source let cached_build = match self - .check_source_out_of_date(command_dispatcher, cached_build, source) + .check_source_out_of_date(command_dispatcher, cached_build) .await? { CachedBuildStatus::UpToDate(cached_build) | CachedBuildStatus::New(cached_build) => { @@ -303,7 +308,7 @@ impl SourceBuildCacheStatusSpec { &self, command_dispatcher: &CommandDispatcher, cached_build: CachedBuild, - source: &PinnedSourceSpec, + manifest_source: &PinnedSourceSpec, ) -> Result> { let Some(source_info) = &cached_build.source else { return Ok(CachedBuildStatus::UpToDate(cached_build)); @@ -318,7 +323,7 @@ impl SourceBuildCacheStatusSpec { // Checkout the source for the package. let source_checkout = command_dispatcher - .checkout_pinned_source(source.clone()) + .checkout_pinned_source(manifest_source.clone()) .await .map_err_with(SourceBuildCacheStatusError::SourceCheckout)?; @@ -356,7 +361,6 @@ impl SourceBuildCacheStatusSpec { &self, command_dispatcher: &CommandDispatcher, cached_build: CachedBuild, - source: &PinnedSourceSpec, ) -> Result> { // If there are no source globs, we always consider the cached package // up-to-date. @@ -365,14 +369,14 @@ impl SourceBuildCacheStatusSpec { }; // Checkout the source for the package. - let source_checkout = command_dispatcher - .checkout_pinned_source(source.clone()) + let source_build_checkout = command_dispatcher + .checkout_pinned_source(self.source.source_code().clone()) .await .map_err_with(SourceBuildCacheStatusError::SourceCheckout)?; // Compute the modification time of the files that match the source input globs. let glob_time = match GlobModificationTime::from_patterns( - &source_checkout.path, + &source_build_checkout.path, source_info.globs.iter().map(String::as_str), ) { Ok(glob_time) => glob_time, diff --git a/crates/pixi_command_dispatcher/src/source_metadata/mod.rs b/crates/pixi_command_dispatcher/src/source_metadata/mod.rs index cc6d01cfe0..8600871518 100644 --- a/crates/pixi_command_dispatcher/src/source_metadata/mod.rs +++ b/crates/pixi_command_dispatcher/src/source_metadata/mod.rs @@ -10,7 +10,7 @@ use futures::TryStreamExt; use itertools::{Either, Itertools}; use miette::Diagnostic; use pixi_build_types::procedures::conda_outputs::CondaOutput; -use pixi_record::{InputHash, PinnedSourceSpec, PixiRecord, SourceRecord}; +use pixi_record::{InputHash, PixiRecord, SourceRecord}; use pixi_spec::{BinarySpec, PixiSpec, SourceAnchor, SourceSpec, SpecConversionError}; use pixi_spec_containers::DependencyMap; use rattler_conda_types::{ @@ -26,7 +26,7 @@ use crate::{ CommandDispatcherError, CommandDispatcherErrorResultExt, PixiEnvironmentSpec, SolvePixiEnvironmentError, build::{ - Dependencies, DependenciesError, PixiRunExports, conversion, + Dependencies, DependenciesError, PixiRunExports, SourceCodeLocation, conversion, source_metadata_cache::MetadataKind, }, executor::ExecutorFutures, @@ -44,13 +44,8 @@ pub struct SourceMetadataSpec { /// The result of building a particular source record. #[derive(Debug, Clone)] pub struct SourceMetadata { - /// Information about the source checkout that was used to build the - /// package. - pub manifest_source: PinnedSourceSpec, - - /// The optional location of where the actual source code is located, - /// this is used mainly for out-of-tree builds - pub build_source: Option, + /// Manifest and optional build source location for this metadata. + pub source: SourceCodeLocation, /// All the source records for this particular package. pub records: Vec, @@ -64,7 +59,8 @@ impl SourceMetadataSpec { skip_all, name = "source-metadata", fields( - source= %self.backend_metadata.manifest_source, + manifest_source= %self.backend_metadata.manifest_source, + preferred_build_source=?self.backend_metadata.preferred_build_source, name = %self.package.as_source(), platform = %self.backend_metadata.build_environment.host_platform, ) @@ -84,26 +80,27 @@ impl SourceMetadataSpec { match &build_backend_metadata.metadata.metadata { MetadataKind::GetMetadata { packages } => { + let source_location = build_backend_metadata.source.clone(); // Convert the metadata to source records. let records = conversion::package_metadata_to_source_records( - &build_backend_metadata.manifest_source, - build_backend_metadata.build_source.as_ref(), + source_location.manifest_source(), + source_location.build_source(), packages, &self.package, &build_backend_metadata.metadata.input_hash, ); Ok(SourceMetadata { - manifest_source: build_backend_metadata.manifest_source.clone(), + source: source_location, records, // As the GetMetadata kind returns all records at once and we don't solve them we can skip this. skipped_packages: Default::default(), - build_source: build_backend_metadata.build_source.clone(), }) } MetadataKind::Outputs { outputs } => { let mut skipped_packages = vec![]; let mut futures = ExecutorFutures::new(command_dispatcher.executor()); + let source_location = build_backend_metadata.source.clone(); for output in outputs { if output.metadata.name != self.package { skipped_packages.push(output.metadata.name.clone()); @@ -113,17 +110,15 @@ impl SourceMetadataSpec { &command_dispatcher, output, build_backend_metadata.metadata.input_hash.clone(), - build_backend_metadata.manifest_source.clone(), - build_backend_metadata.build_source.clone(), + source_location.clone(), reporter.clone(), )); } Ok(SourceMetadata { - manifest_source: build_backend_metadata.manifest_source.clone(), + source: source_location, records: futures.try_collect().await?, skipped_packages, - build_source: build_backend_metadata.build_source.clone(), }) } } @@ -134,10 +129,11 @@ impl SourceMetadataSpec { command_dispatcher: &CommandDispatcher, output: &CondaOutput, input_hash: Option, - manifest_source: PinnedSourceSpec, - build_source: Option, + source: SourceCodeLocation, reporter: Option>, ) -> 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())); // Solve the build environment for the output. @@ -374,9 +370,9 @@ impl SourceMetadataSpec { if dependencies.dependencies.is_empty() { return Ok(vec![]); } - let pin_overrides = self + let preferred_build_source = self .backend_metadata - .pin_override + .preferred_build_source .as_ref() .map(|pinned| BTreeMap::from([(pkg_name.clone(), pinned.clone())])) .unwrap_or_default(); @@ -403,7 +399,7 @@ impl SourceMetadataSpec { variants: self.backend_metadata.variants.clone(), variant_files: self.backend_metadata.variant_files.clone(), enabled_protocols: self.backend_metadata.enabled_protocols.clone(), - pin_overrides, + preferred_build_source, }) .await { diff --git a/crates/pixi_command_dispatcher/tests/integration/event_tree.rs b/crates/pixi_command_dispatcher/tests/integration/event_tree.rs index 234df1521a..ce42d3ad55 100644 --- a/crates/pixi_command_dispatcher/tests/integration/event_tree.rs +++ b/crates/pixi_command_dispatcher/tests/integration/event_tree.rs @@ -162,11 +162,7 @@ impl EventTree { Event::SourceBuildQueued { id, context, spec } => { source_build_label.insert( *id, - format!( - "{} @ {}", - spec.package.name.as_source(), - spec.manifest_source - ), + format!("{} @ {}", spec.package.name.as_source(), spec.source), ); builder.set_event_parent((*id).into(), *context); } diff --git a/crates/pixi_command_dispatcher/tests/integration/main.rs b/crates/pixi_command_dispatcher/tests/integration/main.rs index 37a61241fa..ca0393349f 100644 --- a/crates/pixi_command_dispatcher/tests/integration/main.rs +++ b/crates/pixi_command_dispatcher/tests/integration/main.rs @@ -16,7 +16,7 @@ use pixi_build_frontend::{BackendOverride, InMemoryOverriddenBackends}; use pixi_command_dispatcher::{ BuildEnvironment, CacheDirs, CommandDispatcher, CommandDispatcherError, Executor, InstallPixiEnvironmentSpec, InstantiateToolEnvironmentSpec, PackageIdentifier, - PixiEnvironmentSpec, SourceBuildCacheStatusSpec, + PixiEnvironmentSpec, SourceBuildCacheStatusSpec, build::SourceCodeLocation, }; use pixi_config::default_channel_config; use pixi_record::{PinnedPathSpec, PinnedSourceSpec}; @@ -589,10 +589,13 @@ async fn source_build_cache_status_clear_works() { let spec = SourceBuildCacheStatusSpec { package: pkg, - source: PinnedPathSpec { - path: tmp_dir.path().to_string_lossy().into_owned().into(), - } - .into(), + source: SourceCodeLocation::new( + PinnedPathSpec { + path: tmp_dir.path().to_string_lossy().into_owned().into(), + } + .into(), + None, + ), channels: Vec::::new(), build_environment: build_env, channel_config: default_channel_config(), diff --git a/crates/pixi_command_dispatcher/tests/integration/snapshots/integration__simple_test.snap b/crates/pixi_command_dispatcher/tests/integration/snapshots/integration__simple_test.snap index 635a5822d4..05f2dd033f 100644 --- a/crates/pixi_command_dispatcher/tests/integration/snapshots/integration__simple_test.snap +++ b/crates/pixi_command_dispatcher/tests/integration/snapshots/integration__simple_test.snap @@ -13,7 +13,7 @@ Pixi solve (foobar-desktop) ├── Source metadata (foobar @ https://github.com/wolfv/pixi-build-examples@8d230eda9b4cdaaefd24aad87fd923d4b7c3c78a) └── Conda solve #1 Pixi install (test-env) -├── Source build (foobar @ https://github.com/wolfv/pixi-build-examples@8d230eda9b4cdaaefd24aad87fd923d4b7c3c78a) +├── Source build (foobar @ (manifest-src: https://github.com/wolfv/pixi-build-examples@8d230eda9b4cdaaefd24aad87fd923d4b7c3c78a, build-src: undefined)) │ └── Backend source build (foobar) -└── Source build (foobar-desktop @ https://github.com/wolfv/pixi-build-examples@8d230eda9b4cdaaefd24aad87fd923d4b7c3c78a) +└── Source build (foobar-desktop @ (manifest-src: https://github.com/wolfv/pixi-build-examples@8d230eda9b4cdaaefd24aad87fd923d4b7c3c78a, build-src: undefined)) └── Backend source build (foobar-desktop) diff --git a/crates/pixi_core/src/lock_file/satisfiability/mod.rs b/crates/pixi_core/src/lock_file/satisfiability/mod.rs index 88c32d685c..8c25cf891a 100644 --- a/crates/pixi_core/src/lock_file/satisfiability/mod.rs +++ b/crates/pixi_core/src/lock_file/satisfiability/mod.rs @@ -685,9 +685,7 @@ pub async fn verify_platform_satisfiability( LockedPackageRef::Conda(conda) => { let url = conda.location().clone(); pixi_records.push( - conda - .clone() - .try_into() + PixiRecord::from_conda_package_data(conda.clone(), project_root) .map_err(|e| PlatformUnsat::CorruptedEntry(url.to_string(), e))?, ); } @@ -1334,7 +1332,10 @@ pub(crate) async fn verify_package_platform_satisfiability( )) }) { - let anchored_source = anchor.resolve(source.clone()); + let anchored_location = anchor.resolve(source.location.clone()); + let anchored_source = SourceSpec { + location: anchored_location, + }; conda_queue.push(Dependency::CondaSource( package_name.clone(), spec, @@ -1507,14 +1508,8 @@ pub(crate) async fn verify_package_platform_satisfiability( continue; }; - // Get the manifest directory first - let Some(manifest_path_record) = source_record.manifest_source.as_path() else { - continue; - }; - let manifest_dir = manifest_path_record.resolve(project_root); - // Resolve build_source relative to the manifest directory - build_path_record.resolve(&manifest_dir) + build_path_record.resolve(project_root) } else { let Some(path_record) = source_record.manifest_source.as_path() else { continue; diff --git a/crates/pixi_core/src/lock_file/update.rs b/crates/pixi_core/src/lock_file/update.rs index cc726c39c2..ecefe8d5e0 100644 --- a/crates/pixi_core/src/lock_file/update.rs +++ b/crates/pixi_core/src/lock_file/update.rs @@ -680,7 +680,8 @@ impl<'p> LockFileDerivedData<'p> { LockedPackageRef::Pypi(data, _) => Either::Right(data.name.clone()), }); - let pixi_records = locked_packages_to_pixi_records(conda_packages)?; + let pixi_records = + locked_packages_to_pixi_records(conda_packages, self.workspace.root())?; let pypi_records = pypi_packages .into_iter() @@ -857,7 +858,7 @@ impl<'p> LockFileDerivedData<'p> { } else { Vec::new() }; - let records = locked_packages_to_pixi_records(packages)?; + let records = locked_packages_to_pixi_records(packages, self.workspace.root())?; // Update the conda prefix let CondaPrefixUpdated { @@ -945,12 +946,13 @@ impl PackageFilterNames { fn locked_packages_to_pixi_records( conda_packages: Vec>, + workspace_root: &std::path::Path, ) -> Result, Report> { let pixi_records = conda_packages .into_iter() .filter_map(LockedPackageRef::as_conda) .cloned() - .map(PixiRecord::try_from) + .map(|data| PixiRecord::from_conda_package_data(data, workspace_root)) .collect::, _>>() .into_diagnostic()?; Ok(pixi_records) @@ -1308,6 +1310,7 @@ impl<'p> UpdateContextBuilder<'p> { // Extract the current conda records from the lock-file // TODO: Should we parallelize this? Measure please. + let workspace_root = project.root(); let locked_repodata_records = project .environments() .into_iter() @@ -1321,7 +1324,9 @@ impl<'p> UpdateContextBuilder<'p> { .map(|(platform, records)| { records .cloned() - .map(PixiRecord::try_from) + .map(|data| { + PixiRecord::from_conda_package_data(data, workspace_root) + }) .collect::, _>>() .map(|records| { (platform, Arc::new(PixiRecordsByName::from_iter(records))) @@ -2004,7 +2009,11 @@ impl<'p> UpdateContext<'p> { for platform in environment.platforms() { if let Some(records) = self.take_latest_repodata_records(&environment, platform) { for record in records.into_inner() { - builder.add_conda_package(&environment_name, platform, record.into()); + builder.add_conda_package( + &environment_name, + platform, + record.into_conda_package_data(project.root()), + ); } } if let Some(records) = self.take_latest_pypi_records(&environment, platform) { @@ -2206,7 +2215,7 @@ async fn spawn_solve_conda_environment_task( variants: Some(variants), variant_files: Some(variant_files), enabled_protocols: Default::default(), - pin_overrides, + preferred_build_source: pin_overrides, }) .await .map_err(|source| SolveCondaEnvironmentError::SolveFailed { diff --git a/crates/pixi_global/src/project/mod.rs b/crates/pixi_global/src/project/mod.rs index 7dd78704bd..872dc3e6cf 100644 --- a/crates/pixi_global/src/project/mod.rs +++ b/crates/pixi_global/src/project/mod.rs @@ -1372,7 +1372,7 @@ impl Project { ) -> Result { let command_dispatcher = self.command_dispatcher()?; let checkout = command_dispatcher - .pin_and_checkout(source_spec.location, None) + .pin_and_checkout(source_spec.location) .await .map_err(|e| InferPackageNameError::BuildBackendMetadata(Box::new(e)))?; @@ -1381,6 +1381,7 @@ impl Project { // Create the metadata spec let metadata_spec = BuildBackendMetadataSpec { manifest_source: pinned_source_spec, + preferred_build_source: None, channel_config: self.global_channel_config().clone(), channels: self .config() @@ -1392,7 +1393,6 @@ impl Project { variants: None, variant_files: None, enabled_protocols: Default::default(), - pin_override: None, }; // Get the metadata using the command dispatcher diff --git a/crates/pixi_record/Cargo.toml b/crates/pixi_record/Cargo.toml index 31a4990a4a..351105a7e0 100644 --- a/crates/pixi_record/Cargo.toml +++ b/crates/pixi_record/Cargo.toml @@ -10,8 +10,10 @@ repository.workspace = true version = "0.1.0" [dependencies] +dunce = { workspace = true } file_url = { workspace = true } miette = { workspace = true } +pathdiff = { workspace = true } pixi_git = { workspace = true } pixi_spec = { workspace = true, features = ["rattler_lock"] } rattler_conda_types = { workspace = true } @@ -24,4 +26,6 @@ typed-path = { workspace = true } url = { workspace = true } [dev-dependencies] +insta = { workspace = true, features = ["yaml"] } serde_json = { workspace = true } +serde_yaml = { workspace = true } diff --git a/crates/pixi_record/src/lib.rs b/crates/pixi_record/src/lib.rs index 7992cc29ea..c7a6a583e3 100644 --- a/crates/pixi_record/src/lib.rs +++ b/crates/pixi_record/src/lib.rs @@ -1,6 +1,9 @@ +mod path_utils; mod pinned_source; mod source_record; +use std::path::Path; + pub use pinned_source::{ LockedGitUrl, MutablePinnedSourceSpec, ParseError, PinnedGitCheckout, PinnedGitSpec, PinnedPathSpec, PinnedSourceSpec, PinnedUrlSpec, SourceMismatchError, @@ -38,6 +41,40 @@ impl PixiRecord { } } + /// Convert to CondaPackageData with paths made relative to workspace_root. + /// This should be used when writing to the lock file. + pub fn into_conda_package_data(self, workspace_root: &Path) -> CondaPackageData { + match self { + PixiRecord::Binary(record) => record.into(), + PixiRecord::Source(record) => { + CondaPackageData::Source(record.into_conda_source_data(workspace_root)) + } + } + } + + /// Create PixiRecord from CondaPackageData with paths resolved relative to workspace_root. + /// This should be used when reading from the lock file. + pub fn from_conda_package_data( + data: CondaPackageData, + workspace_root: &std::path::Path, + ) -> Result { + let record = match data { + CondaPackageData::Binary(value) => { + let location = value.location.clone(); + PixiRecord::Binary(value.try_into().map_err(|err| match err { + ConversionError::Missing(field) => ParseLockFileError::Missing(location, field), + ConversionError::LocationToUrlConversionError(err) => { + ParseLockFileError::InvalidRecordUrl(location, err) + } + })?) + } + CondaPackageData::Source(value) => { + PixiRecord::Source(SourceRecord::from_conda_source_data(value, workspace_root)?) + } + }; + Ok(record) + } + /// Returns a reference to the binary record if it is a binary record. pub fn as_binary(&self) -> Option<&RepoDataRecord> { match self { @@ -104,35 +141,6 @@ pub enum ParseLockFileError { PinnedSourceSpecError(#[from] pinned_source::ParseError), } -impl TryFrom for PixiRecord { - type Error = ParseLockFileError; - - fn try_from(value: CondaPackageData) -> Result { - let record = match value { - CondaPackageData::Binary(value) => { - let location = value.location.clone(); - PixiRecord::Binary(value.try_into().map_err(|err| match err { - ConversionError::Missing(field) => ParseLockFileError::Missing(location, field), - ConversionError::LocationToUrlConversionError(err) => { - ParseLockFileError::InvalidRecordUrl(location, err) - } - })?) - } - CondaPackageData::Source(value) => PixiRecord::Source(value.try_into()?), - }; - Ok(record) - } -} - -impl From for CondaPackageData { - fn from(value: PixiRecord) -> Self { - match value { - PixiRecord::Binary(record) => record.into(), - PixiRecord::Source(record) => record.into(), - } - } -} - impl Matches for NamelessMatchSpec { fn matches(&self, record: &PixiRecord) -> bool { match record { diff --git a/crates/pixi_record/src/path_utils.rs b/crates/pixi_record/src/path_utils.rs new file mode 100644 index 0000000000..bd6efccc70 --- /dev/null +++ b/crates/pixi_record/src/path_utils.rs @@ -0,0 +1,95 @@ +use std::path::{Component, Path, PathBuf}; + +use typed_path::{Utf8UnixPathBuf, Utf8WindowsPathBuf}; + +/// Normalize a path lexically (no filesystem access) and strip redundant segments. +pub(crate) fn normalize_path(path: &Path) -> PathBuf { + let simplified = dunce::simplified(path).to_path_buf(); + + let mut prefix: Option = None; + let mut has_root = false; + let mut parts: Vec = Vec::new(); + + for component in simplified.components() { + match component { + Component::Prefix(prefix_component) => { + prefix = Some(prefix_component.as_os_str().to_os_string()); + parts.clear(); + } + Component::RootDir => { + has_root = true; + parts.clear(); + } + Component::CurDir => {} + Component::ParentDir => { + if let Some(last) = parts.last() { + if last.as_os_str() == std::ffi::OsStr::new("..") { + parts.push(std::ffi::OsString::from("..")); + } else { + parts.pop(); + } + } else if !has_root { + parts.push(std::ffi::OsString::from("..")); + } + } + Component::Normal(part) => parts.push(part.to_os_string()), + } + } + + let mut normalized = PathBuf::new(); + if let Some(prefix) = prefix { + normalized.push(prefix); + } + if has_root { + normalized.push(std::path::MAIN_SEPARATOR.to_string()); + } + for part in parts { + normalized.push(part); + } + + normalized +} + +/// Make sure the path we get back out is always unix compatible +pub(crate) fn unixify_relative_path(path: &Path) -> Utf8UnixPathBuf { + // This function should only be called with relative paths + debug_assert!( + !path.is_absolute(), + "unixify_path should only be called with relative paths, got: {path:?}", + ); + + // Parse as Windows path to handle backslashes correctly, then convert to Unix + // because windows also supports forward slashes this should be okay! + Utf8WindowsPathBuf::from(path.to_string_lossy().into_owned()).with_unix_encoding() +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[test] + fn normalize_path_collapses_parent_segments() { + let normalized = normalize_path(Path::new("recipes/../")); + assert!(normalized.as_os_str().is_empty()); + } + + #[test] + fn unixify_windows_path() { + let win = PathBuf::from_str("my-windows\\path").unwrap(); + assert_eq!( + unixify_relative_path(&win).to_string(), + "my-windows/path".to_string() + ); + } + + #[test] + fn unixify_unix_path() { + let unix = PathBuf::from_str("my-unix/path").unwrap(); + assert_eq!( + unixify_relative_path(&unix).to_string(), + "my-unix/path".to_string() + ); + } +} diff --git a/crates/pixi_record/src/pinned_source.rs b/crates/pixi_record/src/pinned_source.rs index aaff706f2c..f0cb922591 100644 --- a/crates/pixi_record/src/pinned_source.rs +++ b/crates/pixi_record/src/pinned_source.rs @@ -5,6 +5,7 @@ use std::{ str::FromStr, }; +use crate::path_utils::unixify_relative_path; use miette::IntoDiagnostic; use pixi_git::{ GitUrl, @@ -19,7 +20,7 @@ use rattler_lock::UrlOrPath; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use thiserror::Error; -use typed_path::Utf8TypedPathBuf; +use typed_path::{Utf8TypedPathBuf, Utf8UnixPathBuf}; use url::Url; /// Describes an exact revision of a source checkout. This is used to pin a @@ -129,6 +130,249 @@ impl PinnedSourceSpec { PinnedSourceSpec::Path(spec) => spec.identifiable_url(), } } + + /// Checks if this pinned source spec matches a given source spec. + /// + /// This method determines if the pinned source and the source spec refer to + /// the same underlying source, ignoring version-specific details like git + /// commits or archive hashes. This is useful for determining if a pinned + /// source satisfies a given source requirement. + /// + /// # Matching Rules + /// + /// - **Path sources**: Paths must be exactly equal (same normalized path) + /// - **Git sources**: Repository URLs must match (ignoring credentials and + /// case), and subdirectories must match if specified in the source spec + /// - **URL sources**: URLs must be exactly equal (including all components) + /// + /// # Examples + /// + /// ``` + /// use pixi_record::{PinnedSourceSpec, PinnedGitSpec, PinnedGitCheckout}; + /// use pixi_spec::{SourceSpec, SourceLocationSpec, GitSpec, GitReference}; + /// use pixi_git::sha::GitSha; + /// use url::Url; + /// use std::str::FromStr; + /// + /// # fn main() -> Result<(), Box> { + /// // Git source matching + /// let pinned_git = PinnedSourceSpec::Git(PinnedGitSpec { + /// git: Url::parse("https://github.com/user/repo")?, + /// source: PinnedGitCheckout { + /// commit: GitSha::from_str("abc123def456")?, + /// subdirectory: None, + /// reference: GitReference::DefaultBranch, + /// }, + /// }); + /// + /// let source_spec = SourceSpec { + /// location: SourceLocationSpec::Git(GitSpec { + /// git: Url::parse("https://github.com/user/repo.git")?, + /// rev: None, + /// subdirectory: None, + /// }), + /// }; + /// + /// assert!(pinned_git.matches_source_spec(&source_spec)); + /// # Ok(()) + /// # } + /// ``` + pub fn matches_source_spec(&self, source_spec: &SourceSpec) -> bool { + match (self, &source_spec.location) { + // Path sources: paths must be exactly equal + (PinnedSourceSpec::Path(pinned_path), SourceLocationSpec::Path(source_path)) => { + pinned_path.path == source_path.path + } + + // Git sources: repository URLs must match, subdirectories must match if specified + (PinnedSourceSpec::Git(pinned_git), SourceLocationSpec::Git(source_git)) => { + use pixi_git::url::RepositoryUrl; + + // Compare repository URLs (ignoring commit/branch details) + let pinned_repo = RepositoryUrl::new(&pinned_git.git); + let source_repo = RepositoryUrl::new(&source_git.git); + + if pinned_repo != source_repo { + return false; + } + + // 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 + } + } + + // URL sources: URLs must be exactly equal + (PinnedSourceSpec::Url(pinned_url), SourceLocationSpec::Url(source_url)) => { + pinned_url.url == source_url.url + } + + // Mismatched types never match + _ => false, + } + } + + /// 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. + /// + /// Returns `None` if: + /// - The base is not a compatible type + /// - The path cannot be resolved + /// + /// # Arguments + /// * `build_source_path` - The possibly relative path from the lock file + /// * `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, + base: &PinnedSourceSpec, + workspace_root: &Path, + ) -> Option { + match base { + // Path-to-Path: Resolve the relative path against the base path + PinnedSourceSpec::Path(base_path) => { + match ( + base_path.path.is_absolute(), + build_source_path.is_absolute(), + ) { + // Both are absolute cannot do anything with these + (true, true) => return None, + // The source path is in a completely different location, + // so we need to return None as we cannot make this relative to the base + (false, true) => return None, + // In this case the source is relative to the absolute base path + // because the `base_path.resolve` will not do anything with absolute paths, + // we should be fine + (true, false) => {} + // Both are relative, so we can just continue + (false, false) => {} + } + + let base_absolute = base_path.resolve(workspace_root); + // We know that possible_relative_path is relative here + let relative_std_path = Path::new(build_source_path.as_str()); + // Join base with relative path to get the target absolute path + let target_path_abs = base_absolute.join(relative_std_path); + + // Normalize the path (resolve . and ..) + let normalized = crate::path_utils::normalize_path(&target_path_abs); + // Convert back to a path that's either absolute or relative to workspace + let path_spec = normalized.strip_prefix(workspace_root).expect( + "the workspace_root should be part of the source build path at this point", + ); + + Some(PinnedSourceSpec::Path(PinnedPathSpec { + path: Utf8TypedPathBuf::from(path_spec.to_string_lossy().as_ref()), + })) + } + + // Git-to-Git: If same repository, convert relative path to subdirectory + PinnedSourceSpec::Git(base_git) => { + // Base subdirectory + let base_subdir = base_git.source.subdirectory.as_deref().unwrap_or(""); + let base_path = Path::new(base_subdir); + + let relative_std_path = Path::new(build_source_path.as_str()); + + // `relative_std_path` is relative to the base_subdir, we want it relative to + // the repository root, because base_subdir is relative to the repo + // we should be able to join + let target_subdir = base_path.join(relative_std_path); + // Normalize the path, join does not do this per-se + let normalized = crate::path_utils::normalize_path(&target_subdir); + + // Convert to string for subdirectory + let subdir_str = normalized.to_string_lossy(); + let subdirectory = if subdir_str.is_empty() { + None + } else { + Some(subdir_str.into_owned()) + }; + + Some(PinnedSourceSpec::Git(PinnedGitSpec { + git: base_git.git.clone(), + source: PinnedGitCheckout { + commit: base_git.source.commit, + subdirectory, + reference: base_git.source.reference.clone(), + }, + })) + } + + PinnedSourceSpec::Url(_) => unreachable!("url specs have not been implemented"), + } + } + + /// 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) + pub fn make_relative_to( + &self, + base: &PinnedSourceSpec, + workspace_root: &Path, + ) -> Option { + match (self, base) { + // Path-to-Path: Make the path relative + (PinnedSourceSpec::Path(this_path), PinnedSourceSpec::Path(base_path)) => { + let this_path = this_path.resolve(workspace_root); + let base_path = base_path.resolve(workspace_root); + + let relative_path = pathdiff::diff_paths(this_path, base_path)?; + + // `pathdiff` yields native separators; convert to `/` for lock-file stability. + Some(Utf8UnixPathBuf::from(unixify_relative_path( + relative_path.as_path(), + ))) + } + // Git-to-Git: If same repository, convert to a relative path based on subdirectories + (PinnedSourceSpec::Git(this_git), PinnedSourceSpec::Git(base_git)) => { + // Check if both point to the same repository + let this_repo = RepositoryUrl::new(&this_git.git); + let base_repo = RepositoryUrl::new(&base_git.git); + + if this_repo != base_repo { + // Different repositories, can't make relative + return None; + } + + if this_git.source.commit != base_git.source.commit { + return None; + } + + // Same repository and commit - compute relative path between subdirectories + // Both subdirectories are relative to the repository root + let base_subdir = base_git.source.subdirectory.as_deref().unwrap_or(""); + let this_subdir = this_git.source.subdirectory.as_deref().unwrap_or(""); + + // Compute the relative path from base to this + let base_path = std::path::Path::new(base_subdir); + 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. + let relative_str = unixify_relative_path(relative.as_path()); + + Some(Utf8UnixPathBuf::from(relative_str)) + } + (PinnedSourceSpec::Url(_), _) => unreachable!("url specs have not been implemented"), + (_, PinnedSourceSpec::Url(_)) => unreachable!("url specs have not been implemented"), + // Different types or incompatible sources + _ => None, + } + } } impl MutablePinnedSourceSpec { @@ -277,6 +521,7 @@ pub struct PinnedGitSpec { /// The URL of the repository without the revision and subdirectory /// fragment. pub git: Url, + /// The resolved git checkout. #[serde(flatten)] pub source: PinnedGitCheckout, @@ -371,12 +616,12 @@ pub struct PinnedPathSpec { impl PinnedPathSpec { /// Resolves the path to an absolute path. - pub fn resolve(&self, project_root: &Path) -> PathBuf { + pub fn resolve(&self, workspace_root: &Path) -> PathBuf { let native_path = Path::new(self.path.as_str()); - if native_path.is_absolute() { - native_path.to_path_buf() + if self.path.is_absolute() { + PathBuf::from(native_path) } else { - project_root.join(native_path) + workspace_root.join(native_path) } } @@ -797,7 +1042,10 @@ mod tests { use pixi_spec::{GitReference, GitSpec}; use url::Url; - use crate::{PinnedGitCheckout, PinnedGitSpec, SourceMismatchError}; + use crate::{PinnedGitCheckout, PinnedGitSpec, PinnedUrlSpec, SourceMismatchError}; + use std::path::Path; + + use crate::{PinnedPathSpec, PinnedSourceSpec}; #[test] fn test_spec_satisfies() { @@ -1013,4 +1261,420 @@ mod tests { SourceMismatchError::GitSubdirectoryMismatch { .. } )); } + + use pixi_spec::{PathSourceSpec, SourceLocationSpec, SourceSpec, UrlSourceSpec}; + use typed_path::Utf8TypedPathBuf; + + #[test] + fn test_path_exact_match() { + let pinned = PinnedSourceSpec::Path(PinnedPathSpec { + path: Utf8TypedPathBuf::from("/path/to/source"), + }); + + let spec = SourceSpec { + location: SourceLocationSpec::Path(PathSourceSpec { + path: Utf8TypedPathBuf::from("/path/to/source"), + }), + }; + + assert!(pinned.matches_source_spec(&spec)); + } + + #[test] + fn test_path_mismatch() { + let pinned = PinnedSourceSpec::Path(PinnedPathSpec { + path: Utf8TypedPathBuf::from("/path/to/source"), + }); + + let spec = SourceSpec { + location: SourceLocationSpec::Path(PathSourceSpec { + path: Utf8TypedPathBuf::from("/different/path"), + }), + }; + + assert!(!pinned.matches_source_spec(&spec)); + } + + #[test] + fn test_git_same_repo_without_git_suffix() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: None, + reference: GitReference::DefaultBranch, + }, + }); + + let spec = SourceSpec { + location: 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)); + } + + #[test] + fn test_git_same_repo_with_git_suffix() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/user/repo.git").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: None, + reference: GitReference::DefaultBranch, + }, + }); + + let spec = SourceSpec { + location: 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)); + } + + #[test] + fn test_git_different_repos() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/user/repo1").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: None, + reference: GitReference::DefaultBranch, + }, + }); + + let spec = SourceSpec { + location: SourceLocationSpec::Git(GitSpec { + git: Url::parse("https://github.com/user/repo2").unwrap(), + rev: None, + subdirectory: None, + }), + }; + + assert!(!pinned.matches_source_spec(&spec)); + } + + #[test] + fn test_git_same_repo_no_subdirectory_in_spec() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: Some("subdir".to_string()), + reference: GitReference::DefaultBranch, + }, + }); + + let spec = SourceSpec { + location: 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)); + } + + #[test] + fn test_git_same_repo_matching_subdirectory() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: Some("subdir".to_string()), + reference: GitReference::DefaultBranch, + }, + }); + + let spec = SourceSpec { + location: 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)); + } + + #[test] + fn test_git_same_repo_mismatching_subdirectory() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: Some("subdir1".to_string()), + reference: GitReference::DefaultBranch, + }, + }); + + let spec = SourceSpec { + location: 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)); + } + + #[test] + fn test_git_spec_requires_subdirectory_but_pinned_has_none() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: None, + reference: GitReference::DefaultBranch, + }, + }); + + let spec = SourceSpec { + location: 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)); + } + + #[test] + fn test_url_exact_match() { + let pinned = PinnedSourceSpec::Url(PinnedUrlSpec { + url: Url::parse("https://example.com/archive.tar.gz").unwrap(), + sha256: rattler_digest::parse_digest_from_hex::( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ) + .unwrap(), + md5: None, + }); + + let spec = SourceSpec { + location: SourceLocationSpec::Url(UrlSourceSpec { + url: Url::parse("https://example.com/archive.tar.gz").unwrap(), + sha256: None, + md5: None, + }), + }; + + assert!(pinned.matches_source_spec(&spec)); + } + + #[test] + fn test_url_mismatch_source_spec() { + let pinned = PinnedSourceSpec::Url(PinnedUrlSpec { + url: Url::parse("https://example.com/archive.tar.gz").unwrap(), + sha256: rattler_digest::parse_digest_from_hex::( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ) + .unwrap(), + md5: None, + }); + + let spec = SourceSpec { + location: SourceLocationSpec::Url(UrlSourceSpec { + url: Url::parse("https://example.com/different.tar.gz").unwrap(), + sha256: None, + md5: None, + }), + }; + + assert!(!pinned.matches_source_spec(&spec)); + } + + #[test] + fn test_type_mismatch_path_vs_git() { + let pinned = PinnedSourceSpec::Path(PinnedPathSpec { + 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, + }), + }; + + assert!(!pinned.matches_source_spec(&spec)); + } + + #[test] + fn test_type_mismatch_git_vs_url() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: None, + reference: GitReference::DefaultBranch, + }, + }); + + let spec = SourceSpec { + location: SourceLocationSpec::Url(UrlSourceSpec { + url: Url::parse("https://example.com/archive.tar.gz").unwrap(), + sha256: None, + md5: None, + }), + }; + + assert!(!pinned.matches_source_spec(&spec)); + } + + #[test] + fn test_type_mismatch_url_vs_path() { + let pinned = PinnedSourceSpec::Url(PinnedUrlSpec { + url: Url::parse("https://example.com/archive.tar.gz").unwrap(), + sha256: rattler_digest::parse_digest_from_hex::( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ) + .unwrap(), + md5: None, + }); + + let spec = SourceSpec { + location: SourceLocationSpec::Path(PathSourceSpec { + path: Utf8TypedPathBuf::from("/path/to/source"), + }), + }; + + assert!(!pinned.matches_source_spec(&spec)); + } + + #[test] + fn test_git_ignores_different_commits() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/user/repo").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: None, + reference: GitReference::Rev("v1.0.0".to_string()), + }, + }); + + 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, + }), + }; + + // Should match - we only compare repository and subdirectory, not the commit/rev + assert!(pinned.matches_source_spec(&spec)); + } + + #[test] + fn test_git_case_insensitive_github() { + let pinned = PinnedSourceSpec::Git(PinnedGitSpec { + git: Url::parse("https://github.com/User/Repo").unwrap(), + source: PinnedGitCheckout { + commit: GitSha::from_str("abc123def456789012345678901234567890abcd").unwrap(), + subdirectory: None, + reference: GitReference::DefaultBranch, + }, + }); + + let spec = SourceSpec { + location: 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)); + } + + #[test] + fn test_relative_to_relative() { + // 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 { + path: "foo/bar".into(), + }); + let base_spec = PinnedSourceSpec::Path(PinnedPathSpec { path: "foo".into() }); + + let result = this_spec.make_relative_to(&base_spec, workspace_root); + + // Both resolve to /workspace/foo/bar and /workspace/foo + // Relative path should be "bar" + let path = result.expect("Should return Some"); + assert_eq!(path.as_str(), "bar"); + } + + #[test] + fn test_absolute_to_absolute() { + // Both paths are absolute + let workspace_root = Path::new("/workspace"); + + let this_spec = PinnedSourceSpec::Path(PinnedPathSpec { + path: "/foo/bar/baz".into(), + }); + let base_spec = PinnedSourceSpec::Path(PinnedPathSpec { + path: "/foo/bar".into(), + }); + + let result = this_spec.make_relative_to(&base_spec, workspace_root); + + // Should compute relative path + let path = result.expect("Should return Some"); + assert_eq!(path.as_str(), "baz"); + } + + #[test] + fn test_relative_to_absolute() { + // Self is relative, base is absolute - after resolution they're both absolute + let workspace_root = Path::new("/workspace"); + + let this_spec = PinnedSourceSpec::Path(PinnedPathSpec { + path: "foo/bar".into(), // Resolves to /workspace/foo/bar + }); + let base_spec = PinnedSourceSpec::Path(PinnedPathSpec { + path: "/other/path".into(), // Already absolute + }); + + let result = this_spec.make_relative_to(&base_spec, workspace_root); + + // Both are absolute after resolution, pathdiff should compute relative path + let path = result.expect("Should return Some"); + // From /other/path to /workspace/foo/bar + assert_eq!(path.as_str(), "../../workspace/foo/bar"); + } + + #[test] + fn test_absolute_with_parent_navigation() { + // Test paths that require .. navigation + let workspace_root = Path::new("/workspace"); + + let this_spec = PinnedSourceSpec::Path(PinnedPathSpec { + path: "/foo/bar/qux".into(), + }); + let base_spec = PinnedSourceSpec::Path(PinnedPathSpec { + path: "/foo/baz/quux".into(), + }); + + let result = this_spec.make_relative_to(&base_spec, workspace_root); + + let path = result.expect("Should return Some"); + // From /foo/baz/quux to /foo/bar/qux requires ../../bar/qux + assert_eq!(path.as_str(), "../../bar/qux"); + } } diff --git a/crates/pixi_record/src/snapshots/pixi_record__source_record__tests__roundtrip_conda_source_data.snap b/crates/pixi_record/src/snapshots/pixi_record__source_record__tests__roundtrip_conda_source_data.snap new file mode 100644 index 0000000000..b79dba6615 --- /dev/null +++ b/crates/pixi_record/src/snapshots/pixi_record__source_record__tests__roundtrip_conda_source_data.snap @@ -0,0 +1,123 @@ +--- +source: crates/pixi_record/src/source_record.rs +expression: roundtrip_lock +--- +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + noarch: + - conda: git+https://github.com/example/mono-repo.git?subdirectory=recipes&branch=main#abc123def456abc123def456abc123def456abc1 + name: git-child-test + - conda: git+https://github.com/example/mono-repo.git?subdirectory=recipes&branch=main#abc123def456abc123def456abc123def456abc1 + name: git-sibling-test + - conda: git+https://github.com/example/repo.git?tag=v1.0.0#abc123def456abc123def456abc123def456abc1 + - conda: /workspace/absolute-recipe + - conda: recipes/my-package + name: path-child-test + - conda: recipes/my-package + name: path-sibling-test + - conda: recipes/no-build +packages: +- conda: git+https://github.com/example/mono-repo.git?subdirectory=recipes&branch=main#abc123def456abc123def456abc123def456abc1 + name: git-child-test + version: 1.1.0 + build: h234567_0 + subdir: noarch + noarch: false + sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + md5: d41d8cd98f00b204e9800998ecf8427e + channel: null + input: + hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + globs: + - '**/*.c' + package_build_source: + path: ../src +- conda: git+https://github.com/example/repo.git?tag=v1.0.0#abc123def456abc123def456abc123def456abc1 + name: git-no-manifest-subdir + version: 3.0.0 + build: h901237_0 + subdir: noarch + noarch: false + sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + md5: d41d8cd98f00b204e9800998ecf8427e + channel: null + input: + hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + globs: + - src/**/*.rs + package_build_source: + path: build/subdir +- conda: git+https://github.com/example/mono-repo.git?subdirectory=recipes&branch=main#abc123def456abc123def456abc123def456abc1 + name: git-sibling-test + version: 1.0.0 + build: h123456_0 + subdir: noarch + noarch: false + sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + md5: d41d8cd98f00b204e9800998ecf8427e + channel: null + input: + hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + globs: + - '**/*.py' + - '**/*.txt' + package_build_source: + path: ../non-nested +- conda: /workspace/absolute-recipe + name: path-absolute-manifest + version: 2.4.0 + build: h901236_0 + subdir: noarch + noarch: false + sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + md5: d41d8cd98f00b204e9800998ecf8427e + input: + hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + globs: + - '**/*.sh' + package_build_source: + path: ../src +- conda: recipes/my-package + name: path-child-test + version: 2.1.0 + build: h890123_0 + subdir: noarch + noarch: false + sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + md5: d41d8cd98f00b204e9800998ecf8427e + input: + hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + globs: + - '**/*.cpp' + package_build_source: + path: ../../src/lib +- conda: recipes/no-build + name: path-no-build-source + version: 2.5.0 + build: h901238_0 + subdir: noarch + noarch: false + sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + md5: d41d8cd98f00b204e9800998ecf8427e + input: + hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + globs: + - '**/*.md' +- conda: recipes/my-package + name: path-sibling-test + version: 2.0.0 + build: h789012_0 + subdir: noarch + noarch: false + sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + md5: d41d8cd98f00b204e9800998ecf8427e + input: + hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + globs: + - '**/*.rs' + package_build_source: + path: ../../other-package/src diff --git a/crates/pixi_record/src/source_record.rs b/crates/pixi_record/src/source_record.rs index 68bf60c0b5..b751c7b27c 100644 --- a/crates/pixi_record/src/source_record.rs +++ b/crates/pixi_record/src/source_record.rs @@ -1,20 +1,22 @@ use std::{ collections::{BTreeSet, HashMap}, + path::Path, str::FromStr, }; -use pixi_git::sha::GitSha; +use pixi_git::{sha::GitSha, url::RepositoryUrl}; use pixi_spec::{GitReference, SourceSpec}; use rattler_conda_types::{MatchSpec, Matches, NamelessMatchSpec, PackageRecord}; use rattler_digest::{Sha256, Sha256Hash}; -use rattler_lock::{CondaPackageData, CondaSourceData, GitShallowSpec, PackageBuildSource}; +use rattler_lock::{CondaSourceData, GitShallowSpec, PackageBuildSource}; use serde::{Deserialize, Serialize}; -use typed_path::Utf8TypedPathBuf; +use typed_path::{Utf8TypedPathBuf, Utf8UnixPathBuf}; +use url::Url; use crate::{ParseLockFileError, PinnedGitCheckout, PinnedSourceSpec}; /// A record of a conda package that still requires building. -#[derive(Debug, Clone, serde::Serialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SourceRecord { /// Information about the conda package. This is metadata of the package /// after it has been build. @@ -24,7 +26,8 @@ pub struct SourceRecord { pub manifest_source: PinnedSourceSpec, /// The optional pinned source where the build should be executed - /// This is used when the manifest is not in the same location ad + /// This is used when the manifest is not in the same location as the + /// source files. pub build_source: Option, /// The hash of the input that was used to build the metadata of the @@ -55,75 +58,101 @@ pub struct InputHash { pub globs: BTreeSet, } -impl From for CondaPackageData { - fn from(value: SourceRecord) -> Self { - let package_build_source = value.build_source.map(|s| match s { - PinnedSourceSpec::Url(pinned_url_spec) => PackageBuildSource::Url { - url: pinned_url_spec.url, - sha256: pinned_url_spec.sha256, - subdir: None, - }, - PinnedSourceSpec::Git(pinned_git_spec) => { - let subdirectory = pinned_git_spec - .source - .subdirectory - .as_deref() - .map(Utf8TypedPathBuf::from); - - let spec = match &pinned_git_spec.source.reference { - GitReference::Branch(branch) => Some(GitShallowSpec::Branch(branch.clone())), - GitReference::Tag(tag) => Some(GitShallowSpec::Tag(tag.clone())), - GitReference::Rev(_) => Some(GitShallowSpec::Rev), - GitReference::DefaultBranch => None, - }; +impl SourceRecord { + /// Convert [`SourceRecord`] into lock-file compatible `CondaSourceData` + /// The `build_source` in the SourceRecord is always relative to the workspace. + /// However, when saving in the lock-file make these relative to the package manifest. + /// This should be used when writing to the lock file. + pub fn into_conda_source_data(self, workspace_root: &Path) -> CondaSourceData { + let package_build_source = if let Some(package_build_source) = self.build_source.clone() { + // See if we can make it relative + let package_build_source_path = package_build_source + .clone() + .make_relative_to(&self.manifest_source, workspace_root) + .map(|path| PackageBuildSource::Path { + path: Utf8TypedPathBuf::Unix(path), + }); - PackageBuildSource::Git { - url: pinned_git_spec.git, - spec, - rev: pinned_git_spec.source.commit.to_string(), - subdir: subdirectory, + if package_build_source_path.is_none() { + match package_build_source { + PinnedSourceSpec::Url(pinned_url_spec) => Some(PackageBuildSource::Url { + url: pinned_url_spec.url, + sha256: pinned_url_spec.sha256, + subdir: None, + }), + PinnedSourceSpec::Git(pinned_git_spec) => Some(PackageBuildSource::Git { + url: pinned_git_spec.git, + spec: to_git_shallow(&pinned_git_spec.source.reference), + rev: pinned_git_spec.source.commit.to_string(), + subdir: pinned_git_spec + .source + .subdirectory + .map(Utf8TypedPathBuf::from), + }), + PinnedSourceSpec::Path(pinned_path_spec) => Some(PackageBuildSource::Path { + path: pinned_path_spec.path, + }), } + } else { + package_build_source_path } - PinnedSourceSpec::Path(pinned_path) => PackageBuildSource::Path { - path: pinned_path.path, - }, - }); - CondaPackageData::Source(CondaSourceData { - package_record: value.package_record, - location: value.manifest_source.clone().into(), + } else { + None + }; + + CondaSourceData { + package_record: self.package_record, + location: self.manifest_source.clone().into(), package_build_source, - input: value.input_hash.map(|i| rattler_lock::InputHash { + input: self.input_hash.map(|i| rattler_lock::InputHash { hash: i.hash, - // TODO: fix this in rattler globs: Vec::from_iter(i.globs), }), - sources: value + sources: self .sources .into_iter() .map(|(k, v)| (k, v.into())) .collect(), - }) + } } -} -impl TryFrom for SourceRecord { - type Error = ParseLockFileError; + /// Create SourceRecord from CondaSourceData with paths resolved relative to workspace_root. + /// This should be used when reading from the lock file. + /// + /// The inverse of `into_conda_source_data`: + /// - manifest_source: relative to workspace_root (or absolute) → resolve to absolute + /// - build_source: relative to manifest_source (or absolute) → resolve to absolute + pub fn from_conda_source_data( + data: CondaSourceData, + workspace_root: &std::path::Path, + ) -> Result { + let manifest_source: PinnedSourceSpec = data.location.try_into()?; - fn try_from(value: CondaSourceData) -> Result { - let pinned_source_spec = value.package_build_source.map(|source| match source { + let build_source = data.package_build_source.map(|source| match source { PackageBuildSource::Git { url, spec, rev, subdir, } => { - let reference = match spec { - Some(GitShallowSpec::Branch(branch)) => GitReference::Branch(branch), - Some(GitShallowSpec::Tag(tag)) => GitReference::Tag(tag), - Some(GitShallowSpec::Rev) => GitReference::Rev(rev.clone()), - None => GitReference::DefaultBranch, - }; + // Check if this is a relative subdirectory (same repo checkout) + if let (Some(subdir), PinnedSourceSpec::Git(manifest_git)) = + (&subdir, &manifest_source) + { + if same_git_checkout_url_commit(manifest_git, &url, &rev) { + // The subdirectory is relative to the manifest, use from_relative_to + let relative_path = Utf8UnixPathBuf::from(subdir.as_str()); + return PinnedSourceSpec::from_relative_to( + relative_path, + &manifest_source, + workspace_root, + ) + .expect("from_relative_to should succeed for same-repo git checkouts, this is a bug"); + } + } + // Different repository + let reference = git_reference_from_shallow(spec, &rev); PinnedSourceSpec::Git(crate::PinnedGitSpec { git: url, source: PinnedGitCheckout { @@ -143,18 +172,34 @@ impl TryFrom for SourceRecord { md5: None, }), PackageBuildSource::Path { path } => { - PinnedSourceSpec::Path(crate::PinnedPathSpec { path }) + // Convert path to Unix format for from_relative_to + let path_unix = match path { + Utf8TypedPathBuf::Unix(ref p) => p, + // If its a windows path, it can *ONLY* be absolute per the `into_conda_source_data` method + // so let's return as-is + Utf8TypedPathBuf::Windows(path) => { + return PinnedSourceSpec::Path(crate::PinnedPathSpec { path: Utf8TypedPathBuf::Windows(path) }) + } + }; + + // Try to resolve relative to manifest_source, or use absolute path if that fails + PinnedSourceSpec::from_relative_to(path_unix.to_path_buf(), &manifest_source, workspace_root) + .unwrap_or( + // If from_relative_to returns None (absolute paths), use as-is + PinnedSourceSpec::Path(crate::PinnedPathSpec { path }) + ) } }); + Ok(Self { - package_record: value.package_record, - manifest_source: value.location.try_into()?, - input_hash: value.input.map(|hash| InputHash { + package_record: data.package_record, + manifest_source, + input_hash: data.input.map(|hash| InputHash { hash: hash.hash, globs: BTreeSet::from_iter(hash.globs), }), - build_source: pinned_source_spec, - sources: value + build_source, + sources: data .sources .into_iter() .map(|(k, v)| (k, SourceSpec::from(v))) @@ -201,6 +246,31 @@ impl AsRef for SourceRecord { } } +/// Returns true when the git URL and commit match the manifest git spec. +/// Used while parsing lock data where only the URL + rev string are available. +fn same_git_checkout_url_commit(manifest_git: &crate::PinnedGitSpec, url: &Url, rev: &str) -> bool { + RepositoryUrl::new(&manifest_git.git) == RepositoryUrl::new(url) + && manifest_git.source.commit.to_string() == rev +} + +fn to_git_shallow(reference: &GitReference) -> Option { + match reference { + GitReference::Branch(branch) => Some(GitShallowSpec::Branch(branch.clone())), + GitReference::Tag(tag) => Some(GitShallowSpec::Tag(tag.clone())), + GitReference::Rev(_) => Some(GitShallowSpec::Rev), + GitReference::DefaultBranch => None, + } +} + +fn git_reference_from_shallow(spec: Option, rev: &str) -> GitReference { + match spec { + Some(GitShallowSpec::Branch(branch)) => GitReference::Branch(branch), + Some(GitShallowSpec::Tag(tag)) => GitReference::Tag(tag), + Some(GitShallowSpec::Rev) => GitReference::Rev(rev.to_string()), + None => GitReference::DefaultBranch, + } +} + #[cfg(test)] mod tests { use super::*; @@ -209,8 +279,15 @@ mod tests { use std::str::FromStr; use url::Url; + use rattler_conda_types::Platform; + use rattler_lock::{ + Channel, CondaPackageData, DEFAULT_ENVIRONMENT_NAME, LockFile, LockFileBuilder, + }; + #[test] - fn package_build_source_roundtrip_preserves_git_subdirectory() { + fn package_build_source_path_is_made_relative() { + use typed_path::Utf8TypedPathBuf; + let package_record: PackageRecord = serde_json::from_value(json!({ "name": "example", "version": "1.0.0", @@ -220,57 +297,324 @@ mod tests { })) .expect("valid package record"); - let git_url = Url::parse("https://example.com/repo.git").unwrap(); - let pinned_source = PinnedSourceSpec::Git(crate::PinnedGitSpec { + // Manifest is in /workspace/recipes directory + let manifest_source = PinnedSourceSpec::Path(crate::PinnedPathSpec { + path: Utf8TypedPathBuf::from("/workspace/recipes"), + }); + + // Build source is in /workspace/src (sibling of recipes) + let build_source = PinnedSourceSpec::Path(crate::PinnedPathSpec { + path: Utf8TypedPathBuf::from("/workspace/src"), + }); + + let record = SourceRecord { + package_record, + manifest_source: manifest_source.clone(), + build_source: Some(build_source), + input_hash: None, + sources: Default::default(), + }; + + // Convert to CondaPackageData (serialization) + let conda_source = record + .clone() + .into_conda_source_data(&std::path::PathBuf::from("/workspace")); + + let package_build_source = conda_source + .package_build_source + .as_ref() + .expect("expected package build source"); + + let PackageBuildSource::Path { path } = package_build_source else { + panic!("expected path package build source"); + }; + + // Because manifest + build live in the same git repo we serialize the build as a git + // source with a subdir relative to the manifest checkout. + assert_eq!( + path.as_str(), + "../src", + "build_source should be relative to manifest_source directory" + ); + + // Convert back to SourceRecord (deserialization) and ensure we recover repo-root subdir + let roundtrip = SourceRecord::from_conda_source_data( + conda_source, + &std::path::PathBuf::from("/workspace"), + ) + .expect("roundtrip should succeed"); + + let Some(PinnedSourceSpec::Path(roundtrip_path)) = roundtrip.build_source else { + panic!("expected path pinned source"); + }; + + // After roundtrip the git subdirectory should be expressed from repo root again. + assert_eq!(roundtrip_path.path.as_str(), "src"); + } + + #[test] + fn package_build_source_roundtrip_git_with_subdir() { + let package_record: PackageRecord = serde_json::from_value(json!({ + "name": "example", + "version": "1.0.0", + "build": "0", + "build_number": 0, + "subdir": "noarch", + })) + .expect("valid package record"); + + let git_url = Url::parse("https://github.com/user/repo.git").unwrap(); + let commit = GitSha::from_str("0123456789abcdef0123456789abcdef01234567").unwrap(); + + // Manifest is in recipes/ subdirectory + let manifest_source = PinnedSourceSpec::Git(crate::PinnedGitSpec { + git: git_url.clone(), + source: PinnedGitCheckout { + commit, + subdirectory: Some("recipes".to_string()), + reference: GitReference::Branch("main".to_string()), + }, + }); + + // Build source is in src/ subdirectory (sibling of recipes) + let build_source = PinnedSourceSpec::Git(crate::PinnedGitSpec { git: git_url.clone(), source: PinnedGitCheckout { - commit: GitSha::from_str("0123456789abcdef0123456789abcdef01234567").unwrap(), - subdirectory: Some("nested/project".to_string()), + commit, + subdirectory: Some("src".to_string()), reference: GitReference::Branch("main".to_string()), }, }); let record = SourceRecord { package_record, - manifest_source: pinned_source.clone(), - build_source: Some(pinned_source.clone()), + manifest_source: manifest_source.clone(), + build_source: Some(build_source), input_hash: None, sources: Default::default(), }; - let CondaPackageData::Source(conda_source) = record.clone().into() else { - panic!("expected source package data"); - }; + // Convert to CondaPackageData (serialization) + let conda_source = record + .clone() + .into_conda_source_data(&std::path::PathBuf::from("/workspace")); let package_build_source = conda_source .package_build_source .as_ref() .expect("expected package build source"); - let PackageBuildSource::Git { - url, - spec, - rev, - subdir, - } = package_build_source - else { - panic!("expected git package build source"); + let PackageBuildSource::Path { path, .. } = package_build_source else { + panic!("expected path build source with relative subdir"); }; - assert_eq!(url.path(), "/repo.git"); - assert_eq!(url.host_str(), Some("example.com")); - assert_eq!(subdir.as_ref().map(|s| s.as_str()), Some("nested/project")); - assert!(matches!(spec, Some(GitShallowSpec::Branch(branch)) if branch == "main")); - assert_eq!(rev, "0123456789abcdef0123456789abcdef01234567"); + // Path is relative to manifest checkout (recipes -> ../src) + assert_eq!(path.as_str(), "../src"); - let roundtrip = SourceRecord::try_from(conda_source).expect("roundtrip should succeed"); - let Some(PinnedSourceSpec::Git(roundtrip_git)) = roundtrip.build_source else { - panic!("expected git pinned source"); + // Convert back to SourceRecord (deserialization) + let roundtrip = SourceRecord::from_conda_source_data( + conda_source, + &std::path::PathBuf::from("/workspace"), + ) + .expect("roundtrip should succeed"); + + let Some(PinnedSourceSpec::Git(roundtrip_path)) = roundtrip.build_source else { + panic!( + "expected path pinned source after roundtrip (deserialized from relative path in lock file)" + ); }; + + // After roundtrip, the path will contain .. components (not normalized) assert_eq!( - roundtrip_git.source.subdirectory.as_deref(), - Some("nested/project") + roundtrip_path + .source + .subdirectory + .expect("subdirectory should be set") + .as_str(), + "src" ); - assert_eq!(roundtrip_git.git, git_url); + } + + #[test] + fn package_build_source_git_different_repos_stays_git() { + let package_record: PackageRecord = serde_json::from_value(json!({ + "name": "example", + "version": "1.0.0", + "build": "0", + "build_number": 0, + "subdir": "noarch", + })) + .expect("valid package record"); + + let manifest_git_url = Url::parse("https://github.com/user/repo1.git").unwrap(); + let build_git_url = Url::parse("https://github.com/user/repo2.git").unwrap(); + let commit1 = GitSha::from_str("0123456789abcdef0123456789abcdef01234567").unwrap(); + let commit2 = GitSha::from_str("abcdef0123456789abcdef0123456789abcdef01").unwrap(); + + // Manifest is in one repository + let manifest_source = PinnedSourceSpec::Git(crate::PinnedGitSpec { + git: manifest_git_url.clone(), + source: PinnedGitCheckout { + commit: commit1, + subdirectory: Some("recipes".to_string()), + reference: GitReference::Branch("main".to_string()), + }, + }); + + // Build source is in a different repository + let build_source = PinnedSourceSpec::Git(crate::PinnedGitSpec { + git: build_git_url.clone(), + source: PinnedGitCheckout { + commit: commit2, + subdirectory: Some("src".to_string()), + reference: GitReference::Branch("main".to_string()), + }, + }); + + let record = SourceRecord { + package_record, + manifest_source: manifest_source.clone(), + build_source: Some(build_source), + input_hash: None, + sources: Default::default(), + }; + + // Convert to CondaPackageData (serialization) + let conda_source = record + .clone() + .into_conda_source_data(&std::path::PathBuf::from("/workspace")); + + let package_build_source = conda_source + .package_build_source + .as_ref() + .expect("expected package build source"); + + let PackageBuildSource::Git { url, subdir, .. } = package_build_source else { + panic!("expected git package build source (different repos should stay git)"); + }; + + // Different repositories - should stay as Git source + assert_eq!(url, &build_git_url); + assert_eq!(subdir.as_ref().map(|s| s.as_str()), Some("src")); + } + + #[test] + fn roundtrip_conda_source_data() { + let workspace_root = std::path::Path::new("/workspace"); + + // Load the lock file from the snapshot content (skip insta frontmatter). + let lock_source = lock_source_from_snapshot(); + let lock_file = LockFile::from_str(&lock_source).expect("failed to load lock file fixture"); + + // Extract Conda source packages from the lock file. + let environment = lock_file + .default_environment() + .expect("expected default environment"); + + let conda_sources: Vec = environment + .conda_packages_by_platform() + .flat_map(|(_, packages)| packages.filter_map(|pkg| pkg.as_source().cloned())) + .collect(); + + // Convert to SourceRecords and roundtrip back to CondaSourceData. + let roundtrip_records: Vec = conda_sources + .iter() + .map(|conda_data| { + SourceRecord::from_conda_source_data(conda_data.clone(), workspace_root) + .expect("from_conda_source_data should succeed") + }) + .collect(); + + let roundtrip_lock = build_lock_from_records(&roundtrip_records, workspace_root); + let mut settings = insta::Settings::clone_current(); + settings.set_sort_maps(true); + settings.bind(|| { + insta::assert_snapshot!(roundtrip_lock); + }); + } + + /// Extract the lock file body from the snapshot by skipping the insta frontmatter. + fn lock_source_from_snapshot() -> String { + let snapshot_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join( + "src/snapshots/pixi_record__source_record__tests__roundtrip_conda_source_data.snap", + ); + #[allow(clippy::disallowed_methods)] + let snap = std::fs::read_to_string(snapshot_path).expect("failed to read snapshot file"); + // Skip insta frontmatter (two --- delimiters) and return the lock file contents + snap.splitn(3, "---") + .nth(2) + .map(|s| s.trim_start_matches('\n')) + .unwrap_or_default() + .to_string() + } + + /// Build a lock file string from a set of SourceRecords. + fn build_lock_from_records( + records: &[SourceRecord], + workspace_root: &std::path::Path, + ) -> String { + let mut builder = LockFileBuilder::new(); + builder.set_channels( + DEFAULT_ENVIRONMENT_NAME, + [Channel::from("https://conda.anaconda.org/conda-forge/")], + ); + + for record in records { + let conda_data = + CondaPackageData::from(record.clone().into_conda_source_data(workspace_root)); + + let platform = Platform::from_str(&conda_data.record().subdir) + .expect("failed to parse platform from subdir"); + builder.add_conda_package(DEFAULT_ENVIRONMENT_NAME, platform, conda_data); + } + + builder + .finish() + .render_to_string() + .expect("failed to render lock file") + } + + #[test] + fn git_reference_conversion_helpers() { + use super::{git_reference_from_shallow, to_git_shallow}; + use pixi_spec::GitReference; + use rattler_lock::GitShallowSpec; + + assert!(matches!( + to_git_shallow(&GitReference::Branch("main".into())), + Some(GitShallowSpec::Branch(branch)) if branch == "main" + )); + + assert!(matches!( + to_git_shallow(&GitReference::Tag("v1".into())), + Some(GitShallowSpec::Tag(tag)) if tag == "v1" + )); + + assert!(matches!( + to_git_shallow(&GitReference::Rev("abc".into())), + Some(GitShallowSpec::Rev) + )); + + assert!(to_git_shallow(&GitReference::DefaultBranch).is_none()); + + assert!(matches!( + git_reference_from_shallow(Some(GitShallowSpec::Branch("dev".into())), "ignored"), + GitReference::Branch(branch) if branch == "dev" + )); + + assert!(matches!( + git_reference_from_shallow(Some(GitShallowSpec::Tag("v2".into())), "ignored"), + GitReference::Tag(tag) if tag == "v2" + )); + + assert!(matches!( + git_reference_from_shallow(Some(GitShallowSpec::Rev), "deadbeef"), + GitReference::Rev(rev) if rev == "deadbeef" + )); + + assert!(matches!( + git_reference_from_shallow(None, "deadbeef"), + GitReference::DefaultBranch + )); } } diff --git a/crates/pixi_spec/src/lib.rs b/crates/pixi_spec/src/lib.rs index 940d4922fe..7f0ca5aec0 100644 --- a/crates/pixi_spec/src/lib.rs +++ b/crates/pixi_spec/src/lib.rs @@ -362,14 +362,14 @@ 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)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SourceSpec { /// The location of the source. pub location: SourceLocationSpec, } /// A specification for a source location. -#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(untagged)] pub enum SourceLocationSpec { /// The spec is represented as an archive that can be downloaded from the diff --git a/crates/pixi_spec/src/source_anchor.rs b/crates/pixi_spec/src/source_anchor.rs index e9a0f12c60..35f05867f3 100644 --- a/crates/pixi_spec/src/source_anchor.rs +++ b/crates/pixi_spec/src/source_anchor.rs @@ -13,62 +13,55 @@ pub enum SourceAnchor { Workspace, /// The source is relative to another source package. - Source(SourceSpec), + Source(SourceLocationSpec), } impl From for SourceAnchor { fn from(value: SourceSpec) -> Self { + SourceAnchor::Source(value.location) + } +} + +impl From for SourceAnchor { + fn from(value: SourceLocationSpec) -> Self { SourceAnchor::Source(value) } } impl SourceAnchor { - /// Resolve a source spec relative to this anchor. - pub fn resolve(&self, spec: SourceSpec) -> SourceSpec { + /// Resolve a source location spec relative to this anchor. + pub fn resolve(&self, spec: SourceLocationSpec) -> SourceLocationSpec { // If this instance is already anchored to the workspace we can simply return // immediately. let SourceAnchor::Source(base) = self else { - return match spec.location { - SourceLocationSpec::Url(url) => SourceSpec { - location: SourceLocationSpec::Url(url), - }, - SourceLocationSpec::Git(git) => SourceSpec { - location: SourceLocationSpec::Git(git), - }, + return match spec { + SourceLocationSpec::Url(url) => SourceLocationSpec::Url(url), + SourceLocationSpec::Git(git) => SourceLocationSpec::Git(git), SourceLocationSpec::Path(PathSourceSpec { path }) => { - SourceSpec { - location: SourceLocationSpec::Path(PathSourceSpec { - // Normalize the input path. - path: normalize_typed(path.to_path()), - }), - } + SourceLocationSpec::Path(PathSourceSpec { + // Normalize the input path. + path: normalize_typed(path.to_path()), + }) } }; }; // Only path specs can be relative. - let SourceSpec { - location: SourceLocationSpec::Path(PathSourceSpec { path }), - } = spec - else { + let SourceLocationSpec::Path(PathSourceSpec { path }) = spec else { return spec; }; // If the path is absolute we can just return it. if path.is_absolute() || path.starts_with("~") { - return SourceSpec { - location: SourceLocationSpec::Path(PathSourceSpec { path }), - }; + return SourceLocationSpec::Path(PathSourceSpec { path }); } - match &base.location { + match base { SourceLocationSpec::Path(PathSourceSpec { path: base }) => { let relative_path = normalize_typed(base.join(path).to_path()); - SourceSpec { - location: SourceLocationSpec::Path(PathSourceSpec { - path: relative_path, - }), - } + SourceLocationSpec::Path(PathSourceSpec { + path: relative_path, + }) } SourceLocationSpec::Url(UrlSourceSpec { .. }) => { unimplemented!("Cannot resolve relative paths for URL sources") @@ -83,13 +76,11 @@ impl SourceAnchor { .join(path) .to_path(), ); - SourceSpec { - location: SourceLocationSpec::Git(GitSpec { - git: git.clone(), - rev: rev.clone(), - subdirectory: Some(relative_subdir.to_string()), - }), - } + SourceLocationSpec::Git(GitSpec { + git: git.clone(), + rev: rev.clone(), + subdirectory: Some(relative_subdir.to_string()), + }) } } } diff --git a/crates/pixi_spec/src/url.rs b/crates/pixi_spec/src/url.rs index 3a47aade01..96b7732dc4 100644 --- a/crates/pixi_spec/src/url.rs +++ b/crates/pixi_spec/src/url.rs @@ -97,7 +97,7 @@ impl Display for UrlSpec { /// A specification of a source archive from a URL. #[serde_as] -#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize)] +#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct UrlSourceSpec { /// The URL of the package pub url: Url, diff --git a/tests/data/satisfiability/out-of-tree-source-parent/CMakeLists.txt b/tests/data/satisfiability/out-of-tree-source-parent/CMakeLists.txt new file mode 100644 index 0000000000..5ff26babb6 --- /dev/null +++ b/tests/data/satisfiability/out-of-tree-source-parent/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.10) +project(out-of-tree-package) + +# Simple test project for out-of-tree builds +add_executable(test_app main.cpp) diff --git a/tests/data/satisfiability/out-of-tree-source-parent/main.cpp b/tests/data/satisfiability/out-of-tree-source-parent/main.cpp new file mode 100644 index 0000000000..67610f5bfd --- /dev/null +++ b/tests/data/satisfiability/out-of-tree-source-parent/main.cpp @@ -0,0 +1,6 @@ +#include + +int main() { + std::cout << "Hello from out-of-tree build!" << std::endl; + return 0; +} diff --git a/tests/data/satisfiability/out-of-tree-source-parent/package/pixi.toml b/tests/data/satisfiability/out-of-tree-source-parent/package/pixi.toml new file mode 100644 index 0000000000..d75fbd81c9 --- /dev/null +++ b/tests/data/satisfiability/out-of-tree-source-parent/package/pixi.toml @@ -0,0 +1,8 @@ +[package] +name = "out-of-tree-package" +version = "0.1.0" + +[package.build] +backend = { name = "pixi-build-cmake", version = "0.3.*" } +# Point to source files in a different directory (out-of-tree build) +source = { path = "../" } diff --git a/tests/data/satisfiability/out-of-tree-source-parent/pixi.lock b/tests/data/satisfiability/out-of-tree-source-parent/pixi.lock new file mode 100644 index 0000000000..0e42219605 --- /dev/null +++ b/tests/data/satisfiability/out-of-tree-source-parent/pixi.lock @@ -0,0 +1,81 @@ +version: 6 +environments: + default: + channels: + - url: https://prefix.dev/pixi-build-backends/ + - url: https://prefix.dev/conda-forge/ + packages: + linux-64: + - conda: https://prefix.dev/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://prefix.dev/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda + - conda: https://prefix.dev/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda + - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda + - conda: ./package +packages: +- conda: https://prefix.dev/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + size: 2562 + timestamp: 1578324546067 +- conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + size: 23621 + timestamp: 1650670423406 +- conda: https://prefix.dev/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda + sha256: 08f9b87578ab981c7713e4e6a7d935e40766e10691732bba376d4964562bcb45 + md5: c0374badb3a5d4b1372db28d19462c53 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgomp 15.2.0 h767d61c_7 + - libgcc-ng ==15.2.0=*_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 822552 + timestamp: 1759968052178 +- conda: https://prefix.dev/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda + sha256: e9fb1c258c8e66ee278397b5822692527c5f5786d372fe7a869b900853f3f5ca + md5: f7b4d76975aac7e5d9e6ad13845f92fe + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 447919 + timestamp: 1759967942498 +- conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda + sha256: 1b981647d9775e1cdeb2fab0a4dd9cd75a6b0de2963f6c3953dbd712f78334b3 + md5: 5b767048b1b3ee9a954b06f4084f93dc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 h767d61c_7 + constrains: + - libstdcxx-ng ==15.2.0=*_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 3898269 + timestamp: 1759968103436 +- conda: ./package + name: out-of-tree-package + version: 0.1.0 + build: hb0f4dca_0 + subdir: linux-64 + depends: + - libstdcxx >=15 + - libgcc >=15 + input: + hash: 616e7d8611fa803e6655c354f183d6f4b405e94adcabc83c260d8b9da5e3c9d8 + globs: [] + package_build_source: + path: .. diff --git a/tests/data/satisfiability/out-of-tree-source-parent/pixi.toml b/tests/data/satisfiability/out-of-tree-source-parent/pixi.toml new file mode 100644 index 0000000000..e319b11972 --- /dev/null +++ b/tests/data/satisfiability/out-of-tree-source-parent/pixi.toml @@ -0,0 +1,10 @@ +[workspace] +channels = [ + "https://prefix.dev/pixi-build-backends", + "https://prefix.dev/conda-forge", +] +platforms = ["linux-64"] +preview = ["pixi-build"] + +[dependencies] +out-of-tree-package = { path = "./package" } diff --git a/tests/data/satisfiability/out-of-tree-source/pixi.lock b/tests/data/satisfiability/out-of-tree-source/pixi.lock index f0d728af97..473a6559f3 100644 --- a/tests/data/satisfiability/out-of-tree-source/pixi.lock +++ b/tests/data/satisfiability/out-of-tree-source/pixi.lock @@ -69,7 +69,7 @@ packages: - conda: build-config name: out-of-tree-package version: 0.1.0 - build: hbf21a9e_0 + build: hb0f4dca_0 subdir: linux-64 depends: - libstdcxx >=15