Skip to main content

miden_project/
workspace.rs

1#[cfg(all(feature = "std", feature = "serde"))]
2use std::string::{String, ToString};
3#[cfg(feature = "std")]
4use std::{boxed::Box, path::Path};
5
6#[cfg(all(feature = "std", feature = "serde"))]
7use miden_assembly_syntax::debuginfo::SourceManager;
8
9use crate::*;
10
11/// Represents a Miden project workspace.
12///
13/// Workspaces are comprised of one or more sub-projects that define the member packages of the
14/// workspace.
15#[derive(Debug)]
16pub struct Workspace {
17    /// The file path of the workspace manifest, if applicable.
18    #[cfg(feature = "std")]
19    manifest_path: Option<Box<Path>>,
20    /// The set of packages which are direct members of this workspace
21    members: Vec<Arc<Package>>,
22}
23
24/// Accessors
25impl Workspace {
26    /// Return the path of the workspace manifest, if known.
27    #[cfg(feature = "std")]
28    pub fn manifest_path(&self) -> Option<&Path> {
29        self.manifest_path.as_deref()
30    }
31
32    /// Return the path of the directory containing the workspace manifest
33    #[cfg(feature = "std")]
34    pub fn workspace_root(&self) -> Option<&Path> {
35        self.manifest_path()?.parent()
36    }
37
38    /// Get the set of packages which are members of this workspace
39    pub fn members(&self) -> &[Arc<Package>] {
40        &self.members
41    }
42
43    /// Look up a workspace member by its package name
44    pub fn get_member_by_name(&self, name: impl AsRef<str>) -> Option<Arc<Package>> {
45        let name = name.as_ref();
46        self.members().iter().find(|member| &**member.name().inner() == name).cloned()
47    }
48
49    /// Look up a workspace member by its workspace-relative path
50    #[cfg(feature = "std")]
51    pub fn get_member_by_relative_path(&self, path: impl AsRef<Path>) -> Option<Arc<Package>> {
52        let path = path.as_ref();
53        let path = self.workspace_root()?.join(path);
54        self.members()
55            .iter()
56            .find(|member| {
57                member.manifest_path().is_some_and(|p| p.parent() == Some(path.as_path()))
58            })
59            .cloned()
60    }
61}
62
63/// Parsing
64#[cfg(all(feature = "std", feature = "serde"))]
65impl Workspace {
66    /// Load a [Workspace] from `source`, using the provided `source_manager` when loading the
67    /// sources of workspace members.
68    pub fn load(
69        source: Arc<SourceFile>,
70        source_manager: &dyn SourceManager,
71    ) -> Result<Box<Self>, Report> {
72        use miden_assembly_syntax::debuginfo::SourceManagerExt;
73
74        use crate::ast::ProjectFileError;
75
76        let file = ast::WorkspaceFile::parse(source.clone())?;
77
78        let manifest_uri = source.content().uri();
79        let manifest_path = if manifest_uri.scheme().is_none_or(|scheme| scheme == "file") {
80            Some(Path::new(manifest_uri.path()).to_path_buf().into_boxed_path())
81        } else {
82            None
83        };
84
85        let members = file.workspace.members.clone();
86
87        let mut workspace = Box::new(Workspace {
88            manifest_path,
89            members: Vec::with_capacity(members.len()),
90        });
91        let mut seen_member_names = Map::<String, SourceSpan>::default();
92
93        for member in members {
94            let Some(workspace_root) = workspace.workspace_root() else {
95                return Err(ProjectFileError::LoadWorkspaceMemberFailed {
96                    source_file: source.clone(),
97                    span: Label::new(
98                        member.span(),
99                        "cannot load workspace members for virtual workspace manifest: manifest path must be resolvable",
100                    ),
101                }
102                .into());
103            };
104            let relative_path = Path::new(member.as_str());
105            let member_dir = absolutize_path(relative_path, workspace_root).map_err(|err| {
106                ProjectFileError::LoadWorkspaceMemberFailed {
107                    source_file: source.clone(),
108                    span: Label::new(member.span(), err.to_string()),
109                }
110            })?;
111            if member_dir.strip_prefix(workspace_root).is_err() {
112                return Err(ProjectFileError::LoadWorkspaceMemberFailed {
113                    source_file: source.clone(),
114                    span: Label::new(
115                        member.span(),
116                        "workspace members must be located within the workspace root",
117                    ),
118                }
119                .into());
120            }
121            let manifest_path = member_dir.join("miden-project.toml");
122            let member_manifest = source_manager.load_file(&manifest_path).map_err(|err| {
123                ProjectFileError::LoadWorkspaceMemberFailed {
124                    source_file: source.clone(),
125                    span: Label::new(member.span(), err.to_string()),
126                }
127            })?;
128            let package = Package::load_from_workspace(member_manifest, &file)?;
129            let package_name = package.name().inner().to_string();
130            if let Some(prev) = seen_member_names.insert(package_name.clone(), member.span()) {
131                return Err(ProjectFileError::DuplicateWorkspaceMember {
132                    name: package_name,
133                    source_file: source.clone(),
134                    span: member.span(),
135                    prev,
136                }
137                .into());
138            }
139            workspace.members.push(Arc::from(package));
140        }
141
142        Ok(workspace)
143    }
144}