1use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12use crate::diagnostics::{source_line, split_source_lines, Diagnostic};
13use crate::manifest::{self, Dependency, DependencySource, GitRev, Manifest, MANIFEST_NAME};
14
15pub type DependencyMap = HashMap<PathBuf, HashMap<String, PathBuf>>;
20
21const MANIFEST_HEADLINE: &str = "very manifest. much confuse.";
22const RESOLVE_HEADLINE: &str = "very dependency. much missing.";
23
24pub fn discover_root(start: &Path) -> Option<PathBuf> {
27 let mut current = Some(start);
28 while let Some(dir) = current {
29 if dir.join(MANIFEST_NAME).is_file() {
30 return Some(dir.to_path_buf());
31 }
32 current = dir.parent();
33 }
34 None
35}
36
37pub fn read_manifest(root: &Path) -> Result<Manifest, Diagnostic> {
39 let path = root.join(MANIFEST_NAME);
40 let source = std::fs::read_to_string(&path).map_err(|err| {
41 Diagnostic::new(
42 path.to_string_lossy(),
43 1,
44 1,
45 "",
46 format!("doge could not read the manifest: {err}"),
47 )
48 .with_headline(MANIFEST_HEADLINE)
49 .with_hint("every project has a doge.toml at its root")
50 })?;
51 manifest::parse(&path.to_string_lossy(), &source)
52}
53
54pub fn resolve_project<F>(root: &Path, git_dir: &mut F) -> Result<DependencyMap, Diagnostic>
58where
59 F: FnMut(&str, &GitRev) -> Result<PathBuf, String>,
60{
61 let mut map = DependencyMap::new();
62 let root_manifest = read_manifest(root)?;
63 resolve_package(root, &root_manifest, &mut map, git_dir)?;
64 Ok(map)
65}
66
67fn resolve_package<F>(
70 pkg: &Path,
71 manifest: &Manifest,
72 map: &mut DependencyMap,
73 git_dir: &mut F,
74) -> Result<(), Diagnostic>
75where
76 F: FnMut(&str, &GitRev) -> Result<PathBuf, String>,
77{
78 let canon = std::fs::canonicalize(pkg).map_err(|err| {
79 Diagnostic::new(
80 pkg.to_string_lossy(),
81 1,
82 1,
83 "",
84 format!("doge could not open the project directory: {err}"),
85 )
86 .with_headline(RESOLVE_HEADLINE)
87 .with_hint("check the project path")
88 })?;
89 map.insert(canon.clone(), HashMap::new());
92
93 let manifest_path = pkg.join(MANIFEST_NAME).to_string_lossy().into_owned();
94 let mut aliases = HashMap::new();
95
96 for dep in &manifest.dependencies {
97 let dep_dir = dep_directory(pkg, &manifest_path, dep, git_dir)?;
98 let dep_manifest = read_dep_manifest(&dep_dir, &manifest_path, dep)?;
99 let entry = dep_dir.join(&dep_manifest.entry);
100 let entry_canon = std::fs::canonicalize(&entry).map_err(|_| {
101 resolve_diag(
102 &manifest_path,
103 dep,
104 &format!(
105 "the {} package has no entry file {}",
106 dep.alias, dep_manifest.entry
107 ),
108 "check the dependency's [package] entry",
109 )
110 })?;
111 aliases.insert(dep.alias.clone(), entry_canon);
112
113 if let Ok(dep_canon) = std::fs::canonicalize(&dep_dir) {
114 if !map.contains_key(&dep_canon) {
115 resolve_package(&dep_dir, &dep_manifest, map, git_dir)?;
116 }
117 }
118 }
119
120 map.insert(canon, aliases);
121 Ok(())
122}
123
124fn dep_directory<F>(
127 pkg: &Path,
128 manifest_path: &str,
129 dep: &Dependency,
130 git_dir: &mut F,
131) -> Result<PathBuf, Diagnostic>
132where
133 F: FnMut(&str, &GitRev) -> Result<PathBuf, String>,
134{
135 match &dep.source {
136 DependencySource::Path(rel) => Ok(pkg.join(rel)),
137 DependencySource::Git { url, rev } => git_dir(url, rev).map_err(|message| {
138 resolve_diag(
139 manifest_path,
140 dep,
141 &message,
142 "run doge bark to fetch dependencies",
143 )
144 }),
145 }
146}
147
148fn read_dep_manifest(
151 dep_dir: &Path,
152 manifest_path: &str,
153 dep: &Dependency,
154) -> Result<Manifest, Diagnostic> {
155 let path = dep_dir.join(MANIFEST_NAME);
156 let source = std::fs::read_to_string(&path).map_err(|_| {
157 resolve_diag(
158 manifest_path,
159 dep,
160 &format!("doge found no package at {}", dep_dir.display()),
161 "a dependency is a directory with its own doge.toml",
162 )
163 })?;
164 manifest::parse(&path.to_string_lossy(), &source)
165}
166
167fn resolve_diag(manifest_path: &str, dep: &Dependency, message: &str, hint: &str) -> Diagnostic {
170 let line = std::fs::read_to_string(manifest_path)
171 .map(|source| source_line(&split_source_lines(&source), dep.line))
172 .unwrap_or_default();
173 Diagnostic::new(manifest_path, dep.line, 1, line, message)
174 .with_headline(RESOLVE_HEADLINE)
175 .with_hint(hint)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 struct TempDir(PathBuf);
183
184 impl TempDir {
185 fn new(tag: &str) -> TempDir {
186 let dir = std::env::temp_dir().join(format!("doge-projtest-{tag}"));
187 let _ = std::fs::remove_dir_all(&dir);
188 std::fs::create_dir_all(&dir).expect("create temp dir");
189 TempDir(dir)
190 }
191 fn write(&self, name: &str, contents: &str) {
192 let path = self.0.join(name);
193 if let Some(parent) = path.parent() {
194 std::fs::create_dir_all(parent).expect("create fixture dir");
195 }
196 std::fs::write(&path, contents).expect("write fixture");
197 }
198 }
199
200 impl Drop for TempDir {
201 fn drop(&mut self) {
202 let _ = std::fs::remove_dir_all(&self.0);
203 }
204 }
205
206 fn canon(path: &Path) -> PathBuf {
207 std::fs::canonicalize(path).expect("path should exist")
208 }
209
210 #[test]
211 fn resolves_a_transitive_path_dependency_graph() {
212 let dir = TempDir::new("path-graph");
213 dir.write("util/doge.toml", "[package]\nname = \"util\"\n");
214 dir.write(
215 "util/main.doge",
216 "such id much n:\n return n\nwow\nwow\n",
217 );
218 dir.write(
219 "greet/doge.toml",
220 "[package]\nname = \"greet\"\n\n[dependencies]\nutil = { path = \"../util\" }\n",
221 );
222 dir.write(
223 "greet/main.doge",
224 "so util\nsuch hi:\n return 1\nwow\nwow\n",
225 );
226 dir.write(
227 "app/doge.toml",
228 "[package]\nname = \"app\"\n\n[dependencies]\ngreet = { path = \"../greet\" }\n",
229 );
230 dir.write("app/main.doge", "so greet\nbark greet.hi()\nwow\n");
231
232 let app = dir.0.join("app");
233 let map = resolve_project(&app, &mut |_, _| unreachable!("no git deps here"))
234 .expect("should resolve");
235
236 assert_eq!(
237 map[&canon(&app)]["greet"],
238 canon(&dir.0.join("greet/main.doge"))
239 );
240 assert_eq!(
241 map[&canon(&dir.0.join("greet"))]["util"],
242 canon(&dir.0.join("util/main.doge"))
243 );
244 assert!(map[&canon(&dir.0.join("util"))].is_empty());
245 }
246
247 #[test]
248 fn a_git_dependency_is_resolved_through_the_closure() {
249 let dir = TempDir::new("git-closure");
250 dir.write("vendor/cool/doge.toml", "[package]\nname = \"cool\"\n");
252 dir.write("vendor/cool/main.doge", "such f:\n return 1\nwow\nwow\n");
253 dir.write(
254 "app/doge.toml",
255 "[package]\nname = \"app\"\n\n[dependencies]\ncool = { git = \"https://example.com/cool.git\", tag = \"v1\" }\n",
256 );
257 dir.write("app/main.doge", "so cool\nbark cool.f()\nwow\n");
258
259 let vendor = dir.0.join("vendor/cool");
260 let mut calls = 0;
261 let map = resolve_project(&dir.0.join("app"), &mut |url, rev| {
262 calls += 1;
263 assert_eq!(url, "https://example.com/cool.git");
264 assert_eq!(rev.as_ref_name(), Some("v1"));
265 Ok(vendor.clone())
266 })
267 .expect("should resolve");
268
269 assert_eq!(calls, 1, "the git closure runs once for the git dep");
270 assert_eq!(
271 map[&canon(&dir.0.join("app"))]["cool"],
272 canon(&vendor.join("main.doge"))
273 );
274 }
275
276 #[test]
277 fn a_missing_path_dependency_is_reported() {
278 let dir = TempDir::new("path-missing");
279 dir.write(
280 "app/doge.toml",
281 "[package]\nname = \"app\"\n\n[dependencies]\ngone = { path = \"../gone\" }\n",
282 );
283 dir.write("app/main.doge", "so gone\nwow\n");
284
285 let err = resolve_project(&dir.0.join("app"), &mut |_, _| unreachable!())
286 .expect_err("a missing path dep should fail");
287 assert_eq!(err.headline, RESOLVE_HEADLINE);
288 assert!(err.message.contains("no package"));
289 }
290}