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    pub async fn new(root: PathBuf, cache_manager: Arc<CacheManager>) -> EnvResult<Self> {
77        let manifest = Self::load_manifest(&root).await?;
78        Self::ensure_cargo_toml(&root, &manifest, &cache_manager).await?;
79        let target_dir = Self::get_target_dir(&root).await?;
80        Ok(Self {
81            root,
82            target_dir,
83            manifest,
84            cache_manager,
85        })
86    }
87
88    /// Write Cargo.toml from Module.toml or Capability.toml if it doesn't exist
89    async fn ensure_cargo_toml(
90        root: &Path,
91        manifest: &crate::cargo::ProjectManifest,
92        cache_manager: &CacheManager,
93    ) -> EnvResult<()> {
94        let cargo_toml_path = root.join("Cargo.toml");
95        if cargo_toml_path.exists() {
96            return Ok(());
97        }
98        let contents =
99            toml::to_string_pretty(&manifest.clone().to_cargo_manifest(Some(cache_manager)))
100                .map_err(|e| EnvironmentError::ParseManifest(e.to_string()))?;
101        fs::write(&cargo_toml_path, contents).await?;
102        Ok(())
103    }
104
105    pub fn name(&self) -> String {
106        self.manifest.ident().name.clone()
107    }
108
109    pub fn version(&self) -> String {
110        self.manifest.ident().version.clone()
111    }
112
113    pub fn author(&self) -> String {
114        self.manifest.ident().author.clone()
115    }
116
117    /// Detect Capability.toml or Module.toml to extract name and version
118    async fn load_manifest(root: &Path) -> EnvResult<ProjectManifest> {
119        tracing::debug!("Loading manifest from {:?}", root);
120        let capability_toml = root.join("Capability.toml");
121        if capability_toml.exists() {
122            let content = tokio::fs::read_to_string(&capability_toml).await?;
123            let manifest: crate::cargo::CapabilityManifest = toml::from_str(&content)
124                .map_err(|e| EnvironmentError::ParseManifest(format!("Capability.toml: {}", e)))?;
125            return Ok(crate::cargo::ProjectManifest::Capability(manifest));
126        }
127
128        let module_toml = root.join("Module.toml");
129        if module_toml.exists() {
130            let content = tokio::fs::read_to_string(&module_toml).await?;
131            let manifest: crate::cargo::ModuleManifest = toml::from_str(&content)
132                .map_err(|e| EnvironmentError::ParseManifest(format!("Module.toml: {}", e)))?;
133            return Ok(crate::cargo::ProjectManifest::Module(manifest));
134        }
135
136        // Default for anon compilations or when no package section is found
137        Err(EnvironmentError::ParseManifest(
138            "No manifest found".to_string(),
139        ))
140    }
141
142    /// Run `cargo metadata` to find the target directory
143    pub async fn get_target_dir(path: &Path) -> EnvResult<PathBuf> {
144        let output = Command::new("cargo")
145            .args(["metadata", "--format-version=1", "--no-deps"])
146            .current_dir(path)
147            .output()
148            .await?;
149
150        if !output.status.success() {
151            return Err(EnvironmentError::Metadata(
152                String::from_utf8_lossy(&output.stderr).to_string(),
153            ));
154        }
155
156        let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)?;
157
158        metadata["target_directory"]
159            .as_str()
160            .map(PathBuf::from)
161            .ok_or(EnvironmentError::MissingTargetDir)
162    }
163
164    pub async fn generate_lockfile(&self) -> EnvResult<String> {
165        run_command(&self.root, &["generate-lockfile"], true).await?;
166
167        Ok(fs::read_to_string(self.root.join("Cargo.lock")).await?)
168    }
169
170    /// Compile the project (defaults to release)
171    pub async fn compile(&self, extra_args: &[&str], capture: bool) -> EnvResult<()> {
172        let mut args = vec!["build", "--release"];
173        args.extend_from_slice(extra_args);
174        run_command(&self.root, &args, capture).await?;
175        Ok(())
176    }
177
178    /// Get path to the compiled wasm artifact
179    pub fn get_wasm_artifact(&self, name: &str) -> EnvResult<PathBuf> {
180        let path = self
181            .target_dir
182            .join("wasm32-unknown-unknown")
183            .join("release")
184            .join(format!("{}.wasm", name.replace('-', "_")));
185
186        if path.exists() {
187            Ok(path)
188        } else {
189            Err(EnvironmentError::ArtifactNotFound(path))
190        }
191    }
192
193    /// Get path to the compiled library artifact (dylib/so/dll)
194    pub async fn get_library_artifact(&self, name: &str) -> EnvResult<CapBinary> {
195        let ext = dylib_extension();
196        let path =
197            self.target_dir
198                .join("release")
199                .join(format!("lib{}.{}", name.replace('-', "_"), ext));
200        if path.exists() {
201            match ext {
202                "dylib" => Ok(CapBinary::MachO(fs::read(&path).await?)),
203                "so" => Ok(CapBinary::Elf(fs::read(&path).await?)),
204                "dll" => Ok(CapBinary::Pe(fs::read(&path).await?)),
205                _ => Err(EnvironmentError::ArtifactNotFound(path)),
206            }
207        } else {
208            Err(EnvironmentError::ArtifactNotFound(path))
209        }
210    }
211
212    /// Load artifacts from an existing target directory without compiling
213    pub async fn load_artifacts_from_target(&self, target_dir: &Path) -> EnvResult<Vec<Artifacts>> {
214        tracing::info!("Loading artifacts from target directory: {:?}", target_dir);
215
216        let name = self.name();
217        let version = self.version();
218        let author = self.author();
219
220        let src_path = self.root.join("src").join("lib.rs");
221        let src_lib_rs = if src_path.exists() {
222            fs::read_to_string(&src_path).await?
223        } else {
224            String::new()
225        };
226
227        match &self.manifest {
228            crate::cargo::ProjectManifest::Capability(cap_manifest) => {
229                let lib = self.get_library_artifact(&name).await.ok();
230
231                let lock_path = self.root.join("Cargo.lock");
232                let cargo_lock = if lock_path.exists() {
233                    fs::read_to_string(&lock_path).await?
234                } else {
235                    String::new()
236                };
237
238                let (interface_rs, interface) =
239                    pyro_macro::ffi::generate_interface(&src_lib_rs, &name, &version).map_err(
240                        |r| EnvironmentError::InterfaceGeneration(format_syn_error(&src_lib_rs, r)),
241                    )?;
242
243                let interface_rs = prettyplease::unparse(&interface_rs);
244
245                let mut artifacts = vec![
246                    Artifacts::CapabilitySource(CapabilitySource {
247                        manifest: cap_manifest.clone(),
248                        cargo_toml: toml::to_string_pretty(
249                            &cap_manifest.clone().to_capability_manifest(),
250                        )
251                        .map_err(|e| EnvironmentError::ParseManifest(e.to_string()))?,
252                        cargo_lock,
253                        src_lib_rs,
254                    }),
255                    Artifacts::Interface(Interface {
256                        manifest: cap_manifest.clone(),
257                        src_lib_rs: interface_rs,
258                        interface: interface.clone(),
259                    }),
260                ];
261
262                if let Some(lib) = lib {
263                    artifacts.push(Artifacts::CapabilityBinary(CapabilityBinary {
264                        ident: CapabilityIdent {
265                            name,
266                            version,
267                            author,
268                        },
269                        libs: vec![lib],
270                        interface: interface.clone(),
271                    }));
272                }
273
274                Ok(artifacts)
275            }
276            crate::cargo::ProjectManifest::Module(module_manifest) => {
277                let wasm_path = self.get_wasm_artifact(&name).ok();
278
279                let source = crate::artifacts::ModuleSource {
280                    dependencies: crate::artifacts::ModuleDependencies {
281                        dependencies: module_manifest.dependencies.clone(),
282                        capabilities: module_manifest.capabilities.values().cloned().collect(),
283                    },
284                    source: src_lib_rs.clone(),
285                };
286                let hash = source.hash();
287
288                let mut artifacts =
289                    vec![Artifacts::Module(crate::artifacts::Module::Source(source))];
290
291                if let Some(path) = wasm_path {
292                    let spec = pyro_macro::module::generate_module_spec(&src_lib_rs)
293                        .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
294                        .map(|func| crate::artifacts::ModuleSpec {
295                            hash,
296                            func,
297                            capabilities: module_manifest.capabilities.values().cloned().collect(),
298                        })
299                        .ok_or_else(|| {
300                            EnvironmentError::InterfaceGeneration(
301                                "Module main function missing".to_string(),
302                            )
303                        })?;
304
305                    let binary = crate::artifacts::ModuleBinary {
306                        wasm: fs::read(path).await?,
307                        spec,
308                    };
309                    artifacts.push(Artifacts::Module(crate::artifacts::Module::Binary(binary)));
310                }
311
312                Ok(artifacts)
313            }
314        }
315    }
316
317    pub async fn package(&self, capture: bool) -> EnvResult<Vec<Artifacts>> {
318        let name = self.name();
319        let version = self.version();
320        let author = self.author();
321
322        match &self.manifest {
323            crate::cargo::ProjectManifest::Capability(cap_manifest) => {
324                tracing::info!("Packaging capability: {:?}", self.root);
325
326                let cargo_toml =
327                    toml::to_string_pretty(&cap_manifest.clone().to_capability_manifest())
328                        .map_err(|e| EnvironmentError::ParseManifest(e.to_string()))?;
329
330                tracing::info!("Compiling capability binary...");
331                self.compile(&["--features", "capability", "-p", &name], capture)
332                    .await?;
333
334                let lib = self.get_library_artifact(&name).await?;
335
336                let lock_path = self.root.join("Cargo.lock");
337                let cargo_lock = if lock_path.exists() {
338                    fs::read_to_string(&lock_path).await?
339                } else {
340                    String::new()
341                };
342
343                let src_path = self.root.join("src").join("lib.rs");
344                let src_lib_rs = if src_path.exists() {
345                    fs::read_to_string(&src_path).await?
346                } else {
347                    String::new()
348                };
349
350                let (interface_rs, interface) =
351                    pyro_macro::ffi::generate_interface(&src_lib_rs, &name, &version).map_err(
352                        |r| EnvironmentError::InterfaceGeneration(format_syn_error(&src_lib_rs, r)),
353                    )?;
354
355                let interface_rs = prettyplease::unparse(&interface_rs);
356
357                Ok(vec![
358                    Artifacts::CapabilitySource(CapabilitySource {
359                        manifest: cap_manifest.clone(),
360                        cargo_toml,
361                        cargo_lock,
362                        src_lib_rs,
363                    }),
364                    Artifacts::CapabilityBinary(CapabilityBinary {
365                        ident: CapabilityIdent {
366                            name,
367                            version,
368                            author,
369                        },
370                        libs: vec![lib],
371                        interface: interface.clone(),
372                    }),
373                    Artifacts::Interface(Interface {
374                        manifest: cap_manifest.clone(),
375                        src_lib_rs: interface_rs,
376                        interface: interface.clone(),
377                    }),
378                ])
379            }
380            crate::cargo::ProjectManifest::Module(module_manifest) => {
381                tracing::info!("Packaging module: {:?}", self.root);
382
383                tracing::info!("Compiling module binary...");
384                self.compile(&["--features", "module", "-p", &name], capture)
385                    .await?;
386
387                let wasm_artifact = self.get_wasm_artifact(&name)?;
388
389                let src_path = self.root.join("src").join("lib.rs");
390                let src_lib_rs = if src_path.exists() {
391                    fs::read_to_string(&src_path).await?
392                } else {
393                    String::new()
394                };
395
396                let source = crate::artifacts::ModuleSource {
397                    dependencies: crate::artifacts::ModuleDependencies {
398                        dependencies: module_manifest.dependencies.clone(),
399                        capabilities: module_manifest.capabilities.values().cloned().collect(),
400                    },
401                    source: src_lib_rs.clone(),
402                };
403                let hash = source.hash();
404
405                let spec = pyro_macro::module::generate_module_spec(&src_lib_rs)
406                    .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
407                    .map(|func| crate::artifacts::ModuleSpec {
408                        hash,
409                        func,
410                        capabilities: module_manifest.capabilities.values().cloned().collect(),
411                    })
412                    .ok_or_else(|| {
413                        EnvironmentError::InterfaceGeneration(
414                            "Module main function missing".to_string(),
415                        )
416                    })?;
417
418                let binary = crate::artifacts::ModuleBinary {
419                    wasm: fs::read(wasm_artifact).await?,
420                    spec,
421                };
422
423                Ok(vec![
424                    Artifacts::Module(crate::artifacts::Module::Source(source)),
425                    Artifacts::Module(crate::artifacts::Module::Binary(binary)),
426                ])
427            }
428        }
429    }
430
431    pub async fn expand_debug(&self) -> EnvResult<()> {
432        let debug_dir = self.root.join("debug");
433        fs::create_dir_all(&debug_dir).await?;
434
435        match &self.manifest {
436            crate::cargo::ProjectManifest::Capability(cap_manifest) => {
437                tracing::info!("Generating debug info for capability: {}", self.name());
438                let name = self.name();
439                let version = self.version();
440
441                let lib = self.get_library_artifact(&name).await?;
442
443                let src_path = self.root.join("src").join("lib.rs");
444                let src_lib_rs = fs::read_to_string(&src_path)
445                    .await
446                    .map_err(|_| EnvironmentError::SourceNotFound(src_path))?;
447
448                let (_, interface) =
449                    pyro_macro::ffi::generate_interface(&src_lib_rs, &name, &version).map_err(
450                        |r| EnvironmentError::InterfaceGeneration(format_syn_error(&src_lib_rs, r)),
451                    )?;
452
453                let binary = CapabilityBinary {
454                    ident: cap_manifest.capability.clone(),
455                    libs: vec![lib],
456                    interface,
457                };
458
459                let symbols = debug::symbols(&binary);
460
461                let code = generate_capability(&src_lib_rs, &name, &version)
462                    .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?;
463
464                let cap_rs = Some(prettyplease::unparse(&code));
465                let debug_info = CapabilityDebug { symbols, cap_rs };
466                debug_info.write_to_directory(&debug_dir).await?;
467            }
468            crate::cargo::ProjectManifest::Module(module_manifest) => {
469                tracing::info!("Generating debug info for module: {}", self.name());
470                let name = self.name();
471
472                let wasm_path = self.get_wasm_artifact(&name)?;
473                let wasm_bytes = fs::read(wasm_path).await?;
474
475                let src_path = self.root.join("src").join("lib.rs");
476                let src_lib_rs = fs::read_to_string(&src_path)
477                    .await
478                    .map_err(|_| EnvironmentError::SourceNotFound(src_path))?;
479
480                let spec = pyro_macro::module::generate_module_spec(&src_lib_rs)
481                    .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
482                    .ok_or_else(|| {
483                        EnvironmentError::InterfaceGeneration(
484                            "Module main function missing".to_string(),
485                        )
486                    })?;
487
488                let source = crate::artifacts::ModuleSource {
489                    dependencies: crate::artifacts::ModuleDependencies {
490                        dependencies: module_manifest.dependencies.clone(),
491                        capabilities: module_manifest.capabilities.values().cloned().collect(),
492                    },
493                    source: src_lib_rs.clone(),
494                };
495                let hash = source.hash();
496
497                let binary = crate::artifacts::ModuleBinary {
498                    wasm: wasm_bytes,
499                    spec: crate::artifacts::ModuleSpec {
500                        hash,
501                        func: spec,
502                        capabilities: vec![], // Capabilities not needed for WAT/RS generation
503                    },
504                };
505
506                let wat =
507                    debug::wat(&binary).map_err(|e| EnvironmentError::InterfaceGeneration(e))?;
508
509                let generated_code = generate_module(&src_lib_rs).map_err(|e| {
510                    EnvironmentError::InterfaceGeneration(format!(
511                        "Module code generation error: {}",
512                        e
513                    ))
514                })?;
515                let cap_rs = Some(prettyplease::unparse(&generated_code));
516
517                let debug_info = ModuleDebug {
518                    wat: Some(wat),
519                    cap_rs,
520                };
521                debug_info.write_to_directory(&debug_dir).await?;
522            }
523        }
524
525        Ok(())
526    }
527
528    pub async fn capability_spec(&self) -> EnvResult<InterfaceSpec<'static>> {
529        let name = self.name();
530        let version = self.version();
531        let src_path = self.root.join("src").join("lib.rs");
532        let src_lib_rs = fs::read_to_string(&src_path)
533            .await
534            .map_err(|_| EnvironmentError::SourceNotFound(src_path))?;
535
536        match &self.manifest {
537            crate::cargo::ProjectManifest::Capability(_) => {
538                let (_, interface) =
539                    pyro_macro::ffi::generate_interface(&src_lib_rs, &name, &version).map_err(
540                        |r| EnvironmentError::InterfaceGeneration(format_syn_error(&src_lib_rs, r)),
541                    )?;
542                Ok(interface)
543            }
544            crate::cargo::ProjectManifest::Module(_) => Err(EnvironmentError::InterfaceGeneration(
545                "Capability spec is only supported for capabilities".to_string(),
546            )),
547        }
548    }
549
550    pub async fn module_spec(&self) -> EnvResult<crate::artifacts::ModuleSpec> {
551        let src_path = self.root.join("src").join("lib.rs");
552        let src_lib_rs = fs::read_to_string(&src_path)
553            .await
554            .map_err(|_| EnvironmentError::SourceNotFound(src_path))?;
555
556        match &self.manifest {
557            crate::cargo::ProjectManifest::Module(module_manifest) => {
558                let source = crate::artifacts::ModuleSource {
559                    dependencies: crate::artifacts::ModuleDependencies {
560                        dependencies: module_manifest.dependencies.clone(),
561                        capabilities: module_manifest.capabilities.values().cloned().collect(),
562                    },
563                    source: src_lib_rs.clone(),
564                };
565                let hash = source.hash();
566
567                pyro_macro::module::generate_module_spec(&src_lib_rs)
568                    .map_err(|e| EnvironmentError::InterfaceGeneration(e.to_string()))?
569                    .map(|func| crate::artifacts::ModuleSpec {
570                        hash,
571                        func,
572                        capabilities: module_manifest.capabilities.values().cloned().collect(),
573                    })
574                    .ok_or_else(|| {
575                        EnvironmentError::InterfaceGeneration(
576                            "Module main function missing".to_string(),
577                        )
578                    })
579            }
580            crate::cargo::ProjectManifest::Capability(_) => {
581                Err(EnvironmentError::InterfaceGeneration(
582                    "Module spec is only supported for modules".to_string(),
583                ))
584            }
585        }
586    }
587}
588
589pub fn dylib_extension() -> &'static str {
590    if cfg!(target_os = "macos") {
591        "dylib"
592    } else if cfg!(target_os = "windows") {
593        "dll"
594    } else {
595        "so"
596    }
597}