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