1use std::collections::{BTreeMap, BTreeSet};
24use std::path::{Path, PathBuf};
25
26use super::lockfile::{integrity_for_directory, LockedPackage, Lockfile};
27use super::manifest::{DepSource, DepSpec, DetailedDep, Manifest};
28use super::store::Store;
29use super::{PkgError, PkgResult};
30
31pub struct Resolver<'a> {
33 pub manifest: &'a Manifest,
35 pub manifest_dir: &'a Path,
37 pub store: &'a Store,
39}
40
41#[derive(Debug, Clone)]
43struct ResolvedDep {
44 name: String,
45 version: String,
46 source: String,
47 integrity: String,
48 deps: Vec<String>,
50 features: Vec<String>,
52}
53
54#[derive(Debug, Clone)]
56pub struct ResolveOutcome {
57 pub lockfile: Lockfile,
59 pub installed: Vec<(String, String, PathBuf)>,
62}
63
64impl<'a> Resolver<'a> {
65 pub fn resolve(&self) -> PkgResult<ResolveOutcome> {
77 self.store.ensure_layout()?;
78
79 let mut graph: BTreeMap<String, ResolvedDep> = BTreeMap::new();
80 let mut installed: Vec<(String, String, PathBuf)> = Vec::new();
81 let mut visiting: BTreeSet<String> = BTreeSet::new();
82
83 let direct = self.collect_direct_deps();
87 for (name, spec) in &direct {
88 let resolved_spec = self.resolve_workspace_dep(name, spec)?;
89 self.walk_dep(
90 name,
91 &resolved_spec,
92 self.manifest_dir,
93 &mut graph,
94 &mut installed,
95 &mut visiting,
96 )?;
97 }
98
99 if let Some(ws) = &self.manifest.workspace {
101 for member_pattern in &ws.members {
102 let member_dirs = expand_workspace_glob(self.manifest_dir, member_pattern)?;
103 for member_dir in member_dirs {
104 let member_manifest_path = member_dir.join("stryke.toml");
105 if !member_manifest_path.is_file() {
106 return Err(PkgError::Resolve(format!(
107 "workspace member {} has no stryke.toml",
108 member_dir.display()
109 )));
110 }
111 let member_manifest = Manifest::from_path(&member_manifest_path)?;
112 for (name, spec) in member_manifest
113 .deps
114 .iter()
115 .chain(member_manifest.dev_deps.iter())
116 .chain(member_manifest.groups.values().flat_map(|g| g.iter()))
117 {
118 let resolved_spec = self.resolve_workspace_dep(name, spec)?;
119 self.walk_dep(
120 name,
121 &resolved_spec,
122 &member_dir,
123 &mut graph,
124 &mut installed,
125 &mut visiting,
126 )?;
127 }
128 }
129 }
130 }
131
132 let mut lockfile = Lockfile::new();
133 for (_, dep) in graph {
134 lockfile.packages.push(LockedPackage {
135 name: dep.name,
136 version: dep.version,
137 source: dep.source,
138 integrity: dep.integrity,
139 features: dep.features,
140 deps: dep.deps,
141 });
142 }
143 lockfile.canonicalize();
144 Ok(ResolveOutcome {
145 lockfile,
146 installed,
147 })
148 }
149
150 fn resolve_workspace_dep(&self, name: &str, spec: &DepSpec) -> PkgResult<DepSpec> {
159 let inherits = matches!(spec, DepSpec::Detailed(d) if d.workspace);
160 if !inherits {
161 return Ok(spec.clone());
162 }
163 let ws = match &self.manifest.workspace {
164 Some(w) => w,
165 None => {
166 return Err(PkgError::Resolve(format!(
167 "dep `{}` has `workspace = true` but the root manifest has no [workspace] table",
168 name
169 )));
170 }
171 };
172 let inherited = ws.deps.get(name).ok_or_else(|| {
173 PkgError::Resolve(format!(
174 "dep `{}` inherits from [workspace.deps] but no such entry exists in the root manifest",
175 name
176 ))
177 })?;
178 let mut absolutized = match inherited.clone() {
179 DepSpec::Detailed(mut d) => {
180 if let Some(p) = d.path.as_ref() {
181 let pp = std::path::Path::new(p);
182 if !pp.is_absolute() {
183 let abs = self.manifest_dir.join(pp);
184 d.path = Some(abs.to_string_lossy().into_owned());
185 }
186 }
187 DepSpec::Detailed(d)
188 }
189 other => other,
190 };
191 if let DepSpec::Detailed(member) = spec {
193 if !member.features.is_empty() {
194 let mut merged = match absolutized {
195 DepSpec::Detailed(d) => d,
196 DepSpec::Version(v) => DetailedDep {
197 version: Some(v),
198 default_features: true,
199 ..DetailedDep::default()
200 },
201 DepSpec::Placeholder => DetailedDep::default(),
202 };
203 for f in &member.features {
204 if !merged.features.contains(f) {
205 merged.features.push(f.clone());
206 }
207 }
208 absolutized = DepSpec::Detailed(merged);
209 }
210 }
211 Ok(absolutized)
212 }
213
214 fn collect_direct_deps(&self) -> Vec<(String, DepSpec)> {
216 let mut out: Vec<(String, DepSpec)> = Vec::new();
217 for (k, v) in &self.manifest.deps {
218 out.push((k.clone(), v.clone()));
219 }
220 for (k, v) in &self.manifest.dev_deps {
221 out.push((k.clone(), v.clone()));
222 }
223 for (_group_name, group_map) in &self.manifest.groups {
224 for (k, v) in group_map {
225 out.push((k.clone(), v.clone()));
226 }
227 }
228 out
229 }
230
231 fn walk_dep(
235 &self,
236 name: &str,
237 spec: &DepSpec,
238 relative_to: &Path,
239 graph: &mut BTreeMap<String, ResolvedDep>,
240 installed: &mut Vec<(String, String, PathBuf)>,
241 visiting: &mut BTreeSet<String>,
242 ) -> PkgResult<()> {
243 match spec.source() {
244 DepSource::Path => {
245 let raw_path = spec.path().expect("path dep has path");
246 let path = resolve_path(relative_to, raw_path);
247 self.install_path_dep(name, &path, graph, installed, visiting)?;
248 Ok(())
249 }
250 DepSource::Git => self.install_git_dep(name, spec, graph, installed, visiting),
251 DepSource::GitHub => self.install_github_dep(name, spec, graph, installed, visiting),
252 DepSource::Registry => Err(PkgError::Resolve(format!(
253 "registry dep `{}` is not supported in this stryke version yet \
254 (RFC phases 7–8 — see docs/PACKAGE_REGISTRY.md). Use `path = \"...\"` \
255 to depend on a local copy in the meantime.",
256 name
257 ))),
258 }
259 }
260
261 fn install_git_dep(
268 &self,
269 name: &str,
270 spec: &DepSpec,
271 graph: &mut BTreeMap<String, ResolvedDep>,
272 installed: &mut Vec<(String, String, PathBuf)>,
273 visiting: &mut BTreeSet<String>,
274 ) -> PkgResult<()> {
275 let url = spec
276 .git()
277 .ok_or_else(|| PkgError::Resolve(format!("git dep `{}` missing url", name)))?
278 .to_string();
279
280 let Some((owner, repo)) = parse_github_url(&url) else {
281 return Err(PkgError::Resolve(format!(
282 "git dep `{}` URL `{}` is not a github.com URL — `s install` \
283 doesn't source-clone, it downloads prebuilt release tarballs. \
284 Rewrite as `{{ github = \"OWNER/REPO\" }}` for github-hosted \
285 packages or `{{ path = \"...\" }}` for local checkouts.",
286 name, url
287 )));
288 };
289
290 let pinned = match spec {
294 DepSpec::Detailed(d) => d.tag.as_deref(),
295 _ => None,
296 };
297 self.install_from_github_release(name, &owner, &repo, pinned, graph, installed, visiting)
298 }
299
300 fn install_github_dep(
304 &self,
305 name: &str,
306 spec: &DepSpec,
307 graph: &mut BTreeMap<String, ResolvedDep>,
308 installed: &mut Vec<(String, String, PathBuf)>,
309 visiting: &mut BTreeSet<String>,
310 ) -> PkgResult<()> {
311 let owner_repo = spec.github().ok_or_else(|| {
312 PkgError::Resolve(format!("github dep `{}` missing OWNER/REPO", name))
313 })?;
314 let (owner, repo) = owner_repo.split_once('/').ok_or_else(|| {
315 PkgError::Resolve(format!(
316 "github dep `{}`: expected `OWNER/REPO`, got `{}`",
317 name, owner_repo
318 ))
319 })?;
320 if owner.is_empty() || repo.is_empty() {
321 return Err(PkgError::Resolve(format!(
322 "github dep `{}`: empty owner or repo in `{}`",
323 name, owner_repo
324 )));
325 }
326 let pinned = spec.pinned_release_version().map(str::to_string);
327 self.install_from_github_release(
328 name,
329 owner,
330 repo,
331 pinned.as_deref(),
332 graph,
333 installed,
334 visiting,
335 )
336 }
337
338 fn install_from_github_release(
345 &self,
346 name: &str,
347 owner: &str,
348 repo: &str,
349 pinned: Option<&str>,
350 graph: &mut BTreeMap<String, ResolvedDep>,
351 installed: &mut Vec<(String, String, PathBuf)>,
352 visiting: &mut BTreeSet<String>,
353 ) -> PkgResult<()> {
354 let (manifest, dst, source) =
355 super::commands::install_global_from_github(&self.store, owner, repo, pinned)
356 .map_err(PkgError::Resolve)?;
357
358 let pkg = manifest
359 .package
360 .as_ref()
361 .ok_or_else(|| PkgError::Resolve(format!("github dep `{}`: tarball missing [package]", name)))?;
362 let version = pkg.version.clone();
363 let integrity = integrity_for_directory(&dst)?;
364 let key = format!("{}@{}", name, version);
365 if graph.contains_key(&key) {
366 return Ok(());
367 }
368 installed.push((name.to_string(), version.clone(), dst.clone()));
369
370 let mut transitive: Vec<String> = Vec::new();
373 for (sub_name, sub_spec) in &manifest.deps {
374 self.walk_dep(sub_name, sub_spec, &dst, graph, installed, visiting)?;
375 let sub_version = graph
376 .values()
377 .find(|d| &d.name == sub_name)
378 .map(|d| d.version.clone())
379 .unwrap_or_else(|| "0.0.0".to_string());
380 transitive.push(format!("{}@{}", sub_name, sub_version));
381 }
382 transitive.sort();
383 transitive.dedup();
384
385 graph.insert(
386 key,
387 ResolvedDep {
388 name: name.to_string(),
389 version,
390 source,
391 integrity,
392 deps: transitive,
393 features: Vec::new(),
394 },
395 );
396 Ok(())
397 }
398
399 fn install_path_dep(
403 &self,
404 name: &str,
405 src: &Path,
406 graph: &mut BTreeMap<String, ResolvedDep>,
407 installed: &mut Vec<(String, String, PathBuf)>,
408 visiting: &mut BTreeSet<String>,
409 ) -> PkgResult<()> {
410 let canonical_src = src.canonicalize().unwrap_or_else(|_| src.to_path_buf());
411 let source = format!("path+file://{}", canonical_src.display());
412 self.install_dir_dep(name, src, source, graph, installed, visiting)
413 }
414
415 fn install_dir_dep(
420 &self,
421 name: &str,
422 src: &Path,
423 source: String,
424 graph: &mut BTreeMap<String, ResolvedDep>,
425 installed: &mut Vec<(String, String, PathBuf)>,
426 visiting: &mut BTreeSet<String>,
427 ) -> PkgResult<()> {
428 if !src.is_dir() {
429 return Err(PkgError::Resolve(format!(
430 "dep `{}` does not exist or is not a directory: {}",
431 name,
432 src.display()
433 )));
434 }
435
436 let nested_manifest_path = src.join("stryke.toml");
437 let nested = if nested_manifest_path.is_file() {
438 Some(Manifest::from_path(&nested_manifest_path)?)
439 } else {
440 None
441 };
442
443 let version = nested
444 .as_ref()
445 .and_then(|m| m.package.as_ref())
446 .map(|p| p.version.clone())
447 .unwrap_or_else(|| "0.0.0".to_string());
448
449 let key = format!("{}@{}", name, version);
450 if graph.contains_key(&key) {
451 return Ok(());
452 }
453 if !visiting.insert(key.clone()) {
454 return Err(PkgError::Resolve(format!(
455 "cyclic dependency detected at `{}`",
456 key
457 )));
458 }
459
460 let integrity = integrity_for_directory(src)?;
461 let manifest_for_copy = nested.clone().unwrap_or_default();
462
463 let ffi_stage = super::commands::ensure_ffi_cdylib(src, &manifest_for_copy)
472 .map_err(PkgError::Resolve)?;
473
474 let dst = self
475 .store
476 .install_path_dep(name, &version, src, &manifest_for_copy)?;
477
478 if let Some((built_lib, lib_filename)) = ffi_stage {
482 let dst_lib_dir = dst.join("lib");
483 std::fs::create_dir_all(&dst_lib_dir).map_err(|e| {
484 PkgError::Io(format!("create {}: {}", dst_lib_dir.display(), e))
485 })?;
486 let dst_lib = dst_lib_dir.join(&lib_filename);
487 if !dst_lib.is_file() {
488 std::fs::copy(&built_lib, &dst_lib).map_err(|e| {
489 PkgError::Io(format!(
490 "stage cdylib {} -> {}: {}",
491 built_lib.display(),
492 dst_lib.display(),
493 e
494 ))
495 })?;
496 eprintln!(" staged {} -> {}", built_lib.display(), dst_lib.display());
497 }
498 }
499
500 installed.push((name.to_string(), version.clone(), dst.clone()));
501
502 let mut transitive: Vec<String> = Vec::new();
503 if let Some(nm) = nested.as_ref() {
504 for (sub_name, sub_spec) in &nm.deps {
505 self.walk_dep(sub_name, sub_spec, src, graph, installed, visiting)?;
506 let sub_version = graph
507 .values()
508 .find(|d| &d.name == sub_name)
509 .map(|d| d.version.clone())
510 .unwrap_or_else(|| "0.0.0".to_string());
511 transitive.push(format!("{}@{}", sub_name, sub_version));
512 }
513 }
514 transitive.sort();
515 transitive.dedup();
516
517 graph.insert(
518 key.clone(),
519 ResolvedDep {
520 name: name.to_string(),
521 version,
522 source,
523 integrity,
524 deps: transitive,
525 features: Vec::new(),
526 },
527 );
528 visiting.remove(&key);
529 Ok(())
530 }
531}
532
533pub(crate) fn parse_github_url(url: &str) -> Option<(String, String)> {
540 let body = if let Some(rest) = url.strip_prefix("https://github.com/") {
541 rest
542 } else if let Some(rest) = url.strip_prefix("http://github.com/") {
543 rest
544 } else if let Some(rest) = url.strip_prefix("git@github.com:") {
545 rest
546 } else if let Some(rest) = url.strip_prefix("github.com/") {
547 rest
548 } else {
549 return None;
550 };
551 let body = body.trim_end_matches('/');
552 let mut parts = body.splitn(2, '/');
553 let owner = parts.next()?;
554 let remainder = parts.next()?;
555 if owner.is_empty() || remainder.contains('/') {
556 return None;
557 }
558 let repo = remainder.strip_suffix(".git").unwrap_or(remainder);
559 if repo.is_empty() {
560 return None;
561 }
562 Some((owner.to_string(), repo.to_string()))
563}
564
565fn resolve_path(relative_to: &Path, raw: &str) -> PathBuf {
567 let p = Path::new(raw);
568 if p.is_absolute() {
569 p.to_path_buf()
570 } else {
571 relative_to.join(p)
572 }
573}
574
575fn expand_workspace_glob(root_dir: &Path, pattern: &str) -> PkgResult<Vec<PathBuf>> {
580 if let Some(prefix) = pattern.strip_suffix("/*") {
581 let parent = root_dir.join(prefix);
582 if !parent.is_dir() {
583 return Ok(Vec::new());
584 }
585 let mut out: Vec<PathBuf> = Vec::new();
586 for entry in std::fs::read_dir(&parent)
587 .map_err(|e| PkgError::Io(format!("read workspace dir {}: {}", parent.display(), e)))?
588 {
589 let entry = entry.map_err(|e| PkgError::Io(e.to_string()))?;
590 if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
591 out.push(entry.path());
592 }
593 }
594 out.sort();
595 Ok(out)
596 } else if pattern.contains('*') {
597 Err(PkgError::Resolve(format!(
598 "workspace member pattern `{}` not supported — only literal dirs and `prefix/*` work today",
599 pattern
600 )))
601 } else {
602 Ok(vec![root_dir.join(pattern)])
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use crate::pkg::manifest::DepSpec;
610 use indexmap::IndexMap;
611
612 fn tempdir(tag: &str) -> PathBuf {
613 let pid = std::process::id();
614 let nanos = std::time::SystemTime::now()
615 .duration_since(std::time::UNIX_EPOCH)
616 .unwrap()
617 .subsec_nanos();
618 let p = std::env::temp_dir().join(format!("stryke-resolver-{}-{}-{}", tag, pid, nanos));
619 let _ = std::fs::remove_dir_all(&p);
620 std::fs::create_dir_all(&p).unwrap();
621 p
622 }
623
624 #[test]
625 fn parse_github_url_https_form() {
626 let (o, r) = parse_github_url("https://github.com/MenkeTechnologies/stryke-arrow").unwrap();
627 assert_eq!(o, "MenkeTechnologies");
628 assert_eq!(r, "stryke-arrow");
629 }
630
631 #[test]
632 fn parse_github_url_strips_dot_git() {
633 let (_, r) = parse_github_url("https://github.com/foo/bar.git").unwrap();
634 assert_eq!(r, "bar");
635 }
636
637 #[test]
638 fn parse_github_url_ssh_form() {
639 let (o, r) = parse_github_url("git@github.com:foo/bar.git").unwrap();
640 assert_eq!(o, "foo");
641 assert_eq!(r, "bar");
642 }
643
644 #[test]
645 fn parse_github_url_bare_host_form() {
646 let (o, r) = parse_github_url("github.com/MenkeTechnologies/stryke-aws").unwrap();
647 assert_eq!(o, "MenkeTechnologies");
648 assert_eq!(r, "stryke-aws");
649 }
650
651 #[test]
652 fn parse_github_url_rejects_non_github() {
653 assert!(parse_github_url("https://gitlab.com/foo/bar").is_none());
655 assert!(parse_github_url("git://example.com/foo.git").is_none());
656 assert!(parse_github_url("file:///tmp/local.git").is_none());
657 }
658
659 #[test]
660 fn parse_github_url_rejects_subpath() {
661 assert!(parse_github_url("https://github.com/foo/bar/tree/main").is_none());
663 assert!(parse_github_url("https://github.com/foo/bar/releases").is_none());
664 }
665
666 #[test]
667 fn parse_github_url_rejects_empty() {
668 assert!(parse_github_url("https://github.com/").is_none());
669 assert!(parse_github_url("https://github.com/foo").is_none());
670 assert!(parse_github_url("https://github.com/foo/").is_none());
671 assert!(parse_github_url("https://github.com/foo/.git").is_none());
672 }
673
674 fn make_path_dep(name: &str, version: &str) -> PathBuf {
675 let dir = tempdir(name);
676 std::fs::create_dir_all(dir.join("lib")).unwrap();
677 std::fs::write(
678 dir.join("stryke.toml"),
679 format!(
680 "[package]\nname = \"{}\"\nversion = \"{}\"\n",
681 name, version
682 ),
683 )
684 .unwrap();
685 std::fs::write(
686 dir.join("lib").join(format!("{}.stk", name)),
687 format!("# {}", name),
688 )
689 .unwrap();
690 dir
691 }
692
693 #[test]
694 fn resolves_single_path_dep() {
695 let dep = make_path_dep("mylib", "1.0.0");
696 let project = tempdir("project");
697 let mut deps = IndexMap::new();
698 deps.insert(
699 "mylib".to_string(),
700 DepSpec::path_dep(dep.to_string_lossy().to_string()),
701 );
702 let m = Manifest {
703 package: Some(crate::pkg::manifest::PackageMeta {
704 name: "myapp".into(),
705 version: "0.1.0".into(),
706 ..Default::default()
707 }),
708 deps,
709 ..Manifest::default()
710 };
711
712 let store_root = tempdir("store");
713 let store = Store::at(&store_root);
714 let r = Resolver {
715 manifest: &m,
716 manifest_dir: &project,
717 store: &store,
718 };
719 let outcome = r.resolve().unwrap();
720 assert_eq!(outcome.lockfile.packages.len(), 1);
721 assert_eq!(outcome.lockfile.packages[0].name, "mylib");
722 assert_eq!(outcome.lockfile.packages[0].version, "1.0.0");
723 assert!(outcome.lockfile.packages[0]
724 .integrity
725 .starts_with("sha256-"));
726 assert!(store.package_dir("mylib", "1.0.0").is_dir());
727 }
728
729 #[test]
730 fn registry_dep_returns_unimplemented_error() {
731 let project = tempdir("project");
732 let mut m = Manifest {
733 package: Some(crate::pkg::manifest::PackageMeta {
734 name: "myapp".into(),
735 version: "0.1.0".into(),
736 ..Default::default()
737 }),
738 ..Manifest::default()
739 };
740 m.deps.insert("http".to_string(), DepSpec::version("1.0"));
741
742 let store_root = tempdir("store");
743 let store = Store::at(&store_root);
744 let r = Resolver {
745 manifest: &m,
746 manifest_dir: &project,
747 store: &store,
748 };
749 let err = r.resolve().unwrap_err();
750 let msg = err.to_string();
751 assert!(msg.contains("registry dep"), "got: {}", msg);
752 assert!(msg.contains("http"), "got: {}", msg);
753 }
754
755 #[test]
756 fn workspace_resolves_member_deps_into_root_lockfile() {
757 let leaf = make_path_dep("shared", "1.0.0");
760
761 let ws_root = tempdir("ws_root");
762 std::fs::create_dir_all(ws_root.join("crates/a/lib")).unwrap();
763 std::fs::create_dir_all(ws_root.join("crates/b/lib")).unwrap();
764 std::fs::write(
765 ws_root.join("stryke.toml"),
766 format!(
767 r#"
768[workspace]
769members = ["crates/*"]
770
771[workspace.deps]
772shared = {{ path = "{}" }}
773"#,
774 leaf.display()
775 ),
776 )
777 .unwrap();
778 std::fs::write(
779 ws_root.join("crates/a/stryke.toml"),
780 "[package]\nname = \"a\"\nversion = \"0.1.0\"\n\n[deps]\nshared = { workspace = true }\n",
781 )
782 .unwrap();
783 std::fs::write(
784 ws_root.join("crates/b/stryke.toml"),
785 "[package]\nname = \"b\"\nversion = \"0.1.0\"\n\n[deps]\nshared = { workspace = true }\n",
786 )
787 .unwrap();
788
789 let ws_manifest = Manifest::from_path(&ws_root.join("stryke.toml")).unwrap();
790 let store_root = tempdir("ws_store");
791 let store = Store::at(&store_root);
792 let r = Resolver {
793 manifest: &ws_manifest,
794 manifest_dir: &ws_root,
795 store: &store,
796 };
797 let outcome = r.resolve().unwrap();
798 assert_eq!(outcome.lockfile.packages.len(), 1);
800 assert_eq!(outcome.lockfile.packages[0].name, "shared");
801 assert_eq!(outcome.lockfile.packages[0].version, "1.0.0");
802 }
803
804 #[test]
805 fn workspace_glob_returns_sorted_member_dirs() {
806 let root = tempdir("ws_glob");
807 for n in ["zeta", "alpha", "beta"] {
808 std::fs::create_dir_all(root.join(format!("crates/{}", n))).unwrap();
809 }
810 let dirs = super::expand_workspace_glob(&root, "crates/*").unwrap();
811 let names: Vec<String> = dirs
812 .iter()
813 .map(|p| p.file_name().unwrap().to_string_lossy().to_string())
814 .collect();
815 assert_eq!(names, vec!["alpha", "beta", "zeta"]);
816 }
817
818 #[test]
819 fn workspace_dep_without_table_is_an_error() {
820 let root = tempdir("ws_err");
821 std::fs::write(
822 root.join("stryke.toml"),
823 "[package]\nname = \"x\"\nversion = \"0.1.0\"\n\n[deps]\nshared = { workspace = true }\n",
824 )
825 .unwrap();
826 let m = Manifest::from_path(&root.join("stryke.toml")).unwrap();
827 let store_root = tempdir("ws_err_store");
828 let store = Store::at(&store_root);
829 let r = Resolver {
830 manifest: &m,
831 manifest_dir: &root,
832 store: &store,
833 };
834 let err = r.resolve().unwrap_err().to_string();
835 assert!(err.contains("workspace = true"), "got: {}", err);
836 }
837
838 #[test]
839 fn transitive_path_dep_recursion() {
840 let leaf = make_path_dep("leaf", "0.1.0");
841 let mid = make_path_dep("mid", "0.2.0");
842 let mid_manifest = format!(
844 "[package]\nname = \"mid\"\nversion = \"0.2.0\"\n\n[deps]\nleaf = {{ path = \"{}\" }}\n",
845 leaf.display()
846 );
847 std::fs::write(mid.join("stryke.toml"), mid_manifest).unwrap();
848
849 let project = tempdir("project");
850 let mut m = Manifest {
851 package: Some(crate::pkg::manifest::PackageMeta {
852 name: "myapp".into(),
853 version: "0.1.0".into(),
854 ..Default::default()
855 }),
856 ..Manifest::default()
857 };
858 m.deps.insert(
859 "mid".to_string(),
860 DepSpec::path_dep(mid.to_string_lossy().to_string()),
861 );
862
863 let store_root = tempdir("store");
864 let store = Store::at(&store_root);
865 let r = Resolver {
866 manifest: &m,
867 manifest_dir: &project,
868 store: &store,
869 };
870 let outcome = r.resolve().unwrap();
871 assert_eq!(outcome.lockfile.packages.len(), 2);
872 let names: Vec<&str> = outcome
873 .lockfile
874 .packages
875 .iter()
876 .map(|p| p.name.as_str())
877 .collect();
878 assert!(names.contains(&"leaf"));
879 assert!(names.contains(&"mid"));
880 let mid_entry = outcome.lockfile.find("mid").unwrap();
881 assert_eq!(mid_entry.deps, vec!["leaf@0.1.0"]);
882 }
883}