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