use crate::{
manifest::{self, Dependency, ManifestFile},
source::{self, Source},
};
use manifest::ManifestFileError;
use petgraph::{visit::EdgeRef, Directed, Direction};
use serde::{Deserialize, Serialize};
use std::{
collections::{hash_map, BTreeMap, HashMap, HashSet},
fmt,
hash::{Hash, Hasher},
path::{Path, PathBuf},
str,
};
use thiserror::Error;
type GraphIx = u32;
pub type Graph = petgraph::stable_graph::StableGraph<Pinned, Dep, Directed, GraphIx>;
pub type EdgeIx = petgraph::graph::EdgeIndex<GraphIx>;
pub type NodeIx = petgraph::graph::NodeIndex<GraphIx>;
type EdgeReference<'a> = petgraph::stable_graph::EdgeReference<'a, Dep, GraphIx>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Dep {
pub kind: DepKind,
pub name: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DepKind {
Library,
Contract,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
pub struct Pkg {
pub name: String,
pub source: Source,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct Pinned {
pub name: String,
pub source: source::Pinned,
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct PinnedId(u64);
pub type PinnedManifests = HashMap<PinnedId, ManifestFile>;
pub type MemberManifests = BTreeMap<String, ManifestFile>;
pub type InvalidDeps = BTreeMap<EdgeIx, InvalidDepCause>;
type FetchedPkgs = HashMap<Pkg, NodeIx>;
struct FetchCtx<'a> {
id: source::FetchId,
fetched: &'a mut FetchedPkgs,
visited: &'a mut HashSet<NodeIx>,
}
#[derive(Debug)]
pub struct Plan {
graph: Graph,
manifests: PinnedManifests,
compilation_order: Vec<NodeIx>,
}
#[derive(Debug, Error)]
pub enum PlanError {
#[error("failed to fetch the package graph: {0}")]
FetchGraph(#[from] FetchGraphError),
#[error("{0}")]
DependencyCycle(#[from] DependencyCycle),
}
#[derive(Debug, Error)]
#[error("failed to fetch graph")]
pub enum FetchGraphError {
#[error("{0}")]
MemberName(#[from] MemberNameNotUnique),
#[error("failed to source dependency package {0:?}: {1}")]
Source(String, source::SourceError),
#[error("failed to pin/fetch dependency package {0:?}: {1}")]
PinAndFetch(String, Box<source::PinAndFetchError>),
#[error("package {0:?}'s dependency named {1:?} failed the depenency manifest check: {2}")]
DepManifest(String, String, DepManifestError),
}
#[derive(Debug, Error)]
#[error("more than one member package with name {0:?}")]
pub struct MemberNameNotUnique(pub String);
#[derive(Debug, Error)]
pub enum DepManifestError {
#[error("declared as a {0:?} dependency, but is actually a {1:?}")]
UnexpectedKind(DepKind, manifest::PackageKind),
#[error(
"dependency name {0:?} must match the manifest project name {1:?} \
unless `package = {1:?}` is specified in the dependency declaration"
)]
UnexpectedName(String, String),
}
#[derive(Debug, Error)]
#[error("failed to parse `PinnedId`: expected 16-char hex string, found {0:?}")]
pub struct PinnedIdFromStrError(String);
#[derive(Debug, Error)]
pub enum InvalidDepCause {
#[error("graph contains none of the given members")]
NoMembersFound,
#[error("{0}")]
Check(#[from] CheckDepError),
}
#[derive(Debug, Error)]
pub enum CheckDepError {
#[error("{0}")]
PathError(#[from] DepPathError),
#[error("{0}")]
DepManifestFileError(#[from] ManifestFileError),
#[error("no entry in parent manifest")]
NoEntryInManifest,
#[error("{0}")]
Source(#[from] source::SourceError),
#[error("the graph source {0:?} does not match the manifest entry {1:?}")]
SourceMismatch(Source, Source),
#[error("{0}")]
DepManifestError(#[from] DepManifestError),
}
#[derive(Debug, Error)]
#[error("failed to find path root: `path` dependency {0:?} has no parent")]
pub struct FindPathRootError(String);
#[derive(Debug, Error)]
pub enum CheckPathRootError {
#[error("{0}")]
NotFound(#[from] FindPathRootError),
#[error("invalid path root for dependency {0:?}: expected {1}, found {2}")]
Invalid(String, PinnedId, PinnedId),
}
#[derive(Debug, Error)]
pub enum DepPathError {
#[error("{0}")]
Source(#[from] source::DepPathError),
#[error("{0}")]
CheckPathRoot(#[from] CheckPathRootError),
#[error("dependency named {0:?} does not appear in manifest of {1:?}")]
NoEntryInManifest(String, String),
#[error("supposed member dependency {0:?} not found in member manifests")]
MemberNotFound(String),
#[error("dependency named {0:?} specified a path {0:?} that does not exist")]
PathDoesNotExist(String, PathBuf),
}
#[derive(Debug, Error)]
#[error("cycle detected between the following packages: {0:?}")]
pub struct DependencyCycle(pub Vec<String>);
impl Dep {
fn new(name: String, kind: DepKind) -> Self {
Self { name, kind }
}
}
impl Pinned {
pub fn id(&self) -> PinnedId {
PinnedId::new(&self.name, &self.source)
}
pub fn unpinned(&self, path: &Path) -> Pkg {
let source = self.source.unpinned(path);
let name = self.name.clone();
Pkg { name, source }
}
}
impl PinnedId {
pub fn new(name: &str, source: &source::Pinned) -> Self {
let mut hasher = hash_map::DefaultHasher::default();
name.hash(&mut hasher);
source.hash(&mut hasher);
Self(hasher.finish())
}
}
impl Plan {
pub fn graph(&self) -> &Graph {
&self.graph
}
pub fn manifests(&self) -> &PinnedManifests {
&self.manifests
}
pub fn compilation_order(&self) -> &[NodeIx] {
&self.compilation_order
}
}
impl fmt::Display for PinnedId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:016x}", self.0)
}
}
impl str::FromStr for PinnedId {
type Err = PinnedIdFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let res = u64::from_str_radix(s, 16);
Ok(Self(res.map_err(|_| PinnedIdFromStrError(s.to_string()))?))
}
}
pub fn from_members(members: &MemberManifests) -> Result<Plan, PlanError> {
let mut graph = Graph::default();
let mut pinned_manifests = PinnedManifests::default();
fetch_graph(members, &mut graph, &mut pinned_manifests)?;
{
let (pinned_manifests2, invalid_deps) = check_graph(&graph, members);
assert!(invalid_deps.is_empty(), "{invalid_deps:?}");
assert_eq!(pinned_manifests, pinned_manifests2);
}
let compilation_order = compilation_order(&graph)?;
Ok(Plan {
graph,
manifests: pinned_manifests,
compilation_order,
})
}
fn fetch_graph(
member_manifests: &MemberManifests,
graph: &mut Graph,
pinned_manifests: &mut PinnedManifests,
) -> Result<HashSet<NodeIx>, FetchGraphError> {
let mut added_nodes = HashSet::default();
for name in member_manifests.keys() {
let added = fetch_graph_from_member(member_manifests, name, graph, pinned_manifests)?;
added_nodes.extend(added);
}
Ok(added_nodes)
}
fn fetch_graph_from_member(
member_manifests: &MemberManifests,
member_name: &str,
graph: &mut Graph,
pinned_manifests: &mut PinnedManifests,
) -> Result<HashSet<NodeIx>, FetchGraphError> {
let member_manifest = &member_manifests[member_name];
let member_node = find_member_node(graph, &member_manifest.pkg.name)?
.unwrap_or_else(|| insert_member_pkg(member_manifest.clone(), graph, pinned_manifests));
let fetch_ts = std::time::Instant::now();
let fetch_id = source::fetch_graph_id(member_manifest.dir(), fetch_ts);
let path_root = graph[member_node].id();
let mut fetched = fetched_pkgs(graph, pinned_manifests);
let mut visited: HashSet<NodeIx> = HashSet::default();
let mut ctx = FetchCtx {
id: fetch_id,
fetched: &mut fetched,
visited: &mut visited,
};
fetch_deps(
member_manifests,
member_node,
path_root,
graph,
pinned_manifests,
&mut ctx,
)
}
fn insert_member_pkg(
member_manifest: ManifestFile,
graph: &mut Graph,
pinned_manifests: &mut PinnedManifests,
) -> NodeIx {
let name = member_manifest.pkg.name.clone();
let source = source::Pinned::MEMBER;
let pkg = Pinned { name, source };
let pkg_id = pkg.id();
pinned_manifests.insert(pkg_id, member_manifest);
graph.add_node(pkg)
}
fn fetched_pkgs(graph: &Graph, pinned_manifests: &PinnedManifests) -> FetchedPkgs {
graph
.node_indices()
.map(|n| {
let pinned = &graph[n];
let manifest = &pinned_manifests[&pinned.id()];
let pkg = pinned.unpinned(manifest.dir());
(pkg, n)
})
.collect()
}
fn manifest_deps(
manifest: &ManifestFile,
) -> impl '_ + Iterator<Item = (String, Dependency, DepKind)> {
let deps = manifest
.deps
.iter()
.map(|(name, dep)| (name.clone(), dep.clone(), DepKind::Library));
let contract_deps = manifest
.contract_deps
.iter()
.map(|(name, dep)| (name.clone(), dep.clone(), DepKind::Contract));
deps.chain(contract_deps)
}
fn fetch_deps(
member_manifests: &MemberManifests,
node: NodeIx,
path_root: PinnedId,
graph: &mut Graph,
pinned_manifests: &mut PinnedManifests,
ctx: &mut FetchCtx,
) -> Result<HashSet<NodeIx>, FetchGraphError> {
let mut added = HashSet::default();
let parent_id = graph[node].id();
let deps: Vec<_> = manifest_deps(&pinned_manifests[&parent_id]).collect();
for (dep_name, dep, dep_kind) in deps {
let pkg_name = dep.package.as_ref().unwrap_or(&dep_name);
let parent_manifest_dir = pinned_manifests[&parent_id].dir();
let source =
Source::from_manifest_dep(parent_manifest_dir, &dep, member_manifests.values())
.map_err(|e| FetchGraphError::Source(pkg_name.to_string(), e))?;
let dep_pkg = Pkg {
name: pkg_name.to_string(),
source,
};
let dep_node = match ctx.fetched.entry(dep_pkg) {
hash_map::Entry::Occupied(entry) => *entry.get(),
hash_map::Entry::Vacant(entry) => {
let pkg = entry.key();
let ctx = source::PinCtx {
_fetch_id: ctx.id,
path_root,
pkg_name: &pkg.name,
};
let source = pkg
.source
.pin_and_fetch(ctx, pinned_manifests)
.map_err(|e| FetchGraphError::PinAndFetch(pkg.name.clone(), Box::new(e)))?;
let name = pkg.name.clone();
let dep_pinned = Pinned { name, source };
let dep_node = graph.add_node(dep_pinned);
added.insert(dep_node);
*entry.insert(dep_node)
}
};
let dep_edge = Dep::new(dep_name.to_string(), dep_kind.clone());
graph.update_edge(node, dep_node, dep_edge.clone());
if !ctx.visited.insert(dep_node) {
continue;
}
let dep_pinned = &graph[dep_node];
let dep_pkg_id = dep_pinned.id();
check_dep_manifest(dep_pinned, &pinned_manifests[&dep_pkg_id], &dep_edge)
.map_err(|e| FetchGraphError::DepManifest(graph[node].name.clone(), dep_name, e))?;
let path_root = match dep_pinned.source {
source::Pinned::Member(_) => dep_pkg_id,
source::Pinned::Path(_) => path_root,
};
added.extend(fetch_deps(
member_manifests,
dep_node,
path_root,
graph,
pinned_manifests,
ctx,
)?);
}
Ok(added)
}
fn find_member_node(g: &Graph, pkg_name: &str) -> Result<Option<NodeIx>, MemberNameNotUnique> {
let mut matching = member_nodes_with_name(g, pkg_name);
let node_opt = matching.next();
match matching.next() {
None => Ok(node_opt),
Some(_) => Err(MemberNameNotUnique(pkg_name.to_string())),
}
}
fn member_nodes(g: &Graph) -> impl '_ + Iterator<Item = NodeIx> {
g.node_indices()
.filter(|&n| g[n].source == source::Pinned::MEMBER)
}
fn member_nodes_with_name<'a>(
g: &'a Graph,
pkg_name: &'a str,
) -> impl 'a + Iterator<Item = NodeIx> {
member_nodes(g).filter(move |&n| g[n].name == pkg_name)
}
fn check_graph(graph: &Graph, members: &MemberManifests) -> (PinnedManifests, InvalidDeps) {
let mut pinned_manifests = PinnedManifests::default();
let mut invalid_deps = InvalidDeps::default();
for n in member_nodes(graph) {
let pinned = &graph[n];
let name = &pinned.name;
let Some(manifest) = members.get(name) else {
continue;
};
pinned_manifests.insert(pinned.id(), manifest.clone());
invalid_deps.extend(check_deps(graph, members, n, &mut pinned_manifests));
}
if pinned_manifests.is_empty() {
invalid_deps.extend(
graph
.edge_indices()
.map(|e| (e, InvalidDepCause::NoMembersFound)),
);
}
(pinned_manifests, invalid_deps)
}
fn check_deps(
graph: &Graph,
members: &MemberManifests,
node: NodeIx,
pinned_manifests: &mut PinnedManifests,
) -> InvalidDeps {
let mut remove = InvalidDeps::default();
for edge in graph.edges_directed(node, Direction::Outgoing) {
let dep_manifest = match check_dep(graph, members, edge, pinned_manifests) {
Ok(manifest) => manifest,
Err(err) => {
remove.insert(edge.id(), InvalidDepCause::Check(err));
continue;
}
};
let dep_node = edge.target();
let dep_id = graph[dep_node].id();
if pinned_manifests.insert(dep_id, dep_manifest).is_none() {
let rm = check_deps(graph, members, dep_node, pinned_manifests);
remove.extend(rm);
}
}
remove
}
fn check_dep(
graph: &Graph,
members: &MemberManifests,
edge: EdgeReference,
pinned_manifests: &PinnedManifests,
) -> Result<ManifestFile, CheckDepError> {
let node = edge.source();
let dep_node = edge.target();
let dep = edge.weight();
let node_manifest = &pinned_manifests[&graph[node].id()];
let dep_path = dep_path(graph, members, node_manifest, dep_node)?;
let dep_manifest_path = dep_path.join(ManifestFile::FILE_NAME);
let dep_manifest = ManifestFile::from_path(&dep_manifest_path)?;
let dep_entry = node_manifest
.dep(&dep.name)
.ok_or(CheckDepError::NoEntryInManifest)?;
let dep_source = Source::from_manifest_dep(node_manifest.dir(), dep_entry, members.values())?;
let dep_pkg = graph[dep_node].unpinned(&dep_path);
if dep_pkg.source != dep_source {
return Err(CheckDepError::SourceMismatch(dep_pkg.source, dep_source));
}
check_dep_manifest(&graph[dep_node], &dep_manifest, dep)?;
Ok(dep_manifest)
}
fn dep_path(
graph: &Graph,
members: &MemberManifests,
node_manifest: &ManifestFile,
dep_node: NodeIx,
) -> Result<PathBuf, DepPathError> {
let dep = &graph[dep_node];
let dep_name = &dep.name;
match dep.source.dep_path(&dep.name)? {
source::DependencyPath::ManifestPath(path) => Ok(path),
source::DependencyPath::Member => members
.values()
.find(|m| m.pkg.name == *dep_name)
.map(|m| m.dir().to_path_buf())
.ok_or_else(|| DepPathError::MemberNotFound(dep_name.to_string())),
source::DependencyPath::Root(path_root) => {
check_path_root(graph, dep_node, path_root)?;
match node_manifest.dep_path(dep_name) {
None => Err(DepPathError::NoEntryInManifest(
dep_name.to_string(),
node_manifest.pkg.name.to_string(),
)),
Some(path) if !path.exists() => {
Err(DepPathError::PathDoesNotExist(dep_name.to_string(), path))
}
Some(path) => Ok(path),
}
}
}
}
fn check_dep_manifest(
dep: &Pinned,
dep_manifest: &ManifestFile,
dep_edge: &Dep,
) -> Result<(), DepManifestError> {
match (&dep_edge.kind, &dep_manifest.pkg.kind) {
(DepKind::Contract, manifest::PackageKind::Contract)
| (DepKind::Library, manifest::PackageKind::Library) => (),
_ => {
let pkg_kind = dep_manifest.pkg.kind.clone();
return Err(DepManifestError::UnexpectedKind(
dep_edge.kind.clone(),
pkg_kind,
));
}
}
if dep.name != dep_manifest.pkg.name {
let pkg_name = dep_manifest.pkg.name.clone();
return Err(DepManifestError::UnexpectedName(dep.name.clone(), pkg_name));
}
Ok(())
}
fn check_path_root(
graph: &Graph,
path_dep: NodeIx,
path_root: PinnedId,
) -> Result<(), CheckPathRootError> {
let path_root_node = find_path_root(graph, path_dep)?;
if graph[path_root_node].id() != path_root {
return Err(CheckPathRootError::Invalid(
graph[path_dep].name.to_string(),
path_root,
graph[path_root_node].id(),
));
}
Ok(())
}
fn find_path_root(graph: &Graph, mut node: NodeIx) -> Result<NodeIx, FindPathRootError> {
loop {
let pkg = &graph[node];
match pkg.source {
source::Pinned::Member(_) => return Ok(node),
source::Pinned::Path(ref src) => {
let parent = graph
.edges_directed(node, Direction::Incoming)
.next()
.map(|edge| edge.source())
.ok_or_else(|| FindPathRootError(format!("{src}")))?;
node = parent;
}
}
}
}
fn compilation_order(graph: &Graph) -> Result<Vec<NodeIx>, DependencyCycle> {
let rev_graph = petgraph::visit::Reversed(&graph);
petgraph::algo::toposort(rev_graph, None).map_err(|_cycle| {
let sccs = petgraph::algo::kosaraju_scc(graph);
let cycle = sccs
.into_iter()
.find(|path| path.len() > 1)
.expect("one cycle must exist")
.into_iter()
.map(|n| graph[n].name.to_string())
.collect();
DependencyCycle(cycle)
})
}