miden_project/
workspace.rs1#[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#[derive(Debug)]
18pub struct Workspace {
19 #[cfg(feature = "std")]
21 manifest_path: Option<Box<Path>>,
22 members: Vec<Arc<Package>>,
24}
25
26impl Workspace {
28 #[cfg(feature = "std")]
30 pub fn manifest_path(&self) -> Option<&Path> {
31 self.manifest_path.as_deref()
32 }
33
34 #[cfg(feature = "std")]
36 pub fn workspace_root(&self) -> Option<&Path> {
37 self.manifest_path()?.parent()
38 }
39
40 pub fn members(&self) -> &[Arc<Package>] {
42 &self.members
43 }
44
45 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 #[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#[cfg(all(feature = "std", feature = "serde"))]
67impl Workspace {
68 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}