Skip to main content

frostmirror_core/
resolver.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
4
5/// A single resolved crate with its concrete version.
6#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
7pub struct ResolvedCrate {
8    pub name: String,
9    pub version: String,
10}
11
12/// The full resolved dependency graph: all crates needed to satisfy `depends.toml`.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ResolvedGraph {
15    /// All resolved crates, deduplicated by (name, version).
16    pub crates: BTreeSet<ResolvedCrate>,
17    /// Direct dependencies as declared in depends.toml.
18    pub roots: Vec<ResolvedCrate>,
19}
20
21/// Sparse index entry for a single crate version.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct IndexEntry {
24    pub name: String,
25    #[serde(rename = "vers")]
26    pub version: String,
27    #[serde(default)]
28    pub deps: Vec<IndexDep>,
29    #[serde(rename = "cksum")]
30    pub checksum: String,
31    #[serde(default)]
32    pub features: BTreeMap<String, Vec<String>>,
33    #[serde(default)]
34    pub features2: BTreeMap<String, Vec<String>>,
35    #[serde(default)]
36    pub yanked: bool,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct IndexDep {
41    pub name: String,
42    pub req: String,
43    pub features: Vec<String>,
44    #[serde(default)]
45    pub optional: bool,
46    #[serde(default = "default_true")]
47    pub default_features: bool,
48    #[serde(default)]
49    pub target: Option<String>,
50    #[serde(default)]
51    pub kind: Option<String>,
52    #[serde(default)]
53    pub registry: Option<String>,
54    #[serde(default)]
55    pub package: Option<String>,
56}
57
58fn default_true() -> bool {
59    true
60}
61
62impl ResolvedGraph {
63    pub fn new() -> Self {
64        Self {
65            crates: BTreeSet::new(),
66            roots: Vec::new(),
67        }
68    }
69
70    /// Number of unique crates in the graph.
71    pub fn crate_count(&self) -> usize {
72        self.crates.len()
73    }
74
75    /// Get the set of (name, version) tuples.
76    pub fn crate_set(&self) -> BTreeSet<(String, String)> {
77        self.crates
78            .iter()
79            .map(|c| (c.name.clone(), c.version.clone()))
80            .collect()
81    }
82}
83
84/// An entry in the resolve queue: a crate we need to resolve plus whether
85/// to use its default features.
86#[derive(Debug, Clone)]
87struct ResolveRequest {
88    name: String,
89    version_req: String,
90    use_default_features: bool,
91}
92
93/// Iterative dependency resolver that works in levels.
94///
95/// Instead of using a callback for index lookups (which would require blocking
96/// HTTP inside an async runtime), this resolver works in a pull-based way:
97///
98/// 1. Call `seed()` with your direct dependencies
99/// 2. Call `pending_crates()` to get crate names that need index data
100/// 3. Fetch those indexes (async, in the caller)
101/// 4. Call `provide_index()` for each fetched index
102/// 5. Call `advance()` to process one level of the graph
103/// 6. Repeat 2-5 until `pending_crates()` returns empty
104/// 7. Call `finish()` to get the resolved graph
105pub struct Resolver {
106    /// Cached index entries, keyed by crate name.
107    index_cache: HashMap<String, Vec<IndexEntry>>,
108    /// Queue of crates to resolve.
109    queue: VecDeque<ResolveRequest>,
110    /// Already resolved crates.
111    resolved: BTreeSet<ResolvedCrate>,
112    /// Already visited (name, version) pairs to avoid cycles.
113    visited: HashSet<(String, String)>,
114    /// Root crates (direct deps).
115    roots: Vec<ResolvedCrate>,
116}
117
118impl Resolver {
119    pub fn new() -> Self {
120        Self {
121            index_cache: HashMap::new(),
122            queue: VecDeque::new(),
123            resolved: BTreeSet::new(),
124            visited: HashSet::new(),
125            roots: Vec::new(),
126        }
127    }
128
129    /// Seed the resolver with direct dependencies from `depends.toml`.
130    pub fn seed(&mut self, deps: &BTreeMap<String, String>) {
131        for (name, version_req) in deps {
132            self.queue.push_back(ResolveRequest {
133                name: name.clone(),
134                version_req: version_req.clone(),
135                use_default_features: true,
136            });
137        }
138    }
139
140    /// Provide index entries for a crate (fetched by the caller).
141    pub fn provide_index(&mut self, name: &str, entries: Vec<IndexEntry>) {
142        self.index_cache.insert(name.to_string(), entries);
143    }
144
145    /// Return the names of crates that are queued but whose index data
146    /// is not yet in the cache. The caller should fetch these and call
147    /// `provide_index` before calling `advance`.
148    pub fn pending_crates(&self) -> Vec<String> {
149        let mut needed: BTreeSet<String> = BTreeSet::new();
150        for req in &self.queue {
151            let dep_name = req.name.clone();
152            if !self.index_cache.contains_key(&dep_name) {
153                needed.insert(dep_name);
154            }
155        }
156        needed.into_iter().collect()
157    }
158
159    /// Returns true if there are still crates to resolve.
160    pub fn has_work(&self) -> bool {
161        !self.queue.is_empty()
162    }
163
164    /// Process all currently queued crates. This resolves their versions,
165    /// computes enabled features, and enqueues their transitive deps.
166    /// After this call, `pending_crates()` may return new names to fetch.
167    pub fn advance(&mut self) -> Result<()> {
168        let current_queue: Vec<ResolveRequest> = self.queue.drain(..).collect();
169
170        for req in current_queue {
171            let entries = match self.index_cache.get(&req.name) {
172                Some(e) => e,
173                None => {
174                    tracing::warn!("no index data for {} (skipping)", req.name);
175                    continue;
176                }
177            };
178
179            let matched = match find_best_match(entries, &req.name, &req.version_req) {
180                Ok(m) => m,
181                Err(e) => {
182                    tracing::warn!("{}", e);
183                    continue;
184                }
185            };
186
187            let key = (matched.name.clone(), matched.version.clone());
188            if self.visited.contains(&key) {
189                continue;
190            }
191            self.visited.insert(key);
192
193            let resolved_crate = ResolvedCrate {
194                name: matched.name.clone(),
195                version: matched.version.clone(),
196            };
197
198            // Track roots (first level)
199            if self.resolved.is_empty() || self.roots.iter().any(|r| r.name == req.name) {
200                if !self.roots.iter().any(|r| r.name == req.name) {
201                    // This is a direct dep being resolved for the first time
202                } else {
203                    // Already in roots
204                }
205            }
206
207            self.resolved.insert(resolved_crate.clone());
208
209            // Compute features and activated optional deps
210            let enabled_features =
211                compute_enabled_features(&matched, req.use_default_features);
212            let activated_optional_deps =
213                compute_activated_deps(&matched, &enabled_features);
214
215            // Enqueue transitive deps
216            for dep in &matched.deps {
217                if dep.kind.as_deref() == Some("dev") || dep.kind.as_deref() == Some("build") {
218                    continue;
219                }
220
221                if dep.optional && !activated_optional_deps.contains(&dep.name) {
222                    continue;
223                }
224
225                let dep_name = dep.package.as_deref().unwrap_or(&dep.name);
226
227                self.queue.push_back(ResolveRequest {
228                    name: dep_name.to_string(),
229                    version_req: dep.req.clone(),
230                    use_default_features: dep.default_features,
231                });
232            }
233        }
234
235        Ok(())
236    }
237
238    /// Consume the resolver and return the final resolved graph.
239    pub fn finish(self, direct_deps: &BTreeMap<String, String>) -> ResolvedGraph {
240        // Reconstruct roots from direct deps
241        let roots: Vec<ResolvedCrate> = direct_deps
242            .keys()
243            .filter_map(|name| {
244                self.resolved
245                    .iter()
246                    .find(|c| c.name == *name)
247                    .cloned()
248            })
249            .collect();
250
251        ResolvedGraph {
252            crates: self.resolved,
253            roots,
254        }
255    }
256}
257
258/// Compute the full set of enabled feature names for an index entry,
259/// starting from "default" (if requested) and recursively expanding
260/// feature-to-feature references.
261fn compute_enabled_features(entry: &IndexEntry, use_default: bool) -> HashSet<String> {
262    let mut enabled = HashSet::new();
263
264    let mut all_features: BTreeMap<String, Vec<String>> = entry.features.clone();
265    for (k, v) in &entry.features2 {
266        all_features
267            .entry(k.clone())
268            .or_default()
269            .extend(v.clone());
270    }
271
272    if use_default {
273        if let Some(defaults) = all_features.get("default") {
274            let mut stack: Vec<String> = defaults.clone();
275            while let Some(feat) = stack.pop() {
276                if enabled.insert(feat.clone()) {
277                    if let Some(sub) = all_features.get(&feat) {
278                        stack.extend(sub.clone());
279                    }
280                }
281            }
282        }
283    }
284
285    enabled
286}
287
288/// Determine which optional dependencies are activated by the set of enabled
289/// features.
290fn compute_activated_deps(entry: &IndexEntry, enabled_features: &HashSet<String>) -> HashSet<String> {
291    let mut activated = HashSet::new();
292
293    let optional_dep_names: HashSet<&str> = entry
294        .deps
295        .iter()
296        .filter(|d| d.optional)
297        .map(|d| d.name.as_str())
298        .collect();
299
300    let mut all_features: BTreeMap<String, Vec<String>> = entry.features.clone();
301    for (k, v) in &entry.features2 {
302        all_features
303            .entry(k.clone())
304            .or_default()
305            .extend(v.clone());
306    }
307
308    for feat_name in enabled_features {
309        if optional_dep_names.contains(feat_name.as_str()) {
310            activated.insert(feat_name.clone());
311        }
312
313        if let Some(values) = all_features.get(feat_name) {
314            for value in values {
315                if let Some(dep_ref) = value.strip_prefix("dep:") {
316                    activated.insert(dep_ref.to_string());
317                } else if optional_dep_names.contains(value.as_str()) {
318                    activated.insert(value.clone());
319                } else if value.contains('/') {
320                    let dep_part = value.split('/').next().unwrap();
321                    if optional_dep_names.contains(dep_part) {
322                        activated.insert(dep_part.to_string());
323                    }
324                }
325            }
326        }
327    }
328
329    activated
330}
331
332/// Find the best matching version for a version requirement.
333fn find_best_match(entries: &[IndexEntry], name: &str, version_req: &str) -> Result<IndexEntry> {
334    let req = semver::VersionReq::parse(version_req)
335        .or_else(|_| semver::VersionReq::parse(&format!("={}", version_req)))
336        .map_err(|e| anyhow::anyhow!("invalid version req '{}' for {}: {}", version_req, name, e))?;
337
338    let mut candidates: Vec<&IndexEntry> = entries
339        .iter()
340        .filter(|e| !e.yanked)
341        .filter(|e| {
342            semver::Version::parse(&e.version)
343                .map(|v| req.matches(&v))
344                .unwrap_or(false)
345        })
346        .collect();
347
348    candidates.sort_by(|a, b| {
349        let va = semver::Version::parse(&a.version).unwrap();
350        let vb = semver::Version::parse(&b.version).unwrap();
351        vb.cmp(&va)
352    });
353
354    candidates
355        .into_iter()
356        .next()
357        .cloned()
358        .ok_or_else(|| anyhow::anyhow!("no matching version for {} {}", name, version_req))
359}