Skip to main content

miden_project/
workspace.rs

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