Skip to content

Commit 34da2b9

Browse files
committed
wip: expand dev sources
1 parent 7718c26 commit 34da2b9

File tree

3 files changed

+223
-0
lines changed

3 files changed

+223
-0
lines changed

crates/pixi_command_dispatcher/src/command_dispatcher/mod.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,41 @@ impl CommandDispatcher {
510510
spec.request(self.clone()).await
511511
}
512512

513+
/// Expands a list of dev sources into their dependencies.
514+
///
515+
/// Dev sources are source packages whose dependencies should be installed
516+
/// without building the packages themselves. This is particularly useful
517+
/// for development environments where you want to work on a package while
518+
/// having all its dependencies available.
519+
///
520+
/// For each dev source, this method:
521+
/// 1. Checks out the source code
522+
/// 2. Extracts all dependencies (build, host, and run) and constraints
523+
/// 3. Filters out dependencies that are themselves dev sources (to avoid
524+
/// duplication)
525+
///
526+
/// The result can then be passed to `solve_pixi_environment` to create
527+
/// an environment with all the dependencies but without the dev source
528+
/// packages themselves.
529+
///
530+
/// # Example use case
531+
///
532+
/// You're developing two packages (`pkgA` and `pkgB`) where `pkgA` depends
533+
/// on `pkgB`. Instead of building both packages, you can use
534+
/// `expand_dev_sources` to:
535+
/// 1. Get the dependencies of both `pkgA` and `pkgB`
536+
/// 2. Remove `pkgB` from `pkgA`'s dependencies (since it's also a dev source)
537+
/// 3. Create an environment with just the external dependencies
538+
///
539+
/// This allows you to work on the source code of both packages directly.
540+
pub async fn expand_dev_sources(
541+
&self,
542+
spec: crate::ExpandDevSourcesSpec,
543+
) -> Result<crate::ExpandedDevSources, CommandDispatcherError<crate::ExpandDevSourcesError>>
544+
{
545+
spec.request(self.clone()).await
546+
}
547+
513548
/// Calls into a pixi build backend to perform a source build.
514549
pub(crate) async fn backend_source_build(
515550
&self,
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
}

crates/pixi_command_dispatcher/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ mod command_dispatcher;
4040
mod command_dispatcher_processor;
4141
mod discover_backend_cache;
4242
mod executor;
43+
mod expand_dev_sources;
4344
mod install_pixi;
4445
mod instantiate_tool_env;
4546
mod limits;
@@ -68,6 +69,9 @@ pub use command_dispatcher::{
6869
CommandDispatcherErrorResultExt, InstantiateBackendError, InstantiateBackendSpec,
6970
};
7071
pub use executor::Executor;
72+
pub use expand_dev_sources::{
73+
DependencyOnlySource, ExpandDevSourcesError, ExpandDevSourcesSpec, ExpandedDevSources,
74+
};
7175
pub use install_pixi::{
7276
InstallPixiEnvironmentError, InstallPixiEnvironmentResult, InstallPixiEnvironmentSpec,
7377
};

0 commit comments

Comments
 (0)