Skip to main content

miden_project/dependencies/
graph.rs

1use alloc::{
2    borrow::ToOwned,
3    collections::{BTreeMap, BTreeSet},
4    format,
5    string::{String, ToString},
6    sync::Arc,
7    vec::Vec,
8};
9use std::{
10    path::{Path, PathBuf},
11    process::Command,
12};
13
14use miden_assembly_syntax::{
15    Report,
16    debuginfo::{DefaultSourceManager, SourceManager, Uri},
17};
18use miden_core::utils::{DisplayHex, hash_string_to_word};
19use miden_mast_package::Package as MastPackage;
20use miden_package_registry::{
21    InMemoryPackageRegistry, PackageId, PackageRecord, PackageRegistry, PackageResolver, Version,
22};
23
24use crate::{
25    Dependency, DependencyVersionScheme, GitRevision, Linkage, Package, SemVer, VersionRequirement,
26};
27
28/// The [ProjectDependencyGraph] represents a materialized dependency graph rooted at a specific
29/// package.
30///
31/// Each node in the graph corresponds to a specific package version, and describes:
32///
33/// * What packages it depends on, and with what linkage
34/// * The provenance of the package, i.e. whether the package was sourced from a pre-assembled
35///   artifact on the local filesystem; assembled from source that is present on the local
36///   filesystem (and where those sources came from); or was fetched from the package registry.
37///
38/// The assembler uses this dependency graph both for validation, and to ensure that all
39/// dependencies of a package are properly linked.
40///
41/// See [ProjectDependencyGraphBuilder] for more details on constructing a [ProjectDependencyGraph].
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ProjectDependencyGraph {
44    root: PackageId,
45    nodes: BTreeMap<PackageId, ProjectDependencyNode>,
46}
47
48impl ProjectDependencyGraph {
49    /// Get the package identifier of the root package
50    pub fn root(&self) -> &PackageId {
51        &self.root
52    }
53
54    /// Get the nodes of the underlying graph
55    pub fn nodes(&self) -> &BTreeMap<PackageId, ProjectDependencyNode> {
56        &self.nodes
57    }
58
59    /// Get the node corresponding to `package`
60    pub fn get(&self, package: &PackageId) -> Option<&ProjectDependencyNode> {
61        self.nodes.get(package)
62    }
63
64    fn insert_node(&mut self, node: ProjectDependencyNode) -> Result<bool, Report> {
65        match self.nodes.get(&node.name) {
66            Some(existing) if existing.same_identity(&node) => Ok(false),
67            Some(existing) => Err(Report::msg(format!(
68                "dependency conflict for '{}': existing node {:?} conflicts with {:?}",
69                node.name, existing.provenance, node.provenance
70            ))),
71            None => {
72                self.nodes.insert(node.name.clone(), node);
73                Ok(true)
74            },
75        }
76    }
77}
78
79/// Represents information about a single package in a [ProjectDependencyGraph]
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ProjectDependencyNode {
82    /// The name of the package
83    pub name: PackageId,
84    /// The semantic version of the package
85    pub version: SemVer,
86    /// Known dependencies of this package, which are also found in the graph.
87    pub dependencies: Vec<ProjectDependencyEdge>,
88    /// The provenance of this package
89    pub provenance: ProjectDependencyNodeProvenance,
90}
91
92impl ProjectDependencyNode {
93    /// Evaluates equality for nodes without consideration for dependencies
94    fn same_identity(&self, other: &Self) -> bool {
95        self.name == other.name
96            && self.version == other.version
97            && self.provenance == other.provenance
98    }
99}
100
101/// Represents a dependency edge in the [ProjectDependencyGraph]
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct ProjectDependencyEdge {
104    /// The package depended upon
105    pub dependency: PackageId,
106    /// The linkage requested by the dependent package
107    pub linkage: Linkage,
108}
109
110/// Represents provenance of a package in the [ProjectDependencyGraph], i.e. how it was obtained.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum ProjectDependencyNodeProvenance {
113    /// We have the sources for the package in question, rather than an already-assembled artifact.
114    Source(ProjectSource),
115    /// The package was resolved from the registry
116    Registry {
117        /// The version requirement expressed by the dependent
118        requirement: VersionRequirement,
119        /// The selected version information resolved from the registry
120        selected: Version,
121    },
122    /// The package is an already assembled artifact referenced by path, bypassing the registry.
123    Preassembled {
124        /// The path to the artifact, i.e. `.masp` file
125        path: PathBuf,
126        /// The version of the preassembled package
127        selected: Version,
128    },
129}
130
131/// Represents information about a package whose provenance is a Miden project in source form.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub enum ProjectSource {
134    Virtual {
135        origin: ProjectSourceOrigin,
136    },
137    Real {
138        /// Where the sources were obtained from
139        origin: ProjectSourceOrigin,
140        /// The path to the package manifest, or `None` if the manifest is virtual
141        manifest_path: PathBuf,
142        /// The directory containing the package
143        project_root: PathBuf,
144        /// The directory of the workspace containing the package, if applicable
145        workspace_root: Option<PathBuf>,
146        /// The path to the library target for this project
147        ///
148        /// This is `None` only when we're assembling an executable target of the root package, and
149        /// this is the source info for the root package itself.
150        library_path: Option<PathBuf>,
151    },
152}
153
154/// Represents the provenance of Miden project sources
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum ProjectSourceOrigin {
157    /// The sources are those of the root package being assembled
158    Root,
159    /// The sources were referenced by path
160    Path,
161    /// The sources were cloned from a Git repository into a locally-cached checkout
162    Git {
163        /// The repository URI
164        repo: Uri,
165        /// The revision that was checked out
166        revision: GitRevision,
167        /// The path where the repo was checked-out locally
168        checkout_path: PathBuf,
169        /// The resolved revision of the checkout as a commit hash.
170        ///
171        /// This is primarily relevant when the requested revision was a branch or tag.
172        resolved_revision: Arc<str>,
173    },
174}
175
176struct CollectedDependencyGraph {
177    root: PackageId,
178    nodes: BTreeMap<PackageId, CollectedDependencyNode>,
179    registry_requirements: BTreeMap<PackageId, VersionRequirement>,
180}
181
182impl CollectedDependencyGraph {
183    fn insert_node(&mut self, node: CollectedDependencyNode) -> Result<bool, Report> {
184        match self.nodes.get(node.name()) {
185            Some(existing) if existing.same_identity(&node) => Ok(false),
186            Some(existing) => Err(Report::msg(format!(
187                "dependency conflict for '{}': existing node {:?} conflicts with {:?}",
188                node.name(),
189                existing.provenance(),
190                node.provenance()
191            ))),
192            None => {
193                self.nodes.insert(node.name().clone(), node);
194                Ok(true)
195            },
196        }
197    }
198
199    fn set_dependencies(
200        &mut self,
201        package: &PackageId,
202        dependencies: Vec<ProjectDependencyEdge>,
203        solver_dependencies: BTreeMap<PackageId, VersionRequirement>,
204    ) -> Result<(), Report> {
205        let node = self
206            .nodes
207            .get_mut(package)
208            .ok_or_else(|| Report::msg(format!("missing dependency node '{package}'")))?;
209        node.graph_node.dependencies = dependencies;
210        node.solver_dependencies = solver_dependencies;
211        Ok(())
212    }
213
214    fn record_registry_requirement(&mut self, package: PackageId, requirement: VersionRequirement) {
215        self.registry_requirements.entry(package).or_insert(requirement);
216    }
217
218    fn root_version(&self) -> Result<SemVer, Report> {
219        self.nodes
220            .get(&self.root)
221            .map(|node| node.graph_node.version.clone())
222            .ok_or_else(|| Report::msg(format!("missing dependency node '{}'", self.root)))
223    }
224
225    fn local_packages(&self) -> BTreeSet<PackageId> {
226        self.nodes.keys().cloned().collect()
227    }
228}
229
230struct CollectedDependencyNode {
231    graph_node: ProjectDependencyNode,
232    solver_dependencies: BTreeMap<PackageId, VersionRequirement>,
233}
234
235impl CollectedDependencyNode {
236    fn name(&self) -> &PackageId {
237        &self.graph_node.name
238    }
239
240    fn provenance(&self) -> &ProjectDependencyNodeProvenance {
241        &self.graph_node.provenance
242    }
243
244    fn same_identity(&self, other: &Self) -> bool {
245        self.graph_node.same_identity(&other.graph_node)
246    }
247
248    fn selected_version(&self) -> Version {
249        match &self.graph_node.provenance {
250            ProjectDependencyNodeProvenance::Source(_) => {
251                Version::from(self.graph_node.version.clone())
252            },
253            ProjectDependencyNodeProvenance::Preassembled { selected, .. } => selected.clone(),
254            ProjectDependencyNodeProvenance::Registry { .. } => {
255                panic!("collected nodes do not store registry provenance")
256            },
257        }
258    }
259}
260
261/// This type handles the details of constructing a [ProjectDependencyGraph] for a package.
262pub struct ProjectDependencyGraphBuilder<'a, R: PackageRegistry + ?Sized> {
263    registry: &'a R,
264    source_manager: Arc<dyn SourceManager>,
265    git_cache_root: PathBuf,
266}
267
268impl<'a, R: PackageRegistry + ?Sized> ProjectDependencyGraphBuilder<'a, R> {
269    /// Construct a new [ProjectDependencyGraphBuilder] which will use the provided `registry` for
270    /// resolving packages.
271    pub fn new(registry: &'a R) -> Self {
272        let git_cache_root = std::env::var_os("MIDENUP_HOME")
273            .map(PathBuf::from)
274            .map(|path| path.join("git").join("checkouts"))
275            .unwrap_or_else(|| std::env::temp_dir().join("midenup").join("git").join("checkouts"));
276        Self {
277            registry,
278            source_manager: Arc::new(DefaultSourceManager::default()),
279            git_cache_root,
280        }
281    }
282
283    /// Use the provided source manager for tracking source information of parsed files
284    pub fn with_source_manager(mut self, source_manager: Arc<dyn SourceManager>) -> Self {
285        self.source_manager = source_manager;
286        self
287    }
288
289    /// Override the default location of the Git checkout cache.
290    ///
291    /// By default, the cache is located in:
292    ///
293    /// * `$MIDENUP_HOME/git/checkouts`, if `$MIDENUP_HOME` is set.
294    /// * `$TMP_DIR/midenup/git/checkouts`, if `$MIDENUP_HOME` is _not_ set.
295    pub fn with_git_cache_root(mut self, git_cache_root: impl AsRef<Path>) -> Self {
296        self.git_cache_root = git_cache_root.as_ref().to_path_buf();
297        self
298    }
299
300    /// Build a [ProjectDependencyGraph] for the project whose manifest is located at
301    /// `manifest_path`
302    pub fn build_from_path(
303        &self,
304        manifest_path: impl AsRef<Path>,
305    ) -> Result<ProjectDependencyGraph, Report> {
306        let loaded = self.load_package_from_manifest(manifest_path.as_ref())?;
307        self.build_from_loaded_package(loaded)
308    }
309
310    /// Build a [ProjectDependencyGraph] for `package`
311    pub fn build(&self, package: Arc<Package>) -> Result<ProjectDependencyGraph, Report> {
312        let loaded = self.loaded_package_from_arc(package, None)?;
313        self.build_from_loaded_package(loaded)
314    }
315
316    fn build_from_loaded_package(
317        &self,
318        loaded: LoadedSourcePackage,
319    ) -> Result<ProjectDependencyGraph, Report> {
320        let graph = self.collect_dependency_graph(loaded)?;
321        let selected = self.solve_dependency_graph(&graph)?;
322        self.materialize_dependency_graph(graph, &selected)
323    }
324
325    fn collect_dependency_graph(
326        &self,
327        loaded: LoadedSourcePackage,
328    ) -> Result<CollectedDependencyGraph, Report> {
329        let root = loaded.package.name().into_inner();
330        let mut graph = CollectedDependencyGraph {
331            root: root.clone(),
332            nodes: BTreeMap::new(),
333            registry_requirements: BTreeMap::new(),
334        };
335        let mut visited = BTreeSet::new();
336        self.collect_source_package(
337            &mut graph,
338            &mut visited,
339            loaded,
340            ProjectSourceOrigin::Root,
341            true,
342        )?;
343        Ok(graph)
344    }
345
346    fn collect_source_package(
347        &self,
348        graph: &mut CollectedDependencyGraph,
349        visited: &mut BTreeSet<PackageId>,
350        package: LoadedSourcePackage,
351        origin: ProjectSourceOrigin,
352        allow_missing_library: bool,
353    ) -> Result<PackageId, Report> {
354        let package_id = package.package.name().into_inner();
355        let node = CollectedDependencyNode {
356            graph_node: ProjectDependencyNode {
357                dependencies: Vec::new(),
358                name: package_id.clone(),
359                provenance: ProjectDependencyNodeProvenance::Source(
360                    match package.manifest_path.as_ref() {
361                        Some(manifest_path) => ProjectSource::Real {
362                            library_path: self.library_path(
363                                &package.package,
364                                manifest_path,
365                                allow_missing_library,
366                            )?,
367                            manifest_path: manifest_path.to_path_buf(),
368                            origin,
369                            project_root: package.project_root.clone().unwrap(),
370                            workspace_root: package.workspace_root.clone(),
371                        },
372                        None => ProjectSource::Virtual { origin },
373                    },
374                ),
375                version: package.package.version().into_inner().clone(),
376            },
377            solver_dependencies: BTreeMap::new(),
378        };
379
380        let is_new = graph.insert_node(node)?;
381        if !is_new || !visited.insert(package_id.clone()) {
382            return Ok(package_id);
383        }
384
385        let mut edges = Vec::new();
386        let mut solver_dependencies = BTreeMap::new();
387        for dependency in package.package.dependencies() {
388            let resolved = self.resolve_dependency(dependency, &package)?;
389            let dependency_name = resolved.name();
390            edges.push(ProjectDependencyEdge {
391                dependency: dependency_name.clone(),
392                linkage: dependency.linkage(),
393            });
394            solver_dependencies.insert(dependency_name, resolved.solver_requirement());
395
396            match resolved {
397                ResolvedDependencyNode::Source { package, origin } => {
398                    self.collect_source_package(graph, visited, package, origin, false)?;
399                },
400                ResolvedDependencyNode::Local(node) => {
401                    graph.insert_node(node)?;
402                },
403                ResolvedDependencyNode::Registry { package, requirement } => {
404                    graph.record_registry_requirement(package, requirement);
405                },
406            }
407        }
408
409        graph.set_dependencies(&package_id, edges, solver_dependencies)?;
410        Ok(package_id)
411    }
412
413    fn solve_dependency_graph(
414        &self,
415        graph: &CollectedDependencyGraph,
416    ) -> Result<BTreeMap<PackageId, Version>, Report> {
417        let registry = self.build_resolution_registry(graph)?;
418        let selected =
419            PackageResolver::for_package(graph.root.clone(), graph.root_version()?, &registry)
420                .resolve()
421                .map_err(|error| Report::msg(error.to_string()))?;
422        Ok(selected.into_iter().collect())
423    }
424
425    fn build_resolution_registry(
426        &self,
427        graph: &CollectedDependencyGraph,
428    ) -> Result<InMemoryPackageRegistry, Report> {
429        let mut registry = InMemoryPackageRegistry::default();
430        let local_packages = graph.local_packages();
431
432        for node in graph.nodes.values() {
433            let record = PackageRecord::new(
434                node.selected_version(),
435                node.solver_dependencies
436                    .iter()
437                    .map(|(package, requirement)| (package.clone(), requirement.clone())),
438            );
439            registry
440                .insert_record(node.name().clone(), record)
441                .map_err(|error| Report::msg(error.to_string()))?;
442        }
443
444        let mut pending = BTreeSet::new();
445        for node in graph.nodes.values() {
446            for dependency in node.solver_dependencies.keys() {
447                if !local_packages.contains(dependency) {
448                    pending.insert(dependency.clone());
449                }
450            }
451        }
452
453        self.populate_resolution_registry(&mut registry, &local_packages, pending)?;
454        Ok(registry)
455    }
456
457    fn populate_resolution_registry(
458        &self,
459        registry: &mut InMemoryPackageRegistry,
460        local_packages: &BTreeSet<PackageId>,
461        mut pending: BTreeSet<PackageId>,
462    ) -> Result<(), Report> {
463        let mut copied = BTreeSet::new();
464
465        while let Some(package) = pending.pop_first() {
466            if local_packages.contains(&package) {
467                return Err(Report::msg(format!(
468                    "dependency conflict for '{package}': local source or preassembled dependency conflicts with a registry dependency"
469                )));
470            }
471
472            if !copied.insert(package.clone()) {
473                continue;
474            }
475
476            let Some(versions) = self.registry.available_versions(&package) else {
477                continue;
478            };
479
480            for record in versions.values() {
481                registry
482                    .insert_record(package.clone(), record.clone())
483                    .map_err(|error| Report::msg(error.to_string()))?;
484
485                for dependency in record.dependencies().keys() {
486                    if local_packages.contains(dependency) {
487                        return Err(Report::msg(format!(
488                            "dependency conflict for '{dependency}': local source or preassembled dependency conflicts with a registry dependency"
489                        )));
490                    }
491                    if !copied.contains(dependency) {
492                        pending.insert(dependency.clone());
493                    }
494                }
495            }
496        }
497
498        Ok(())
499    }
500
501    fn materialize_dependency_graph(
502        &self,
503        collected: CollectedDependencyGraph,
504        selected: &BTreeMap<PackageId, Version>,
505    ) -> Result<ProjectDependencyGraph, Report> {
506        let CollectedDependencyGraph { root, nodes, registry_requirements } = collected;
507        let local_packages = nodes.keys().cloned().collect::<BTreeSet<_>>();
508        let mut graph = ProjectDependencyGraph {
509            root: root.clone(),
510            nodes: BTreeMap::new(),
511        };
512
513        let direct_registry_dependencies = nodes
514            .values()
515            .flat_map(|node| {
516                node.graph_node.dependencies.iter().map(|edge| edge.dependency.clone())
517            })
518            .filter(|package| !local_packages.contains(package))
519            .collect::<BTreeSet<_>>();
520
521        for node in nodes.into_values() {
522            graph.insert_node(node.graph_node)?;
523        }
524
525        for package in direct_registry_dependencies {
526            let selected_version = selected.get(&package).ok_or_else(|| {
527                Report::msg(format!(
528                    "dependency resolution did not select a version for direct dependency '{package}'"
529                ))
530            })?;
531            let record = self.registry.get_by_version(&package, selected_version).ok_or_else(|| {
532                Report::msg(format!(
533                    "resolved registry dependency '{package}@{selected_version}' is not available"
534                ))
535            })?;
536            let requirement = registry_requirements
537                .get(&package)
538                .cloned()
539                .unwrap_or_else(|| VersionRequirement::from(record.version().clone()));
540            graph.insert_node(ProjectDependencyNode {
541                dependencies: Vec::new(),
542                name: package,
543                provenance: ProjectDependencyNodeProvenance::Registry {
544                    requirement,
545                    selected: record.version().clone(),
546                },
547                version: record.semantic_version().clone(),
548            })?;
549        }
550
551        Ok(graph)
552    }
553
554    fn resolve_dependency(
555        &self,
556        dependency: &Dependency,
557        parent: &LoadedSourcePackage,
558    ) -> Result<ResolvedDependencyNode, Report> {
559        match dependency.scheme() {
560            DependencyVersionScheme::Registry(requirement) => {
561                Ok(ResolvedDependencyNode::Registry {
562                    package: PackageId::from(dependency.name().clone()),
563                    requirement: requirement.clone(),
564                })
565            },
566            DependencyVersionScheme::Workspace { member, .. } => {
567                let workspace_root = parent.workspace_root.as_ref().ok_or_else(|| {
568                    Report::msg(format!(
569                        "workspace dependency '{}' cannot be resolved outside of a workspace",
570                        dependency.name()
571                    ))
572                })?;
573                let path = crate::absolutize_path(Path::new(member.path()), workspace_root)
574                    .map_err(|error| Report::msg(error.to_string()))?;
575                let package = self.load_dependency_source(&path, dependency.name().as_ref())?;
576                self.validate_source_dependency(dependency, &package.package)?;
577                Ok(ResolvedDependencyNode::Source {
578                    origin: ProjectSourceOrigin::Path,
579                    package,
580                })
581            },
582            DependencyVersionScheme::WorkspacePath { path, version } => {
583                let workspace_root = parent.workspace_root.as_ref().ok_or_else(|| {
584                    Report::msg(format!(
585                        "workspace dependency '{}' cannot be resolved outside of a workspace",
586                        dependency.name()
587                    ))
588                })?;
589                let resolved_path = crate::absolutize_path(Path::new(path.path()), workspace_root)
590                    .map_err(|error| Report::msg(error.to_string()))?;
591                if resolved_path.extension().is_some_and(|extension| extension == "masp") {
592                    let node = self.load_preassembled_dependency(
593                        &resolved_path,
594                        dependency.name().as_ref(),
595                        version.as_ref(),
596                    )?;
597                    Ok(ResolvedDependencyNode::Local(node))
598                } else {
599                    let package =
600                        self.load_dependency_source(&resolved_path, dependency.name().as_ref())?;
601                    if let Some(requirement) = version.as_ref() {
602                        self.ensure_version_satisfies(
603                            dependency.name(),
604                            requirement,
605                            Version::from(package.package.version().into_inner().clone()),
606                        )?;
607                    }
608                    Ok(ResolvedDependencyNode::Source {
609                        origin: ProjectSourceOrigin::Path,
610                        package,
611                    })
612                }
613            },
614            DependencyVersionScheme::Path { path, version } => {
615                let Some(parent_manifest_path) = parent.manifest_path.as_ref() else {
616                    return Err(Report::msg(format!(
617                        "package '{}' is missing a manifest path",
618                        parent.package.name().inner()
619                    )));
620                };
621                let resolved_path = self.resolve_dependency_path(parent_manifest_path, path)?;
622                if resolved_path.extension().is_some_and(|extension| extension == "masp") {
623                    let node = self.load_preassembled_dependency(
624                        &resolved_path,
625                        dependency.name().as_ref(),
626                        version.as_ref(),
627                    )?;
628                    Ok(ResolvedDependencyNode::Local(node))
629                } else {
630                    let package =
631                        self.load_dependency_source(&resolved_path, dependency.name().as_ref())?;
632                    if let Some(requirement) = version.as_ref() {
633                        self.ensure_version_satisfies(
634                            dependency.name(),
635                            requirement,
636                            Version::from(package.package.version().into_inner().clone()),
637                        )?;
638                    }
639                    Ok(ResolvedDependencyNode::Source {
640                        origin: ProjectSourceOrigin::Path,
641                        package,
642                    })
643                }
644            },
645            DependencyVersionScheme::Git { repo, revision, version } => {
646                let checkout = self.checkout_git_dependency(repo.inner(), revision)?;
647                let package = self
648                    .load_dependency_source(&checkout.manifest_path, dependency.name().as_ref())?;
649                self.ensure_dependency_name(
650                    dependency.name(),
651                    package.package.name().into_inner().as_ref(),
652                    Some(&checkout.manifest_path),
653                )?;
654                if let Some(requirement) = version.as_ref() {
655                    self.ensure_version_req_matches(
656                        dependency.name(),
657                        requirement.inner(),
658                        package.package.version().into_inner(),
659                    )?;
660                }
661                Ok(ResolvedDependencyNode::Source {
662                    origin: ProjectSourceOrigin::Git {
663                        checkout_path: checkout.checkout_path,
664                        repo: repo.inner().clone(),
665                        resolved_revision: checkout.resolved_revision,
666                        revision: revision.inner().clone(),
667                    },
668                    package,
669                })
670            },
671        }
672    }
673
674    fn load_dependency_source(
675        &self,
676        path: &Path,
677        expected_name: &str,
678    ) -> Result<LoadedSourcePackage, Report> {
679        let loaded = self.load_project_reference(path, expected_name)?;
680        self.ensure_dependency_name(
681            expected_name,
682            loaded.package.name().into_inner().as_ref(),
683            loaded.manifest_path.as_deref(),
684        )?;
685        Ok(loaded)
686    }
687
688    fn load_project_reference(
689        &self,
690        path: &Path,
691        expected_name: &str,
692    ) -> Result<LoadedSourcePackage, Report> {
693        let project =
694            crate::Project::load_project_reference(expected_name, path, &self.source_manager)?;
695
696        match project {
697            crate::Project::Package(package) => self.loaded_package_from_arc(package, None),
698            crate::Project::WorkspacePackage { package, workspace } => {
699                let workspace_root = workspace.workspace_root().map(|path| path.to_path_buf());
700                self.loaded_package_from_arc(package, workspace_root)
701            },
702        }
703    }
704
705    fn load_package_from_manifest(
706        &self,
707        manifest_path: &Path,
708    ) -> Result<LoadedSourcePackage, Report> {
709        let project = crate::Project::load(manifest_path, &self.source_manager)?;
710
711        match project {
712            crate::Project::Package(package) => self.loaded_package_from_arc(package, None),
713            crate::Project::WorkspacePackage { package, workspace } => {
714                let workspace_root = workspace.workspace_root().map(|path| path.to_path_buf());
715                self.loaded_package_from_arc(package, workspace_root)
716            },
717        }
718    }
719
720    fn loaded_package_from_arc(
721        &self,
722        package: Arc<Package>,
723        workspace_root: Option<PathBuf>,
724    ) -> Result<LoadedSourcePackage, Report> {
725        let manifest_path = package.manifest_path().map(|path| path.to_path_buf());
726        let project_root = match manifest_path.as_ref() {
727            Some(manifest_path) => Some(
728                manifest_path
729                    .parent()
730                    .ok_or_else(|| {
731                        Report::msg(format!(
732                            "manifest '{}' has no parent directory",
733                            manifest_path.display()
734                        ))
735                    })?
736                    .to_path_buf(),
737            ),
738            None => None,
739        };
740
741        Ok(LoadedSourcePackage {
742            manifest_path,
743            package,
744            project_root,
745            workspace_root,
746        })
747    }
748
749    fn load_preassembled_dependency(
750        &self,
751        path: &Path,
752        expected_name: &str,
753        requirement: Option<&VersionRequirement>,
754    ) -> Result<CollectedDependencyNode, Report> {
755        use miden_core::serde::Deserializable;
756
757        let path = path.canonicalize().map_err(|error| Report::msg(error.to_string()))?;
758        let bytes = std::fs::read(&path).map_err(|error| Report::msg(error.to_string()))?;
759        let package =
760            MastPackage::read_from_bytes(&bytes).map_err(|error| Report::msg(error.to_string()))?;
761        self.ensure_dependency_name(expected_name, &package.name, Some(&path))?;
762        let semver = package.version.clone();
763        let selected = Version::new(semver, package.digest());
764        if let Some(requirement) = requirement {
765            self.ensure_version_satisfies(expected_name, requirement, selected.clone())?;
766        }
767
768        Ok(CollectedDependencyNode {
769            graph_node: ProjectDependencyNode {
770                dependencies: Vec::new(),
771                name: PackageId::from(expected_name),
772                provenance: ProjectDependencyNodeProvenance::Preassembled {
773                    path,
774                    selected: selected.clone(),
775                },
776                version: selected.version.clone(),
777            },
778            solver_dependencies: BTreeMap::new(),
779        })
780    }
781
782    fn resolve_dependency_path(&self, manifest_path: &Path, uri: &Uri) -> Result<PathBuf, Report> {
783        if let Some(scheme) = uri.scheme()
784            && scheme != "file"
785        {
786            return Err(Report::msg(format!(
787                "unsupported path dependency scheme '{scheme}' in '{}'",
788                manifest_path.display()
789            )));
790        }
791        let base = manifest_path.parent().ok_or_else(|| {
792            Report::msg(format!("manifest '{}' has no parent directory", manifest_path.display()))
793        })?;
794        crate::absolutize_path(Path::new(uri.path()), base)
795            .map_err(|error| Report::msg(error.to_string()))
796    }
797
798    fn library_path(
799        &self,
800        package: &Package,
801        manifest_path: &Path,
802        allow_missing: bool,
803    ) -> Result<Option<PathBuf>, Report> {
804        let target = match package.library_target() {
805            Some(target) => target,
806            None if allow_missing => return Ok(None),
807            None => {
808                return Err(Report::msg(format!(
809                    "dependency '{}' must define a library target",
810                    package.name().inner()
811                )));
812            },
813        };
814
815        Ok(target.path.as_ref().map(|path| {
816            manifest_path.parent().expect("manifest path has a parent").join(path.path())
817        }))
818    }
819
820    fn ensure_dependency_name(
821        &self,
822        expected_name: &str,
823        actual_name: &str,
824        location: Option<&Path>,
825    ) -> Result<(), Report> {
826        if expected_name == actual_name {
827            Ok(())
828        } else if let Some(location) = location {
829            Err(Report::msg(format!(
830                "dependency '{}' resolved to package '{}' at '{}'",
831                expected_name,
832                actual_name,
833                location.display()
834            )))
835        } else {
836            Err(Report::msg(format!(
837                "dependency '{}' resolved to package '{}'",
838                expected_name, actual_name,
839            )))
840        }
841    }
842
843    fn ensure_version_satisfies(
844        &self,
845        dependency_name: impl AsRef<str>,
846        requirement: &VersionRequirement,
847        actual: Version,
848    ) -> Result<(), Report> {
849        if actual.satisfies(requirement) {
850            Ok(())
851        } else {
852            Err(Report::msg(format!(
853                "dependency '{}' requires '{}', but resolved version was '{}'",
854                dependency_name.as_ref(),
855                requirement,
856                actual
857            )))
858        }
859    }
860
861    fn ensure_version_req_matches(
862        &self,
863        dependency_name: impl AsRef<str>,
864        requirement: &miden_package_registry::VersionReq,
865        actual: &SemVer,
866    ) -> Result<(), Report> {
867        if requirement.matches(actual) {
868            Ok(())
869        } else {
870            Err(Report::msg(format!(
871                "dependency '{}' requires '{}', but resolved version was '{}'",
872                dependency_name.as_ref(),
873                requirement,
874                actual
875            )))
876        }
877    }
878
879    fn validate_source_dependency(
880        &self,
881        dependency: &Dependency,
882        package: &Package,
883    ) -> Result<(), Report> {
884        let requirement = dependency.required_version();
885        self.ensure_version_satisfies(
886            dependency.name(),
887            &requirement,
888            Version::from(package.version().into_inner().clone()),
889        )?;
890        Ok(())
891    }
892}
893
894/// Git dependencies
895impl<'a, R: PackageRegistry + ?Sized> ProjectDependencyGraphBuilder<'a, R> {
896    fn checkout_git_dependency(
897        &self,
898        repo: &Uri,
899        revision: &GitRevision,
900    ) -> Result<GitCheckout, Report> {
901        use alloc::vec;
902
903        std::fs::create_dir_all(&self.git_cache_root)
904            .map_err(|error| Report::msg(error.to_string()))?;
905        let cache_key = format!("{repo}@{revision}");
906        let key = hash_string_to_word(cache_key.as_str());
907        let checkout_path =
908            self.git_cache_root.join(format!("0x{}", DisplayHex::new(&key.as_bytes())));
909        if !checkout_path.exists() {
910            let mut args = vec!["clone"];
911            match revision {
912                GitRevision::Branch(name) => {
913                    args.extend_from_slice(&["--branch", name.as_ref()]);
914                },
915                GitRevision::Commit(_) => (),
916            };
917            args.push(repo.as_str());
918            let checkout_path = checkout_path.to_string_lossy();
919            args.push(checkout_path.as_ref());
920            self.run_git(&args)?;
921        } else {
922            self.run_git_in(&checkout_path, &["fetch", "--all", "--tags", "--force"])?;
923        }
924
925        let target = match revision {
926            GitRevision::Branch(branch) => format!("origin/{branch}"),
927            GitRevision::Commit(commit) => commit.to_string(),
928        };
929        self.run_git_in(&checkout_path, &["checkout", "--force", &target])?;
930        let resolved_revision = self.run_git_capture(&checkout_path, &["rev-parse", "HEAD"])?;
931        let manifest_path = checkout_path.join("miden-project.toml");
932
933        Ok(GitCheckout {
934            checkout_path,
935            manifest_path,
936            resolved_revision: resolved_revision.trim().to_owned().into(),
937        })
938    }
939
940    fn run_git(&self, args: &[&str]) -> Result<(), Report> {
941        let status = Command::new("git")
942            .args(args)
943            .status()
944            .map_err(|error| Report::msg(error.to_string()))?;
945        if status.success() {
946            Ok(())
947        } else {
948            Err(Report::msg(format!("git command failed: git {}", args.join(" "))))
949        }
950    }
951
952    fn run_git_in(&self, dir: &Path, args: &[&str]) -> Result<(), Report> {
953        let output = Command::new("git")
954            .current_dir(dir)
955            .args(args)
956            .output()
957            .map_err(|error| Report::msg(error.to_string()))?;
958        if output.status.success() {
959            Ok(())
960        } else {
961            Err(Report::msg(format!(
962                "git command failed in '{}': git {}: {}",
963                dir.display(),
964                args.join(" "),
965                String::from_utf8_lossy(&output.stderr)
966            )))
967        }
968    }
969
970    fn run_git_capture(&self, dir: &Path, args: &[&str]) -> Result<String, Report> {
971        let output = Command::new("git")
972            .current_dir(dir)
973            .args(args)
974            .output()
975            .map_err(|error| Report::msg(error.to_string()))?;
976        if output.status.success() {
977            Ok(String::from_utf8_lossy(&output.stdout).into_owned())
978        } else {
979            Err(Report::msg(format!(
980                "git command failed in '{}': git {}: {}",
981                dir.display(),
982                args.join(" "),
983                String::from_utf8_lossy(&output.stderr)
984            )))
985        }
986    }
987}
988
989enum ResolvedDependencyNode {
990    Source {
991        package: LoadedSourcePackage,
992        origin: ProjectSourceOrigin,
993    },
994    Local(CollectedDependencyNode),
995    Registry {
996        package: PackageId,
997        requirement: VersionRequirement,
998    },
999}
1000
1001impl ResolvedDependencyNode {
1002    fn name(&self) -> PackageId {
1003        match self {
1004            Self::Source { package, .. } => package.package.name().into_inner(),
1005            Self::Local(node) => node.name().clone(),
1006            Self::Registry { package, .. } => package.clone(),
1007        }
1008    }
1009
1010    fn solver_requirement(&self) -> VersionRequirement {
1011        match self {
1012            Self::Source { package, .. } => VersionRequirement::from(Version::from(
1013                package.package.version().into_inner().clone(),
1014            )),
1015            Self::Local(node) => VersionRequirement::from(node.selected_version()),
1016            Self::Registry { requirement, .. } => requirement.clone(),
1017        }
1018    }
1019}
1020
1021struct LoadedSourcePackage {
1022    manifest_path: Option<PathBuf>,
1023    package: Arc<Package>,
1024    project_root: Option<PathBuf>,
1025    workspace_root: Option<PathBuf>,
1026}
1027
1028struct GitCheckout {
1029    checkout_path: PathBuf,
1030    manifest_path: PathBuf,
1031    resolved_revision: Arc<str>,
1032}
1033
1034#[cfg(test)]
1035mod tests {
1036    use alloc::{boxed::Box, string::ToString};
1037    use std::{collections::BTreeMap, fs, sync::Arc};
1038
1039    use miden_assembly_syntax::{
1040        ast::Path as AstPath,
1041        debuginfo::{DefaultSourceManager, SourceManagerExt, Span},
1042    };
1043    use miden_core::{assert_matches, serde::Serializable, utils::hash_string_to_word};
1044    use miden_mast_package::{Package as MastPackage, TargetType};
1045    use miden_package_registry::{PackageIndex, PackageRecord, PackageRegistry, PackageVersions};
1046    use tempfile::TempDir;
1047
1048    use super::*;
1049    use crate::Target;
1050
1051    /// A basic in-memory package registry
1052    #[derive(Default)]
1053    struct TestRegistry {
1054        packages: BTreeMap<PackageId, PackageVersions>,
1055    }
1056
1057    impl TestRegistry {
1058        fn insert(&mut self, name: &str, version: Version) {
1059            let record_version = version.clone();
1060            self.insert_record(
1061                PackageId::from(name),
1062                PackageRecord::new(record_version, std::iter::empty()),
1063            )
1064            .expect("failed to insert test package");
1065        }
1066
1067        fn insert_record(&mut self, id: PackageId, record: PackageRecord) -> Result<(), Report> {
1068            use std::collections::btree_map::Entry;
1069
1070            let semver = record.semantic_version().clone();
1071            match self.packages.entry(id.clone()).or_default().entry(semver.clone()) {
1072                Entry::Vacant(entry) => {
1073                    entry.insert(record);
1074                    Ok(())
1075                },
1076                Entry::Occupied(_) => Err(Report::msg(format!(
1077                    "package '{}' version '{}' is already registered",
1078                    id, semver
1079                ))),
1080            }
1081        }
1082    }
1083
1084    impl PackageRegistry for TestRegistry {
1085        fn available_versions(&self, package: &PackageId) -> Option<&PackageVersions> {
1086            self.packages.get(package)
1087        }
1088    }
1089
1090    impl PackageIndex for TestRegistry {
1091        type Error = Report;
1092
1093        fn register(&mut self, name: PackageId, record: PackageRecord) -> Result<(), Self::Error> {
1094            self.insert_record(name, record)
1095        }
1096    }
1097
1098    #[test]
1099    fn builds_path_dependency_graph() {
1100        let tempdir = TempDir::new().unwrap();
1101        let dependency_dir = tempdir.path().join("dep");
1102        write_package(&dependency_dir, "dep", "1.0.0", Some("export.foo\nend\n"), []);
1103
1104        let root_dir = tempdir.path().join("root");
1105        let root_manifest = write_package(
1106            &root_dir,
1107            "root",
1108            "1.0.0",
1109            Some("export.foo\nend\n"),
1110            [Dependency::new(
1111                Span::unknown("dep".into()),
1112                DependencyVersionScheme::Path {
1113                    path: Span::unknown(Uri::new("../dep")),
1114                    version: None,
1115                },
1116                Linkage::Dynamic,
1117            )],
1118        );
1119
1120        let registry = TestRegistry::default();
1121        let graph = builder(&registry, &tempdir.path().join("git"))
1122            .build_from_path(&root_manifest)
1123            .unwrap();
1124
1125        assert!(graph.get(&PackageId::from("root")).is_some());
1126        assert!(graph.get(&PackageId::from("dep")).is_some());
1127        assert_eq!(graph.get(&PackageId::from("root")).unwrap().dependencies.len(), 1);
1128    }
1129
1130    #[test]
1131    fn path_dependency_without_version_uses_referenced_source_version() {
1132        let tempdir = TempDir::new().unwrap();
1133        let dependency_dir = tempdir.path().join("dep");
1134        write_package(&dependency_dir, "dep", "9.9.9", Some("export.foo\nend\n"), []);
1135
1136        let root_dir = tempdir.path().join("root");
1137        let root_manifest = write_package(
1138            &root_dir,
1139            "root",
1140            "1.0.0",
1141            Some("export.foo\nend\n"),
1142            [Dependency::new(
1143                Span::unknown("dep".into()),
1144                DependencyVersionScheme::Path {
1145                    path: Span::unknown(Uri::new("../dep")),
1146                    version: None,
1147                },
1148                Linkage::Dynamic,
1149            )],
1150        );
1151
1152        let registry = TestRegistry::default();
1153        let graph = builder(&registry, &tempdir.path().join("git"))
1154            .build_from_path(&root_manifest)
1155            .unwrap();
1156        let dep = graph.get(&PackageId::from("dep")).unwrap();
1157
1158        assert_eq!(dep.version, "9.9.9".parse().unwrap());
1159    }
1160
1161    #[test]
1162    fn path_dependency_version_requirement_must_match_source_version() {
1163        let tempdir = TempDir::new().unwrap();
1164        let dependency_dir = tempdir.path().join("dep");
1165        write_package(&dependency_dir, "dep", "1.0.0", Some("export.foo\nend\n"), []);
1166
1167        let root_dir = tempdir.path().join("root");
1168        let root_manifest = write_package(
1169            &root_dir,
1170            "root",
1171            "1.0.0",
1172            Some("export.foo\nend\n"),
1173            [Dependency::new(
1174                Span::unknown("dep".into()),
1175                DependencyVersionScheme::Path {
1176                    path: Span::unknown(Uri::new("../dep")),
1177                    version: Some(VersionRequirement::Semantic(Span::unknown(
1178                        "=2.0.0".parse().unwrap(),
1179                    ))),
1180                },
1181                Linkage::Dynamic,
1182            )],
1183        );
1184
1185        let registry = TestRegistry::default();
1186        let error = builder(&registry, &tempdir.path().join("git"))
1187            .build_from_path(&root_manifest)
1188            .expect_err("mismatched path dependency version should fail");
1189
1190        assert!(error.to_string().contains("requires '=2.0.0'"));
1191    }
1192
1193    #[test]
1194    fn path_source_dependency_rejects_digest_requirement() {
1195        let tempdir = TempDir::new().unwrap();
1196        let dependency_dir = tempdir.path().join("dep");
1197        write_package(&dependency_dir, "dep", "1.0.0", Some("export.foo\nend\n"), []);
1198
1199        let root_dir = tempdir.path().join("root");
1200        let root_manifest = write_package(
1201            &root_dir,
1202            "root",
1203            "1.0.0",
1204            Some("export.foo\nend\n"),
1205            [Dependency::new(
1206                Span::unknown("dep".into()),
1207                DependencyVersionScheme::Path {
1208                    path: Span::unknown(Uri::new("../dep")),
1209                    version: Some(VersionRequirement::Digest(Span::unknown(hash_string_to_word(
1210                        "dep-digest",
1211                    )))),
1212                },
1213                Linkage::Dynamic,
1214            )],
1215        );
1216
1217        let registry = TestRegistry::default();
1218        let error = builder(&registry, &tempdir.path().join("git"))
1219            .build_from_path(&root_manifest)
1220            .expect_err("digest requirements on source paths should fail");
1221
1222        assert!(error.to_string().contains("resolved version was '1.0.0'"));
1223    }
1224
1225    #[test]
1226    fn path_source_dependency_rejects_exact_published_requirement() {
1227        let tempdir = TempDir::new().unwrap();
1228        let dependency_dir = tempdir.path().join("dep");
1229        write_package(&dependency_dir, "dep", "1.0.0", Some("export.foo\nend\n"), []);
1230
1231        let root_dir = tempdir.path().join("root");
1232        let root_manifest = write_package(
1233            &root_dir,
1234            "root",
1235            "1.0.0",
1236            Some("export.foo\nend\n"),
1237            [Dependency::new(
1238                Span::unknown("dep".into()),
1239                DependencyVersionScheme::Path {
1240                    path: Span::unknown(Uri::new("../dep")),
1241                    version: Some(VersionRequirement::Exact(Version::new(
1242                        "1.0.0".parse().unwrap(),
1243                        hash_string_to_word("dep-digest"),
1244                    ))),
1245                },
1246                Linkage::Dynamic,
1247            )],
1248        );
1249
1250        let registry = TestRegistry::default();
1251        let error = builder(&registry, &tempdir.path().join("git"))
1252            .build_from_path(&root_manifest)
1253            .expect_err("exact published requirements on source paths should fail");
1254
1255        assert!(error.to_string().contains("resolved version was '1.0.0'"));
1256    }
1257
1258    #[test]
1259    fn resolves_workspace_root_by_dependency_name() {
1260        let tempdir = TempDir::new().unwrap();
1261        let workspace_root = tempdir.path().join("workspace");
1262        write_file(
1263            &workspace_root.join("miden-project.toml"),
1264            "[workspace]\nmembers = [\"dep\"]\n",
1265        );
1266        write_package(&workspace_root.join("dep"), "dep", "1.0.0", Some("export.foo\nend\n"), []);
1267
1268        let root_dir = tempdir.path().join("root");
1269        let root_manifest = write_package(
1270            &root_dir,
1271            "root",
1272            "1.0.0",
1273            Some("export.foo\nend\n"),
1274            [Dependency::new(
1275                Span::unknown("dep".into()),
1276                DependencyVersionScheme::Path {
1277                    path: Span::unknown(Uri::new("../workspace")),
1278                    version: None,
1279                },
1280                Linkage::Dynamic,
1281            )],
1282        );
1283
1284        let registry = TestRegistry::default();
1285        let graph = builder(&registry, &tempdir.path().join("git"))
1286            .build_from_path(&root_manifest)
1287            .unwrap();
1288
1289        assert!(graph.get(&PackageId::from("dep")).is_some());
1290    }
1291
1292    #[test]
1293    fn resolves_registry_semver_leaf() {
1294        let tempdir = TempDir::new().unwrap();
1295        let root_dir = tempdir.path().join("root");
1296        let root_manifest = write_package(
1297            &root_dir,
1298            "root",
1299            "1.0.0",
1300            Some("export.foo\nend\n"),
1301            [Dependency::new(
1302                Span::unknown("dep".into()),
1303                DependencyVersionScheme::Registry(VersionRequirement::Semantic(Span::unknown(
1304                    "^1.0.0".parse().unwrap(),
1305                ))),
1306                Linkage::Dynamic,
1307            )],
1308        );
1309
1310        let mut registry = TestRegistry::default();
1311        registry.insert("dep", "1.2.0".parse().unwrap());
1312
1313        let graph = builder(&registry, &tempdir.path().join("git"))
1314            .build_from_path(&root_manifest)
1315            .unwrap();
1316        let dep = graph.get(&PackageId::from("dep")).unwrap();
1317        assert_eq!(dep.version, "1.2.0".parse().unwrap());
1318        assert!(matches!(dep.provenance, ProjectDependencyNodeProvenance::Registry { .. }));
1319    }
1320
1321    #[test]
1322    fn resolves_registry_digest_leaf() {
1323        let tempdir = TempDir::new().unwrap();
1324        let digest = hash_string_to_word("dep");
1325        let root_dir = tempdir.path().join("root");
1326        let root_manifest = write_package(
1327            &root_dir,
1328            "root",
1329            "1.0.0",
1330            Some("export.foo\nend\n"),
1331            [Dependency::new(
1332                Span::unknown("dep".into()),
1333                DependencyVersionScheme::Registry(VersionRequirement::Digest(Span::unknown(
1334                    digest,
1335                ))),
1336                Linkage::Dynamic,
1337            )],
1338        );
1339
1340        let mut registry = TestRegistry::default();
1341        registry.insert("dep", Version::new("1.2.0".parse().unwrap(), digest));
1342
1343        let graph = builder(&registry, &tempdir.path().join("git"))
1344            .build_from_path(&root_manifest)
1345            .unwrap();
1346        let dep = graph.get(&PackageId::from("dep")).unwrap();
1347        assert_eq!(dep.version, "1.2.0".parse().unwrap());
1348    }
1349
1350    #[test]
1351    fn resolves_shared_registry_version_across_source_dependencies() {
1352        let tempdir = TempDir::new().unwrap();
1353        let depa_dir = tempdir.path().join("depa");
1354        let depb_dir = tempdir.path().join("depb");
1355        write_package(
1356            &depa_dir,
1357            "depa",
1358            "1.0.0",
1359            Some("export.call_shared\nend\n"),
1360            [Dependency::new(
1361                Span::unknown("shared".into()),
1362                DependencyVersionScheme::Registry(VersionRequirement::Semantic(Span::unknown(
1363                    "^1.0.0".parse().unwrap(),
1364                ))),
1365                Linkage::Dynamic,
1366            )],
1367        );
1368        write_package(
1369            &depb_dir,
1370            "depb",
1371            "1.0.0",
1372            Some("export.call_shared\nend\n"),
1373            [Dependency::new(
1374                Span::unknown("shared".into()),
1375                DependencyVersionScheme::Registry(VersionRequirement::Semantic(Span::unknown(
1376                    "=1.2.0".parse().unwrap(),
1377                ))),
1378                Linkage::Dynamic,
1379            )],
1380        );
1381
1382        let root_dir = tempdir.path().join("root");
1383        let root_manifest = write_package(
1384            &root_dir,
1385            "root",
1386            "1.0.0",
1387            Some("export.entry\nend\n"),
1388            [
1389                Dependency::new(
1390                    Span::unknown("depa".into()),
1391                    DependencyVersionScheme::Path {
1392                        path: Span::unknown(Uri::new("../depa")),
1393                        version: None,
1394                    },
1395                    Linkage::Dynamic,
1396                ),
1397                Dependency::new(
1398                    Span::unknown("depb".into()),
1399                    DependencyVersionScheme::Path {
1400                        path: Span::unknown(Uri::new("../depb")),
1401                        version: None,
1402                    },
1403                    Linkage::Dynamic,
1404                ),
1405            ],
1406        );
1407
1408        let mut registry = TestRegistry::default();
1409        registry.insert("shared", "1.0.0".parse().unwrap());
1410        registry.insert("shared", "1.2.0".parse().unwrap());
1411        registry.insert("shared", "1.3.0".parse().unwrap());
1412
1413        let graph = builder(&registry, &tempdir.path().join("git"))
1414            .build_from_path(&root_manifest)
1415            .expect("compatible source dependency requirements should resolve");
1416        let shared = graph.get(&PackageId::from("shared")).expect("shared dependency missing");
1417        assert_eq!(shared.version, "1.2.0".parse().unwrap());
1418        assert_matches!(
1419            shared.provenance,
1420            ProjectDependencyNodeProvenance::Registry { ref selected, .. }
1421                if selected.version == "1.2.0".parse().unwrap()
1422        );
1423    }
1424
1425    #[test]
1426    fn rejects_incompatible_shared_registry_version_requirements() {
1427        let tempdir = TempDir::new().unwrap();
1428        let depa_dir = tempdir.path().join("depa");
1429        let depb_dir = tempdir.path().join("depb");
1430        write_package(
1431            &depa_dir,
1432            "depa",
1433            "1.0.0",
1434            Some("export.call_shared\nend\n"),
1435            [Dependency::new(
1436                Span::unknown("shared".into()),
1437                DependencyVersionScheme::Registry(VersionRequirement::Semantic(Span::unknown(
1438                    "=1.0.0".parse().unwrap(),
1439                ))),
1440                Linkage::Dynamic,
1441            )],
1442        );
1443        write_package(
1444            &depb_dir,
1445            "depb",
1446            "1.0.0",
1447            Some("export.call_shared\nend\n"),
1448            [Dependency::new(
1449                Span::unknown("shared".into()),
1450                DependencyVersionScheme::Registry(VersionRequirement::Semantic(Span::unknown(
1451                    "=2.0.0".parse().unwrap(),
1452                ))),
1453                Linkage::Dynamic,
1454            )],
1455        );
1456
1457        let root_dir = tempdir.path().join("root");
1458        let root_manifest = write_package(
1459            &root_dir,
1460            "root",
1461            "1.0.0",
1462            Some("export.entry\nend\n"),
1463            [
1464                Dependency::new(
1465                    Span::unknown("depa".into()),
1466                    DependencyVersionScheme::Path {
1467                        path: Span::unknown(Uri::new("../depa")),
1468                        version: None,
1469                    },
1470                    Linkage::Dynamic,
1471                ),
1472                Dependency::new(
1473                    Span::unknown("depb".into()),
1474                    DependencyVersionScheme::Path {
1475                        path: Span::unknown(Uri::new("../depb")),
1476                        version: None,
1477                    },
1478                    Linkage::Dynamic,
1479                ),
1480            ],
1481        );
1482
1483        let mut registry = TestRegistry::default();
1484        registry.insert("shared", "1.0.0".parse().unwrap());
1485        registry.insert("shared", "2.0.0".parse().unwrap());
1486
1487        let error = builder(&registry, &tempdir.path().join("git"))
1488            .build_from_path(&root_manifest)
1489            .expect_err("incompatible source dependency requirements should fail");
1490        let error = error.to_string();
1491        assert!(error.contains("dependency resolution failed"));
1492        assert!(error.contains("shared"));
1493        assert!(error.contains("1.0.0"));
1494        assert!(error.contains("2.0.0"));
1495    }
1496
1497    #[test]
1498    fn records_missing_library_source_path() {
1499        let tempdir = TempDir::new().unwrap();
1500        let dependency_dir = tempdir.path().join("dep");
1501        write_package(&dependency_dir, "dep", "1.0.0", None, []);
1502
1503        let root_dir = tempdir.path().join("root");
1504        let root_manifest = write_package(
1505            &root_dir,
1506            "root",
1507            "1.0.0",
1508            Some("export.foo\nend\n"),
1509            [Dependency::new(
1510                Span::unknown("dep".into()),
1511                DependencyVersionScheme::Path {
1512                    path: Span::unknown(Uri::new("../dep")),
1513                    version: None,
1514                },
1515                Linkage::Dynamic,
1516            )],
1517        );
1518
1519        let registry = TestRegistry::default();
1520        let graph = builder(&registry, &tempdir.path().join("git"))
1521            .build_from_path(&root_manifest)
1522            .unwrap();
1523        let dep = graph.get(&PackageId::from("dep")).unwrap();
1524        match &dep.provenance {
1525            ProjectDependencyNodeProvenance::Source(source) => {
1526                assert_matches!(source, ProjectSource::Real { library_path, .. } if library_path.is_none());
1527            },
1528            _ => panic!("expected source provenance"),
1529        }
1530    }
1531
1532    #[test]
1533    fn path_to_masp_is_leaf() {
1534        let tempdir = TempDir::new().unwrap();
1535        let package = build_registry_test_package("dep", "1.0.0");
1536        let package_path = tempdir.path().join("dep.masp");
1537        fs::write(&package_path, package.to_bytes()).unwrap();
1538
1539        let root_dir = tempdir.path().join("root");
1540        let root_manifest = write_package(
1541            &root_dir,
1542            "root",
1543            "1.0.0",
1544            Some("export.foo\nend\n"),
1545            [Dependency::new(
1546                Span::unknown("dep".into()),
1547                DependencyVersionScheme::Path {
1548                    path: Span::unknown(Uri::from(package_path.as_path())),
1549                    version: None,
1550                },
1551                Linkage::Dynamic,
1552            )],
1553        );
1554
1555        let registry = TestRegistry::default();
1556        let graph = builder(&registry, &tempdir.path().join("git"))
1557            .build_from_path(&root_manifest)
1558            .unwrap();
1559        let dep = graph.get(&PackageId::from("dep")).unwrap();
1560        assert!(dep.dependencies.is_empty());
1561        assert!(matches!(dep.provenance, ProjectDependencyNodeProvenance::Preassembled { .. }));
1562    }
1563
1564    #[test]
1565    fn preassembled_path_dependency_accepts_exact_published_requirement() {
1566        let tempdir = TempDir::new().unwrap();
1567        let package = build_registry_test_package("dep", "1.0.0");
1568        let digest = package.digest();
1569        let package_path = tempdir.path().join("dep.masp");
1570        fs::write(&package_path, package.to_bytes()).unwrap();
1571
1572        let root_dir = tempdir.path().join("root");
1573        let root_manifest = write_package(
1574            &root_dir,
1575            "root",
1576            "1.0.0",
1577            Some("export.foo\nend\n"),
1578            [Dependency::new(
1579                Span::unknown("dep".into()),
1580                DependencyVersionScheme::Path {
1581                    path: Span::unknown(Uri::from(package_path.as_path())),
1582                    version: Some(VersionRequirement::Exact(Version::new(
1583                        "1.0.0".parse().unwrap(),
1584                        digest,
1585                    ))),
1586                },
1587                Linkage::Dynamic,
1588            )],
1589        );
1590
1591        let registry = TestRegistry::default();
1592        let graph = builder(&registry, &tempdir.path().join("git"))
1593            .build_from_path(&root_manifest)
1594            .unwrap();
1595        let dep = graph.get(&PackageId::from("dep")).unwrap();
1596
1597        assert_eq!(dep.version, "1.0.0".parse().unwrap());
1598        assert_matches!(
1599            dep.provenance,
1600            ProjectDependencyNodeProvenance::Preassembled {
1601                ref path,
1602                ref selected,
1603            } if path == &package_path.canonicalize().unwrap()
1604                && *selected == Version::new("1.0.0".parse().unwrap(), digest)
1605        );
1606    }
1607
1608    #[test]
1609    fn preassembled_path_dependency_validates_digest_requirement_against_artifact_digest() {
1610        let tempdir = TempDir::new().unwrap();
1611        let package = build_registry_test_package("dep", "1.0.0");
1612        let digest = package.digest();
1613        let package_path = tempdir.path().join("dep.masp");
1614        fs::write(&package_path, package.to_bytes()).unwrap();
1615
1616        let ok_root_dir = tempdir.path().join("root-ok");
1617        let ok_manifest = write_package(
1618            &ok_root_dir,
1619            "root-ok",
1620            "1.0.0",
1621            Some("export.foo\nend\n"),
1622            [Dependency::new(
1623                Span::unknown("dep".into()),
1624                DependencyVersionScheme::Path {
1625                    path: Span::unknown(Uri::from(package_path.as_path())),
1626                    version: Some(VersionRequirement::Digest(Span::unknown(digest))),
1627                },
1628                Linkage::Dynamic,
1629            )],
1630        );
1631
1632        let registry = TestRegistry::default();
1633        let graph = builder(&registry, &tempdir.path().join("git"))
1634            .build_from_path(&ok_manifest)
1635            .unwrap();
1636        let dep = graph.get(&PackageId::from("dep")).unwrap();
1637        assert_eq!(dep.version, "1.0.0".parse().unwrap());
1638
1639        let bad_root_dir = tempdir.path().join("root-bad");
1640        let bad_manifest = write_package(
1641            &bad_root_dir,
1642            "root-bad",
1643            "1.0.0",
1644            Some("export.foo\nend\n"),
1645            [Dependency::new(
1646                Span::unknown("dep".into()),
1647                DependencyVersionScheme::Path {
1648                    path: Span::unknown(Uri::from(package_path.as_path())),
1649                    version: Some(VersionRequirement::Digest(Span::unknown(hash_string_to_word(
1650                        "wrong-digest",
1651                    )))),
1652                },
1653                Linkage::Dynamic,
1654            )],
1655        );
1656
1657        let error = builder(&registry, &tempdir.path().join("git"))
1658            .build_from_path(&bad_manifest)
1659            .expect_err("mismatched digest requirement should fail for preassembled packages");
1660
1661        assert!(error.to_string().contains("resolved version was '1.0.0#"));
1662    }
1663
1664    #[test]
1665    fn validates_bin_path_is_required() {
1666        let tempdir = TempDir::new().unwrap();
1667        let manifest_path = tempdir.path().join("miden-project.toml");
1668        write_file(
1669            &manifest_path,
1670            "[package]\nname = \"root\"\nversion = \"1.0.0\"\n\n[[bin]]\nname = \"cli\"\n",
1671        );
1672
1673        let source_manager = Arc::new(DefaultSourceManager::default());
1674        let source = source_manager.load_file(&manifest_path).unwrap();
1675        let error = Package::load(source).expect_err("manifest should be rejected");
1676        assert!(error.to_string().contains("invalid build target configuration"));
1677    }
1678
1679    #[test]
1680    fn resolves_git_dependency_using_local_repo() {
1681        let tempdir = TempDir::new().unwrap();
1682        let repo_dir = tempdir.path().join("repo");
1683        fs::create_dir_all(&repo_dir).unwrap();
1684        write_package(&repo_dir, "dep", "1.0.0", Some("export.foo\nend\n"), []);
1685        run_git(&repo_dir, &["init", "-b", "main"]);
1686        run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
1687        run_git(&repo_dir, &["config", "user.name", "Test"]);
1688        run_git(&repo_dir, &["config", "commit.gpgsign", "false"]);
1689        run_git(&repo_dir, &["add", "."]);
1690        run_git(&repo_dir, &["commit", "-m", "init"]);
1691
1692        let root_dir = tempdir.path().join("root");
1693        let root_manifest = write_package(
1694            &root_dir,
1695            "root",
1696            "1.0.0",
1697            Some("export.foo\nend\n"),
1698            [Dependency::new(
1699                Span::unknown("dep".into()),
1700                DependencyVersionScheme::Git {
1701                    repo: Span::unknown(Uri::from(repo_dir.as_path())),
1702                    revision: Span::unknown(GitRevision::Branch("main".into())),
1703                    version: None,
1704                },
1705                Linkage::Dynamic,
1706            )],
1707        );
1708
1709        let registry = TestRegistry::default();
1710        let graph = builder(&registry, &tempdir.path().join("git-cache"))
1711            .build_from_path(&root_manifest)
1712            .unwrap();
1713        let dep = graph.get(&PackageId::from("dep")).unwrap();
1714        assert_matches!(
1715            dep.provenance,
1716            ProjectDependencyNodeProvenance::Source(ProjectSource::Real {
1717                origin: ProjectSourceOrigin::Git { .. },
1718                ..
1719            })
1720        );
1721    }
1722
1723    #[test]
1724    fn resolves_commit_pinned_git_dependency_after_repo_advances() {
1725        let tempdir = TempDir::new().unwrap();
1726        let repo_dir = tempdir.path().join("repo");
1727        fs::create_dir_all(&repo_dir).unwrap();
1728        write_package(&repo_dir, "dep", "1.0.0", Some("export.foo\nend\n"), []);
1729        run_git(&repo_dir, &["init", "-b", "main"]);
1730        run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
1731        run_git(&repo_dir, &["config", "user.name", "Test"]);
1732        run_git(&repo_dir, &["config", "commit.gpgsign", "false"]);
1733        run_git(&repo_dir, &["add", "."]);
1734        run_git(&repo_dir, &["commit", "-m", "init"]);
1735        let initial_revision = run_git_capture(&repo_dir, &["rev-parse", "HEAD"]);
1736
1737        write_package(&repo_dir, "dep", "2.0.0", Some("export.foo\nend\n"), []);
1738        run_git(&repo_dir, &["add", "."]);
1739        run_git(&repo_dir, &["commit", "-m", "change"]);
1740
1741        let root_dir = tempdir.path().join("root");
1742        let root_manifest = write_package(
1743            &root_dir,
1744            "root",
1745            "1.0.0",
1746            Some("export.foo\nend\n"),
1747            [Dependency::new(
1748                Span::unknown("dep".into()),
1749                DependencyVersionScheme::Git {
1750                    repo: Span::unknown(Uri::from(repo_dir.as_path())),
1751                    revision: Span::unknown(GitRevision::Commit(initial_revision.clone().into())),
1752                    version: None,
1753                },
1754                Linkage::Dynamic,
1755            )],
1756        );
1757
1758        let registry = TestRegistry::default();
1759        let graph = builder(&registry, &tempdir.path().join("git-cache"))
1760            .build_from_path(&root_manifest)
1761            .unwrap();
1762        let dep = graph.get(&PackageId::from("dep")).unwrap();
1763
1764        assert_eq!(dep.version, "1.0.0".parse().unwrap());
1765        assert_matches!(
1766            &dep.provenance,
1767            ProjectDependencyNodeProvenance::Source(ProjectSource::Real {
1768                origin: ProjectSourceOrigin::Git {
1769                    revision,
1770                    resolved_revision,
1771                    ..
1772                },
1773                ..
1774            }) if *revision == GitRevision::Commit(initial_revision.clone().into())
1775                && resolved_revision.as_ref() == initial_revision
1776        );
1777    }
1778
1779    #[test]
1780    fn git_dependency_without_version_uses_checked_out_source_version() {
1781        let tempdir = TempDir::new().unwrap();
1782        let repo_dir = tempdir.path().join("repo");
1783        fs::create_dir_all(&repo_dir).unwrap();
1784        write_package(&repo_dir, "dep", "9.9.9", Some("export.foo\nend\n"), []);
1785        run_git(&repo_dir, &["init", "-b", "main"]);
1786        run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
1787        run_git(&repo_dir, &["config", "user.name", "Test"]);
1788        run_git(&repo_dir, &["config", "commit.gpgsign", "false"]);
1789        run_git(&repo_dir, &["add", "."]);
1790        run_git(&repo_dir, &["commit", "-m", "init"]);
1791
1792        let root_dir = tempdir.path().join("root");
1793        let root_manifest = write_package(
1794            &root_dir,
1795            "root",
1796            "1.0.0",
1797            Some("export.foo\nend\n"),
1798            [Dependency::new(
1799                Span::unknown("dep".into()),
1800                DependencyVersionScheme::Git {
1801                    repo: Span::unknown(Uri::from(repo_dir.as_path())),
1802                    revision: Span::unknown(GitRevision::Branch("main".into())),
1803                    version: None,
1804                },
1805                Linkage::Dynamic,
1806            )],
1807        );
1808
1809        let registry = TestRegistry::default();
1810        let graph = builder(&registry, &tempdir.path().join("git-cache"))
1811            .build_from_path(&root_manifest)
1812            .unwrap();
1813        let dep = graph.get(&PackageId::from("dep")).unwrap();
1814
1815        assert_eq!(dep.version, "9.9.9".parse().unwrap());
1816    }
1817
1818    #[test]
1819    fn git_dependency_version_requirement_must_match_checked_out_source_version() {
1820        let tempdir = TempDir::new().unwrap();
1821        let repo_dir = tempdir.path().join("repo");
1822        fs::create_dir_all(&repo_dir).unwrap();
1823        write_package(&repo_dir, "dep", "1.0.0", Some("export.foo\nend\n"), []);
1824        run_git(&repo_dir, &["init", "-b", "main"]);
1825        run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
1826        run_git(&repo_dir, &["config", "user.name", "Test"]);
1827        run_git(&repo_dir, &["config", "commit.gpgsign", "false"]);
1828        run_git(&repo_dir, &["add", "."]);
1829        run_git(&repo_dir, &["commit", "-m", "init"]);
1830
1831        let root_dir = tempdir.path().join("root");
1832        let root_manifest = write_package(
1833            &root_dir,
1834            "root",
1835            "1.0.0",
1836            Some("export.foo\nend\n"),
1837            [Dependency::new(
1838                Span::unknown("dep".into()),
1839                DependencyVersionScheme::Git {
1840                    repo: Span::unknown(Uri::from(repo_dir.as_path())),
1841                    revision: Span::unknown(GitRevision::Branch("main".into())),
1842                    version: Some(Span::unknown("=2.0.0".parse().unwrap())),
1843                },
1844                Linkage::Dynamic,
1845            )],
1846        );
1847
1848        let registry = TestRegistry::default();
1849        let error = builder(&registry, &tempdir.path().join("git-cache"))
1850            .build_from_path(&root_manifest)
1851            .expect_err("mismatched git dependency version should fail");
1852
1853        assert!(error.to_string().contains("requires '=2.0.0'"));
1854    }
1855
1856    #[test]
1857    fn workspace_dependency_stays_on_the_workspace_member_version() {
1858        let tempdir = TempDir::new().unwrap();
1859        let root_dir = tempdir.path().join("workspace-dep");
1860        fs::create_dir_all(&root_dir).unwrap();
1861        fs::create_dir_all(root_dir.join("dep")).unwrap();
1862        fs::create_dir_all(root_dir.join("app")).unwrap();
1863
1864        write_file(
1865            &root_dir.join("miden-project.toml"),
1866            "[workspace]\nmembers = [\"dep\", \"app\"]\n\n[workspace.dependencies]\ndep = { path = \"dep\" }\n",
1867        );
1868        write_file(
1869            &root_dir.join("dep").join("miden-project.toml"),
1870            "[package]\nname = \"dep\"\nversion = \"0.2.0\"\n",
1871        );
1872        let app_manifest = root_dir.join("app").join("miden-project.toml");
1873        write_file(
1874            &app_manifest,
1875            "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ndep.workspace = true\n",
1876        );
1877
1878        let mut registry = TestRegistry::default();
1879        let dep_id = PackageId::from("dep");
1880        let version010 = "0.1.0".parse::<miden_package_registry::SemVer>().unwrap();
1881        let version999 = "9.9.9".parse::<miden_package_registry::SemVer>().unwrap();
1882        registry.insert(&dep_id, Version::from(version010.clone()));
1883        registry.insert(&dep_id, Version::from(version999.clone()));
1884        let graph = builder(&registry, &tempdir.path().join("git-cache"))
1885            .build_from_path(&app_manifest)
1886            .unwrap();
1887        let dep = graph.get(&PackageId::from("dep")).unwrap();
1888        assert_eq!(dep.version.to_string(), "0.2.0");
1889    }
1890
1891    #[test]
1892    fn workspace_dependency_rejects_mismatched_workspace_requirement() {
1893        let tempdir = TempDir::new().unwrap();
1894        let root_dir = tempdir.path().join("workspace-dep");
1895        fs::create_dir_all(&root_dir).unwrap();
1896        fs::create_dir_all(root_dir.join("dep")).unwrap();
1897        fs::create_dir_all(root_dir.join("app")).unwrap();
1898
1899        write_file(
1900            &root_dir.join("miden-project.toml"),
1901            "[workspace]\nmembers = [\"dep\", \"app\"]\n\n[workspace.dependencies]\ndep = { path = \"dep\", version = \"=0.1.0\" }\n",
1902        );
1903        write_file(
1904            &root_dir.join("dep").join("miden-project.toml"),
1905            "[package]\nname = \"dep\"\nversion = \"0.2.0\"\n",
1906        );
1907        let app_manifest = root_dir.join("app").join("miden-project.toml");
1908        write_file(
1909            &app_manifest,
1910            "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ndep.workspace = true\n",
1911        );
1912
1913        let registry = TestRegistry::default();
1914        let error = builder(&registry, &tempdir.path().join("git-cache"))
1915            .build_from_path(&app_manifest)
1916            .expect_err("mismatched workspace dependency version should fail");
1917        assert!(error.to_string().contains("requires '=0.1.0'"));
1918        assert!(error.to_string().contains("resolved version was '0.2.0'"));
1919    }
1920
1921    #[test]
1922    fn non_member_path_dependency_inside_workspace_root_is_resolved_by_path() {
1923        let tempdir = TempDir::new().unwrap();
1924        let root_dir = tempdir.path().join("workspace-dep");
1925        let app_dir = root_dir.join("app");
1926        let dep_dir = root_dir.join("vendor").join("dep");
1927        fs::create_dir_all(&app_dir).unwrap();
1928        fs::create_dir_all(&dep_dir).unwrap();
1929
1930        write_file(
1931            &root_dir.join("miden-project.toml"),
1932            "[workspace]\nmembers = [\"app\"]\n\n[workspace.dependencies]\ndep = { path = \"vendor/dep\" }\n",
1933        );
1934        write_package(&dep_dir, "dep", "0.3.0", Some("export.foo\nend\n"), []);
1935        let app_manifest = app_dir.join("miden-project.toml");
1936        write_file(
1937            &app_manifest,
1938            "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ndep.workspace = true\n",
1939        );
1940
1941        let registry = TestRegistry::default();
1942        let graph = builder(&registry, &tempdir.path().join("git-cache"))
1943            .build_from_path(&app_manifest)
1944            .unwrap();
1945        let dep = graph.get(&PackageId::from("dep")).unwrap();
1946
1947        assert_eq!(dep.version.to_string(), "0.3.0");
1948        assert_matches!(
1949            dep.provenance,
1950            ProjectDependencyNodeProvenance::Source(ProjectSource::Real {
1951                origin: ProjectSourceOrigin::Path,
1952                workspace_root: None,
1953                ..
1954            })
1955        );
1956    }
1957
1958    #[test]
1959    fn preassembled_path_dependency_inside_workspace_root_is_not_treated_as_workspace_member() {
1960        let tempdir = TempDir::new().unwrap();
1961        let root_dir = tempdir.path().join("workspace-dep");
1962        let app_dir = root_dir.join("app");
1963        let artifacts_dir = root_dir.join("artifacts");
1964        fs::create_dir_all(&app_dir).unwrap();
1965        fs::create_dir_all(&artifacts_dir).unwrap();
1966
1967        write_file(
1968            &root_dir.join("miden-project.toml"),
1969            "[workspace]\nmembers = [\"app\"]\n\n[workspace.dependencies]\ndep = { path = \"artifacts/dep.masp\" }\n",
1970        );
1971        let dep_package = build_registry_test_package("dep", "1.0.0");
1972        let dep_package_path = artifacts_dir.join("dep.masp");
1973        fs::write(&dep_package_path, dep_package.to_bytes()).unwrap();
1974        let app_manifest = app_dir.join("miden-project.toml");
1975        write_file(
1976            &app_manifest,
1977            "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ndep.workspace = true\n",
1978        );
1979
1980        let registry = TestRegistry::default();
1981        let graph = builder(&registry, &tempdir.path().join("git-cache"))
1982            .build_from_path(&app_manifest)
1983            .unwrap();
1984        let dep = graph.get(&PackageId::from("dep")).unwrap();
1985
1986        assert_eq!(dep.version.to_string(), "1.0.0");
1987        assert_matches!(
1988            dep.provenance,
1989            ProjectDependencyNodeProvenance::Preassembled { ref path, .. }
1990                if path == &dep_package_path.canonicalize().unwrap()
1991        );
1992    }
1993
1994    // ------ TEST UTILS
1995
1996    fn build_registry_test_package(name: &str, version: &str) -> Box<MastPackage> {
1997        MastPackage::generate(name.into(), version.parse().unwrap(), TargetType::Library, [])
1998    }
1999
2000    fn write_package(
2001        dir: &Path,
2002        name: &str,
2003        version: &str,
2004        module_body: Option<&str>,
2005        dependencies: impl IntoIterator<Item = Dependency>,
2006    ) -> PathBuf {
2007        let target = if module_body.is_some() {
2008            Target::library(AstPath::new(name)).with_path("lib/mod.masm")
2009        } else {
2010            Target::library(AstPath::new(name))
2011        };
2012        let manifest = Package::new(name, target)
2013            .with_version(version.parse().unwrap())
2014            .with_dependencies(dependencies);
2015
2016        let manifest = manifest.to_toml().unwrap();
2017        let manifest_path = dir.join("miden-project.toml");
2018        write_file(&manifest_path, &manifest);
2019        if let Some(module_body) = module_body {
2020            write_file(&dir.join("lib/mod.masm"), module_body);
2021        }
2022        manifest_path
2023    }
2024
2025    fn run_git(dir: &Path, args: &[&str]) {
2026        let output = Command::new("git").current_dir(dir).args(args).output().unwrap();
2027        assert!(
2028            output.status.success(),
2029            "git {} failed in '{}': {}",
2030            args.join(" "),
2031            dir.display(),
2032            String::from_utf8_lossy(&output.stderr)
2033        );
2034    }
2035
2036    fn run_git_capture(dir: &Path, args: &[&str]) -> String {
2037        let output = Command::new("git").current_dir(dir).args(args).output().unwrap();
2038        assert!(
2039            output.status.success(),
2040            "git {} failed in '{}': {}",
2041            args.join(" "),
2042            dir.display(),
2043            String::from_utf8_lossy(&output.stderr)
2044        );
2045        String::from_utf8(output.stdout).unwrap().trim().to_owned()
2046    }
2047
2048    fn write_file(path: &Path, contents: &str) {
2049        if let Some(parent) = path.parent() {
2050            fs::create_dir_all(parent).unwrap();
2051        }
2052        fs::write(path, contents).unwrap();
2053    }
2054
2055    fn builder<'a, R: PackageRegistry + ?Sized>(
2056        registry: &'a R,
2057        git_cache_root: &Path,
2058    ) -> ProjectDependencyGraphBuilder<'a, R> {
2059        ProjectDependencyGraphBuilder::new(registry)
2060            .with_git_cache_root(git_cache_root)
2061            .with_source_manager(Arc::new(DefaultSourceManager::default()))
2062    }
2063}