1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use callisto_manifests::{detect_npm_workspace_kind, open, OpenContext, WorkspaceCargoResolver};
6use callisto_model::{
7 CommandRunner, DepEdge, Ecosystem, ManifestDecl, ManifestFormat, ManifestRole, Package,
8 PackageId, PublishTarget, ReleaseTrigger,
9};
10
11use crate::config::ResolvedConfig;
12use crate::crosscheck::crosscheck_declared_edges;
13use crate::error::GraphError;
14use crate::identity::{IdentityIndex, IdentityResolver};
15use crate::locate::ProjectLocator;
16use crate::resolver::ManifestWalkResolver;
17
18impl ManifestWalkResolver {
19 pub fn build<L: ProjectLocator, R: CommandRunner>(
20 root: &Path,
21 locator: &L,
22 _runner: &R,
23 _cfg: &ResolvedConfig,
24 ) -> Result<Self, GraphError> {
25 let projects = locator.projects()?;
26
27 let cargo_workspace = if root.join("Cargo.toml").exists() {
28 if let Ok(resolver) = WorkspaceCargoResolver::load(&root.join("Cargo.toml")) {
29 resolver.inheritance().ok().map(Arc::new)
30 } else {
31 None
32 }
33 } else {
34 None
35 };
36
37 let npm_workspace_kind = detect_npm_workspace_kind(root).ok().flatten();
38
39 let ctx = OpenContext {
40 workspace_root: root,
41 cargo_workspace,
42 npm_workspace_kind,
43 };
44
45 let identity_resolver = IdentityResolver::new(root)?;
46 let mut package_manifest_decls: BTreeMap<PackageId, (PathBuf, Vec<ManifestDecl>)> =
47 BTreeMap::new();
48 let mut index = IdentityIndex::default();
49 let mut diagnostics = Vec::new();
50
51 let mut by_path: BTreeMap<PathBuf, Vec<(Ecosystem, PackageId)>> = BTreeMap::new();
52 for proj in &projects {
53 let id = identity_resolver.resolve(&proj.path, proj.ecosystem)?;
54 by_path
55 .entry(proj.path.clone())
56 .or_default()
57 .push((proj.ecosystem, id));
58 }
59
60 for (rel_path, mut list) in by_path {
61 list.sort_by_key(|a| a.0);
62 let primary_id = list[0].1.clone();
63
64 index
65 .bare
66 .insert(primary_id.name().to_string(), primary_id.clone());
67
68 let mut decls = Vec::new();
69 for (eco, _id) in &list {
70 let (fmt, filename) = match eco {
71 Ecosystem::Cargo => (ManifestFormat::CargoToml, "Cargo.toml"),
72 Ecosystem::Npm => (ManifestFormat::PackageJson, "package.json"),
73 _ => (ManifestFormat::PackageJson, "package.json"),
74 };
75 let manifest_rel = rel_path.join(filename);
76 if let Ok(decl) = ManifestDecl::new(manifest_rel, ManifestRole::Canonical, fmt) {
77 decls.push(decl);
78 }
79 index
80 .native
81 .insert((*eco, primary_id.name().to_string()), primary_id.clone());
82 }
83
84 package_manifest_decls.insert(primary_id, (rel_path, decls));
85 }
86
87 let mut packages = BTreeMap::new();
88 for (id, (rel_path, decls)) in package_manifest_decls {
89 let ch_path = rel_path.join("CHANGELOG.md");
90 let mut publish_to = Vec::new();
91 for decl in &decls {
92 if let Ok(editor) = callisto_manifests::open(decl, &ctx) {
93 for target in editor.publish_targets() {
94 if target != PublishTarget::None && !publish_to.contains(&target) {
95 publish_to.push(target);
96 }
97 }
98 }
99 }
100 if publish_to.is_empty() {
101 publish_to.push(PublishTarget::None);
102 }
103
104 let pkg = Package {
105 id: id.clone(),
106 manifests: decls,
107 changelog: Some(ch_path),
108 release_trigger: ReleaseTrigger::Changeset,
109 publish_to,
110 tag_template: None,
111 };
112 packages.insert(id, pkg);
113 }
114
115 let mut edges = Vec::new();
116 let mut out_index: BTreeMap<PackageId, Vec<usize>> = BTreeMap::new();
117 let mut in_index: BTreeMap<PackageId, Vec<usize>> = BTreeMap::new();
118
119 for pkg in packages.values() {
120 for decl in &pkg.manifests {
121 if decl.role != ManifestRole::Canonical {
122 continue;
123 }
124 if let Ok(m) = open(decl, &ctx) {
125 for entry in m.iter_dependencies() {
126 let (spec, declaring_path) = if entry.inherited {
127 if let Some(ref inh) = ctx.cargo_workspace {
128 if let Some(inherited_dep) = inh.inherited(&entry.name) {
129 (
130 inherited_dep.spec.clone(),
131 inherited_dep.declared_in.to_path_buf(),
132 )
133 } else {
134 (entry.spec.clone(), decl.path.clone())
135 }
136 } else {
137 (entry.spec.clone(), decl.path.clone())
138 }
139 } else {
140 (entry.spec.clone(), decl.path.clone())
141 };
142
143 if let Some(to) = index.resolve_native(decl.ecosystem(), &entry.name) {
144 let idx = edges.len();
145 let edge = DepEdge {
146 from: pkg.id.clone(),
147 to: to.clone(),
148 kind: entry.kind,
149 spec,
150 from_manifest: declaring_path,
151 inherited: entry.inherited,
152 };
153 edges.push(edge);
154
155 out_index.entry(pkg.id.clone()).or_default().push(idx);
156 in_index.entry(to.clone()).or_default().push(idx);
157 }
158 }
159 }
160 }
161 }
162
163 if let Some(declared) = locator.declared_edges() {
164 let cross_diags = crosscheck_declared_edges(
165 &ManifestWalkResolver {
166 packages: packages.clone(),
167 edges: edges.clone(),
168 out_index: out_index.clone(),
169 in_index: in_index.clone(),
170 index: index.clone(),
171 diagnostics: Vec::new(),
172 },
173 &declared,
174 );
175 diagnostics.extend(cross_diags);
176 }
177
178 Ok(ManifestWalkResolver {
179 packages,
180 edges,
181 out_index,
182 in_index,
183 index,
184 diagnostics,
185 })
186 }
187}