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
66pub 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 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 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 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 Err(EnvironmentError::ParseManifest(
138 "No manifest found".to_string(),
139 ))
140 }
141
142 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 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 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 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 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![], },
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}