|
| 1 | +use std::collections::{BTreeMap, HashSet}; |
| 2 | + |
| 3 | +use miette::Diagnostic; |
| 4 | +use pixi_build_discovery::EnabledProtocols; |
| 5 | +use pixi_spec::{BinarySpec, PixiSpec, SourceSpec}; |
| 6 | +use pixi_spec_containers::DependencyMap; |
| 7 | +use rattler_conda_types::{ChannelConfig, ChannelUrl, PackageName}; |
| 8 | +use thiserror::Error; |
| 9 | +use tracing::instrument; |
| 10 | + |
| 11 | +use crate::{ |
| 12 | + BuildEnvironment, CommandDispatcher, CommandDispatcherError, CommandDispatcherErrorResultExt, |
| 13 | + GetOutputDependenciesError, GetOutputDependenciesSpec, SourceCheckoutError, |
| 14 | +}; |
| 15 | + |
| 16 | +/// A source package whose dependencies should be installed without building |
| 17 | +/// the package itself. This is useful for development environments where you |
| 18 | +/// want the dependencies of a package but not the package itself. |
| 19 | +#[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize)] |
| 20 | +pub struct DependencyOnlySource { |
| 21 | + /// The source specification (path/git/url) |
| 22 | + pub source: SourceSpec, |
| 23 | + |
| 24 | + /// The name of the output to extract dependencies from |
| 25 | + pub output_name: PackageName, |
| 26 | +} |
| 27 | + |
| 28 | +/// Result of expanding dev_sources into their dependencies. |
| 29 | +#[derive(Debug, Clone, Default)] |
| 30 | +pub struct ExpandedDevSources { |
| 31 | + /// All dependencies (build, host, and run) extracted from dev_sources |
| 32 | + pub dependencies: DependencyMap<PackageName, PixiSpec>, |
| 33 | + |
| 34 | + /// All constraints (build, host, and run) extracted from dev_sources |
| 35 | + pub constraints: DependencyMap<PackageName, BinarySpec>, |
| 36 | +} |
| 37 | + |
| 38 | +/// A specification for expanding dev sources into their dependencies. |
| 39 | +/// |
| 40 | +/// Dev sources are source packages whose dependencies should be installed |
| 41 | +/// without building the packages themselves. This is useful for development |
| 42 | +/// environments where you want to work on a package while having its |
| 43 | +/// dependencies available. |
| 44 | +#[derive(Debug, Clone, serde::Serialize)] |
| 45 | +pub struct ExpandDevSourcesSpec { |
| 46 | + /// The list of dev sources to expand |
| 47 | + pub dev_sources: Vec<DependencyOnlySource>, |
| 48 | + |
| 49 | + /// The channel configuration to use |
| 50 | + pub channel_config: ChannelConfig, |
| 51 | + |
| 52 | + /// The channels to use for solving |
| 53 | + #[serde(skip_serializing_if = "Vec::is_empty")] |
| 54 | + pub channels: Vec<ChannelUrl>, |
| 55 | + |
| 56 | + /// Information about the build environment |
| 57 | + pub build_environment: BuildEnvironment, |
| 58 | + |
| 59 | + /// Variant configuration |
| 60 | + pub variants: Option<BTreeMap<String, Vec<String>>>, |
| 61 | + |
| 62 | + /// The protocols that are enabled for source packages |
| 63 | + #[serde(skip_serializing_if = "crate::is_default")] |
| 64 | + pub enabled_protocols: EnabledProtocols, |
| 65 | +} |
| 66 | + |
| 67 | +/// An error that can occur while expanding dev sources. |
| 68 | +#[derive(Debug, Error, Diagnostic)] |
| 69 | +pub enum ExpandDevSourcesError { |
| 70 | + #[error("failed to checkout source for package '{name}'")] |
| 71 | + SourceCheckout { |
| 72 | + name: String, |
| 73 | + #[source] |
| 74 | + #[diagnostic_source] |
| 75 | + error: SourceCheckoutError, |
| 76 | + }, |
| 77 | + |
| 78 | + #[error("failed to get output dependencies for package '{}'", .name.as_source())] |
| 79 | + GetOutputDependencies { |
| 80 | + name: PackageName, |
| 81 | + #[source] |
| 82 | + #[diagnostic_source] |
| 83 | + error: GetOutputDependenciesError, |
| 84 | + }, |
| 85 | +} |
| 86 | + |
| 87 | +impl ExpandDevSourcesSpec { |
| 88 | + /// Expands dev_sources into their dependencies. |
| 89 | + /// |
| 90 | + /// When a dev_source has a dependency on another package that is also |
| 91 | + /// in dev_sources, that dependency is not added to the result (since it |
| 92 | + /// will be processed separately). |
| 93 | + #[instrument( |
| 94 | + skip_all, |
| 95 | + name = "expand-dev-sources", |
| 96 | + fields( |
| 97 | + count = self.dev_sources.len(), |
| 98 | + platform = %self.build_environment.host_platform, |
| 99 | + ) |
| 100 | + )] |
| 101 | + pub(crate) async fn request( |
| 102 | + self, |
| 103 | + command_dispatcher: CommandDispatcher, |
| 104 | + ) -> Result<ExpandedDevSources, CommandDispatcherError<ExpandDevSourcesError>> { |
| 105 | + let mut result = ExpandedDevSources::default(); |
| 106 | + |
| 107 | + // Create a lookup set for dev_sources by output_name |
| 108 | + // We'll use this to skip dependencies that are also dev_sources |
| 109 | + // TODO: In the future, we might want to also match by source location for more |
| 110 | + // precision |
| 111 | + let dev_source_names: HashSet<_> = self |
| 112 | + .dev_sources |
| 113 | + .iter() |
| 114 | + .map(|ds| ds.output_name.clone()) |
| 115 | + .collect(); |
| 116 | + |
| 117 | + // Process each dev_source |
| 118 | + for dev_source in self.dev_sources { |
| 119 | + // Pin and checkout the source |
| 120 | + let pinned_source = command_dispatcher |
| 121 | + .pin_and_checkout(dev_source.source.clone()) |
| 122 | + .await |
| 123 | + .map_err_with(|error| ExpandDevSourcesError::SourceCheckout { |
| 124 | + name: dev_source.output_name.as_source().to_string(), |
| 125 | + error, |
| 126 | + })?; |
| 127 | + |
| 128 | + // Get the output dependencies |
| 129 | + let spec = GetOutputDependenciesSpec { |
| 130 | + source: pinned_source.pinned, |
| 131 | + output_name: dev_source.output_name.clone(), |
| 132 | + channel_config: self.channel_config.clone(), |
| 133 | + channels: self.channels.clone(), |
| 134 | + build_environment: self.build_environment.clone(), |
| 135 | + variants: self.variants.clone(), |
| 136 | + enabled_protocols: self.enabled_protocols.clone(), |
| 137 | + }; |
| 138 | + |
| 139 | + let output_deps = command_dispatcher |
| 140 | + .get_output_dependencies(spec) |
| 141 | + .await |
| 142 | + .map_err_with(|error| ExpandDevSourcesError::GetOutputDependencies { |
| 143 | + name: dev_source.output_name.clone(), |
| 144 | + error, |
| 145 | + })?; |
| 146 | + |
| 147 | + // Helper to process dependencies |
| 148 | + let mut process_deps = |deps: Option<DependencyMap<PackageName, PixiSpec>>| { |
| 149 | + if let Some(deps) = deps { |
| 150 | + for (name, spec) in deps.into_specs() { |
| 151 | + // Skip dependencies that are also dev_sources |
| 152 | + // TODO: Currently matching by name only. In the future, we might want to |
| 153 | + // also check if the source location matches for more precise matching. |
| 154 | + if !dev_source_names.contains(&name) { |
| 155 | + result.dependencies.insert(name, spec); |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + }; |
| 160 | + |
| 161 | + // Process all dependency types |
| 162 | + process_deps(output_deps.build_dependencies); |
| 163 | + process_deps(output_deps.host_dependencies); |
| 164 | + process_deps(Some(output_deps.run_dependencies)); |
| 165 | + |
| 166 | + // Merge constraints |
| 167 | + if let Some(build_constraints) = output_deps.build_constraints { |
| 168 | + for (name, spec) in build_constraints.into_specs() { |
| 169 | + result.constraints.insert(name, spec); |
| 170 | + } |
| 171 | + } |
| 172 | + if let Some(host_constraints) = output_deps.host_constraints { |
| 173 | + for (name, spec) in host_constraints.into_specs() { |
| 174 | + result.constraints.insert(name, spec); |
| 175 | + } |
| 176 | + } |
| 177 | + for (name, spec) in output_deps.run_constraints.into_specs() { |
| 178 | + result.constraints.insert(name, spec); |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + Ok(result) |
| 183 | + } |
| 184 | +} |
0 commit comments