use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::hash::digest_bytes;
use crate::observe::{self, Observation};
use crate::resolution::rust::dependency::RustDependency;
use crate::resolution::rust::edition::CargoEdition;
use crate::resolution::rust::error::RustProjectError;
use crate::resolution::rust::identity::{PackageId, ProjectAuthority, TargetId, index_of};
use crate::resolution::rust::limits::ResolutionLimits;
use crate::resolution::rust::manifest::ManifestDocument;
use crate::resolution::rust::members;
use crate::resolution::rust::package::RustPackage;
use crate::resolution::rust::paths;
use crate::resolution::rust::project::RustProject;
use crate::resolution::rust::target::RustTarget;
use crate::resolution::rust::toml_view;
use super::dependency::{DeclaredEdge, EdgeContext, declared_edges};
use super::package::{WorkspaceSource, package_facts};
use super::target::{TargetContext, TargetDraft, discover_targets};
struct ManifestEntry {
document: ManifestDocument,
edges: Box<[DeclaredEdge]>,
member: bool,
}
struct LoadedManifests {
entries: Box<[ManifestEntry]>,
workspace: Option<toml::Table>,
workspace_manifest: Arc<str>,
}
struct AssemblyContext<'a> {
root: &'a Path,
authority: ProjectAuthority,
workspace: WorkspaceSource<'a>,
}
#[derive(Default)]
struct ProjectDraft {
packages: Vec<RustPackage>,
targets: Vec<RustTarget>,
libraries: Vec<Option<TargetId>>,
manifests: BTreeMap<PathBuf, PackageId>,
next_package: u32,
next_target: u32,
}
impl ProjectDraft {
fn library_of(&self, package: PackageId) -> Option<TargetId> {
self.libraries
.get(index_of(package.index()))
.copied()
.flatten()
}
}
pub(in crate::resolution::rust) fn load_project(
root: &Path,
limits: ResolutionLimits,
) -> Result<RustProject, RustProjectError> {
observe::record(Observation::ProjectLoad);
let root_path = paths::canonical_root(root)?;
let loaded = collect_manifests(&root_path, limits)?;
assemble(&root_path, limits, &loaded)
}
fn collect_manifests(
root: &Path,
limits: ResolutionLimits,
) -> Result<LoadedManifests, RustProjectError> {
let root_manifest =
paths::canonical_manifest(root)?.ok_or_else(|| RustProjectError::InvalidRoot {
path: paths::path_text(root),
reason: Box::from("the project root holds no Cargo.toml"),
})?;
let root_document = ManifestDocument::read(root, &root_manifest)?;
let workspace = root_document.workspace().cloned();
let workspace_manifest = Arc::clone(root_document.relative());
let members = member_manifests(root, &root_document, limits)?;
let root_entry = read_entry(root, root_document, (&members, workspace.as_ref()))?;
let mut pending: VecDeque<PathBuf> = members.iter().cloned().collect();
pending.extend(edge_manifests(&root_entry));
let mut seen = BTreeSet::from([root_manifest]);
let mut entries = vec![root_entry];
while let Some(path) = pending.pop_front() {
if seen.contains(&path) {
continue;
}
check_manifest_limit(entries.len().saturating_add(1), limits)?;
let document = ManifestDocument::read(root, &path)?;
let entry = read_entry(root, document, (&members, workspace.as_ref()))?;
pending.extend(edge_manifests(&entry));
entries.push(entry);
seen.insert(path);
}
entries.sort_by(|left, right| left.document.relative().cmp(right.document.relative()));
Ok(LoadedManifests {
entries: entries.into_boxed_slice(),
workspace,
workspace_manifest,
})
}
fn read_entry(
root: &Path,
document: ManifestDocument,
inheritance: (&BTreeSet<PathBuf>, Option<&toml::Table>),
) -> Result<ManifestEntry, RustProjectError> {
let (members, workspace) = inheritance;
let context = EdgeContext {
root,
directory: document.directory(),
workspace,
};
let edges = declared_edges(&context, document.table())?;
Ok(ManifestEntry {
member: members.contains(document.path()),
edges,
document,
})
}
fn edge_manifests(entry: &ManifestEntry) -> impl Iterator<Item = PathBuf> + '_ {
entry.edges.iter().filter_map(|edge| edge.manifest.clone())
}
fn check_manifest_limit(count: usize, limits: ResolutionLimits) -> Result<(), RustProjectError> {
let ceiling = usize::try_from(limits.max_manifests).unwrap_or(usize::MAX);
match count > ceiling {
true => Err(RustProjectError::LimitExceeded {
limit: limits.max_manifests,
}),
false => Ok(()),
}
}
fn member_manifests(
root: &Path,
document: &ManifestDocument,
limits: ResolutionLimits,
) -> Result<BTreeSet<PathBuf>, RustProjectError> {
let mut manifests = BTreeSet::new();
if document.package().is_some() {
manifests.extend(paths::canonical_manifest(root)?);
}
let workspace = match document.workspace() {
Some(workspace) => workspace,
None => return Ok(manifests),
};
let directories = members::member_directories(
root,
(
&toml_view::strings(workspace, "members"),
&toml_view::strings(workspace, "exclude"),
),
limits,
)?;
for directory in directories.iter() {
manifests.extend(paths::canonical_manifest(directory)?);
}
Ok(manifests)
}
fn assemble(
root: &Path,
limits: ResolutionLimits,
loaded: &LoadedManifests,
) -> Result<RustProject, RustProjectError> {
let context = AssemblyContext {
root,
authority: ProjectAuthority::new(
root_fingerprint(root)?,
manifest_revision(&loaded.entries),
),
workspace: WorkspaceSource {
package: loaded
.workspace
.as_ref()
.and_then(|workspace| toml_view::table(workspace, "package")),
manifest_path: &loaded.workspace_manifest,
},
};
let mut draft = ProjectDraft::default();
for entry in loaded.entries.iter() {
add_package(&mut draft, &context, entry)?;
}
let dependencies = build_dependencies(loaded, &draft);
Ok(RustProject {
root: Box::from(root),
limits,
authority: context.authority,
manifests: loaded
.entries
.iter()
.map(|entry| entry.document.fingerprint())
.collect(),
packages: draft.packages.into_boxed_slice(),
targets: draft.targets.into_boxed_slice(),
dependencies,
})
}
fn add_package(
draft: &mut ProjectDraft,
context: &AssemblyContext<'_>,
entry: &ManifestEntry,
) -> Result<(), RustProjectError> {
let table = match entry.document.package() {
Some(table) => table,
None => return Ok(()),
};
let facts = package_facts(table, entry.document.relative(), &context.workspace)?;
let id = PackageId::new(context.authority, draft.next_package);
draft.next_package = draft.next_package.saturating_add(1);
let drafts = discover_targets(&TargetContext {
root: context.root,
directory: entry.document.directory(),
manifest: entry.document.table(),
manifest_path: entry.document.relative(),
package: table,
package_name: &facts.name,
})?;
let library = push_targets(draft, context.authority, id, facts.edition, &drafts);
draft.packages.push(RustPackage {
id,
name: facts.name,
version: facts.version,
rust_version: facts.rust_version,
edition: facts.edition,
manifest_path: Arc::clone(entry.document.relative()),
relative_directory: relative_directory(entry.document.relative()),
directory: entry.document.directory().into(),
workspace_member: entry.member,
});
draft.libraries.push(library);
draft
.manifests
.insert(entry.document.path().to_path_buf(), id);
Ok(())
}
fn push_targets(
draft: &mut ProjectDraft,
authority: ProjectAuthority,
package: PackageId,
edition: CargoEdition,
drafts: &[TargetDraft],
) -> Option<TargetId> {
let mut library = None;
for target in drafts {
let id = TargetId::new(authority, draft.next_target);
draft.next_target = draft.next_target.saturating_add(1);
library = library.or(matches!(
target.kind,
crate::resolution::rust::target::CargoTargetKind::Library
)
.then_some(id));
draft.targets.push(RustTarget {
id,
package,
name: Arc::clone(&target.name),
kind: target.kind,
entry_path: Arc::clone(&target.entry),
edition,
});
}
library
}
fn build_dependencies(loaded: &LoadedManifests, draft: &ProjectDraft) -> Box<[RustDependency]> {
let mut dependencies = Vec::new();
for entry in loaded.entries.iter() {
let source = match draft.manifests.get(entry.document.path()) {
Some(id) => *id,
None => continue,
};
dependencies.extend(
entry
.edges
.iter()
.map(|edge| build_dependency(draft, source, edge)),
);
}
dependencies.into_boxed_slice()
}
fn build_dependency(
draft: &ProjectDraft,
source: PackageId,
edge: &DeclaredEdge,
) -> RustDependency {
let package = edge
.manifest
.as_ref()
.and_then(|manifest| draft.manifests.get(manifest))
.copied();
RustDependency {
source,
name: Arc::clone(&edge.name),
package_name: Arc::clone(&edge.package_name),
kind: edge.kind,
activation: edge.activation.clone(),
package,
library: package.and_then(|id| draft.library_of(id)),
}
}
fn relative_directory(manifest_path: &Arc<str>) -> Arc<str> {
match manifest_path.rsplit_once('/') {
Some((directory, _)) => Arc::from(directory),
None => Arc::from(""),
}
}
fn manifest_revision(entries: &[ManifestEntry]) -> [u8; 32] {
let mut buffer = Vec::new();
for entry in entries {
buffer.extend_from_slice(entry.document.relative().as_bytes());
buffer.push(0);
buffer.extend_from_slice(entry.document.digest());
}
digest_bytes(&buffer)
}
fn root_fingerprint(root: &Path) -> Result<[u8; 32], RustProjectError> {
let text = root.to_str().ok_or_else(|| RustProjectError::NonUtf8Path {
path: paths::path_text(root),
})?;
Ok(digest_bytes(text.as_bytes()))
}