burn_central_workspace/entity/projects/
mod.rs1use std::path::{Path, PathBuf};
2use std::sync::{Arc, Mutex};
3
4use cargo_metadata::TargetKind;
5
6use crate::entity::projects::burn_dir::{BurnDir, project::BurnCentralProject};
7use crate::event::Reporter;
8use crate::execution::cancellable::CancellationToken;
9use crate::tools::function_discovery::{
10 DiscoveryConfig, DiscoveryError, DiscoveryEvent, FunctionDiscovery, PkgId,
11};
12use crate::tools::functions_registry::FunctionRegistry;
13
14pub mod burn_dir;
15
16#[derive(Debug)]
17pub enum ErrorKind {
18 ManifestNotFound,
19 Parsing,
20 BurnDirInitialization,
21 BurnDirNotInitialized,
22 Unexpected,
23}
24
25#[derive(thiserror::Error, Debug)]
26pub struct ProjectContextError {
27 message: String,
28 kind: ErrorKind,
29 #[source]
30 source: Option<anyhow::Error>,
31}
32
33impl ProjectContextError {
34 pub fn new(message: String, kind: ErrorKind, source: Option<anyhow::Error>) -> Self {
35 Self {
36 message,
37 kind,
38 source,
39 }
40 }
41
42 pub fn kind(&self) -> &ErrorKind {
43 &self.kind
44 }
45
46 pub fn is_burn_dir_not_initialized(&self) -> bool {
47 matches!(self.kind, ErrorKind::BurnDirNotInitialized)
48 }
49}
50
51impl std::fmt::Display for ProjectContextError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "{}", self.message)
54 }
55}
56
57pub struct ProjectContext {
58 pub workspace_info: WorkspaceInfo,
59 pub build_profile: String,
60 pub burn_dir: BurnDir,
61 pub project: BurnCentralProject,
62 function_registry: Mutex<FunctionRegistry>,
63}
64
65pub struct WorkspaceInfo {
66 pub workspace_name: String,
67 pub workspace_root: PathBuf,
68 pub metadata: cargo_metadata::Metadata,
69}
70
71impl WorkspaceInfo {
72 pub fn load_from_path(manifest_path: &Path) -> Result<Self, ProjectContextError> {
73 if !manifest_path.is_file() {
74 return Err(ProjectContextError::new(
75 format!(
76 "Cargo.toml not found at specified path '{}'",
77 manifest_path.display()
78 ),
79 ErrorKind::ManifestNotFound,
80 None,
81 ));
82 }
83 let metadata = cargo_metadata::MetadataCommand::new()
84 .manifest_path(manifest_path)
85 .no_deps()
86 .exec()
87 .map_err(|e| {
88 ProjectContextError::new(
89 format!(
90 "Failed to load cargo metadata for manifest at '{}': {}",
91 manifest_path.display(),
92 e
93 ),
94 ErrorKind::Parsing,
95 Some(anyhow::anyhow!(e)),
96 )
97 })?;
98
99 let workspace_root = metadata.workspace_root.clone().into_std_path_buf();
100
101 let workspace_toml_path = workspace_root.join("Cargo.toml");
103 if !workspace_toml_path.exists() {
104 return Err(ProjectContextError::new(
105 format!(
106 "Cargo.toml not found at workspace root '{}'. This is not a valid cargo project.",
107 workspace_toml_path.display()
108 ),
109 ErrorKind::ManifestNotFound,
110 None,
111 ));
112 }
113
114 let toml_str = std::fs::read_to_string(&workspace_toml_path).map_err(|e| {
115 ProjectContextError::new(
116 format!(
117 "Failed to read Cargo.toml at '{}': {}",
118 workspace_toml_path.display(),
119 e
120 ),
121 ErrorKind::Parsing,
122 Some(anyhow::anyhow!(e)),
123 )
124 })?;
125
126 let workspace_name = extract_workspace_name_from_toml(&toml_str).unwrap_or_else(|| {
127 workspace_root
128 .file_name()
129 .and_then(|name| name.to_str())
130 .unwrap_or("workspace")
131 .to_string()
132 });
133
134 Ok(WorkspaceInfo {
135 workspace_name,
136 workspace_root,
137 metadata,
138 })
139 }
140
141 pub fn get_ws_root(&self) -> PathBuf {
142 self.metadata.workspace_root.clone().into_std_path_buf()
143 }
144
145 pub fn get_manifest_path(&self) -> PathBuf {
146 self.workspace_root.join(PathBuf::from("Cargo.toml"))
147 }
148}
149
150impl ProjectContext {
151 pub fn load_workspace_info(manifest_path: &Path) -> Result<WorkspaceInfo, ProjectContextError> {
152 WorkspaceInfo::load_from_path(manifest_path)
153 }
154
155 pub fn load(manifest_path: &Path, burn_dir_name: &str) -> Result<Self, ProjectContextError> {
156 let workspace_info = WorkspaceInfo::load_from_path(manifest_path)?;
157 let burn_dir_root = workspace_info
158 .workspace_root
159 .join(PathBuf::from(burn_dir_name));
160 let burn_dir = BurnDir::new(burn_dir_root);
161 burn_dir.init().map_err(|e| {
162 ProjectContextError::new(
163 "Failed to initialize Burn directory".to_string(),
164 ErrorKind::BurnDirInitialization,
165 Some(e.into()),
166 )
167 })?;
168
169 let project = burn_dir
170 .load_project()
171 .map_err(|e| {
172 ProjectContextError::new(
173 "Failed to load project metadata from Burn directory".to_string(),
174 ErrorKind::BurnDirNotInitialized,
175 Some(e.into()),
176 )
177 })?
178 .ok_or_else(|| {
179 ProjectContextError::new(
180 "No Burn Central project linked to this repository".to_string(),
181 ErrorKind::BurnDirNotInitialized,
182 None,
183 )
184 })?;
185
186 Ok(Self {
187 workspace_info,
188 build_profile: "release".to_string(),
189 burn_dir,
190 project,
191 function_registry: Mutex::new(Default::default()),
192 })
193 }
194
195 pub fn init(
196 project: BurnCentralProject,
197 manifest_path: &Path,
198 burn_dir_name: &str,
199 ) -> Result<Self, ProjectContextError> {
200 let workspace_info = WorkspaceInfo::load_from_path(manifest_path)?;
201
202 let burn_dir_root = workspace_info
203 .workspace_root
204 .join(PathBuf::from(burn_dir_name));
205 let burn_dir = BurnDir::new(burn_dir_root);
206 burn_dir.init().map_err(|e| {
207 ProjectContextError::new(
208 "Failed to initialize Burn directory".to_string(),
209 ErrorKind::BurnDirInitialization,
210 Some(e.into()),
211 )
212 })?;
213
214 burn_dir.save_project(&project).map_err(|e| {
215 ProjectContextError::new(
216 "Failed to save project metadata to Burn directory".to_string(),
217 ErrorKind::BurnDirInitialization,
218 Some(e.into()),
219 )
220 })?;
221
222 Ok(Self {
223 workspace_info,
224 build_profile: "release".to_string(),
225 burn_dir,
226 project: project.clone(),
227 function_registry: Mutex::new(Default::default()),
228 })
229 }
230
231 pub fn unlink(manifest_path: &Path, burn_dir_name: &str) -> Result<(), ProjectContextError> {
232 let workspace_info = WorkspaceInfo::load_from_path(manifest_path)?;
233
234 let burn_dir_root = workspace_info
235 .workspace_root
236 .join(PathBuf::from(burn_dir_name));
237 let burn_dir = BurnDir::new(burn_dir_root);
238
239 std::fs::remove_dir_all(burn_dir.root()).map_err(|e| {
240 ProjectContextError::new(
241 "Failed to remove Burn directory".to_string(),
242 ErrorKind::Unexpected,
243 Some(e.into()),
244 )
245 })?;
246
247 Ok(())
248 }
249
250 pub fn get_project(&self) -> &BurnCentralProject {
251 &self.project
252 }
253
254 pub fn get_workspace_name(&self) -> &str {
255 &self.workspace_info.workspace_name
256 }
257
258 pub fn get_workspace_path(&self) -> &Path {
259 &self.workspace_info.workspace_root
260 }
261
262 pub fn get_workspace_root(&self) -> &Path {
263 &self.workspace_info.workspace_root
264 }
265
266 pub fn get_manifest_path(&self) -> PathBuf {
267 self.workspace_info.get_manifest_path()
268 }
269
270 pub fn burn_dir(&self) -> &BurnDir {
271 &self.burn_dir
272 }
273
274 pub fn cwd(&self) -> &Path {
275 &self.workspace_info.workspace_root
276 }
277
278 pub fn load_functions(
279 &self,
280 reporter: Option<Arc<dyn Reporter<DiscoveryEvent>>>,
281 ) -> Result<FunctionRegistry, DiscoveryError> {
282 let token = CancellationToken::new();
283 self.load_functions_cancellable(&token, reporter)
284 }
285
286 pub fn load_functions_cancellable(
287 &self,
288 cancel_token: &CancellationToken,
289 reporter: Option<Arc<dyn Reporter<DiscoveryEvent>>>,
290 ) -> Result<FunctionRegistry, DiscoveryError> {
291 let mut functions = self.function_registry.lock().unwrap();
292 if functions.is_empty() {
293 let workspace_pkgids = self.workspace_info.metadata.workspace_members.clone();
295 let workspace_packages: Vec<_> = self
296 .workspace_info
297 .metadata
298 .packages
299 .iter()
300 .filter(|pkg| workspace_pkgids.contains(&pkg.id))
301 .collect();
302
303 let pkgids = workspace_packages
304 .iter()
305 .filter(|pkg| {
306 pkg.targets
307 .iter()
308 .any(|t| t.kind.iter().any(|k| matches!(k, TargetKind::Lib)))
309 })
310 .map(|pkg| PkgId {
311 name: pkg.name.to_string(),
312 version: Some(pkg.version.to_string()),
313 })
314 .collect::<Vec<_>>();
315
316 let config = DiscoveryConfig {
317 packages: pkgids,
318 target_dir: Some(self.burn_dir.target_dir()),
319 };
320
321 let result = FunctionDiscovery::new(self.get_workspace_root()).discover_functions(
322 &config,
323 cancel_token,
324 reporter,
325 )?;
326
327 let mut registry = FunctionRegistry::new();
328 for (pkgid, funcs) in result.functions.into_iter() {
329 let package = workspace_packages
330 .iter()
331 .find(|pkg| pkg.name.as_str() == pkgid.name)
332 .expect("Discovered package should be in workspace");
333 registry
334 .get_or_create_package_entry((*package).clone())
335 .extend(funcs);
336 }
337
338 *functions = registry;
339 }
340 Ok(functions.clone())
341 }
342
343 pub fn get_workspace_packages(&self) -> Vec<&cargo_metadata::Package> {
344 self.workspace_info
345 .metadata
346 .packages
347 .iter()
348 .filter(|pkg| {
349 self.workspace_info
350 .metadata
351 .workspace_members
352 .contains(&pkg.id)
353 })
354 .collect()
355 }
356
357 pub fn find_package_by_name(&self, name: &str) -> Option<&cargo_metadata::Package> {
358 self.get_workspace_packages()
359 .into_iter()
360 .find(|pkg| pkg.name.as_str() == name)
361 }
362}
363
364fn extract_workspace_name_from_toml(toml_content: &str) -> Option<String> {
369 let workspace_document = toml_content.parse::<toml::Value>().ok()?;
370
371 workspace_document
372 .get("workspace")
373 .and_then(|ws| ws.get("package"))
374 .and_then(|pkg| pkg.get("name"))
375 .and_then(|name| name.as_str())
376 .or_else(|| {
377 workspace_document
378 .get("package")
379 .and_then(|pkg| pkg.get("name"))
380 .and_then(|name| name.as_str())
381 })
382 .map(|s| s.to_string())
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_extract_workspace_name_from_workspace_package() {
391 let toml_content = r#"
392[workspace]
393members = ["crate1", "crate2"]
394
395[workspace.package]
396name = "my-awesome-workspace"
397version = "0.1.0"
398"#;
399 let result = extract_workspace_name_from_toml(toml_content);
400 assert_eq!(result, Some("my-awesome-workspace".to_string()));
401 }
402
403 #[test]
404 fn test_extract_workspace_name_from_package() {
405 let toml_content = r#"
406[package]
407name = "single-crate-project"
408version = "0.1.0"
409"#;
410 let result = extract_workspace_name_from_toml(toml_content);
411 assert_eq!(result, Some("single-crate-project".to_string()));
412 }
413
414 #[test]
415 fn test_extract_workspace_name_prefers_workspace_over_package() {
416 let toml_content = r#"
417[workspace]
418members = ["crate1"]
419
420[workspace.package]
421name = "workspace-name"
422
423[package]
424name = "package-name"
425"#;
426 let result = extract_workspace_name_from_toml(toml_content);
427 assert_eq!(result, Some("workspace-name".to_string()));
428 }
429
430 #[test]
431 fn test_extract_workspace_name_returns_none_when_no_name() {
432 let toml_content = r#"
433[workspace]
434members = ["crate1", "crate2"]
435"#;
436 let result = extract_workspace_name_from_toml(toml_content);
437 assert_eq!(result, None);
438 }
439
440 #[test]
441 fn test_extract_workspace_name_returns_none_for_invalid_toml() {
442 let toml_content = "this is not valid toml { [ }";
443 let result = extract_workspace_name_from_toml(toml_content);
444 assert_eq!(result, None);
445 }
446
447 #[test]
448 fn test_extract_workspace_name_returns_none_for_empty_toml() {
449 let toml_content = "";
450 let result = extract_workspace_name_from_toml(toml_content);
451 assert_eq!(result, None);
452 }
453}