cargo_plumbing/ops/
resolve.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use cargo::core::{
4    PackageId, PackageIdSpec, Resolve, ResolveVersion, SourceId, SourceKind, Workspace,
5};
6use cargo::util::Graph;
7use cargo::CargoResult;
8use cargo_plumbing_schemas::lockfile::{NormalizedDependency, NormalizedPatch};
9
10use crate::cargo::core::resolver::encode::build_path_deps;
11
12/// Converts plumbing messages into an incomplete [`Resolve`]
13///
14/// The `features` and `summaries` fields of the returned struct is empty.
15pub fn into_resolve(
16    ws: &Workspace<'_>,
17    version: Option<u32>,
18    packages: Vec<NormalizedDependency>,
19    patch: NormalizedPatch,
20) -> CargoResult<Resolve> {
21    let path_deps = build_path_deps(ws)?;
22
23    let mut checksums = HashMap::new();
24
25    let live_pkgs = {
26        let mut all_pkgs = HashSet::new();
27        let mut live_pkgs = HashMap::new();
28        for pkg in packages.iter() {
29            if !all_pkgs.insert(pkg.id.clone()) {
30                anyhow::bail!("package `{}` is specified twice", pkg.id.name());
31            }
32
33            let source_id = get_path_deps_source_id(&path_deps, pkg.id.name(), pkg.id.version());
34            let Ok(Some(id)) = spec_to_id(pkg.id.clone(), source_id) else {
35                continue;
36            };
37
38            if let Some(cksum) = &pkg.checksum {
39                checksums.insert(id, Some(cksum.clone()));
40            }
41
42            live_pkgs.insert(pkg.id.clone(), (id, pkg));
43        }
44        live_pkgs
45    };
46
47    // When decoding a V2 version the edges in `dependencies` aren't
48    // guaranteed to have either version or source information. This `map`
49    // is used to find package ids even if dependencies have missing
50    // information. This map is from name to version to source to actual
51    // package ID. (various levels to drill down step by step)
52    let mut map = HashMap::new();
53    for (id, _) in live_pkgs.values() {
54        map.entry(id.name().as_str())
55            .or_insert_with(HashMap::new)
56            .entry(id.version())
57            .or_insert_with(HashMap::new)
58            .insert(id.source_id(), *id);
59    }
60
61    let lookup_id = |pkg_id: &PackageIdSpec, source_id: Option<&SourceId>| -> Option<PackageId> {
62        let by_version = map.get(pkg_id.name())?;
63
64        let by_source = match &pkg_id.version() {
65            Some(version) => by_version.get(version)?,
66            None => {
67                if by_version.len() == 1 {
68                    by_version.values().next().unwrap()
69                } else {
70                    return None;
71                }
72            }
73        };
74
75        match &source_id {
76            Some(source) => by_source.get(source).cloned(),
77            None => {
78                let mut path_packages = by_source.values().filter(|p| p.source_id().is_path());
79                if let Some(path) = path_packages.next() {
80                    if path_packages.next().is_some() {
81                        None
82                    } else {
83                        Some(*path)
84                    }
85                } else if by_source.len() == 1 {
86                    Some(*by_source.values().next().unwrap())
87                } else {
88                    None
89                }
90            }
91        }
92    };
93
94    let graph = {
95        let mut g = Graph::new();
96
97        for (id, _) in live_pkgs.values() {
98            g.add(*id);
99        }
100
101        for &(ref id, pkg) in live_pkgs.values() {
102            let Some(ref deps) = pkg.dependencies else {
103                continue;
104            };
105
106            for edge in deps.iter() {
107                let package_id = spec_to_id(edge.clone(), None)?;
108                let source_id = package_id.map(|p| p.source_id());
109                if let Some(to_depend_on) = lookup_id(edge, source_id.as_ref()) {
110                    g.link(*id, to_depend_on);
111                }
112            }
113        }
114        g
115    };
116
117    let replacements = {
118        let mut replacements = HashMap::new();
119        for &(ref id, pkg) in live_pkgs.values() {
120            if let Some(ref replace) = pkg.replace {
121                assert!(pkg.dependencies.is_none());
122                let source_id = id.source_id();
123                if let Some(replace_id) = lookup_id(replace, Some(&source_id)) {
124                    replacements.insert(*id, replace_id);
125                }
126            }
127        }
128        replacements
129    };
130
131    let unused_patches = {
132        let mut unused_patches = Vec::new();
133        for pkg in patch.unused {
134            let source_id = get_path_deps_source_id(&path_deps, pkg.id.name(), pkg.id.version());
135            let Ok(Some(id)) = spec_to_id(pkg.id.clone(), source_id) else {
136                continue;
137            };
138            unused_patches.push(id);
139        }
140        unused_patches
141    };
142
143    let metadata = BTreeMap::new();
144    let features = HashMap::new();
145    let summaries = HashMap::new();
146
147    let version = match version {
148        Some(4) => ResolveVersion::V4,
149        Some(3) => ResolveVersion::V3,
150        Some(2) => ResolveVersion::V2,
151        Some(1) => ResolveVersion::V1,
152        None => ResolveVersion::V2,
153        Some(_) => anyhow::bail!("invalid lockfile version"),
154    };
155
156    Ok(Resolve::new(
157        graph,
158        replacements,
159        features,
160        checksums,
161        metadata,
162        unused_patches,
163        version,
164        summaries,
165    ))
166}
167
168pub fn get_path_deps_source_id<'a>(
169    path_deps: &'a HashMap<String, HashMap<semver::Version, SourceId>>,
170    package_name: &str,
171    package_version: Option<semver::Version>,
172) -> Option<&'a SourceId> {
173    path_deps.iter().find_map(|(name, version_source)| {
174        if name != package_name || version_source.is_empty() {
175            return None;
176        }
177
178        if version_source.len() == 1 {
179            return Some(version_source.values().next().unwrap());
180        }
181
182        if let Some(pkg_version) = &package_version {
183            if let Some(source_id) = version_source.get(pkg_version) {
184                return Some(source_id);
185            }
186        }
187
188        None
189    })
190}
191
192pub fn spec_to_id(
193    spec: PackageIdSpec,
194    source_id: Option<&SourceId>,
195) -> CargoResult<Option<PackageId>> {
196    if let Some(kind) = spec.kind() {
197        if let Some(url) = spec.url() {
198            if let Some(version) = spec.version() {
199                let name = spec.name();
200                let source_id = match kind {
201                    SourceKind::Git(git_ref) => SourceId::for_git(url, git_ref.clone()),
202                    SourceKind::Registry | SourceKind::SparseRegistry => {
203                        SourceId::for_registry(url)
204                    }
205                    _ => anyhow::bail!("unsupported source"),
206                }?;
207
208                return Ok(Some(PackageId::new(name.into(), version, source_id)));
209            }
210        }
211    }
212
213    if let Some(source_id) = source_id {
214        if let Some(version) = spec.version() {
215            let name = spec.name();
216            return Ok(Some(PackageId::new(name.into(), version, *source_id)));
217        }
218    }
219
220    Ok(None)
221}