Skip to main content

miden_project/
lib.rs

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};
27// Re-exported for consistency
28pub 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
44/// An alias for [`alloc::collections::BTreeMap`].
45pub type Map<K, V> = alloc::collections::BTreeMap<K, V>;
46
47/// Represents arbitrary metadata in key/value format
48///
49/// This representation provides spans for both keys and values
50pub type Metadata = Map<Span<Arc<str>>, Span<Value>>;
51
52/// Represents a set of named metadata tables, where each table is represented by [Metadata].
53///
54/// This representation provides spans for the table name, and each entry in that table's metadata.
55pub type MetadataSet = Map<Span<Arc<str>>, Metadata>;
56
57/// Represents any Miden project type, i.e. either a workspace, or a standalone package.
58#[derive(Debug, Clone)]
59pub enum Project {
60    /// A specific member of a Miden workspace
61    WorkspacePackage {
62        /// The member package
63        package: Arc<Package>,
64        /// The containing Miden workspace
65        workspace: Arc<Workspace>,
66    },
67    /// A standalone Miden package
68    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    /// Returns true if this project is a member of a workspace
85    pub fn is_workspace_member(&self) -> bool {
86        matches!(self, Self::WorkspacePackage { .. })
87    }
88
89    /// Get the underlying [Package] for this project
90    pub fn package(&self) -> Arc<Package> {
91        match self {
92            Self::WorkspacePackage { package, .. } | Self::Package(package) => Arc::clone(package),
93        }
94    }
95
96    /// Returns the manifest from which this project was loaded
97    #[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/// Parsing
108#[cfg(all(feature = "std", feature = "serde"))]
109impl Project {
110    /// Load a project manifest from `path`.
111    ///
112    /// If the given manifest source belongs to a package within a larger workspace, this function
113    /// will attempt to resolve the workspace and extract the package from it.
114    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    /// Load a project manifest from `path`, expected to be named `name`
129    ///
130    /// If the given manifest source belongs to a package within a larger workspace, this function
131    /// will attempt to resolve the workspace and extract the package from it.
132    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/// A utility function for making a path absolute and canonical.
278///
279/// Relative paths are made absolute relative to `workspace_root`.
280#[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}