use super::{Resolve, ResolveVersion};
use crate::util::data_structures::{HashMap, HashSet};
use crate::util::errors::CargoResult;
use crate::util::interning::InternedString;
use crate::util::{Graph, internal};
use crate::workspace::{Dependency, GitReference, Package, PackageId, Patch, SourceId, Workspace};
use anyhow::{Context as _, bail};
use cargo_util_schemas::lockfile::{
TomlLockfile, TomlLockfileDependency, TomlLockfilePackageId, TomlLockfilePatch,
TomlLockfileSourceId,
};
use serde::ser;
use tracing::debug;
pub fn into_resolve(
resolve: TomlLockfile,
original: &str,
ws: &Workspace<'_>,
) -> CargoResult<Resolve> {
let path_deps: HashMap<String, HashMap<semver::Version, SourceId>> = build_path_deps(ws)?;
let mut checksums = HashMap::default();
let mut version = match resolve.version {
Some(n @ 5) if ws.gctx().nightly_features_allowed => {
if ws.gctx().cli_unstable().next_lockfile_bump {
ResolveVersion::V5
} else {
anyhow::bail!("lock file version `{n}` requires `-Znext-lockfile-bump`");
}
}
Some(4) => ResolveVersion::V4,
Some(3) => ResolveVersion::V3,
Some(n) => bail!(
"lock file version `{}` was found, but this version of Cargo \
does not understand this lock file, perhaps Cargo needs \
to be updated?",
n,
),
None => ResolveVersion::V1,
};
let packages = {
let mut packages = resolve.package.unwrap_or_default();
if let Some(root) = resolve.root {
packages.insert(0, root);
}
packages
};
let live_pkgs = {
let mut live_pkgs = HashMap::default();
let mut all_pkgs = HashSet::default();
for pkg in packages.iter() {
let enc_id = TomlLockfilePackageId {
name: pkg.name.clone(),
version: Some(pkg.version.clone()),
source: pkg.source.clone(),
};
if !all_pkgs.insert(enc_id.clone()) {
anyhow::bail!("package `{}` is specified twice in the lockfile", pkg.name);
}
let id = match pkg
.source
.as_ref()
.map(|source| SourceId::from_url(&source.source_str()))
.transpose()?
.or_else(|| get_source_id(&path_deps, &pkg).copied())
{
None => {
debug!("path dependency now missing {} v{}", pkg.name, pkg.version);
continue;
}
Some(source) => PackageId::try_new(&pkg.name, &pkg.version, source)?,
};
if let Some(cksum) = &pkg.checksum {
version = version.max(ResolveVersion::V2);
checksums.insert(id, Some(cksum.clone()));
}
assert!(live_pkgs.insert(enc_id, (id, pkg)).is_none())
}
live_pkgs
};
let mut map = HashMap::default();
for (id, _) in live_pkgs.values() {
map.entry(id.name().as_str())
.or_insert_with(HashMap::default)
.entry(id.version().to_string())
.or_insert_with(HashMap::default)
.insert(id.source_id(), *id);
}
let mut lookup_id = |enc_id: &TomlLockfilePackageId| -> Option<PackageId> {
let by_version = map.get(enc_id.name.as_str())?;
let by_source = match &enc_id.version {
Some(version) => by_version.get(version)?,
None => {
version = version.max(ResolveVersion::V2);
if by_version.len() == 1 {
by_version.values().next().unwrap()
} else {
return None;
}
}
};
match &enc_id.source {
Some(source) => by_source
.get(&SourceId::from_url(&source.source_str()).unwrap())
.cloned(),
None => {
let mut path_packages = by_source.values().filter(|p| p.source_id().is_path());
if let Some(path) = path_packages.next() {
if path_packages.next().is_some() {
return None;
}
Some(*path)
} else if by_source.len() == 1 {
let id = by_source.values().next().unwrap();
version = version.max(ResolveVersion::V2);
Some(*id)
} else {
None
}
}
}
};
let mut g = Graph::new();
for (id, _) in live_pkgs.values() {
g.add(*id);
}
for &(ref id, pkg) in live_pkgs.values() {
let Some(ref deps) = pkg.dependencies else {
continue;
};
for edge in deps.iter() {
if let Some(to_depend_on) = lookup_id(edge) {
g.link(*id, to_depend_on);
}
}
}
let replacements = {
let mut replacements = HashMap::default();
for &(ref id, pkg) in live_pkgs.values() {
if let Some(ref replace) = pkg.replace {
assert!(pkg.dependencies.is_none());
if let Some(replace_id) = lookup_id(replace) {
replacements.insert(*id, replace_id);
}
}
}
replacements
};
let mut metadata = resolve.metadata.unwrap_or_default();
let prefix = "checksum ";
let mut to_remove = Vec::new();
for (k, v) in metadata.iter().filter(|p| p.0.starts_with(prefix)) {
to_remove.push(k.to_string());
let k = k.strip_prefix(prefix).unwrap();
let enc_id: TomlLockfilePackageId = k
.parse()
.with_context(|| internal("invalid encoding of checksum in lockfile"))?;
let Some(id) = lookup_id(&enc_id) else {
continue;
};
let v = if v == "<none>" {
None
} else {
Some(v.to_string())
};
checksums.insert(id, v);
}
if !to_remove.is_empty() && version >= ResolveVersion::V2 {
checksums.drain();
}
for k in to_remove {
metadata.remove(&k);
}
let mut unused_patches = Vec::new();
for pkg in resolve.patch.unused {
let id = match pkg
.source
.as_ref()
.map(|source| SourceId::from_url(&source.source_str()))
.transpose()?
.or_else(|| get_source_id(&path_deps, &pkg).copied())
{
Some(src) => PackageId::try_new(&pkg.name, &pkg.version, src)?,
None => continue,
};
unused_patches.push(id);
}
if original.ends_with('\n')
&& !original.ends_with("\n\n")
&& version == ResolveVersion::V1
&& g.iter().count() == 1
{
version = ResolveVersion::V2;
}
return Ok(Resolve::new(
g,
replacements,
HashMap::default(),
checksums,
metadata,
unused_patches,
version,
HashMap::default(),
));
fn get_source_id<'a>(
path_deps: &'a HashMap<String, HashMap<semver::Version, SourceId>>,
pkg: &'a TomlLockfileDependency,
) -> Option<&'a SourceId> {
path_deps.iter().find_map(|(name, version_source)| {
if name != &pkg.name || version_source.len() == 0 {
return None;
}
if version_source.len() == 1 {
return Some(version_source.values().next().unwrap());
}
if let Ok(pkg_version) = pkg.version.parse::<semver::Version>() {
if let Some(source_id) = version_source.get(&pkg_version) {
return Some(source_id);
}
}
None
})
}
}
fn build_path_deps(
ws: &Workspace<'_>,
) -> CargoResult<HashMap<String, HashMap<semver::Version, SourceId>>> {
let members = ws
.members()
.filter(|p| p.package_id().source_id().is_path())
.collect::<Vec<_>>();
let mut ret: HashMap<String, HashMap<semver::Version, SourceId>> = HashMap::default();
let mut visited = HashSet::default();
for member in members.iter() {
ret.entry(member.package_id().name().to_string())
.or_insert_with(HashMap::default)
.insert(
member.package_id().version().clone(),
member.package_id().source_id(),
);
visited.insert(member.package_id().source_id());
}
for member in members.iter() {
build_pkg(member, ws, &mut ret, &mut visited);
}
for deps in ws.root_patch()?.values() {
for Patch { dep, loc: _ } in deps {
build_dep(dep, ws, &mut ret, &mut visited);
}
}
for (_, dep) in ws.root_replace() {
build_dep(dep, ws, &mut ret, &mut visited);
}
return Ok(ret);
fn build_pkg(
pkg: &Package,
ws: &Workspace<'_>,
ret: &mut HashMap<String, HashMap<semver::Version, SourceId>>,
visited: &mut HashSet<SourceId>,
) {
for dep in pkg.dependencies() {
build_dep(dep, ws, ret, visited);
}
}
fn build_dep(
dep: &Dependency,
ws: &Workspace<'_>,
ret: &mut HashMap<String, HashMap<semver::Version, SourceId>>,
visited: &mut HashSet<SourceId>,
) {
let id = dep.source_id();
if visited.contains(&id) || !id.is_path() {
return;
}
let path = match id.url().to_file_path() {
Ok(p) => p.join("Cargo.toml"),
Err(_) => return,
};
let Ok(pkg) = ws.load(&path) else { return };
ret.entry(pkg.package_id().name().to_string())
.or_insert_with(HashMap::default)
.insert(
pkg.package_id().version().clone(),
pkg.package_id().source_id(),
);
visited.insert(pkg.package_id().source_id());
build_pkg(&pkg, ws, ret, visited);
}
}
impl ser::Serialize for Resolve {
#[tracing::instrument(skip_all)]
fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
let mut ids: Vec<_> = self.iter().collect();
ids.sort();
let state = EncodeState::new(self);
let encodable = ids
.iter()
.map(|&id| encodable_resolve_node(id, self, &state))
.collect::<Vec<_>>();
let mut metadata = self.metadata().clone();
if self.version() == ResolveVersion::V1 {
for &id in ids.iter().filter(|id| !id.source_id().is_path()) {
let checksum = match self.checksums()[&id] {
Some(ref s) => &s[..],
None => "<none>",
};
let id = encodable_package_id(id, &state, self.version());
metadata.insert(format!("checksum {}", id), checksum.to_string());
}
}
let metadata = if metadata.is_empty() {
None
} else {
Some(metadata)
};
let patch = TomlLockfilePatch {
unused: self
.unused_patches()
.iter()
.map(|id| TomlLockfileDependency {
name: id.name().to_string(),
version: id.version().to_string(),
source: encodable_source_id(id.source_id(), self.version()),
dependencies: None,
replace: None,
checksum: if self.version() >= ResolveVersion::V2 {
self.checksums().get(id).and_then(|x| x.clone())
} else {
None
},
})
.collect(),
};
TomlLockfile {
package: Some(encodable),
root: None,
metadata,
patch,
version: match self.version() {
ResolveVersion::V5 => Some(5),
ResolveVersion::V4 => Some(4),
ResolveVersion::V3 => Some(3),
ResolveVersion::V2 | ResolveVersion::V1 => None,
},
}
.serialize(s)
}
}
pub struct EncodeState<'a> {
counts: Option<HashMap<InternedString, HashMap<&'a semver::Version, usize>>>,
}
impl<'a> EncodeState<'a> {
pub fn new(resolve: &'a Resolve) -> EncodeState<'a> {
let counts = if resolve.version() >= ResolveVersion::V2 {
let mut map = HashMap::default();
for id in resolve.iter() {
let slot = map
.entry(id.name())
.or_insert_with(HashMap::default)
.entry(id.version())
.or_insert(0);
*slot += 1;
}
Some(map)
} else {
None
};
EncodeState { counts }
}
}
fn encodable_resolve_node(
id: PackageId,
resolve: &Resolve,
state: &EncodeState<'_>,
) -> TomlLockfileDependency {
let (replace, deps) = match resolve.replacement(id) {
Some(id) => (
Some(encodable_package_id(id, state, resolve.version())),
None,
),
None => {
let mut deps = resolve
.deps_not_replaced(id)
.map(|(id, _)| encodable_package_id(id, state, resolve.version()))
.collect::<Vec<_>>();
deps.sort();
(None, Some(deps))
}
};
TomlLockfileDependency {
name: id.name().to_string(),
version: id.version().to_string(),
source: encodable_source_id(id.source_id(), resolve.version()),
dependencies: deps,
replace,
checksum: if resolve.version() >= ResolveVersion::V2 {
resolve.checksums().get(&id).and_then(|s| s.clone())
} else {
None
},
}
}
pub fn encodable_package_id(
id: PackageId,
state: &EncodeState<'_>,
resolve_version: ResolveVersion,
) -> TomlLockfilePackageId {
let mut version = Some(id.version().to_string());
let mut id_to_encode = id.source_id();
if resolve_version <= ResolveVersion::V2 {
if let Some(GitReference::Branch(b)) = id_to_encode.git_reference() {
if b == "master" {
id_to_encode =
SourceId::for_git(id_to_encode.url(), GitReference::DefaultBranch).unwrap();
}
}
}
let mut source = encodable_source_id(id_to_encode.without_precise(), resolve_version);
if let Some(counts) = &state.counts {
let version_counts = &counts[&id.name()];
if version_counts[&id.version()] == 1 {
source = None;
if version_counts.len() == 1 {
version = None;
}
}
}
TomlLockfilePackageId {
name: id.name().to_string(),
version,
source,
}
}
fn encodable_source_id(id: SourceId, version: ResolveVersion) -> Option<TomlLockfileSourceId> {
if id.is_path() {
None
} else {
Some(
if version >= ResolveVersion::V4 {
TomlLockfileSourceId::new(id.as_encoded_url().to_string())
} else {
TomlLockfileSourceId::new(id.as_url().to_string())
}
.expect("source ID should have valid URLs"),
)
}
}