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#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Dependency {
21 name: Span<Arc<str>>,
23 version: DependencyVersionScheme,
25 linkage: Linkage,
27}
28
29impl Dependency {
30 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 pub fn name(&self) -> &Arc<str> {
41 &self.name
42 }
43
44 pub fn scheme(&self) -> &DependencyVersionScheme {
46 &self.version
47 }
48
49 pub const fn linkage(&self) -> Linkage {
51 self.linkage
52 }
53
54 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#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum DependencyVersionScheme {
76 Registry(VersionRequirement),
82 Workspace { member: Span<Uri> },
84 Path {
86 path: Span<Uri>,
89 version: Option<VersionRequirement>,
95 },
96 Git {
98 repo: Span<Uri>,
102 revision: Span<GitRevision>,
104 version: Option<Span<VersionReq>>,
110 },
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum GitRevision {
116 Branch(Arc<str>),
118 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 #[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 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 (!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}