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        if matches!(self.manifest, ProjectManifest::Module(_)) {
230            args.push("--target");
231            args.push("wasm32-unknown-unknown");
232        }
233        run_command(&self.root, &args, capture).await.map_err(|e| {
234            tracing::error!(error = ?e, "Cargo compilation failed");
235            EnvironmentError::CommandError(e)
236        })?;
237        tracing::debug!("Cargo compilation completed successfully");
238        Ok(())
239    }
240
241    /// Get path to the compiled wasm artifact
242    pub fn get_wasm_artifact(&self, name: &str) -> EnvResult<PathBuf> {
243        let path = self
244            .target_dir
245            .join("wasm32-unknown-unknown")
246            .join("release")
247            .join(format!("{}.wasm", name.replace('-', "_")));
248
249        if path.exists() {
250            Ok(path)
251        } else {
252            Err(EnvironmentError::ArtifactNotFound(path))
253        }
254    }
255
256    /// Get path to the compiled library artifact (dylib/so/dll)
257    pub async fn get_library_artifact(&self, name: &str) -> EnvResult<CapBinary> {
258        let ext = dylib_extension();
259        let path =
260            self.target_dir
261                .join("release")
262                .join(format!("lib{}.{}", name.replace('-', "_"), ext));
263        if path.exists() {
264            match ext {
265                "dylib" => Ok(CapBinary::MachO(fs::read(&path).await?)),
266                "so" => Ok(CapBinary::Elf(fs::read(&path).await?)),
267                "dll" => Ok(CapBinary::Pe(fs::read(&path).await?)),
268                _ => Err(EnvironmentError::ArtifactNotFound(path)),
269            }
270        } else {
271            Err(EnvironmentError::ArtifactNotFound(path))
272        }
273    }
274
275    /// Load artifacts from an existing target directory without compiling
276    #[tracing::instrument(skip(self, target_dir), fields(target_dir = %target_dir.display()))]
277    pub async fn load_artifacts_from_target(&self, target_dir: &Path) -> EnvResult<Vec<Artifacts>> {
278        tracing::debug!("Loading artifacts from target directory");
279
280        let package = self.package();
281        let version = self.version();
282        let author = self.author();
283
284        let src_path = self.root.join("src").join("lib.rs");
285        let src_lib_rs = if src_path.exists() {
286            fs::read_to_string(&src_path).await?
287        } else {
288            String::new()
289        };
290
291        let res = async {
292            match &self.manifest {
293                crate::cargo::ProjectManifest::Capability(cap_manifest) => {
294                    let lib = self.get_library_artifact(&package).await.ok();
295
296                    let lock_path = self.root.join("Cargo.lock");
297                    let cargo_lock = if lock_path.exists() {
298                        fs::read_to_string(&lock_path).await?
299                    } else {
300                        String::new()
301                    };
302
303                    let (interface_rs, interface) =
304                        pyro_macro::ffi::generate_interface(&src_lib_rs, &package, &version)
305                            .map_err(|r| {
306                                EnvironmentError::InterfaceGeneration(format_syn_error(
307                                    &src_lib_rs,
308                                    r,
309                                ))
310                            })?;
311
312                    let interface_rs = prettyplease::unparse(&interface_rs);
313
314                    let mut artifacts = vec![
315                        Artifacts::CapabilitySource(CapabilitySource {
316                            manifest: cap_manifest.clone(),
317                            cargo_toml: toml::to_string_pretty(
318                                &cap_manifest.clone().to_capability_manifest(),
319                            )
320                            .map_err(|e| EnvironmentError::ParseManifest(e.to_string()))?,
321                            cargo_lock,
322                            src_lib_rs,
323                        }),
324                        Artifacts::Interface(Interface {
325                            manifest: cap_manifest.clone(),
326                            src_lib_rs: interface_rs,
327                            interface: interface.clone(),
328                        }),
329                    ];
330
331                    if let Some(lib) = lib {
332                        artifacts.push(Artifacts::CapabilityBinary(CapabilityBinary {
333                            ident: CapabilityIdent {
334                                package,
335                                version,
336                                author,
337                            },
338                            libs: vec![lib],
339                            interface: interface.clone(),
340                        }));
341                    }
342
343                    Ok(artifacts)
344                }
345                crate::cargo::ProjectManifest::Module(module_manifest) => {
346                    let wasm_path = self.get_wasm_artifact(&package).ok();
347
348                    let source = crate::artifacts::PlaybookSource {
349                        manifest: module_manifest.clone(),
350                        source: src_lib_rs.clone(),
351                    };
352                    let hash = source.hash();
353
354                    let mut artifacts = vec![Artifacts::Playbook(
355                        crate::artifacts::Playbook::Source(source.clone()),
356                    )];
357
358                    if let Some(path) = wasm_path {
359                        let mut dep_interfaces = Vec::new();
360                        for cap in source.dependencies().capabilities.iter() {
361                            if let Ok(spec_str) = self
362                                .cache_manager
363                                .capability_interface_spec(&cap.author, &cap.package, &cap.version)
364                                .await
365                            {
366                                if let Ok(spec) =
367                                    serde_json::from_str::<pyro_spec::InterfaceSpec>(&spec_str)
368                                {
369                                    dep_interfaces.push(spec);
370                                }
371                            }
372                        }
373
374                        let spec =
375                            pyro_macro::module::generate_module_spec(&src_lib_rs, &dep_interfaces)
376                                .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
377                                .map(|func| crate::artifacts::PlaybookSpec {
378                                    ident: source.ident(),
379                                    hash,
380                                    func,
381                                    capabilities: source.dependencies().capabilities,
382                                    interconnect: source.manifest.interconnect.clone(),
383                                })
384                                .ok_or_else(|| {
385                                    EnvironmentError::InterfaceGeneration(
386                                        "Module main function missing".to_string(),
387                                    )
388                                })?;
389
390                        let binary = crate::artifacts::PlaybookBinary {
391                            wasm: fs::read(path).await?,
392                            spec,
393                            configurations: source.configurations(),
394                        };
395                        artifacts.push(Artifacts::Playbook(crate::artifacts::Playbook::Binary(
396                            binary,
397                        )));
398                    }
399
400                    Ok(artifacts)
401                }
402            }
403        }
404        .await;
405
406        if let Err(ref e) = res {
407            tracing::error!(error = ?e, "Failed to load artifacts from target directory");
408        } else {
409            tracing::debug!("Successfully loaded artifacts from target directory");
410        }
411        res
412    }
413
414    #[tracing::instrument(skip(self))]
415    pub async fn pack(&self, capture: bool) -> EnvResult<Vec<Artifacts>> {
416        tracing::debug!("Packaging project artifacts");
417        let package = self.package();
418        let version = self.version();
419        let author = self.author();
420
421        let res = async {
422            match &self.manifest {
423                crate::cargo::ProjectManifest::Capability(cap_manifest) => {
424                    tracing::debug!(dir = ?self.root, "Packaging capability");
425
426                    let cargo_toml =
427                        toml::to_string_pretty(&cap_manifest.clone().to_capability_manifest())
428                            .map_err(|e| EnvironmentError::ParseManifest(e.to_string()))?;
429
430                    tracing::info!(dir = ?self.root, "Compiling capability binary...");
431                    self.compile(&["--features", "capability", "-p", &package], capture)
432                        .await?;
433
434                    let lib = self.get_library_artifact(&package).await?;
435
436                    let lock_path = self.root.join("Cargo.lock");
437                    let cargo_lock = if lock_path.exists() {
438                        fs::read_to_string(&lock_path).await?
439                    } else {
440                        String::new()
441                    };
442
443                    let src_path = self.root.join("src").join("lib.rs");
444                    let src_lib_rs = if src_path.exists() {
445                        fs::read_to_string(&src_path).await?
446                    } else {
447                        String::new()
448                    };
449
450                    tracing::debug!(dir = ?self.root, "Generating interface for capability...");
451                    let (interface_rs, interface) =
452                        pyro_macro::ffi::generate_interface(&src_lib_rs, &package, &version)
453                            .map_err(|r| {
454                                EnvironmentError::InterfaceGeneration(format_syn_error(
455                                    &src_lib_rs,
456                                    r,
457                                ))
458                            })?;
459
460                    let interface_rs = prettyplease::unparse(&interface_rs);
461
462                    Ok(vec![
463                        Artifacts::CapabilitySource(CapabilitySource {
464                            manifest: cap_manifest.clone(),
465                            cargo_toml,
466                            cargo_lock,
467                            src_lib_rs,
468                        }),
469                        Artifacts::CapabilityBinary(CapabilityBinary {
470                            ident: CapabilityIdent {
471                                package,
472                                version,
473                                author,
474                            },
475                            libs: vec![lib],
476                            interface: interface.clone(),
477                        }),
478                        Artifacts::Interface(Interface {
479                            manifest: cap_manifest.clone(),
480                            src_lib_rs: interface_rs,
481                            interface: interface.clone(),
482                        }),
483                    ])
484                }
485                crate::cargo::ProjectManifest::Module(module_manifest) => {
486                    tracing::debug!("Packaging module: {:?}", self.root);
487
488                    tracing::info!("Compiling module binary...");
489                    self.compile(&["-p", &package], capture).await?;
490
491                    let wasm_artifact = self.get_wasm_artifact(&package)?;
492
493                    let src_path = self.root.join("src").join("lib.rs");
494                    let src_lib_rs = if src_path.exists() {
495                        fs::read_to_string(&src_path).await?
496                    } else {
497                        String::new()
498                    };
499
500                    let source = crate::artifacts::PlaybookSource {
501                        manifest: module_manifest.clone(),
502                        source: src_lib_rs.clone(),
503                    };
504                    let hash = source.hash();
505
506                    let mut dep_interfaces = Vec::new();
507                    for cap in source.dependencies().capabilities.iter() {
508                        if let Ok(spec_str) = self
509                            .cache_manager
510                            .capability_interface_spec(&cap.author, &cap.package, &cap.version)
511                            .await
512                        {
513                            if let Ok(spec) =
514                                serde_json::from_str::<pyro_spec::InterfaceSpec>(&spec_str)
515                            {
516                                dep_interfaces.push(spec);
517                            }
518                        }
519                    }
520
521                    let spec =
522                        pyro_macro::module::generate_module_spec(&src_lib_rs, &dep_interfaces)
523                            .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
524                            .map(|func| crate::artifacts::PlaybookSpec {
525                                ident: source.ident(),
526                                hash,
527                                func,
528                                capabilities: source.dependencies().capabilities,
529                                interconnect: source.manifest.interconnect.clone(),
530                            })
531                            .ok_or_else(|| {
532                                EnvironmentError::InterfaceGeneration(
533                                    "Module main function missing".to_string(),
534                                )
535                            })?;
536
537                    let binary = crate::artifacts::PlaybookBinary {
538                        wasm: fs::read(wasm_artifact).await?,
539                        spec,
540                        configurations: source.configurations(),
541                    };
542
543                    Ok(vec![
544                        Artifacts::Playbook(crate::artifacts::Playbook::Source(source)),
545                        Artifacts::Playbook(crate::artifacts::Playbook::Binary(binary)),
546                    ])
547                }
548            }
549        }
550        .await;
551
552        if let Err(ref e) = res {
553            tracing::error!(error = ?e, "Failed to package project artifacts");
554        } else {
555            tracing::debug!("Successfully packaged project artifacts");
556        }
557        res
558    }
559
560    #[tracing::instrument(skip(self))]
561    pub async fn expand_debug(&self) -> EnvResult<()> {
562        let debug_dir = self.root.join("debug");
563        fs::create_dir_all(&debug_dir).await?;
564
565        tracing::debug!("Generating expanded debug files");
566        let res = async {
567            match &self.manifest {
568                crate::cargo::ProjectManifest::Capability(cap_manifest) => {
569                    tracing::debug!("Generating debug info for capability: {}", self.package());
570                    let package = self.package();
571                    let version = self.version();
572
573                    let lib = self.get_library_artifact(&package).await?;
574
575                    let src_path = self.root.join("src").join("lib.rs");
576                    let src_lib_rs = fs::read_to_string(&src_path)
577                        .await
578                        .map_err(|_| EnvironmentError::SourceNotFound(src_path))?;
579
580                    let (_, interface) =
581                        pyro_macro::ffi::generate_interface(&src_lib_rs, &package, &version)
582                            .map_err(|r| {
583                                EnvironmentError::InterfaceGeneration(format_syn_error(
584                                    &src_lib_rs,
585                                    r,
586                                ))
587                            })?;
588
589                    let binary = CapabilityBinary {
590                        ident: cap_manifest.capability.clone(),
591                        libs: vec![lib],
592                        interface,
593                    };
594
595                    let symbols = debug::symbols(&binary);
596
597                    let code = generate_capability(&src_lib_rs, &package, &version)
598                        .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?;
599
600                    let cap_rs = Some(prettyplease::unparse(&code));
601                    let debug_info = CapabilityDebug { symbols, cap_rs };
602                    debug_info.write_to_directory(&debug_dir).await?;
603                }
604                crate::cargo::ProjectManifest::Module(module_manifest) => {
605                    tracing::debug!("Generating debug info for module: {}", self.package());
606                    let package = self.package();
607
608                    let wasm_path = self.get_wasm_artifact(&package)?;
609                    let wasm_bytes = fs::read(wasm_path).await?;
610
611                    let src_path = self.root.join("src").join("lib.rs");
612                    let src_lib_rs = fs::read_to_string(&src_path)
613                        .await
614                        .map_err(|_| EnvironmentError::SourceNotFound(src_path))?;
615
616                    let mut resolved_capabilities = Vec::new();
617                    let mut dep_interfaces = Vec::new();
618                    for cap in module_manifest.capabilities.values() {
619                        let cap_ident = CapabilityIdent {
620                            author: cap.author.clone(),
621                            package: cap.package.clone(),
622                            version: cap.version.clone(),
623                        };
624                        if let Ok(spec_str) = self
625                            .cache_manager
626                            .capability_interface_spec(
627                                &cap_ident.author,
628                                &cap_ident.package,
629                                &cap_ident.version,
630                            )
631                            .await
632                        {
633                            if let Ok(spec) =
634                                serde_json::from_str::<pyro_spec::InterfaceSpec>(&spec_str)
635                            {
636                                dep_interfaces.push(spec);
637                            }
638                        }
639                        resolved_capabilities.push(cap_ident);
640                    }
641
642                    let spec =
643                        pyro_macro::module::generate_module_spec(&src_lib_rs, &dep_interfaces)
644                            .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
645                            .ok_or_else(|| {
646                                EnvironmentError::InterfaceGeneration(
647                                    "Module main function missing".to_string(),
648                                )
649                            })?;
650
651                    let dummy_ident = crate::artifacts::PlaybookIdent {
652                        author: "dummy".to_string(),
653                        package: "dummy".to_string(),
654                        version: "0.0.0".to_string(),
655                    };
656                    let source = crate::artifacts::PlaybookSource::new(
657                        dummy_ident.clone(),
658                        crate::artifacts::ModuleDependencies {
659                            dependencies: module_manifest.dependencies.clone(),
660                            capabilities: resolved_capabilities.clone(),
661                        },
662                        std::vec::Vec::new(),
663                        src_lib_rs.clone(),
664                        module_manifest.interconnect.clone(),
665                    );
666                    let hash = source.hash();
667
668                    let binary = crate::artifacts::PlaybookBinary {
669                        wasm: wasm_bytes,
670                        spec: crate::artifacts::PlaybookSpec {
671                            ident: dummy_ident,
672                            hash,
673                            func: spec,
674                            capabilities: vec![], // Capabilities not needed for WAT/RS generation
675                            interconnect: std::collections::BTreeMap::new(),
676                        },
677                        configurations: std::vec::Vec::new(),
678                    };
679
680                    let wat = debug::wat(&binary).map_err(EnvironmentError::InterfaceGeneration)?;
681
682                    let generated_code = generate_module(&src_lib_rs).map_err(|e| {
683                        EnvironmentError::InterfaceGeneration(format!(
684                            "Module code generation error: {}",
685                            e
686                        ))
687                    })?;
688                    let cap_rs = Some(prettyplease::unparse(&generated_code));
689
690                    let debug_info = ModuleDebug {
691                        wat: Some(wat),
692                        cap_rs,
693                    };
694                    debug_info.write_to_directory(&debug_dir).await?;
695                }
696            }
697            Ok(())
698        }
699        .await;
700
701        if let Err(ref e) = res {
702            tracing::error!(error = ?e, "Failed to generate expanded debug files");
703        } else {
704            tracing::debug!("Successfully generated expanded debug files");
705        }
706        res
707    }
708
709    #[tracing::instrument(skip(self))]
710    pub async fn capability_spec(&self) -> EnvResult<InterfaceSpec<'static>> {
711        tracing::debug!("Resolving capability spec");
712        let package = self.package();
713        let version = self.version();
714        let src_path = self.root.join("src").join("lib.rs");
715        let src_lib_rs = fs::read_to_string(&src_path).await.map_err(|_| {
716            let err = EnvironmentError::SourceNotFound(src_path.clone());
717            tracing::error!(error = ?err, "Failed to read src/lib.rs for capability spec");
718            err
719        })?;
720
721        let res = match &self.manifest {
722            crate::cargo::ProjectManifest::Capability(_) => {
723                let (_, interface) = pyro_macro::ffi::generate_interface(
724                    &src_lib_rs,
725                    &package,
726                    &version,
727                )
728                .map_err(|r| {
729                    let err =
730                        EnvironmentError::InterfaceGeneration(format_syn_error(&src_lib_rs, r));
731                    tracing::error!(error = ?err, "Failed to generate interface spec");
732                    err
733                })?;
734                Ok(interface)
735            }
736            crate::cargo::ProjectManifest::Module(_) => {
737                let err = EnvironmentError::InterfaceGeneration(
738                    "Capability spec is only supported for capabilities".to_string(),
739                );
740                tracing::error!(error = ?err, "Invalid manifest type for capability spec");
741                Err(err)
742            }
743        };
744        res
745    }
746
747    #[tracing::instrument(skip(self))]
748    pub async fn playbook_spec(&self) -> EnvResult<crate::artifacts::PlaybookSpec> {
749        tracing::debug!("Resolving playbook spec");
750        let src_path = self.root.join("src").join("lib.rs");
751        let src_lib_rs = fs::read_to_string(&src_path).await.map_err(|_| {
752            let err = EnvironmentError::SourceNotFound(src_path.clone());
753            tracing::error!(error = ?err, "Failed to read src/lib.rs for playbook spec");
754            err
755        })?;
756
757        let res = match &self.manifest {
758            crate::cargo::ProjectManifest::Module(module_manifest) => {
759                let mut resolved_capabilities = Vec::new();
760                let mut dep_interfaces = Vec::new();
761                for cap in module_manifest.capabilities.values() {
762                    let cap_ident = CapabilityIdent {
763                        author: cap.author.clone(),
764                        package: cap.package.clone(),
765                        version: cap.version.clone(),
766                    };
767                    if let Ok(spec_str) = self
768                        .cache_manager
769                        .capability_interface_spec(
770                            &cap_ident.author,
771                            &cap_ident.package,
772                            &cap_ident.version,
773                        )
774                        .await
775                    {
776                        if let Ok(spec) =
777                            serde_json::from_str::<pyro_spec::InterfaceSpec>(&spec_str)
778                        {
779                            dep_interfaces.push(spec);
780                        }
781                    }
782                    resolved_capabilities.push(cap_ident);
783                }
784
785                let ident = crate::artifacts::PlaybookIdent {
786                    author: self.author(),
787                    package: self.package(),
788                    version: self.version(),
789                };
790                let source = crate::artifacts::PlaybookSource::new(
791                    ident.clone(),
792                    crate::artifacts::ModuleDependencies {
793                        dependencies: module_manifest.dependencies.clone(),
794                        capabilities: resolved_capabilities.clone(),
795                    },
796                    std::vec::Vec::new(),
797                    src_lib_rs.clone(),
798                    module_manifest.interconnect.clone(),
799                );
800                let hash = source.hash();
801
802                pyro_macro::module::generate_module_spec(&src_lib_rs, &dep_interfaces)
803                    .map_err(|e| {
804                        let err = EnvironmentError::InterfaceGeneration(e.to_string());
805                        tracing::error!(error = ?err, "Failed to generate playbook spec");
806                        err
807                    })?
808                    .map(|func| crate::artifacts::PlaybookSpec {
809                        ident,
810                        hash,
811                        func,
812                        capabilities: resolved_capabilities,
813                        interconnect: module_manifest.interconnect.clone(),
814                    })
815                    .ok_or_else(|| {
816                        let err = EnvironmentError::InterfaceGeneration(
817                            "Module main function missing".to_string(),
818                        );
819                        tracing::error!(error = ?err, "Playbook spec missing main function");
820                        err
821                    })
822            }
823            crate::cargo::ProjectManifest::Capability(_) => {
824                let err = EnvironmentError::InterfaceGeneration(
825                    "Module spec is only supported for modules".to_string(),
826                );
827                tracing::error!(error = ?err, "Invalid manifest type for playbook spec");
828                Err(err)
829            }
830        };
831        res
832    }
833}
834
835pub fn dylib_extension() -> &'static str {
836    if cfg!(target_os = "macos") {
837        "dylib"
838    } else if cfg!(target_os = "windows") {
839        "dll"
840    } else {
841        "so"
842    }
843}