1#![warn(missing_docs)]
7
8use nargo_types::{Error, Result, Span};
9use serde::{Deserialize, Serialize};
10use std::{
11 collections::{HashMap, HashSet},
12 fs,
13 path::{Path, PathBuf},
14};
15use tracing::{debug, info, warn};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct WorkspaceMember {
20 pub name: String,
22 pub path: PathBuf,
24 pub version: String,
26 pub is_root: bool,
28 pub dependencies: Vec<String>,
30 pub dev_dependencies: Vec<String>,
32}
33
34impl WorkspaceMember {
35 pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
37 Self { name: name.into(), path: path.into(), version: "0.0.0".to_string(), is_root: false, dependencies: Vec::new(), dev_dependencies: Vec::new() }
38 }
39
40 pub fn with_version(mut self, version: impl Into<String>) -> Self {
42 self.version = version.into();
43 self
44 }
45
46 pub fn as_root(mut self) -> Self {
48 self.is_root = true;
49 self
50 }
51
52 pub fn add_dependency(&mut self, dep: impl Into<String>) {
54 self.dependencies.push(dep.into());
55 }
56
57 pub fn add_dev_dependency(&mut self, dep: impl Into<String>) {
59 self.dev_dependencies.push(dep.into());
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct Workspace {
66 pub root: PathBuf,
68 pub members: HashMap<String, WorkspaceMember>,
70 pub member_order: Vec<String>,
72 pub shared_config: Option<SharedConfig>,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct SharedConfig {
79 pub version: Option<String>,
81 pub authors: Vec<String>,
83 pub license: Option<String>,
85 pub repository: Option<String>,
87 pub dependencies: HashMap<String, String>,
89 pub dev_dependencies: HashMap<String, String>,
91}
92
93impl Default for Workspace {
94 fn default() -> Self {
95 Self { root: PathBuf::from("."), members: HashMap::new(), member_order: Vec::new(), shared_config: None }
96 }
97}
98
99impl Workspace {
100 pub fn new(root: impl Into<PathBuf>) -> Self {
102 Self { root: root.into(), ..Default::default() }
103 }
104
105 pub fn discover(root: &Path) -> Result<Self> {
107 let nargo_toml = root.join("Nargo.toml");
108
109 if !nargo_toml.exists() {
110 return Err(Error::external_error("workspace".to_string(), format!("No Nargo.toml found in {:?}", root), Span::unknown()));
111 }
112
113 let mut workspace = Self::new(root);
115
116 let member = WorkspaceMember::new("default", root.to_path_buf()).with_version("0.0.0").as_root();
118
119 workspace.members.insert(member.name.clone(), member.clone());
120 workspace.member_order.push(member.name);
121
122 info!("Discovered workspace with {} members", workspace.members.len());
123 Ok(workspace)
124 }
125
126 pub fn discover_or_single(root: &Path) -> Result<Self> {
128 Self::discover(root).or_else(|_| {
129 let nargo_toml = root.join("Nargo.toml");
130 if nargo_toml.exists() {
131 let member = WorkspaceMember::new("default", root.to_path_buf()).with_version("0.0.0").as_root();
133
134 let mut workspace = Self::new(root);
135 workspace.members.insert(member.name.clone(), member.clone());
136 workspace.member_order.push(member.name);
137
138 Ok(workspace)
139 }
140 else {
141 Err(Error::external_error("workspace".to_string(), format!("No Nargo.toml found in {:?}", root), Span::unknown()))
142 }
143 })
144 }
145
146 fn discover_members(&mut self, root: &Path, pattern: &str) -> Result<()> {
147 if pattern.ends_with("/*") {
148 let base = root.join(pattern.trim_end_matches("/*"));
149 if base.is_dir() {
150 for entry in fs::read_dir(&base)? {
151 let entry = entry?;
152 let path = entry.path();
153 if path.is_dir() && path.join("Nargo.toml").exists() {
154 self.add_member_from_path(&path)?;
155 }
156 }
157 }
158 }
159 else {
160 let path = root.join(pattern);
161 if path.is_dir() && path.join("Nargo.toml").exists() {
162 self.add_member_from_path(&path)?;
163 }
164 }
165 Ok(())
166 }
167
168 fn add_member_from_path(&mut self, path: &Path) -> Result<()> {
169 let nargo_toml = path.join("Nargo.toml");
170 let _content = fs::read_to_string(&nargo_toml).map_err(|e| Error::external_error("workspace".to_string(), format!("Failed to read {:?}: {}", nargo_toml, e), Span::unknown()))?;
171
172 let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
174 let member = WorkspaceMember::new(&name, path).with_version("0.0.0");
175
176 self.members.insert(name.clone(), member);
177 Ok(())
178 }
179
180 fn compute_dependency_order(&mut self) -> Result<()> {
181 let mut order = Vec::new();
182 let mut visited = HashSet::new();
183 let mut temp_marks = HashSet::new();
184
185 fn visit(name: &str, members: &HashMap<String, WorkspaceMember>, visited: &mut HashSet<String>, temp_marks: &mut HashSet<String>, order: &mut Vec<String>) -> Result<()> {
186 if visited.contains(name) {
187 return Ok(());
188 }
189 if temp_marks.contains(name) {
190 return Err(Error::external_error("workspace".to_string(), format!("Circular dependency detected involving {}", name), Span::unknown()));
191 }
192
193 temp_marks.insert(name.to_string());
194
195 if let Some(member) = members.get(name) {
196 for dep in &member.dependencies {
197 if members.contains_key(dep) {
198 visit(dep, members, visited, temp_marks, order)?;
199 }
200 }
201 }
202
203 temp_marks.remove(name);
204 visited.insert(name.to_string());
205 order.push(name.to_string());
206
207 Ok(())
208 }
209
210 for name in self.members.keys() {
211 visit(name, &self.members, &mut visited, &mut temp_marks, &mut order)?;
212 }
213
214 self.member_order = order;
215 Ok(())
216 }
217
218 pub fn get_member(&self, name: &str) -> Option<&WorkspaceMember> {
220 self.members.get(name)
221 }
222
223 pub fn get_member_by_path(&self, path: &Path) -> Option<&WorkspaceMember> {
225 self.members.values().find(|m| m.path == path)
226 }
227
228 pub fn member_names(&self) -> &[String] {
230 &self.member_order
231 }
232
233 pub fn len(&self) -> usize {
235 self.members.len()
236 }
237
238 pub fn is_empty(&self) -> bool {
240 self.members.is_empty()
241 }
242
243 pub fn is_single(&self) -> bool {
245 self.members.len() == 1
246 }
247
248 pub fn root_member(&self) -> Option<&WorkspaceMember> {
250 self.members.values().find(|m| m.is_root)
251 }
252
253 pub fn list_members(&self) {
255 println!("Workspace members:");
256 for name in &self.member_order {
257 if let Some(member) = self.members.get(name) {
258 let rel_path = member.path.strip_prefix(&self.root).unwrap_or(&member.path);
259 println!(" {} @ {} ({})", name, member.version, rel_path.display());
260 }
261 }
262 }
263
264 pub fn dependents_of(&self, package_name: &str) -> Vec<&WorkspaceMember> {
266 self.members.values().filter(|m| m.dependencies.contains(&package_name.to_string())).collect()
267 }
268
269 pub fn dependencies_of(&self, package_name: &str) -> Vec<&WorkspaceMember> {
271 if let Some(member) = self.members.get(package_name) {
272 member.dependencies.iter().filter_map(|dep| self.members.get(dep)).collect()
273 }
274 else {
275 Vec::new()
276 }
277 }
278
279 pub fn filter_members<F>(&self, predicate: F) -> Vec<&WorkspaceMember>
281 where
282 F: Fn(&WorkspaceMember) -> bool,
283 {
284 self.member_order
285 .iter()
286 .filter_map(|name| {
287 let member = self.members.get(name)?;
288 if predicate(member) {
289 Some(member)
290 }
291 else {
292 None
293 }
294 })
295 .collect()
296 }
297
298 pub fn root(&self) -> &Path {
300 &self.root
301 }
302
303 pub fn target_dir(&self) -> PathBuf {
305 self.root.join("target")
306 }
307
308 pub fn shared_config(&self) -> Option<&SharedConfig> {
310 self.shared_config.as_ref()
311 }
312}