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 #[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 #[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 #[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 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 #[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 #[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 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 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 #[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![], 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}