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