1pub mod compat;
12mod constraint;
13mod context;
14mod filter;
15mod package;
16mod path;
17mod requires;
18mod skill;
19mod types;
20mod version;
21
22use std::collections::HashMap;
23use std::path::Path;
24
25#[cfg(test)]
26use indexmap::IndexMap;
27
28pub use constraint::parse_version_constraint;
29pub use context::ResolverContext;
30pub(crate) use requires::{
31 check_consumer_package_requirements, validate_package_requirement_syntax,
32};
33pub use types::*;
34
35pub(crate) use package::{PackageResolutionState, PendingSource, RegisteredPackage};
36#[cfg(test)]
37pub(crate) use path::apply_subpath;
38
39use crate::config::{EffectiveConfig, Manifest, SourceSpec};
40use crate::diagnostic::DiagnosticCollector;
41use crate::error::{MarsError, ResolutionError};
42use crate::lock::LockFile;
43use crate::source::{AvailableVersion, ResolvedRef};
44use crate::types::SourceName;
45use crate::types::SourceUrl;
46use filter::is_item_excluded;
47use package::resolve_package_bottom_up;
48use skill::{parse_pending_item_skill_deps, resolve_skill_ref};
49use version::validate_all_constraints;
50
51type EngineExclusions =
52 HashMap<(SourceName, semver::Version), Vec<requires::EngineRequirementFailure>>;
53
54#[derive(Debug)]
55enum VersionAction {
56 Process,
57 Skip,
58}
59
60fn apply_item_version_policy(
61 pending_item: &PendingItem,
62 check: VersionCheckResult,
63 diag: &mut DiagnosticCollector,
64) -> Result<VersionAction, ResolutionError> {
65 match check {
66 VersionCheckResult::NotSeen => Ok(VersionAction::Process),
67 VersionCheckResult::SameVersion => Ok(VersionAction::Skip),
68 VersionCheckResult::PotentiallyConflicting {
69 existing,
70 requested,
71 } => {
72 diag.warn(
73 "potential-version-drift",
74 format!(
75 "potential version drift: item '{}' from '{}' requested as {} but already seen as {}",
76 pending_item.item, pending_item.package, requested, existing
77 ),
78 );
79 Ok(VersionAction::Skip)
80 }
81 VersionCheckResult::DifferentVersion {
82 existing,
83 requested,
84 } => {
85 if pending_item.is_local {
86 return Ok(VersionAction::Skip);
87 }
88 Err(ResolutionError::ItemVersionConflict {
89 item: pending_item.item.to_string(),
90 package: pending_item.package.to_string(),
91 existing: existing.to_string(),
92 requested: requested.to_string(),
93 chain: pending_item.required_by.clone(),
94 })
95 }
96 }
97}
98
99fn same_resolved_ref(a: &ResolvedRef, b: &ResolvedRef) -> bool {
100 a.version == b.version
101 && a.version_tag == b.version_tag
102 && a.commit == b.commit
103 && a.tree_path == b.tree_path
104}
105
106fn describe_resolved_ref(resolved: &ResolvedRef) -> String {
107 let version = resolved
108 .version_tag
109 .clone()
110 .or_else(|| resolved.version.as_ref().map(ToString::to_string))
111 .unwrap_or_else(|| "no-version".to_string());
112 let commit = resolved.commit.as_deref().unwrap_or("no-commit");
113 format!("{version}@{commit}")
114}
115
116pub trait VersionLister {
118 fn list_versions(&self, url: &SourceUrl) -> Result<Vec<AvailableVersion>, MarsError>;
119}
120
121pub trait SourceFetcher {
123 fn fetch_git_version(
125 &self,
126 url: &SourceUrl,
127 version: &AvailableVersion,
128 source_name: &str,
129 preferred_commit: Option<&str>,
130 diag: &mut DiagnosticCollector,
131 ) -> Result<ResolvedRef, MarsError>;
132
133 fn fetch_git_ref(
135 &self,
136 url: &SourceUrl,
137 ref_name: &str,
138 source_name: &str,
139 preferred_commit: Option<&str>,
140 diag: &mut DiagnosticCollector,
141 ) -> Result<ResolvedRef, MarsError>;
142
143 fn fetch_git_commit(
145 &self,
146 url: &SourceUrl,
147 commit: &str,
148 source_name: &str,
149 diag: &mut DiagnosticCollector,
150 ) -> Result<ResolvedRef, MarsError>;
151
152 fn fetch_path(
154 &self,
155 path: &Path,
156 source_name: &str,
157 diag: &mut DiagnosticCollector,
158 ) -> Result<ResolvedRef, MarsError>;
159}
160
161pub trait ManifestReader {
163 fn read_manifest(
164 &self,
165 source_tree: &Path,
166 diag: &mut DiagnosticCollector,
167 ) -> Result<Option<Manifest>, MarsError>;
168}
169
170pub trait SourceProvider: VersionLister + SourceFetcher + ManifestReader {}
172
173impl<T> SourceProvider for T where T: VersionLister + SourceFetcher + ManifestReader {}
174
175pub fn resolve(
207 config: &EffectiveConfig,
208 provider: &dyn SourceProvider,
209 locked: Option<&LockFile>,
210 options: &ResolveOptions,
211 diag: &mut DiagnosticCollector,
212) -> Result<ResolvedGraph, MarsError> {
213 let direct_requests: Vec<PendingSource> = {
215 let mut reqs = Vec::new();
216 for (name, source) in &config.dependencies {
217 let constraint = match &source.spec {
218 SourceSpec::Git(git) => options
219 .direct_constraint_for(name, parse_version_constraint(git.version.as_deref())),
220 SourceSpec::Path(_) => VersionConstraint::Latest,
221 };
222 reqs.push(PendingSource {
223 name: name.clone(),
224 declared_source_id: source.declared_source_id.clone(),
225 source_id: source.source_id.clone(),
226 spec: source.spec.clone(),
227 subpath: source.subpath.clone(),
228 constraint,
229 filter: source.filter.clone(),
230 required_by: "mars.toml".to_string(),
231 });
232 }
233 reqs
234 };
235
236 let mut version_overrides: HashMap<
239 SourceName,
240 (
241 ResolvedRef,
242 RootedSourceRef,
243 crate::staging::HookSurfaceState,
244 ),
245 > = HashMap::new();
246 let mut restart_history: HashMap<SourceName, Vec<ResolvedRef>> = HashMap::new();
248 let mut exclusions = EngineExclusions::new();
249
250 let ctx = loop {
254 let mut ctx = ResolverContext::new();
255 ctx.set_version_overrides(version_overrides.clone());
256
257 let bottom_up_result = (|| -> Result<(), MarsError> {
259 for request in direct_requests
260 .iter()
261 .filter(|request| filter::is_unfiltered_request(&request.filter))
262 {
263 resolve_package_bottom_up(
264 request,
265 true,
266 provider,
267 locked,
268 options,
269 config,
270 diag,
271 &mut ctx,
272 &mut exclusions,
273 )?;
274 }
275 for request in direct_requests
276 .iter()
277 .filter(|request| !filter::is_unfiltered_request(&request.filter))
278 {
279 resolve_package_bottom_up(
280 request,
281 true,
282 provider,
283 locked,
284 options,
285 config,
286 diag,
287 &mut ctx,
288 &mut exclusions,
289 )?;
290 }
291 Ok(())
292 })();
293
294 match bottom_up_result {
295 Err(MarsError::ResolutionRestartNeeded { package }) => {
296 version_overrides = ctx.version_overrides().clone();
297 let Some((pkg_name, new_ref, new_rooted, hook_surface)) =
299 ctx.take_pending_restart()
300 else {
301 return Err(MarsError::Internal(format!(
302 "missing pending restart payload for `{package}`"
303 )));
304 };
305 let history = restart_history.entry(pkg_name.clone()).or_default();
306 if let Some(cycle_start) = history
307 .iter()
308 .position(|seen| same_resolved_ref(seen, &new_ref))
309 {
310 let mut cycle: Vec<String> = history[cycle_start..]
311 .iter()
312 .map(describe_resolved_ref)
313 .collect();
314 cycle.push(describe_resolved_ref(&new_ref));
315 return Err(MarsError::Resolution(ResolutionError::VersionConflict {
316 name: pkg_name.to_string(),
317 message: format!(
318 "resolution oscillation detected for `{pkg_name}`: {}",
319 cycle.join(" -> ")
320 ),
321 }));
322 }
323 history.push(new_ref.clone());
324 version_overrides.insert(pkg_name, (new_ref, new_rooted, hook_surface));
325 continue;
327 }
328 Err(other) => return Err(other),
329 Ok(()) => break ctx,
330 }
331 };
332
333 let mut ctx = ctx;
335 while let Some(pending_item) = ctx.pop_pending() {
336 let (resolved_ref, skill_deps) = {
337 let Some(package) = ctx.registry().get(&pending_item.package) else {
338 return Err(ResolutionError::SourceNotFound {
339 name: pending_item.package.to_string(),
340 }
341 .into());
342 };
343
344 if package
345 .item(pending_item.kind, &pending_item.item)
346 .is_none()
347 {
348 continue;
349 }
350
351 let skill_deps = parse_pending_item_skill_deps(&pending_item, package)?;
352 (package.node.resolved_ref.clone(), skill_deps)
353 };
354
355 match apply_item_version_policy(
356 &pending_item,
357 ctx.visited().check_version(
358 &pending_item.package,
359 &pending_item.item,
360 &pending_item.constraint,
361 ),
362 diag,
363 )
364 .map_err(MarsError::from)?
365 {
366 VersionAction::Process => {}
367 VersionAction::Skip => continue,
368 }
369
370 ctx.package_versions_mut()
371 .check_or_insert(
372 &pending_item.package,
373 &resolved_ref,
374 &pending_item.constraint,
375 &pending_item.required_by,
376 pending_item.is_local,
377 )
378 .map_err(MarsError::from)?;
379
380 ctx.visited_mut().insert(
381 pending_item.package.clone(),
382 pending_item.item.clone(),
383 pending_item.constraint.clone(),
384 resolved_ref,
385 );
386
387 for skill_dep in skill_deps {
388 let resolved_skill = resolve_skill_ref(
389 &skill_dep,
390 &pending_item,
391 ctx.registry(),
392 ctx.version_constraints(),
393 )?;
394 if is_item_excluded(
395 ctx.materialization_filters(),
396 ctx.registry(),
397 &resolved_skill.package,
398 resolved_skill.kind,
399 &resolved_skill.item,
400 ) {
401 continue;
402 }
403 ctx.add_filter(
404 &resolved_skill.package,
405 crate::config::FilterMode::Include {
406 agents: Vec::new(),
407 skills: vec![resolved_skill.item.clone()],
408 },
409 );
410 ctx.push_pending(resolved_skill);
411 }
412 }
413
414 let version_constraints = ctx.version_constraints().clone();
415 let graph = ctx.into_graph();
416
417 validate_all_constraints(&graph.nodes, &version_constraints)?;
418 diag.reconcile_engine_fallbacks(&graph);
419
420 Ok(graph)
421}
422
423#[cfg(test)]
424fn alphabetical_order(nodes: &IndexMap<SourceName, ResolvedNode>) -> Vec<SourceName> {
425 let mut order: Vec<SourceName> = nodes.keys().cloned().collect();
426 order.sort();
427 order
428}
429
430#[cfg(test)]
431mod tests;