1pub mod compat;
12mod constraint;
13mod context;
14mod filter;
15mod package;
16mod path;
17mod skill;
18mod types;
19mod version;
20
21use std::path::Path;
22
23#[cfg(test)]
24use indexmap::IndexMap;
25
26pub use constraint::parse_version_constraint;
27pub use context::ResolverContext;
28pub use types::*;
29
30pub(crate) use package::{PackageResolutionState, PendingSource, RegisteredPackage};
31#[cfg(test)]
32pub(crate) use path::apply_subpath;
33
34use crate::config::{EffectiveConfig, Manifest, SourceSpec};
35use crate::diagnostic::DiagnosticCollector;
36use crate::error::{MarsError, ResolutionError};
37use crate::lock::LockFile;
38use crate::source::{AvailableVersion, ResolvedRef};
39#[cfg(test)]
40use crate::types::SourceName;
41use crate::types::SourceUrl;
42use filter::is_item_excluded;
43use package::resolve_package_bottom_up;
44use skill::{parse_pending_item_skill_deps, resolve_skill_ref};
45use version::validate_all_constraints;
46
47#[derive(Debug)]
48enum VersionAction {
49 Process,
50 Skip,
51}
52
53fn apply_item_version_policy(
54 pending_item: &PendingItem,
55 check: VersionCheckResult,
56 diag: &mut DiagnosticCollector,
57) -> Result<VersionAction, ResolutionError> {
58 match check {
59 VersionCheckResult::NotSeen => Ok(VersionAction::Process),
60 VersionCheckResult::SameVersion => Ok(VersionAction::Skip),
61 VersionCheckResult::PotentiallyConflicting {
62 existing,
63 requested,
64 } => {
65 diag.warn(
66 "potential-version-drift",
67 format!(
68 "potential version drift: item '{}' from '{}' requested as {} but already seen as {}",
69 pending_item.item, pending_item.package, requested, existing
70 ),
71 );
72 Ok(VersionAction::Skip)
73 }
74 VersionCheckResult::DifferentVersion {
75 existing,
76 requested,
77 } => {
78 if pending_item.is_local {
79 return Ok(VersionAction::Skip);
80 }
81 Err(ResolutionError::ItemVersionConflict {
82 item: pending_item.item.to_string(),
83 package: pending_item.package.to_string(),
84 existing: existing.to_string(),
85 requested: requested.to_string(),
86 chain: pending_item.required_by.clone(),
87 })
88 }
89 }
90}
91
92pub trait VersionLister {
94 fn list_versions(&self, url: &SourceUrl) -> Result<Vec<AvailableVersion>, MarsError>;
95}
96
97pub trait SourceFetcher {
99 fn fetch_git_version(
101 &self,
102 url: &SourceUrl,
103 version: &AvailableVersion,
104 source_name: &str,
105 preferred_commit: Option<&str>,
106 diag: &mut DiagnosticCollector,
107 ) -> Result<ResolvedRef, MarsError>;
108
109 fn fetch_git_ref(
111 &self,
112 url: &SourceUrl,
113 ref_name: &str,
114 source_name: &str,
115 preferred_commit: Option<&str>,
116 diag: &mut DiagnosticCollector,
117 ) -> Result<ResolvedRef, MarsError>;
118
119 fn fetch_path(
121 &self,
122 path: &Path,
123 source_name: &str,
124 diag: &mut DiagnosticCollector,
125 ) -> Result<ResolvedRef, MarsError>;
126}
127
128pub trait ManifestReader {
130 fn read_manifest(
131 &self,
132 source_tree: &Path,
133 diag: &mut DiagnosticCollector,
134 ) -> Result<Option<Manifest>, MarsError>;
135}
136
137pub trait SourceProvider: VersionLister + SourceFetcher + ManifestReader {}
139
140impl<T> SourceProvider for T where T: VersionLister + SourceFetcher + ManifestReader {}
141
142pub fn resolve(
152 config: &EffectiveConfig,
153 provider: &dyn SourceProvider,
154 locked: Option<&LockFile>,
155 options: &ResolveOptions,
156 diag: &mut DiagnosticCollector,
157) -> Result<ResolvedGraph, MarsError> {
158 let mut ctx = ResolverContext::new();
159
160 let mut direct_requests: Vec<PendingSource> = Vec::new();
161 for (name, source) in &config.dependencies {
162 let is_upgrade_target = options.maximize
163 && (options.upgrade_targets.is_empty() || options.upgrade_targets.contains(name));
164 let constraint = match &source.spec {
165 SourceSpec::Git(git) => {
166 if options.bump_direct_constraints && is_upgrade_target {
167 VersionConstraint::Latest
168 } else {
169 parse_version_constraint(git.version.as_deref())
170 }
171 }
172 SourceSpec::Path(_) => VersionConstraint::Latest,
173 };
174 direct_requests.push(PendingSource {
175 name: name.clone(),
176 source_id: source.id.clone(),
177 spec: source.spec.clone(),
178 subpath: source.subpath.clone(),
179 constraint,
180 filter: source.filter.clone(),
181 required_by: "mars.toml".to_string(),
182 });
183 }
184
185 for request in direct_requests
186 .iter()
187 .filter(|request| filter::is_unfiltered_request(&request.filter))
188 {
189 resolve_package_bottom_up(request, true, provider, locked, options, diag, &mut ctx)?;
190 }
191 for request in direct_requests
192 .iter()
193 .filter(|request| !filter::is_unfiltered_request(&request.filter))
194 {
195 resolve_package_bottom_up(request, true, provider, locked, options, diag, &mut ctx)?;
196 }
197
198 while let Some(pending_item) = ctx.pop_pending() {
199 let (resolved_ref, skill_deps) = {
200 let Some(package) = ctx.registry().get(&pending_item.package) else {
201 return Err(ResolutionError::SourceNotFound {
202 name: pending_item.package.to_string(),
203 }
204 .into());
205 };
206
207 if package
208 .item(pending_item.kind, &pending_item.item)
209 .is_none()
210 {
211 continue;
212 }
213
214 let skill_deps = parse_pending_item_skill_deps(&pending_item, package)?;
215 (package.node.resolved_ref.clone(), skill_deps)
216 };
217
218 match apply_item_version_policy(
219 &pending_item,
220 ctx.visited().check_version(
221 &pending_item.package,
222 &pending_item.item,
223 &pending_item.constraint,
224 ),
225 diag,
226 )
227 .map_err(MarsError::from)?
228 {
229 VersionAction::Process => {}
230 VersionAction::Skip => continue,
231 }
232
233 ctx.package_versions_mut()
234 .check_or_insert(
235 &pending_item.package,
236 &resolved_ref,
237 &pending_item.constraint,
238 &pending_item.required_by,
239 pending_item.is_local,
240 )
241 .map_err(MarsError::from)?;
242
243 ctx.visited_mut().insert(
244 pending_item.package.clone(),
245 pending_item.item.clone(),
246 pending_item.constraint.clone(),
247 resolved_ref,
248 );
249
250 for skill_dep in skill_deps {
251 let resolved_skill = resolve_skill_ref(
252 &skill_dep,
253 &pending_item,
254 ctx.registry(),
255 ctx.version_constraints(),
256 )?;
257 if is_item_excluded(
258 ctx.materialization_filters(),
259 ctx.registry(),
260 &resolved_skill.package,
261 resolved_skill.kind,
262 &resolved_skill.item,
263 ) {
264 continue;
265 }
266 ctx.add_filter(
267 &resolved_skill.package,
268 crate::config::FilterMode::Include {
269 agents: Vec::new(),
270 skills: vec![resolved_skill.item.clone()],
271 },
272 );
273 ctx.push_pending(resolved_skill);
274 }
275 }
276
277 let version_constraints = ctx.version_constraints().clone();
278 let graph = ctx.into_graph();
279
280 validate_all_constraints(&graph.nodes, &version_constraints)?;
281
282 Ok(graph)
283}
284
285#[cfg(test)]
286fn alphabetical_order(nodes: &IndexMap<SourceName, ResolvedNode>) -> Vec<SourceName> {
287 let mut order: Vec<SourceName> = nodes.keys().cloned().collect();
288 order.sort();
289 order
290}
291
292#[cfg(test)]
293mod tests;