1#![no_std]
2
3#[macro_use]
4extern crate alloc;
5
6#[cfg(any(test, feature = "std"))]
7extern crate std;
8
9#[cfg(feature = "serde")]
10pub mod ast;
11mod dependencies;
12mod linkage;
13mod package;
14mod profile;
15mod target;
16#[cfg(all(test, feature = "std", feature = "serde"))]
17mod tests;
18mod workspace;
19
20use alloc::{sync::Arc, vec::Vec};
21
22use miden_assembly_syntax::{
23 Report,
24 debuginfo::{SourceSpan, Span},
25 diagnostics::{Diagnostic, miette},
26};
27pub use miden_assembly_syntax::{Word, debuginfo::Uri, semver};
29#[cfg(feature = "serde")]
30use miden_assembly_syntax::{
31 debuginfo::{SourceFile, SourceId},
32 diagnostics::{Label, RelatedError, RelatedLabel},
33};
34pub use miden_mast_package::TargetType;
35#[cfg(feature = "serde")]
36use serde::{Deserialize, Serialize};
37pub use toml::Value;
38
39pub use self::{
40 dependencies::*, linkage::Linkage, package::Package, profile::Profile, target::Target,
41 workspace::Workspace,
42};
43
44pub type Map<K, V> = alloc::collections::BTreeMap<K, V>;
46
47pub type Metadata = Map<Span<Arc<str>>, Span<Value>>;
51
52pub type MetadataSet = Map<Span<Arc<str>>, Metadata>;
56
57#[derive(Debug, Clone)]
59pub enum Project {
60 WorkspacePackage {
62 package: Arc<Package>,
64 workspace: Arc<Workspace>,
66 },
67 Package(Arc<Package>),
69}
70
71impl From<alloc::boxed::Box<Package>> for Project {
72 fn from(value: alloc::boxed::Box<Package>) -> Self {
73 Self::Package(value.into())
74 }
75}
76
77impl From<Arc<Package>> for Project {
78 fn from(value: Arc<Package>) -> Self {
79 Self::Package(value)
80 }
81}
82
83impl Project {
84 pub fn is_workspace_member(&self) -> bool {
86 matches!(self, Self::WorkspacePackage { .. })
87 }
88
89 pub fn package(&self) -> Arc<Package> {
91 match self {
92 Self::WorkspacePackage { package, .. } | Self::Package(package) => Arc::clone(package),
93 }
94 }
95
96 #[cfg(feature = "std")]
98 pub fn manifest_path(&self) -> Option<&std::path::Path> {
99 match self {
100 Self::WorkspacePackage { package, .. } | Self::Package(package) => {
101 package.manifest_path()
102 },
103 }
104 }
105}
106
107#[cfg(all(feature = "std", feature = "serde"))]
109impl Project {
110 pub fn load(
115 path: impl AsRef<std::path::Path>,
116 source_manager: &dyn miden_assembly_syntax::debuginfo::SourceManager,
117 ) -> Result<Self, Report> {
118 let path = path.as_ref();
119 let manifest_path = if path.is_dir() {
120 path.join("miden-project.toml").canonicalize().map_err(Report::msg)?
121 } else {
122 path.canonicalize().map_err(Report::msg)?
123 };
124
125 Self::try_load_as_workspace_member(None, &manifest_path, source_manager)
126 }
127
128 pub fn load_project_reference(
133 name: &str,
134 path: impl AsRef<std::path::Path>,
135 source_manager: &dyn miden_assembly_syntax::debuginfo::SourceManager,
136 ) -> Result<Self, Report> {
137 let path = path.as_ref();
138 let manifest_path = if path.is_dir() {
139 path.join("miden-project.toml").canonicalize().map_err(Report::msg)?
140 } else {
141 path.canonicalize().map_err(Report::msg)?
142 };
143
144 Self::try_load_as_workspace_member(Some(name), &manifest_path, source_manager)
145 }
146
147 fn try_load_as_workspace_member(
148 name: Option<&str>,
149 manifest_path: impl AsRef<std::path::Path>,
150 source_manager: &dyn miden_assembly_syntax::debuginfo::SourceManager,
151 ) -> Result<Self, Report> {
152 use miden_assembly_syntax::debuginfo::SourceManagerExt;
153
154 let manifest_path = manifest_path.as_ref();
155 let ancestors = manifest_path
156 .parent()
157 .ok_or_else(|| {
158 Report::msg(format!(
159 "manifest '{}' has no parent directory",
160 manifest_path.display()
161 ))
162 })?
163 .ancestors();
164
165 let initial_package_dir = manifest_path.parent();
166 for ancestor in ancestors {
167 let workspace_manifest = ancestor.join("miden-project.toml");
168 if !workspace_manifest.exists() {
169 continue;
170 }
171
172 let source = source_manager.load_file(&workspace_manifest).map_err(Report::msg)?;
173
174 let contents = toml::from_str::<toml::Table>(source.as_str()).map_err(|err| {
175 Report::msg(format!("could not parse {}: {err}", workspace_manifest.display()))
176 })?;
177 if contents.contains_key("workspace") {
178 let workspace_file = ast::WorkspaceFile::parse(source.clone())?;
179 let is_workspace_manifest = manifest_path == workspace_manifest;
180
181 if !is_workspace_manifest
182 && !workspace_declares_member(
183 &workspace_file,
184 &workspace_manifest,
185 manifest_path,
186 )
187 {
188 break;
189 }
190 if is_workspace_manifest && name.is_none() {
191 break;
192 }
193
194 let workspace = Workspace::load(source, source_manager)?;
195 let package = if let Some(package) = workspace
196 .members()
197 .iter()
198 .find(|member| member.manifest_path().is_some_and(|path| path == manifest_path))
199 .cloned()
200 {
201 package
202 } else if manifest_path == workspace_manifest {
203 let Some(name) = name else {
204 break;
205 };
206 workspace.get_member_by_name(name).ok_or_else(|| {
207 Report::msg(format!(
208 "workspace '{}' does not contain a member named '{name}'",
209 workspace_manifest.display(),
210 ))
211 })?
212 } else {
213 break;
214 };
215
216 validate_package_name(name, &package)?;
217
218 return Ok(Self::WorkspacePackage { package, workspace: workspace.into() });
219 } else if Some(ancestor) != initial_package_dir {
220 break;
221 }
222 }
223
224 let source = source_manager.load_file(manifest_path).map_err(Report::msg)?;
225 let package = Package::load(source)?;
226 validate_package_name(name, &package)?;
227 Ok(Self::Package(package.into()))
228 }
229}
230
231#[cfg(all(feature = "std", feature = "serde"))]
232fn validate_package_name(expected_name: Option<&str>, package: &Package) -> Result<(), Report> {
233 let Some(expected_name) = expected_name else {
234 return Ok(());
235 };
236
237 let actual_name = package.name();
238 if &**actual_name.inner() == expected_name {
239 Ok(())
240 } else if let Some(location) = package.manifest_path() {
241 Err(Report::msg(format!(
242 "dependency '{}' resolved to package '{}' at '{}'",
243 expected_name,
244 actual_name.inner(),
245 location.display()
246 )))
247 } else {
248 Err(Report::msg(format!(
249 "dependency '{}' resolved to package '{}'",
250 expected_name,
251 actual_name.inner(),
252 )))
253 }
254}
255
256#[cfg(all(feature = "std", feature = "serde"))]
257fn workspace_declares_member(
258 workspace: &ast::WorkspaceFile,
259 workspace_manifest: &std::path::Path,
260 manifest_path: &std::path::Path,
261) -> bool {
262 let Some(workspace_root) = workspace_manifest.parent() else {
263 return false;
264 };
265
266 workspace.workspace.members.iter().any(|member| {
267 let member_dir =
268 match absolutize_path(std::path::Path::new(member.inner().path()), workspace_root) {
269 Ok(member_dir) => member_dir,
270 Err(_) => return false,
271 };
272
273 member_dir.join("miden-project.toml") == manifest_path
274 })
275}
276
277#[cfg(all(feature = "std", feature = "serde"))]
281pub(crate) fn absolutize_path(
282 path: &std::path::Path,
283 workspace_root: &std::path::Path,
284) -> Result<std::path::PathBuf, std::io::Error> {
285 if path.is_absolute() {
286 path.canonicalize()
287 } else {
288 workspace_root.join(path).canonicalize()
289 }
290}