Skip to main content

pyro_artifacts/
environment.rs

1use crate::artifacts::{
2    Artifact, Artifacts, CapBinary, CapabilityBinary, CapabilitySource, Interface,
3};
4use crate::cache::{CacheError, CacheManager};
5use crate::cargo::{CapabilityIdent, ProjectManifest};
6use crate::debug::{self, CapabilityDebug, ModuleDebug};
7use crate::{
8    build::BuildError,
9    command::{CommandError, format_syn_error, run_command},
10};
11use pyro_macro::{ffi::generate_capability, module::generate_module};
12use pyro_spec::InterfaceSpec;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use thiserror::Error;
16use tokio::fs;
17use tokio::process::Command;
18
19#[derive(Error, Debug)]
20pub enum EnvironmentError {
21    #[error("IO error: {0}")]
22    Io(#[from] std::io::Error),
23
24    #[error("Cargo metadata failed: {0}")]
25    Metadata(String),
26
27    #[error(transparent)]
28    CommandError(#[from] CommandError),
29
30    #[error("Failed to parse or write: {0}")]
31    Serde(String),
32
33    #[error("Missing target directory in metadata")]
34    MissingTargetDir,
35
36    #[error("Artifact not found: {0}")]
37    ArtifactNotFound(PathBuf),
38
39    #[error("Utf8 error: {0}")]
40    Utf8(#[from] std::string::FromUtf8Error),
41
42    #[error("Failed to parse manifest: {0}")]
43    ParseManifest(String),
44
45    #[error("Interface generation failed: {0}")]
46    InterfaceGeneration(String),
47
48    #[error("Source not found: {0}")]
49    SourceNotFound(PathBuf),
50
51    #[error("Cache error: {0}")]
52    Cache(#[from] CacheError),
53
54    #[error("Build error: {0}")]
55    Build(#[from] BuildError),
56}
57
58impl From<serde_json::Error> for EnvironmentError {
59    fn from(value: serde_json::Error) -> Self {
60        Self::Serde(value.to_string())
61    }
62}
63
64pub type EnvResult<T> = std::result::Result<T, EnvironmentError>;
65
66/// Central context to manage cargo compilation environment
67pub struct Environment {
68    pub root: PathBuf,
69    pub target_dir: PathBuf,
70    pub manifest: crate::cargo::ProjectManifest,
71    pub cache_manager: Arc<CacheManager>,
72}
73
74impl Environment {
75    /// Create a new Environment by fetching metadata and detecting manifests from the given root
76    #[tracing::instrument(skip(root, cache_manager), fields(root = %root.display()))]
77    pub async fn new(root: PathBuf, cache_manager: Arc<CacheManager>) -> EnvResult<Self> {
78        tracing::debug!("Creating Environment instance");
79        let res = async {
80            let manifest = Self::load_manifest(&root).await?;
81            Self::ensure_cargo_toml(&root, &manifest, &cache_manager).await?;
82            let target_dir = Self::get_target_dir(&root).await?;
83            Ok(Self {
84                root,
85                target_dir,
86                manifest,
87                cache_manager,
88            })
89        }
90        .await;
91
92        if let Err(ref e) = res {
93            tracing::error!(error = ?e, "Failed to create Environment");
94        } else {
95            tracing::debug!("Environment successfully created");
96        }
97        res
98    }
99
100    /// Write Cargo.toml from Module.toml or Capability.toml if it doesn't exist
101    #[tracing::instrument(skip(root, manifest, cache_manager), fields(root = %root.display()))]
102    async fn ensure_cargo_toml(
103        root: &Path,
104        manifest: &crate::cargo::ProjectManifest,
105        cache_manager: &CacheManager,
106    ) -> EnvResult<()> {
107        let cargo_toml_path = root.join("Cargo.toml");
108        if cargo_toml_path.exists() {
109            tracing::debug!("Cargo.toml already exists, skipping generation");
110            return Ok(());
111        }
112        tracing::debug!("Cargo.toml not found, generating from Pyroduct manifest");
113        let contents =
114            toml::to_string_pretty(&manifest.clone().to_cargo_manifest(Some(cache_manager)))
115                .map_err(|e| {
116                    let err = EnvironmentError::ParseManifest(e.to_string());
117                    tracing::error!(error = ?err, "Failed to serialize generated Cargo.toml");
118                    err
119                })?;
120        fs::write(&cargo_toml_path, contents).await.map_err(|e| {
121            tracing::error!(error = ?e, "Failed to write generated Cargo.toml at {:?}", cargo_toml_path);
122            e
123        })?;
124        tracing::debug!("Successfully wrote Cargo.toml");
125        Ok(())
126    }
127
128    pub fn package(&self) -> String {
129        self.manifest.ident().package.clone()
130    }
131
132    pub fn version(&self) -> String {
133        self.manifest.ident().version.clone()
134    }
135
136    pub fn author(&self) -> String {
137        self.manifest.ident().author.clone()
138    }
139
140    /// Detect Capability.toml or Module.toml to extract name and version
141    #[tracing::instrument(skip(root), fields(root = %root.display()))]
142    async fn load_manifest(root: &Path) -> EnvResult<ProjectManifest> {
143        tracing::debug!("Loading manifest from {:?}", root);
144        let capability_toml = root.join("Capability.toml");
145        if capability_toml.exists() {
146            tracing::debug!("Detected Capability.toml");
147            let content = tokio::fs::read_to_string(&capability_toml).await?;
148            let manifest: crate::cargo::CapabilityManifest =
149                toml::from_str(&content).map_err(|e| {
150                    let err = EnvironmentError::ParseManifest(format!("Capability.toml: {}", e));
151                    tracing::error!(error = ?err, "Failed to parse Capability.toml");
152                    err
153                })?;
154            return Ok(crate::cargo::ProjectManifest::Capability(manifest));
155        }
156
157        let module_toml = root.join("Module.toml");
158        if module_toml.exists() {
159            tracing::debug!("Detected Module.toml");
160            let content = tokio::fs::read_to_string(&module_toml).await?;
161            let manifest: crate::cargo::ModuleManifest = toml::from_str(&content).map_err(|e| {
162                let err = EnvironmentError::ParseManifest(format!("Module.toml: {}", e));
163                tracing::error!(error = ?err, "Failed to parse Module.toml");
164                err
165            })?;
166            return Ok(crate::cargo::ProjectManifest::Module(manifest));
167        }
168
169        // Default for anon compilations or when no package section is found
170        let err = EnvironmentError::ParseManifest("No manifest found".to_string());
171        tracing::error!(error = ?err, "No Capability.toml or Module.toml manifest found in {:?}", root);
172        Err(err)
173    }
174
175    /// Run `cargo metadata` to find the target directory
176    #[tracing::instrument(skip(path), fields(path = %path.display()))]
177    pub async fn get_target_dir(path: &Path) -> EnvResult<PathBuf> {
178        tracing::debug!("Retrieving cargo metadata target directory");
179        let output = Command::new("cargo")
180            .args(["metadata", "--format-version=1", "--no-deps"])
181            .current_dir(path)
182            .output()
183            .await
184            .map_err(|e| {
185                tracing::error!(error = ?e, "Failed to launch cargo metadata");
186                e
187            })?;
188
189        if !output.status.success() {
190            let stderr_str = String::from_utf8_lossy(&output.stderr).to_string();
191            tracing::error!(stderr = %stderr_str, "Cargo metadata execution failed");
192            return Err(EnvironmentError::Metadata(stderr_str));
193        }
194
195        let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)?;
196
197        metadata["target_directory"]
198            .as_str()
199            .map(PathBuf::from)
200            .ok_or_else(|| {
201                tracing::error!("Missing target_directory in cargo metadata JSON");
202                EnvironmentError::MissingTargetDir
203            })
204    }
205
206    #[tracing::instrument(skip(self))]
207    pub async fn generate_lockfile(&self) -> EnvResult<String> {
208        tracing::debug!("Generating Cargo.lock file");
209        run_command(&self.root, &["generate-lockfile"], true)
210            .await
211            .map_err(|e| {
212                tracing::error!(error = ?e, "Failed to generate Cargo.lock");
213                EnvironmentError::CommandError(e)
214            })?;
215
216        let lockfile_path = self.root.join("Cargo.lock");
217        fs::read_to_string(&lockfile_path).await.map_err(|e| {
218            tracing::error!(error = ?e, "Failed to read generated Cargo.lock");
219            EnvironmentError::Io(e)
220        })
221    }
222
223    /// Compile the project (defaults to release)
224    #[tracing::instrument(skip(self))]
225    pub async fn compile(&self, extra_args: &[&str], capture: bool) -> EnvResult<()> {
226        tracing::debug!("Starting Cargo compilation in environment");
227        let mut args = vec!["build", "--release"];
228        args.extend_from_slice(extra_args);
229        run_command(&self.root, &args, capture).await.map_err(|e| {
230            tracing::error!(error = ?e, "Cargo compilation failed");
231            EnvironmentError::CommandError(e)
232        })?;
233        tracing::debug!("Cargo compilation completed successfully");
234        Ok(())
235    }
236
237    /// Get path to the compiled wasm artifact
238    pub fn get_wasm_artifact(&self, name: &str) -> EnvResult<PathBuf> {
239        let path = self
240            .target_dir
241            .join("wasm32-unknown-unknown")
242            .join("release")
243            .join(format!("{}.wasm", name.replace('-', "_")));
244
245        if path.exists() {
246            Ok(path)
247        } else {
248            Err(EnvironmentError::ArtifactNotFound(path))
249        }
250    }
251
252    /// Get path to the compiled library artifact (dylib/so/dll)
253    pub async fn get_library_artifact(&self, name: &str) -> EnvResult<CapBinary> {
254        let ext = dylib_extension();
255        let path =
256            self.target_dir
257                .join("release")
258                .join(format!("lib{}.{}", name.replace('-', "_"), ext));
259        if path.exists() {
260            match ext {
261                "dylib" => Ok(CapBinary::MachO(fs::read(&path).await?)),
262                "so" => Ok(CapBinary::Elf(fs::read(&path).await?)),
263                "dll" => Ok(CapBinary::Pe(fs::read(&path).await?)),
264                _ => Err(EnvironmentError::ArtifactNotFound(path)),
265            }
266        } else {
267            Err(EnvironmentError::ArtifactNotFound(path))
268        }
269    }
270
271    /// Load artifacts from an existing target directory without compiling
272    #[tracing::instrument(skip(self, target_dir), fields(target_dir = %target_dir.display()))]
273    pub async fn load_artifacts_from_target(&self, target_dir: &Path) -> EnvResult<Vec<Artifacts>> {
274        tracing::debug!("Loading artifacts from target directory");
275
276        let package = self.package();
277        let version = self.version();
278        let author = self.author();
279
280        let src_path = self.root.join("src").join("lib.rs");
281        let src_lib_rs = if src_path.exists() {
282            fs::read_to_string(&src_path).await?
283        } else {
284            String::new()
285        };
286
287        let res = async {
288            match &self.manifest {
289                crate::cargo::ProjectManifest::Capability(cap_manifest) => {
290                    let lib = self.get_library_artifact(&package).await.ok();
291
292                    let lock_path = self.root.join("Cargo.lock");
293                    let cargo_lock = if lock_path.exists() {
294                        fs::read_to_string(&lock_path).await?
295                    } else {
296                        String::new()
297                    };
298
299                    let (interface_rs, interface) =
300                        pyro_macro::ffi::generate_interface(&src_lib_rs, &package, &version)
301                            .map_err(|r| {
302                                EnvironmentError::InterfaceGeneration(format_syn_error(
303                                    &src_lib_rs,
304                                    r,
305                                ))
306                            })?;
307
308                    let interface_rs = prettyplease::unparse(&interface_rs);
309
310                    let mut artifacts = vec![
311                        Artifacts::CapabilitySource(CapabilitySource {
312                            manifest: cap_manifest.clone(),
313                            cargo_toml: toml::to_string_pretty(
314                                &cap_manifest.clone().to_capability_manifest(),
315                            )
316                            .map_err(|e| EnvironmentError::ParseManifest(e.to_string()))?,
317                            cargo_lock,
318                            src_lib_rs,
319                        }),
320                        Artifacts::Interface(Interface {
321                            manifest: cap_manifest.clone(),
322                            src_lib_rs: interface_rs,
323                            interface: interface.clone(),
324                        }),
325                    ];
326
327                    if let Some(lib) = lib {
328                        artifacts.push(Artifacts::CapabilityBinary(CapabilityBinary {
329                            ident: CapabilityIdent {
330                                package,
331                                version,
332                                author,
333                            },
334                            libs: vec![lib],
335                            interface: interface.clone(),
336                        }));
337                    }
338
339                    Ok(artifacts)
340                }
341                crate::cargo::ProjectManifest::Module(module_manifest) => {
342                    let wasm_path = self.get_wasm_artifact(&package).ok();
343
344                    let source = crate::artifacts::PlaybookSource {
345                        manifest: module_manifest.clone(),
346                        source: src_lib_rs.clone(),
347                    };
348                    let hash = source.hash();
349
350                    let mut artifacts = vec![Artifacts::Playbook(
351                        crate::artifacts::Playbook::Source(source.clone()),
352                    )];
353
354                    if let Some(path) = wasm_path {
355                        let mut dep_interfaces = Vec::new();
356                        for cap in source.dependencies().capabilities.iter() {
357                            if let Ok(spec_str) = self
358                                .cache_manager
359                                .capability_interface_spec(&cap.author, &cap.package, &cap.version)
360                                .await
361                            {
362                                if let Ok(spec) = serde_json::from_str::<pyro_spec::InterfaceSpec>(&spec_str) {
363                                    dep_interfaces.push(spec);
364                                }
365                            }
366                        }
367
368                        let spec = pyro_macro::module::generate_module_spec(&src_lib_rs, &dep_interfaces)
369                            .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
370                            .map(|func| crate::artifacts::PlaybookSpec {
371                                ident: source.ident(),
372                                hash,
373                                func,
374                                capabilities: source.dependencies().capabilities,
375                                interconnect: source.manifest.interconnect.clone(),
376                            })
377                            .ok_or_else(|| {
378                                EnvironmentError::InterfaceGeneration(
379                                    "Module main function missing".to_string(),
380                                )
381                            })?;
382
383                        let binary = crate::artifacts::PlaybookBinary {
384                            wasm: fs::read(path).await?,
385                            spec,
386                            configurations: source.configurations(),
387                        };
388                        artifacts.push(Artifacts::Playbook(crate::artifacts::Playbook::Binary(
389                            binary,
390                        )));
391                    }
392
393                    Ok(artifacts)
394                }
395            }
396        }
397        .await;
398
399        if let Err(ref e) = res {
400            tracing::error!(error = ?e, "Failed to load artifacts from target directory");
401        } else {
402            tracing::debug!("Successfully loaded artifacts from target directory");
403        }
404        res
405    }
406
407    #[tracing::instrument(skip(self))]
408    pub async fn pack(&self, capture: bool) -> EnvResult<Vec<Artifacts>> {
409        tracing::debug!("Packaging project artifacts");
410        let package = self.package();
411        let version = self.version();
412        let author = self.author();
413
414        let res = async {
415            match &self.manifest {
416                crate::cargo::ProjectManifest::Capability(cap_manifest) => {
417                    tracing::debug!(dir = ?self.root, "Packaging capability");
418
419                    let cargo_toml =
420                        toml::to_string_pretty(&cap_manifest.clone().to_capability_manifest())
421                            .map_err(|e| EnvironmentError::ParseManifest(e.to_string()))?;
422
423                    tracing::info!(dir = ?self.root, "Compiling capability binary...");
424                    self.compile(&["--features", "capability", "-p", &package], capture)
425                        .await?;
426
427                    let lib = self.get_library_artifact(&package).await?;
428
429                    let lock_path = self.root.join("Cargo.lock");
430                    let cargo_lock = if lock_path.exists() {
431                        fs::read_to_string(&lock_path).await?
432                    } else {
433                        String::new()
434                    };
435
436                    let src_path = self.root.join("src").join("lib.rs");
437                    let src_lib_rs = if src_path.exists() {
438                        fs::read_to_string(&src_path).await?
439                    } else {
440                        String::new()
441                    };
442
443                    tracing::debug!(dir = ?self.root, "Generating interface for capability...");
444                    let (interface_rs, interface) =
445                        pyro_macro::ffi::generate_interface(&src_lib_rs, &package, &version)
446                            .map_err(|r| {
447                                EnvironmentError::InterfaceGeneration(format_syn_error(
448                                    &src_lib_rs,
449                                    r,
450                                ))
451                            })?;
452
453                    let interface_rs = prettyplease::unparse(&interface_rs);
454
455                    Ok(vec![
456                        Artifacts::CapabilitySource(CapabilitySource {
457                            manifest: cap_manifest.clone(),
458                            cargo_toml,
459                            cargo_lock,
460                            src_lib_rs,
461                        }),
462                        Artifacts::CapabilityBinary(CapabilityBinary {
463                            ident: CapabilityIdent {
464                                package,
465                                version,
466                                author,
467                            },
468                            libs: vec![lib],
469                            interface: interface.clone(),
470                        }),
471                        Artifacts::Interface(Interface {
472                            manifest: cap_manifest.clone(),
473                            src_lib_rs: interface_rs,
474                            interface: interface.clone(),
475                        }),
476                    ])
477                }
478                crate::cargo::ProjectManifest::Module(module_manifest) => {
479                    tracing::debug!("Packaging module: {:?}", self.root);
480
481                    tracing::info!("Compiling module binary...");
482                    self.compile(&["-p", &package], capture).await?;
483
484                    let wasm_artifact = self.get_wasm_artifact(&package)?;
485
486                    let src_path = self.root.join("src").join("lib.rs");
487                    let src_lib_rs = if src_path.exists() {
488                        fs::read_to_string(&src_path).await?
489                    } else {
490                        String::new()
491                    };
492
493                    let source = crate::artifacts::PlaybookSource {
494                        manifest: module_manifest.clone(),
495                        source: src_lib_rs.clone(),
496                    };
497                    let hash = source.hash();
498
499                    let mut dep_interfaces = Vec::new();
500                    for cap in source.dependencies().capabilities.iter() {
501                        if let Ok(spec_str) = self
502                            .cache_manager
503                            .capability_interface_spec(&cap.author, &cap.package, &cap.version)
504                            .await
505                        {
506                            if let Ok(spec) = serde_json::from_str::<pyro_spec::InterfaceSpec>(&spec_str) {
507                                dep_interfaces.push(spec);
508                            }
509                        }
510                    }
511
512                    let spec = pyro_macro::module::generate_module_spec(&src_lib_rs, &dep_interfaces)
513                        .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
514                        .map(|func| crate::artifacts::PlaybookSpec {
515                            ident: source.ident(),
516                            hash,
517                            func,
518                            capabilities: source.dependencies().capabilities,
519                            interconnect: source.manifest.interconnect.clone(),
520                        })
521                        .ok_or_else(|| {
522                            EnvironmentError::InterfaceGeneration(
523                                "Module main function missing".to_string(),
524                            )
525                        })?;
526
527                    let binary = crate::artifacts::PlaybookBinary {
528                        wasm: fs::read(wasm_artifact).await?,
529                        spec,
530                        configurations: source.configurations(),
531                    };
532
533                    Ok(vec![
534                        Artifacts::Playbook(crate::artifacts::Playbook::Source(source)),
535                        Artifacts::Playbook(crate::artifacts::Playbook::Binary(binary)),
536                    ])
537                }
538            }
539        }
540        .await;
541
542        if let Err(ref e) = res {
543            tracing::error!(error = ?e, "Failed to package project artifacts");
544        } else {
545            tracing::debug!("Successfully packaged project artifacts");
546        }
547        res
548    }
549
550    #[tracing::instrument(skip(self))]
551    pub async fn expand_debug(&self) -> EnvResult<()> {
552        let debug_dir = self.root.join("debug");
553        fs::create_dir_all(&debug_dir).await?;
554
555        tracing::debug!("Generating expanded debug files");
556        let res = async {
557            match &self.manifest {
558                crate::cargo::ProjectManifest::Capability(cap_manifest) => {
559                    tracing::debug!("Generating debug info for capability: {}", self.package());
560                    let package = self.package();
561                    let version = self.version();
562
563                    let lib = self.get_library_artifact(&package).await?;
564
565                    let src_path = self.root.join("src").join("lib.rs");
566                    let src_lib_rs = fs::read_to_string(&src_path)
567                        .await
568                        .map_err(|_| EnvironmentError::SourceNotFound(src_path))?;
569
570                    let (_, interface) =
571                        pyro_macro::ffi::generate_interface(&src_lib_rs, &package, &version)
572                            .map_err(|r| {
573                                EnvironmentError::InterfaceGeneration(format_syn_error(
574                                    &src_lib_rs,
575                                    r,
576                                ))
577                            })?;
578
579                    let binary = CapabilityBinary {
580                        ident: cap_manifest.capability.clone(),
581                        libs: vec![lib],
582                        interface,
583                    };
584
585                    let symbols = debug::symbols(&binary);
586
587                    let code = generate_capability(&src_lib_rs, &package, &version)
588                        .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?;
589
590                    let cap_rs = Some(prettyplease::unparse(&code));
591                    let debug_info = CapabilityDebug { symbols, cap_rs };
592                    debug_info.write_to_directory(&debug_dir).await?;
593                }
594                crate::cargo::ProjectManifest::Module(module_manifest) => {
595                    tracing::debug!("Generating debug info for module: {}", self.package());
596                    let package = self.package();
597
598                    let wasm_path = self.get_wasm_artifact(&package)?;
599                    let wasm_bytes = fs::read(wasm_path).await?;
600
601                    let src_path = self.root.join("src").join("lib.rs");
602                    let src_lib_rs = fs::read_to_string(&src_path)
603                        .await
604                        .map_err(|_| EnvironmentError::SourceNotFound(src_path))?;
605
606                    let mut resolved_capabilities = Vec::new();
607                    let mut dep_interfaces = Vec::new();
608                    for cap in module_manifest.capabilities.values() {
609                        let cap_ident = CapabilityIdent {
610                            author: cap.author.clone(),
611                            package: cap.package.clone(),
612                            version: cap.version.clone(),
613                        };
614                        if let Ok(spec_str) = self
615                            .cache_manager
616                            .capability_interface_spec(&cap_ident.author, &cap_ident.package, &cap_ident.version)
617                            .await
618                        {
619                            if let Ok(spec) = serde_json::from_str::<pyro_spec::InterfaceSpec>(&spec_str) {
620                                dep_interfaces.push(spec);
621                            }
622                        }
623                        resolved_capabilities.push(cap_ident);
624                    }
625
626                    let spec = pyro_macro::module::generate_module_spec(&src_lib_rs, &dep_interfaces)
627                        .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
628                        .ok_or_else(|| {
629                            EnvironmentError::InterfaceGeneration(
630                                "Module main function missing".to_string(),
631                            )
632                        })?;
633
634                    let dummy_ident = crate::artifacts::PlaybookIdent {
635                        author: "dummy".to_string(),
636                        package: "dummy".to_string(),
637                        version: "0.0.0".to_string(),
638                    };
639                    let source = crate::artifacts::PlaybookSource::new(
640                        dummy_ident.clone(),
641                        crate::artifacts::ModuleDependencies {
642                            dependencies: module_manifest.dependencies.clone(),
643                            capabilities: resolved_capabilities.clone(),
644                        },
645                        std::vec::Vec::new(),
646                        src_lib_rs.clone(),
647                        module_manifest.interconnect.clone(),
648                    );
649                    let hash = source.hash();
650
651                    let binary = crate::artifacts::PlaybookBinary {
652                        wasm: wasm_bytes,
653                        spec: crate::artifacts::PlaybookSpec {
654                            ident: dummy_ident,
655                            hash,
656                            func: spec,
657                            capabilities: vec![], // Capabilities not needed for WAT/RS generation
658                            interconnect: std::collections::BTreeMap::new(),
659                        },
660                        configurations: std::vec::Vec::new(),
661                    };
662
663                    let wat = debug::wat(&binary).map_err(EnvironmentError::InterfaceGeneration)?;
664
665                    let generated_code = generate_module(&src_lib_rs).map_err(|e| {
666                        EnvironmentError::InterfaceGeneration(format!(
667                            "Module code generation error: {}",
668                            e
669                        ))
670                    })?;
671                    let cap_rs = Some(prettyplease::unparse(&generated_code));
672
673                    let debug_info = ModuleDebug {
674                        wat: Some(wat),
675                        cap_rs,
676                    };
677                    debug_info.write_to_directory(&debug_dir).await?;
678                }
679            }
680            Ok(())
681        }
682        .await;
683
684        if let Err(ref e) = res {
685            tracing::error!(error = ?e, "Failed to generate expanded debug files");
686        } else {
687            tracing::debug!("Successfully generated expanded debug files");
688        }
689        res
690    }
691
692    #[tracing::instrument(skip(self))]
693    pub async fn capability_spec(&self) -> EnvResult<InterfaceSpec<'static>> {
694        tracing::debug!("Resolving capability spec");
695        let package = self.package();
696        let version = self.version();
697        let src_path = self.root.join("src").join("lib.rs");
698        let src_lib_rs = fs::read_to_string(&src_path).await.map_err(|_| {
699            let err = EnvironmentError::SourceNotFound(src_path.clone());
700            tracing::error!(error = ?err, "Failed to read src/lib.rs for capability spec");
701            err
702        })?;
703
704        let res = match &self.manifest {
705            crate::cargo::ProjectManifest::Capability(_) => {
706                let (_, interface) = pyro_macro::ffi::generate_interface(
707                    &src_lib_rs,
708                    &package,
709                    &version,
710                )
711                .map_err(|r| {
712                    let err =
713                        EnvironmentError::InterfaceGeneration(format_syn_error(&src_lib_rs, r));
714                    tracing::error!(error = ?err, "Failed to generate interface spec");
715                    err
716                })?;
717                Ok(interface)
718            }
719            crate::cargo::ProjectManifest::Module(_) => {
720                let err = EnvironmentError::InterfaceGeneration(
721                    "Capability spec is only supported for capabilities".to_string(),
722                );
723                tracing::error!(error = ?err, "Invalid manifest type for capability spec");
724                Err(err)
725            }
726        };
727        res
728    }
729
730    #[tracing::instrument(skip(self))]
731    pub async fn playbook_spec(&self) -> EnvResult<crate::artifacts::PlaybookSpec> {
732        tracing::debug!("Resolving playbook spec");
733        let src_path = self.root.join("src").join("lib.rs");
734        let src_lib_rs = fs::read_to_string(&src_path).await.map_err(|_| {
735            let err = EnvironmentError::SourceNotFound(src_path.clone());
736            tracing::error!(error = ?err, "Failed to read src/lib.rs for playbook spec");
737            err
738        })?;
739
740        let res = match &self.manifest {
741            crate::cargo::ProjectManifest::Module(module_manifest) => {
742                let mut resolved_capabilities = Vec::new();
743                let mut dep_interfaces = Vec::new();
744                for cap in module_manifest.capabilities.values() {
745                    let cap_ident = CapabilityIdent {
746                        author: cap.author.clone(),
747                        package: cap.package.clone(),
748                        version: cap.version.clone(),
749                    };
750                    if let Ok(spec_str) = self
751                        .cache_manager
752                        .capability_interface_spec(&cap_ident.author, &cap_ident.package, &cap_ident.version)
753                        .await
754                    {
755                        if let Ok(spec) = serde_json::from_str::<pyro_spec::InterfaceSpec>(&spec_str) {
756                            dep_interfaces.push(spec);
757                        }
758                    }
759                    resolved_capabilities.push(cap_ident);
760                }
761
762                let ident = crate::artifacts::PlaybookIdent {
763                    author: self.author(),
764                    package: self.package(),
765                    version: self.version(),
766                };
767                let source = crate::artifacts::PlaybookSource::new(
768                    ident.clone(),
769                    crate::artifacts::ModuleDependencies {
770                        dependencies: module_manifest.dependencies.clone(),
771                        capabilities: resolved_capabilities.clone(),
772                    },
773                    std::vec::Vec::new(),
774                    src_lib_rs.clone(),
775                    module_manifest.interconnect.clone(),
776                );
777                let hash = source.hash();
778
779                pyro_macro::module::generate_module_spec(&src_lib_rs, &dep_interfaces)
780                    .map_err(|e| {
781                        let err = EnvironmentError::InterfaceGeneration(e.to_string());
782                        tracing::error!(error = ?err, "Failed to generate playbook spec");
783                        err
784                    })?
785                    .map(|func| crate::artifacts::PlaybookSpec {
786                        ident,
787                        hash,
788                        func,
789                        capabilities: resolved_capabilities,
790                        interconnect: module_manifest.interconnect.clone(),
791                    })
792                    .ok_or_else(|| {
793                        let err = EnvironmentError::InterfaceGeneration(
794                            "Module main function missing".to_string(),
795                        );
796                        tracing::error!(error = ?err, "Playbook spec missing main function");
797                        err
798                    })
799            }
800            crate::cargo::ProjectManifest::Capability(_) => {
801                let err = EnvironmentError::InterfaceGeneration(
802                    "Module spec is only supported for modules".to_string(),
803                );
804                tracing::error!(error = ?err, "Invalid manifest type for playbook spec");
805                Err(err)
806            }
807        };
808        res
809    }
810}
811
812pub fn dylib_extension() -> &'static str {
813    if cfg!(target_os = "macos") {
814        "dylib"
815    } else if cfg!(target_os = "windows") {
816        "dll"
817    } else {
818        "so"
819    }
820}