use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use anyhow::bail;
use semver::Version;
use serde::Serialize;
use crate::manifest::{Manifest, Realm};
use crate::package_id::PackageId;
use crate::package_req::PackageReq;
use crate::package_source::{PackageSourceId, PackageSourceMap};
#[derive(Debug, Default, Serialize)]
pub struct Resolve {
pub activated: BTreeSet<PackageId>,
pub metadata: BTreeMap<PackageId, ResolvePackageMetadata>,
pub shared_dependencies: BTreeMap<PackageId, BTreeMap<String, PackageId>>,
pub server_dependencies: BTreeMap<PackageId, BTreeMap<String, PackageId>>,
}
impl Resolve {
fn activate(&mut self, source: PackageId, dep_name: String, dep_realm: Realm, dep: PackageId) {
self.activated.insert(dep.clone());
let dependencies = match dep_realm {
Realm::Shared => self.shared_dependencies.entry(source.clone()).or_default(),
Realm::Server => self.server_dependencies.entry(source.clone()).or_default(),
};
dependencies.insert(dep_name, dep);
}
}
#[derive(Debug, Serialize)]
pub struct ResolvePackageMetadata {
pub realm: Realm,
pub server_only: bool,
}
pub fn resolve(
root_manifest: &Manifest,
try_to_use: &BTreeSet<PackageId>,
package_sources: &PackageSourceMap,
) -> anyhow::Result<Resolve> {
let mut resolve = Resolve::default();
resolve.activated.insert(root_manifest.package_id());
resolve.metadata.insert(
root_manifest.package_id(),
ResolvePackageMetadata {
realm: root_manifest.package.realm,
server_only: root_manifest.package.realm == Realm::Server,
},
);
let mut packages_to_visit = VecDeque::new();
for (alias, req) in &root_manifest.dependencies {
packages_to_visit.push_back(DependencyRequest {
request_source: root_manifest.package_id(),
request_realm: Realm::Shared,
whole_path_server_only: false,
package_alias: alias.clone(),
package_req: req.clone(),
});
}
for (alias, req) in &root_manifest.server_dependencies {
packages_to_visit.push_back(DependencyRequest {
request_source: root_manifest.package_id(),
request_realm: Realm::Server,
whole_path_server_only: true,
package_alias: alias.clone(),
package_req: req.clone(),
});
}
'outer: while let Some(dependency_request) = packages_to_visit.pop_front() {
let mut matching_activated: Vec<_> = resolve
.activated
.iter()
.filter(|package_id| package_id.name() == dependency_request.package_req.name())
.cloned()
.collect();
matching_activated.sort_by(|a, b| b.version().cmp(a.version()));
for package_id in &matching_activated {
if dependency_request.package_req.matches_id(package_id) {
resolve.activate(
dependency_request.request_source.clone(),
dependency_request.package_alias.clone(),
dependency_request.request_realm,
package_id.clone(),
);
let metadata = resolve
.metadata
.get_mut(package_id)
.expect("activated package was missing metadata");
if dependency_request.request_realm != Realm::Server {
metadata.server_only = false;
}
continue 'outer;
}
}
let default_registry = package_sources
.get(&PackageSourceId::DefaultRegistry)
.unwrap();
let mut candidates = default_registry.query(&dependency_request.package_req)?;
candidates.sort_by(|a, b| {
let contains_a = try_to_use.contains(&a.package_id());
let contains_b = try_to_use.contains(&b.package_id());
match (contains_a, contains_b) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => b.package.version.cmp(&a.package.version),
}
});
let filtered_candidates = candidates.iter().filter(|candidate| {
use Realm::*;
match (dependency_request.request_realm, candidate.package.realm) {
(Shared, Shared) => true,
(Server, Server) | (Server, Shared) => true,
(Shared, Server) => false,
}
});
let mut conflicting = Vec::new();
for candidate in filtered_candidates {
let has_conflicting = matching_activated
.iter()
.any(|activated| compatible(&candidate.package.version, activated.version()));
if has_conflicting {
conflicting.push(candidate.package_id());
continue;
}
let candidate_id = PackageId::new(
candidate.package.name.clone(),
candidate.package.version.clone(),
);
resolve.activate(
dependency_request.request_source.clone(),
dependency_request.package_alias.to_owned(),
dependency_request.request_realm,
candidate_id.clone(),
);
resolve.metadata.insert(
candidate_id.clone(),
ResolvePackageMetadata {
realm: candidate.package.realm,
server_only: dependency_request.whole_path_server_only,
},
);
for (alias, req) in &candidate.dependencies {
packages_to_visit.push_back(DependencyRequest {
request_source: candidate_id.clone(),
request_realm: Realm::Shared,
whole_path_server_only: dependency_request.whole_path_server_only,
package_alias: alias.clone(),
package_req: req.clone(),
})
}
for (alias, req) in &candidate.server_dependencies {
packages_to_visit.push_back(DependencyRequest {
request_source: candidate_id.clone(),
request_realm: Realm::Server,
whole_path_server_only: dependency_request.whole_path_server_only,
package_alias: alias.clone(),
package_req: req.clone(),
})
}
continue 'outer;
}
if conflicting.is_empty() {
bail!(
"No packages were found that matched {req} ({req_realm:?}).",
req = dependency_request.package_req,
req_realm = dependency_request.request_realm,
);
} else {
let conflicting_debug: Vec<_> = conflicting
.into_iter()
.map(|id| format!("{:?}", id))
.collect();
bail!(
"All possible candidates for package {req} ({req_realm:?}) \
conflicted with other packages that were already installed. \
These packages were previously selected: {conflicting}",
req = dependency_request.package_req,
req_realm = dependency_request.request_realm,
conflicting = conflicting_debug.join(", "),
);
}
}
Ok(resolve)
}
fn compatible(a: &Version, b: &Version) -> bool {
if a == b {
return true;
}
if a.major == 0 && b.major == 0 {
a.minor == b.minor
} else {
a.major == b.major
}
}
pub struct DependencyRequest {
request_source: PackageId,
request_realm: Realm,
whole_path_server_only: bool,
package_alias: String,
package_req: PackageReq,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
package_name::PackageName, package_source::InMemoryRegistry, test_package::PackageBuilder,
};
fn test_project(registry: InMemoryRegistry, package: PackageBuilder) -> anyhow::Result<()> {
let package_sources = PackageSourceMap::new(Box::new(registry.source()));
let manifest = package.into_manifest();
let resolve = resolve(&manifest, &Default::default(), &package_sources)?;
insta::assert_yaml_snapshot!(resolve);
Ok(())
}
#[test]
fn minimal() -> anyhow::Result<()> {
let registry = InMemoryRegistry::new();
let root = PackageBuilder::new("biff/minimal@0.1.0");
test_project(registry, root)
}
#[test]
fn one_dependency() -> anyhow::Result<()> {
let registry = InMemoryRegistry::new();
registry.publish(PackageBuilder::new("biff/minimal@0.1.0"));
registry.publish(PackageBuilder::new("biff/minimal@0.2.0"));
let root = PackageBuilder::new("biff/one-dependency@0.1.0")
.with_dep("Minimal", "biff/minimal@0.1.0");
test_project(registry, root)
}
#[test]
fn transitive_dependency() -> anyhow::Result<()> {
let registry = InMemoryRegistry::new();
registry.publish(PackageBuilder::new("biff/minimal@0.1.0"));
registry.publish(
PackageBuilder::new("biff/one-dependency@0.1.0")
.with_dep("Minimal", "biff/minimal@0.1.0"),
);
let root = PackageBuilder::new("biff/transitive-dependency@0.1.0")
.with_dep("OneDependency", "biff/one-dependency@0.1.0");
test_project(registry, root)
}
#[test]
fn unified_dependencies() -> anyhow::Result<()> {
let registry = InMemoryRegistry::new();
registry.publish(PackageBuilder::new("biff/b@1.0.0").with_dep("D", "biff/d@1.0.0"));
registry.publish(PackageBuilder::new("biff/c@1.0.0").with_dep("D", "biff/d@1.0.0"));
registry.publish(PackageBuilder::new("biff/d@1.0.0"));
let root = PackageBuilder::new("biff/a@1.0.0")
.with_dep("B", "biff/b@1.0.0")
.with_dep("C", "biff/c@1.0.0");
test_project(registry, root)
}
#[test]
fn server_to_shared() -> anyhow::Result<()> {
let registry = InMemoryRegistry::new();
registry.publish(PackageBuilder::new("biff/shared@1.0.0"));
let root =
PackageBuilder::new("biff/root@1.0.0").with_server_dep("Shared", "biff/shared@1.0.0");
test_project(registry, root)
}
#[test]
fn shared_to_server() -> anyhow::Result<()> {
let registry = InMemoryRegistry::new();
registry.publish(PackageBuilder::new("biff/server@1.0.0").with_realm(Realm::Server));
let root =
PackageBuilder::new("biff/root@1.0.0").with_server_dep("Server", "biff/server@1.0.0");
test_project(registry, root)
}
#[test]
fn fail_server_in_shared() {
let registry = InMemoryRegistry::new();
registry.publish(PackageBuilder::new("biff/server@1.0.0").with_realm(Realm::Server));
let root = PackageBuilder::new("biff/root@1.0.0").with_dep("Server", "biff/server@1.0.0");
let package_sources = PackageSourceMap::new(Box::new(registry.source()));
let err = resolve(root.manifest(), &Default::default(), &package_sources).unwrap_err();
insta::assert_display_snapshot!(err);
}
#[test]
fn one_dependency_no_upgrade() -> anyhow::Result<()> {
let registry = InMemoryRegistry::new();
registry.publish(PackageBuilder::new("biff/minimal@1.0.0"));
let root = PackageBuilder::new("biff/one-dependency@1.0.0")
.with_dep("Minimal", "biff/minimal@1.0.0");
let package_sources = PackageSourceMap::new(Box::new(registry.source()));
let resolved = resolve(&root.manifest(), &Default::default(), &package_sources)?;
insta::assert_yaml_snapshot!("one_dependency_no_upgrade", resolved);
registry.publish(PackageBuilder::new("biff/minimal@1.1.0"));
let new_resolved = resolve(&root.manifest(), &resolved.activated, &package_sources)?;
insta::assert_yaml_snapshot!("one_dependency_no_upgrade", new_resolved);
Ok(())
}
#[test]
fn one_dependency_yes_upgrade() -> anyhow::Result<()> {
let registry = InMemoryRegistry::new();
registry.publish(PackageBuilder::new("biff/minimal@1.0.0"));
let root = PackageBuilder::new("biff/one-dependency@1.0.0")
.with_dep("Minimal", "biff/minimal@1.0.0");
let package_sources = PackageSourceMap::new(Box::new(registry.source()));
let resolved = resolve(&root.manifest(), &Default::default(), &package_sources)?;
insta::assert_yaml_snapshot!(resolved);
let remove_this: PackageName = "biff/minimal".parse().unwrap();
let try_to_use = resolved
.activated
.into_iter()
.filter(|id| id.name() != &remove_this)
.collect();
registry.publish(PackageBuilder::new("biff/minimal@1.1.0"));
let new_resolved = resolve(&root.manifest(), &try_to_use, &package_sources)?;
insta::assert_yaml_snapshot!(new_resolved);
Ok(())
}
}