Skip to main content

miden_project/
dependencies.rs

1#[cfg(feature = "resolver")]
2mod resolver;
3mod version;
4mod version_requirement;
5
6use alloc::{format, sync::Arc, vec};
7
8use miden_assembly_syntax::debuginfo::Spanned;
9
10#[cfg(feature = "resolver")]
11pub use self::resolver::*;
12pub use self::{
13    version::{SemVer, Version, VersionReq},
14    version_requirement::VersionRequirement,
15};
16use crate::{Diagnostic, Linkage, SourceSpan, Span, Uri, miette};
17
18/// Represents a project/package dependency declaration
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Dependency {
21    /// The name of the dependency.
22    name: Span<Arc<str>>,
23    /// The version requirement and resolution scheme for this dependency.
24    version: DependencyVersionScheme,
25    /// The linkage for this dependency
26    linkage: Linkage,
27}
28
29impl Dependency {
30    /// Construct a new [Dependency] with the given name and version scheme
31    pub const fn new(
32        name: Span<Arc<str>>,
33        version: DependencyVersionScheme,
34        linkage: Linkage,
35    ) -> Self {
36        Self { name, version, linkage }
37    }
38
39    /// Get the name of this dependency
40    pub fn name(&self) -> &Arc<str> {
41        &self.name
42    }
43
44    /// Get the versioning scheme/requirement for this dependency
45    pub fn scheme(&self) -> &DependencyVersionScheme {
46        &self.version
47    }
48
49    /// Get the linkage mode for this dependency
50    pub const fn linkage(&self) -> Linkage {
51        self.linkage
52    }
53
54    /// Get the version requirement for this dependency, if one was given
55    pub fn required_version(&self) -> Option<VersionRequirement> {
56        match &self.version {
57            DependencyVersionScheme::Registry(version) => Some(version.clone()),
58            DependencyVersionScheme::Workspace { .. } => None,
59            DependencyVersionScheme::Path { version, .. } => version.clone(),
60            DependencyVersionScheme::Git { version, .. } => {
61                version.as_ref().map(|spanned| VersionRequirement::Semantic(spanned.clone()))
62            },
63        }
64    }
65}
66
67impl Spanned for Dependency {
68    fn span(&self) -> SourceSpan {
69        self.name.span()
70    }
71}
72
73/// Represents the versioning requirement and resolution method for a specific dependency.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum DependencyVersionScheme {
76    /// Resolve the given semantic version requirement or digest using the configured package
77    /// registry, to an assembled Miden package artifact.
78    ///
79    /// Resolution of packages using this scheme relies on the specific implementation of the
80    /// package registry in use, which can vary depending on context.
81    Registry(VersionRequirement),
82    /// Resolve the given path to a member of the current workspace.
83    Workspace { member: Span<Uri> },
84    /// Resolve the given path to a Miden project/workspace, or assembled Miden package artifact.
85    Path {
86        /// The path to a Miden project directory containing a `miden-project.toml` OR a Miden
87        /// package file (i.e. a file with the `.masp` extension, as produced by the assembler).
88        path: Span<Uri>,
89        /// If specified, the version of the referenced project/package _must_ match this version
90        /// requirement.
91        ///
92        /// If unspecified, the version requirement is presumed to be an exact match for the
93        /// version found in the package/project at the given path.
94        version: Option<VersionRequirement>,
95    },
96    /// Resolve the given Git repository to a Miden project/workspace.
97    Git {
98        /// The Git repository URI.
99        ///
100        /// NOTE: Supports any URI scheme supported by the `git` CLI.
101        repo: Span<Uri>,
102        /// The specific revision to clone.
103        revision: Span<GitRevision>,
104        /// If specified, the version declared in the manifest found in the cloned repository
105        /// _must_ match this version requirement.
106        ///
107        /// If unspecified, the version requirement is presumed to be an exact match for the
108        /// version found in the project manifest of the cloned repo.
109        version: Option<Span<VersionReq>>,
110    },
111}
112
113/// A reference to a revision in Git
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum GitRevision {
116    /// A reference to the HEAD revision of the given branch.
117    Branch(Arc<str>),
118    /// A reference to a specific revision with the given hash identifier
119    Commit(Arc<str>),
120}
121
122#[derive(Debug, thiserror::Error, Diagnostic)]
123pub enum InvalidDependencySpecError {
124    #[error("package is not a member of a workspace")]
125    NotAWorkspace {
126        #[label(primary)]
127        span: SourceSpan,
128    },
129    #[error("digests cannot be used with 'git' dependencies")]
130    #[diagnostic(help(
131        "Package digests are only valid when depending on an already-assembled package"
132    ))]
133    GitWithDigest {
134        #[label(primary)]
135        span: SourceSpan,
136    },
137    #[error("'git' dependencies must also specify a revision using either 'branch' or 'rev'")]
138    MissingGitRevision {
139        #[label(primary)]
140        span: SourceSpan,
141    },
142    #[error(
143        "conflicting 'git' revisions: 'branch' and 'rev' may refer to different commits, you cannot specify both"
144    )]
145    ConflictingGitRevision {
146        #[label(primary)]
147        first: SourceSpan,
148        #[label]
149        second: SourceSpan,
150    },
151    #[error("missing version: expected one of 'version', 'git', or 'digest' to be provided")]
152    MissingVersion {
153        #[label(primary)]
154        span: SourceSpan,
155    },
156}
157
158#[cfg(feature = "serde")]
159impl TryFrom<Span<&crate::ast::DependencySpec>> for DependencyVersionScheme {
160    type Error = InvalidDependencySpecError;
161
162    fn try_from(ast: Span<&crate::ast::DependencySpec>) -> Result<Self, Self::Error> {
163        if ast.inherits_workspace_version() {
164            return Err(InvalidDependencySpecError::NotAWorkspace { span: ast.span() });
165        }
166
167        if ast.is_host_resolved() {
168            ast.version()
169                .cloned()
170                .map(Self::Registry)
171                .ok_or(InvalidDependencySpecError::MissingVersion { span: ast.span() })
172        } else if ast.is_git() {
173            let version = match ast.version() {
174                Some(VersionRequirement::Digest(digest)) => {
175                    return Err(InvalidDependencySpecError::GitWithDigest { span: digest.span() });
176                },
177                Some(VersionRequirement::Semantic(v)) => Some(v.clone()),
178                None => None,
179            };
180            if let Some(branch) = ast.branch.as_ref()
181                && let Some(rev) = ast.rev.as_ref()
182            {
183                return Err(InvalidDependencySpecError::ConflictingGitRevision {
184                    first: branch.span(),
185                    second: rev.span(),
186                });
187            }
188            let revision = ast
189                .branch
190                .as_ref()
191                .map(|branch| Span::new(branch.span(), GitRevision::Branch(branch.inner().clone())))
192                .or_else(|| {
193                    ast.rev
194                        .as_ref()
195                        .map(|rev| Span::new(rev.span(), GitRevision::Commit(rev.inner().clone())))
196                })
197                .ok_or_else(|| InvalidDependencySpecError::MissingGitRevision {
198                    span: ast.span(),
199                })?;
200            Ok(Self::Git {
201                repo: ast.git.clone().unwrap(),
202                revision,
203                version,
204            })
205        } else {
206            Ok(Self::Path {
207                path: ast.path.clone().unwrap(),
208                version: ast.version_or_digest.clone(),
209            })
210        }
211    }
212}
213
214#[cfg(feature = "serde")]
215impl DependencyVersionScheme {
216    /// Parse a dependency spec into [DependencyVersionScheme], taking into account workspace
217    /// context.
218    #[cfg(feature = "std")]
219    pub fn try_from_in_workspace(
220        spec: Span<&crate::ast::DependencySpec>,
221        workspace: &crate::ast::WorkspaceFile,
222    ) -> Result<Self, InvalidDependencySpecError> {
223        use std::path::Path;
224
225        use crate::absolutize_path;
226
227        // If the dependency is a path dependency, check if the path refers to any of the workspace
228        // members, and if so, convert the dependency version scheme to `Workspace` to aid in
229        // dependency resolution
230        match Self::try_from(spec)? {
231            Self::Path { path: uri, version } => {
232                let workspace_path = workspace
233                    .source_file
234                    .as_ref()
235                    .map(|file| Path::new(file.content().uri().path()));
236                if uri.scheme().is_none_or(|scheme| scheme == "file")
237                    && let Some(workspace_path) = workspace_path.and_then(|p| p.canonicalize().ok())
238                    && let Some(workspace_root) = workspace_path.parent()
239                    && let Ok(resolved_uri) = absolutize_path(Path::new(uri.path()), workspace_root)
240                    && resolved_uri.strip_prefix(workspace_root).is_ok()
241                {
242                    Ok(Self::Workspace { member: uri.clone() })
243                } else {
244                    Ok(Self::Path { path: uri, version })
245                }
246            },
247            scheme => Ok(scheme),
248        }
249    }
250
251    #[cfg(not(feature = "std"))]
252    pub fn try_from_in_workspace(
253        spec: Span<&crate::ast::DependencySpec>,
254        workspace: &crate::ast::WorkspaceFile,
255    ) -> Result<Self, InvalidDependencySpecError> {
256        match Self::try_from(spec)? {
257            Self::Path { path: uri, version } => {
258                let workspace_path =
259                    workspace.source_file.as_ref().map(|file| file.content().uri().path());
260                if uri.scheme().is_none_or(|scheme| scheme == "file") &&
261                    let Some(workspace_root) = workspace_path.and_then(|p| p.strip_suffix("miden-project.toml")) &&
262                    uri.path().strip_prefix(workspace_root).is_some() &&
263                    // Make sure the uri is relative to workspace root
264                    (!workspace_root.is_empty() && !(uri.path().starts_with('/') || uri.path().starts_with("..")))
265                {
266                    Ok(Self::Workspace { member: uri.clone() })
267                } else {
268                    Ok(Self::Path { path: uri, version })
269                }
270            },
271            scheme => Ok(scheme),
272        }
273    }
274}