1#[cfg(all(feature = "std", feature = "serde"))]
2mod graph;
3
4use alloc::{format, sync::Arc, vec};
5use core::fmt;
6
7use miden_assembly_syntax::debuginfo::Spanned;
8pub use miden_package_registry::{SemVer, Version, VersionReq, VersionRequirement};
9
10#[cfg(all(feature = "std", feature = "serde"))]
11pub use self::graph::*;
12use crate::{Diagnostic, Linkage, SourceSpan, Span, Uri, miette};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Dependency {
17 name: Span<Arc<str>>,
19 version: DependencyVersionScheme,
21 linkage: Linkage,
23}
24
25impl Dependency {
26 pub const fn new(
28 name: Span<Arc<str>>,
29 version: DependencyVersionScheme,
30 linkage: Linkage,
31 ) -> Self {
32 Self { name, version, linkage }
33 }
34
35 pub fn name(&self) -> &Arc<str> {
37 &self.name
38 }
39
40 pub fn scheme(&self) -> &DependencyVersionScheme {
42 &self.version
43 }
44
45 pub const fn linkage(&self) -> Linkage {
47 self.linkage
48 }
49
50 pub fn required_version(&self) -> VersionRequirement {
52 let req = match &self.version {
53 DependencyVersionScheme::Registry(version) => return version.clone(),
54 DependencyVersionScheme::Workspace { version, .. } => version.clone(),
55 DependencyVersionScheme::WorkspacePath { version, .. } => version.clone(),
56 DependencyVersionScheme::Path { version, .. } => version.clone(),
57 DependencyVersionScheme::Git { version, .. } => {
58 version.as_ref().map(|spanned| VersionRequirement::Semantic(spanned.clone()))
59 },
60 };
61 req.unwrap_or_else(|| VersionRequirement::from(VersionReq::STAR.clone()))
62 }
63}
64
65impl Spanned for Dependency {
66 fn span(&self) -> SourceSpan {
67 self.name.span()
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum DependencyVersionScheme {
74 Registry(VersionRequirement),
80 Workspace {
82 member: Span<Uri>,
84 version: Option<VersionRequirement>,
87 },
88 WorkspacePath {
91 path: Span<Uri>,
93 version: Option<VersionRequirement>,
96 },
97 Path {
99 path: Span<Uri>,
102 version: Option<VersionRequirement>,
108 },
109 Git {
111 repo: Span<Uri>,
115 revision: Span<GitRevision>,
117 version: Option<Span<VersionReq>>,
123 },
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum GitRevision {
129 Branch(Arc<str>),
131 Commit(Arc<str>),
133}
134
135impl fmt::Display for GitRevision {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 match self {
138 Self::Branch(name) => f.write_str(name.as_ref()),
139 Self::Commit(rev) => write!(f, "sha256:{rev}"),
140 }
141 }
142}
143
144#[derive(Debug, thiserror::Error, Diagnostic)]
145pub enum InvalidDependencySpecError {
146 #[error("package is not a member of a workspace")]
147 NotAWorkspace {
148 #[label(primary)]
149 span: SourceSpan,
150 },
151 #[error("digests cannot be used with 'git' dependencies")]
152 #[diagnostic(help(
153 "Package digests are only valid when depending on an already-assembled package"
154 ))]
155 GitWithDigest {
156 #[label(primary)]
157 span: SourceSpan,
158 },
159 #[error("'git' dependencies must also specify a revision using either 'branch' or 'rev'")]
160 MissingGitRevision {
161 #[label(primary)]
162 span: SourceSpan,
163 },
164 #[error(
165 "conflicting 'git' revisions: 'branch' and 'rev' may refer to different commits, you cannot specify both"
166 )]
167 ConflictingGitRevision {
168 #[label(primary)]
169 first: SourceSpan,
170 #[label]
171 second: SourceSpan,
172 },
173 #[error("missing version: expected one of 'version', 'git', or 'digest' to be provided")]
174 MissingVersion {
175 #[label(primary)]
176 span: SourceSpan,
177 },
178}
179
180#[cfg(feature = "serde")]
181impl TryFrom<Span<&crate::ast::DependencySpec>> for DependencyVersionScheme {
182 type Error = InvalidDependencySpecError;
183
184 fn try_from(ast: Span<&crate::ast::DependencySpec>) -> Result<Self, Self::Error> {
185 if ast.inherits_workspace_version() {
186 return Err(InvalidDependencySpecError::NotAWorkspace { span: ast.span() });
187 }
188
189 if ast.is_host_resolved() {
190 ast.version()
191 .cloned()
192 .map(Self::Registry)
193 .ok_or(InvalidDependencySpecError::MissingVersion { span: ast.span() })
194 } else if ast.is_git() {
195 let version = match ast.version() {
196 Some(VersionRequirement::Digest(digest)) => {
197 return Err(InvalidDependencySpecError::GitWithDigest { span: digest.span() });
198 },
199 Some(VersionRequirement::Exact(_)) => {
200 return Err(InvalidDependencySpecError::GitWithDigest { span: ast.span() });
201 },
202 Some(VersionRequirement::Semantic(v)) => Some(v.clone()),
203 None => None,
204 };
205 if let Some(branch) = ast.branch.as_ref()
206 && let Some(rev) = ast.rev.as_ref()
207 {
208 return Err(InvalidDependencySpecError::ConflictingGitRevision {
209 first: branch.span(),
210 second: rev.span(),
211 });
212 }
213 let revision = ast
214 .branch
215 .as_ref()
216 .map(|branch| Span::new(branch.span(), GitRevision::Branch(branch.inner().clone())))
217 .or_else(|| {
218 ast.rev
219 .as_ref()
220 .map(|rev| Span::new(rev.span(), GitRevision::Commit(rev.inner().clone())))
221 })
222 .ok_or_else(|| InvalidDependencySpecError::MissingGitRevision {
223 span: ast.span(),
224 })?;
225 Ok(Self::Git {
226 repo: ast.git.clone().unwrap(),
227 revision,
228 version,
229 })
230 } else {
231 Ok(Self::Path {
232 path: ast.path.clone().unwrap(),
233 version: ast.version_or_digest.clone(),
234 })
235 }
236 }
237}
238
239#[cfg(feature = "serde")]
240impl DependencyVersionScheme {
241 #[cfg(feature = "std")]
244 pub fn try_from_in_workspace(
245 spec: Span<&crate::ast::DependencySpec>,
246 workspace: &crate::ast::WorkspaceFile,
247 ) -> Result<Self, InvalidDependencySpecError> {
248 use std::path::Path;
249
250 use crate::absolutize_path;
251
252 match Self::try_from(spec)? {
256 Self::Path { path: uri, version } => {
257 let workspace_path = workspace
258 .source_file
259 .as_ref()
260 .map(|file| Path::new(file.content().uri().path()));
261 if uri.scheme().is_none_or(|scheme| scheme == "file")
262 && let Some(workspace_path) = workspace_path.and_then(|p| p.canonicalize().ok())
263 && let Some(workspace_root) = workspace_path.parent()
264 && let Ok(resolved_uri) = absolutize_path(Path::new(uri.path()), workspace_root)
265 {
266 let is_member = workspace.workspace.members.iter().any(|member| {
267 let member_path = member.path();
268 uri.path() == member_path
269 || uri.path() == format!("{member_path}/miden-project.toml")
270 || absolutize_path(Path::new(member_path), workspace_root)
271 .ok()
272 .is_some_and(|member_dir| {
273 resolved_uri == member_dir
274 || resolved_uri == member_dir.join("miden-project.toml")
275 })
276 });
277 if is_member {
278 Ok(Self::Workspace { member: uri.clone(), version })
279 } else {
280 Ok(Self::WorkspacePath { path: uri.clone(), version })
281 }
282 } else {
283 Ok(Self::Path { path: uri, version })
284 }
285 },
286 scheme => Ok(scheme),
287 }
288 }
289
290 #[cfg(not(feature = "std"))]
291 pub fn try_from_in_workspace(
292 spec: Span<&crate::ast::DependencySpec>,
293 workspace: &crate::ast::WorkspaceFile,
294 ) -> Result<Self, InvalidDependencySpecError> {
295 use alloc::format;
296
297 match Self::try_from(spec)? {
298 Self::Path { path: uri, version } => {
299 let workspace_path =
300 workspace.source_file.as_ref().map(|file| file.content().uri().path());
301 if uri.scheme().is_none_or(|scheme| scheme == "file") &&
302 let Some(workspace_root) = workspace_path.and_then(|p| p.strip_suffix("miden-project.toml")) &&
303 (!workspace_root.is_empty() && !(uri.path().starts_with('/') || uri.path().starts_with("..")))
305 {
306 let is_member = workspace.workspace.members.iter().any(|member| {
307 let member_path = member.path();
308 uri.path() == member_path
309 || uri.path() == format!("{member_path}/miden-project.toml")
310 });
311 if is_member {
312 Ok(Self::Workspace { member: uri.clone(), version })
313 } else {
314 Ok(Self::WorkspacePath { path: uri.clone(), version })
315 }
316 } else {
317 Ok(Self::Path { path: uri, version })
318 }
319 },
320 scheme => Ok(scheme),
321 }
322 }
323}