Skip to main content

nargo_workspace/
lib.rs

1//! Workspace management for Nargo package manager.
2//!
3//! This crate provides workspace discovery, member management, and
4//! task orchestration for multi-package projects.
5
6#![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/// Represents a workspace member package.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct WorkspaceMember {
20    /// Member package name.
21    pub name: String,
22    /// Path to the member package.
23    pub path: PathBuf,
24    /// Member package version.
25    pub version: String,
26    /// Whether this is the root package.
27    pub is_root: bool,
28    /// Dependencies of this member.
29    pub dependencies: Vec<String>,
30    /// Dev dependencies of this member.
31    pub dev_dependencies: Vec<String>,
32}
33
34impl WorkspaceMember {
35    /// Creates a new workspace member.
36    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    /// Sets the version.
41    pub fn with_version(mut self, version: impl Into<String>) -> Self {
42        self.version = version.into();
43        self
44    }
45
46    /// Marks as root package.
47    pub fn as_root(mut self) -> Self {
48        self.is_root = true;
49        self
50    }
51
52    /// Adds a dependency.
53    pub fn add_dependency(&mut self, dep: impl Into<String>) {
54        self.dependencies.push(dep.into());
55    }
56
57    /// Adds a dev dependency.
58    pub fn add_dev_dependency(&mut self, dep: impl Into<String>) {
59        self.dev_dependencies.push(dep.into());
60    }
61}
62
63/// Workspace configuration and state.
64#[derive(Debug, Clone)]
65pub struct Workspace {
66    /// Root directory of the workspace.
67    pub root: PathBuf,
68    /// All member packages.
69    pub members: HashMap<String, WorkspaceMember>,
70    /// Member discovery order (for builds).
71    pub member_order: Vec<String>,
72    /// Shared workspace configuration.
73    pub shared_config: Option<SharedConfig>,
74}
75
76/// Shared configuration inherited by all members.
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct SharedConfig {
79    /// Shared version.
80    pub version: Option<String>,
81    /// Shared authors.
82    pub authors: Vec<String>,
83    /// Shared license.
84    pub license: Option<String>,
85    /// Shared repository.
86    pub repository: Option<String>,
87    /// Shared dependencies.
88    pub dependencies: HashMap<String, String>,
89    /// Shared dev dependencies.
90    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    /// Creates a new empty workspace.
101    pub fn new(root: impl Into<PathBuf>) -> Self {
102        Self { root: root.into(), ..Default::default() }
103    }
104
105    /// Discovers workspace from a root directory.
106    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        // 简化实现,实际需要根据 nargo_config 的 API 进行调整
114        let mut workspace = Self::new(root);
115
116        // 添加一个默认成员
117        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    /// Discovers workspace or returns a single-package workspace.
127    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                // 简化实现,实际需要根据 nargo_config 的 API 进行调整
132                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        // 简化实现,实际需要根据 nargo_config 的 API 进行调整
173        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    /// Gets a member by name.
219    pub fn get_member(&self, name: &str) -> Option<&WorkspaceMember> {
220        self.members.get(name)
221    }
222
223    /// Gets a member by path.
224    pub fn get_member_by_path(&self, path: &Path) -> Option<&WorkspaceMember> {
225        self.members.values().find(|m| m.path == path)
226    }
227
228    /// Returns all member names in dependency order.
229    pub fn member_names(&self) -> &[String] {
230        &self.member_order
231    }
232
233    /// Returns the number of members.
234    pub fn len(&self) -> usize {
235        self.members.len()
236    }
237
238    /// Checks if the workspace is empty.
239    pub fn is_empty(&self) -> bool {
240        self.members.is_empty()
241    }
242
243    /// Checks if this is a single-package workspace.
244    pub fn is_single(&self) -> bool {
245        self.members.len() == 1
246    }
247
248    /// Returns the root member if this is a single-package workspace.
249    pub fn root_member(&self) -> Option<&WorkspaceMember> {
250        self.members.values().find(|m| m.is_root)
251    }
252
253    /// Lists all members.
254    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    /// Returns members that depend on a given package.
265    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    /// Returns members that a given package depends on.
270    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    /// Filters members by a predicate.
280    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    /// Returns the workspace root path.
299    pub fn root(&self) -> &Path {
300        &self.root
301    }
302
303    /// Returns the target directory for the workspace.
304    pub fn target_dir(&self) -> PathBuf {
305        self.root.join("target")
306    }
307
308    /// Returns the shared configuration.
309    pub fn shared_config(&self) -> Option<&SharedConfig> {
310        self.shared_config.as_ref()
311    }
312}