1use cargo_toml::Dependency;
2use flate2::Compression;
3use flate2::read::GzDecoder;
4use flate2::write::GzEncoder;
5use pyro_spec::{InterfaceSpec, ModuleFunc};
6use sha2::{Digest, Sha256};
7use std::collections::{BTreeMap, HashMap};
8use std::future::Future;
9use std::io::{self, Read, Write};
10use std::ops::Deref;
11use std::path::Path;
12use tar::{Builder, Header};
13use tokio::fs;
14
15use crate::cargo::{CapabilityIdent, CapabilityManifest, ConfiguredCapability, ModuleManifest};
16
17pub enum CapBinary {
18 Pe(Vec<u8>),
19 MachO(Vec<u8>),
20 Elf(Vec<u8>),
21}
22
23impl Deref for CapBinary {
24 type Target = [u8];
25
26 fn deref(&self) -> &Self::Target {
27 match self {
28 CapBinary::Pe(items) => items,
29 CapBinary::MachO(items) => items,
30 CapBinary::Elf(items) => items,
31 }
32 }
33}
34
35pub struct CapabilityBinary {
36 pub ident: CapabilityIdent,
37 pub libs: Vec<CapBinary>,
38 pub interface: InterfaceSpec<'static>,
39}
40
41pub struct CapabilitySource {
42 pub manifest: CapabilityManifest,
43 pub cargo_toml: String,
44 pub cargo_lock: String,
45 pub src_lib_rs: String,
46}
47
48pub struct Interface {
49 pub manifest: CapabilityManifest,
50 pub src_lib_rs: String,
51 pub interface: InterfaceSpec<'static>,
52}
53
54#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
55pub struct ModuleDependencies {
56 pub dependencies: BTreeMap<String, Dependency>,
57 pub capabilities: Vec<CapabilityIdent>,
58}
59
60#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct PlaybookIdent {
63 pub author: String,
64 pub package: String,
65 pub version: String,
66}
67
68#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
69pub struct PlaybookSource {
70 pub manifest: ModuleManifest,
71 pub source: String,
72}
73
74#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
75pub struct PlaybookSpec {
76 pub ident: PlaybookIdent,
77 pub hash: String,
78 pub func: ModuleFunc<'static>,
79 pub capabilities: Vec<CapabilityIdent>,
80 #[serde(default)]
81 pub interconnect: BTreeMap<String, PlaybookIdent>,
82}
83
84#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
85pub struct PlaybookBinary {
86 pub wasm: Vec<u8>,
88 pub spec: PlaybookSpec,
89 #[serde(default)]
90 pub configurations: Vec<ConfiguredCapability>,
91}
92
93#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
94pub enum Playbook {
95 Source(PlaybookSource),
96 Binary(PlaybookBinary),
97}
98
99#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
100#[serde(tag = "type", rename_all = "lowercase")]
101pub struct CapabilityConfig {
102 pub classes: HashMap<String, Option<serde_json::Value>>,
104}
105
106impl std::hash::Hash for CapabilityConfig {
107 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
108 let mut entries: Vec<_> = self.classes.iter().collect();
109 entries.sort_by_key(|(k, _)| *k);
110 for (k, v) in entries {
111 k.hash(state);
112 if let Some(val) = v {
113 if let Ok(json_str) = serde_json::to_string(val) {
114 json_str.hash(state);
115 }
116 } else {
117 0.hash(state);
118 }
119 }
120 }
121}
122
123impl PlaybookSource {
124 pub fn ident(&self) -> PlaybookIdent {
125 PlaybookIdent {
126 author: self.manifest.module.author.clone(),
127 package: self.manifest.module.package.clone(),
128 version: self.manifest.module.version.clone(),
129 }
130 }
131
132 pub fn dependencies(&self) -> ModuleDependencies {
133 let mut resolved_capabilities = Vec::new();
134 for cap in self.manifest.capabilities.values() {
135 resolved_capabilities.push(CapabilityIdent {
136 author: cap.author.clone(),
137 package: cap.package.clone(),
138 version: cap.version.clone(),
139 });
140 }
141 ModuleDependencies {
142 dependencies: self.manifest.dependencies.clone(),
143 capabilities: resolved_capabilities,
144 }
145 }
146
147 pub fn configurations(&self) -> Vec<ConfiguredCapability> {
148 self.manifest.capabilities.values().cloned().collect()
149 }
150
151 pub fn hash(&self) -> String {
153 let mut resolved_capabilities = Vec::new();
154 for cap in self.manifest.capabilities.values() {
155 resolved_capabilities.push(CapabilityIdent {
156 author: cap.author.clone(),
157 package: cap.package.clone(),
158 version: cap.version.clone(),
159 });
160 }
161 let base_hash = Self::compute_hash(
162 &self.source,
163 &self.manifest.dependencies,
164 &resolved_capabilities,
165 );
166 let mut hasher = Sha256::new();
167 hasher.update(base_hash.as_bytes());
168 if let Ok(interconnect_json) = serde_json::to_string(&self.manifest.interconnect) {
169 hasher.update(interconnect_json.as_bytes());
170 }
171 format!("{:x}", hasher.finalize())
172 }
173
174 pub fn compute_hash(
175 code: &str,
176 dependencies: &std::collections::BTreeMap<String, cargo_toml::Dependency>,
177 capabilities: &[crate::cargo::CapabilityIdent],
178 ) -> String {
179 let mut hasher = Sha256::new();
180
181 hasher.update(code.as_bytes());
182
183 if let Ok(deps_json) = serde_json::to_string(dependencies) {
184 hasher.update(deps_json.as_bytes());
185 }
186
187 let mut sorted_caps = capabilities.to_vec();
188 sorted_caps.sort_by(|a, b| {
189 a.package
190 .cmp(&b.package)
191 .then_with(|| a.author.cmp(&b.author))
192 .then_with(|| a.version.cmp(&b.version))
193 });
194
195 if let Ok(caps_json) = serde_json::to_string(&sorted_caps) {
196 hasher.update(caps_json.as_bytes());
197 }
198
199 format!("{:x}", hasher.finalize())
200 }
201
202 pub fn new(
203 ident: PlaybookIdent,
204 dependencies: ModuleDependencies,
205 configurations: Vec<ConfiguredCapability>,
206 source: String,
207 interconnect: BTreeMap<String, PlaybookIdent>,
208 ) -> Self {
209 let mut capabilities_map: BTreeMap<String, ConfiguredCapability> = BTreeMap::new();
210 for cap in configurations {
211 capabilities_map.insert(cap.package.clone(), cap);
212 }
213 for cap in dependencies.capabilities.iter() {
214 if !capabilities_map.contains_key(&cap.package) {
215 capabilities_map.insert(
216 cap.package.clone(),
217 ConfiguredCapability {
218 author: cap.author.clone(),
219 package: cap.package.clone(),
220 version: cap.version.clone(),
221 configuration: CapabilityConfig {
222 classes: std::collections::HashMap::new(),
223 },
224 },
225 );
226 }
227 }
228 let pyroduct_dep =
229 cargo_toml::Dependency::Inherited(cargo_toml::InheritedDependencyDetail {
230 workspace: true,
231 ..Default::default()
232 });
233 let manifest = ModuleManifest::<toml::Value> {
234 module: CapabilityIdent {
235 package: ident.package,
236 version: ident.version,
237 author: ident.author,
238 },
239 workspace: None,
240 pyroduct: pyroduct_dep,
241 capabilities: capabilities_map,
242 dependencies: dependencies.dependencies,
243 dev_dependencies: Default::default(),
244 build_dependencies: Default::default(),
245 target: Default::default(),
246 features: Default::default(),
247 patch: Default::default(),
248 lib: None,
249 profile: Default::default(),
250 badges: Default::default(),
251 bin: Vec::new(),
252 bench: Vec::new(),
253 test: Vec::new(),
254 example: Vec::new(),
255 lints: Default::default(),
256 interconnect,
257 };
258 Self { manifest, source }
259 }
260}
261
262impl PlaybookBinary {
263 pub fn hash(&self) -> String {
264 self.spec.hash.clone()
265 }
266}
267
268impl Playbook {
269 pub fn hash(&self) -> String {
270 match self {
271 Playbook::Source(m) => m.hash(),
272 Playbook::Binary(m) => m.hash(),
273 }
274 }
275}
276
277pub enum Artifacts {
278 CapabilityBinary(CapabilityBinary),
279 CapabilitySource(CapabilitySource),
280 Interface(Interface),
281 Playbook(Playbook),
282}
283
284impl From<CapabilityBinary> for Artifacts {
285 fn from(value: CapabilityBinary) -> Self {
286 Artifacts::CapabilityBinary(value)
287 }
288}
289
290impl From<CapabilitySource> for Artifacts {
291 fn from(value: CapabilitySource) -> Self {
292 Artifacts::CapabilitySource(value)
293 }
294}
295
296impl From<Interface> for Artifacts {
297 fn from(value: Interface) -> Self {
298 Artifacts::Interface(value)
299 }
300}
301
302impl From<Playbook> for Artifacts {
303 fn from(value: Playbook) -> Self {
304 Artifacts::Playbook(value)
305 }
306}
307
308impl From<PlaybookBinary> for Artifacts {
309 fn from(value: PlaybookBinary) -> Self {
310 Artifacts::Playbook(Playbook::Binary(value))
311 }
312}
313
314impl From<PlaybookSource> for Artifacts {
315 fn from(value: PlaybookSource) -> Self {
316 Artifacts::Playbook(Playbook::Source(value))
317 }
318}
319
320pub trait Artifact: Sized {
323 fn write_to_directory(&self, path: &Path) -> impl Future<Output = io::Result<()>> + Send;
324 fn to_tarball(&self) -> Result<Vec<u8>, io::Error>;
325
326 fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error>;
327 fn from_dir(path: &Path) -> impl Future<Output = Result<Self, io::Error>> + Send;
328}
329
330pub(crate) fn append_file<W: Write>(
332 tar: &mut Builder<W>,
333 name: &str,
334 data: &[u8],
335) -> Result<(), io::Error> {
336 let mut header = Header::new_gnu();
337 header.set_size(data.len() as u64);
338 header.set_mode(0o644);
339 header.set_cksum();
340 tar.append_data(&mut header, name, data)
341}
342
343impl Artifact for CapabilityBinary {
348 #[tracing::instrument(skip(self, path), fields(path = %path.display(), ident = ?self.ident))]
349 async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
350 tracing::debug!("Writing CapabilityBinary to directory");
351 fs::create_dir_all(path).await?;
352 for lib in &self.libs {
353 match lib {
354 CapBinary::Pe(bytes) => fs::write(path.join("lib.dll"), bytes).await?,
355 CapBinary::MachO(bytes) => fs::write(path.join("lib.dylib"), bytes).await?,
356 CapBinary::Elf(bytes) => fs::write(path.join("lib.so"), bytes).await?,
357 }
358 }
359 fs::write(
360 path.join("ident.json"),
361 serde_json::to_string(&self.ident).map_err(|e| {
362 let err = io::Error::new(io::ErrorKind::InvalidData, e);
363 tracing::error!(error = ?err, "Failed to serialize ident");
364 err
365 })?,
366 )
367 .await?;
368 let interface_json = serde_json::to_string_pretty(&self.interface).map_err(|e| {
369 let err = io::Error::new(io::ErrorKind::InvalidData, e);
370 tracing::error!(error = ?err, "Failed to serialize interface");
371 err
372 })?;
373 fs::write(path.join("interface.json"), interface_json).await?;
374 Ok(())
375 }
376
377 fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
378 let encoder = GzEncoder::new(Vec::new(), Compression::default());
379 let mut tar = Builder::new(encoder);
380
381 for lib in &self.libs {
382 match lib {
383 CapBinary::Pe(bytes) => append_file(&mut tar, "lib.dll", bytes)?,
384 CapBinary::MachO(bytes) => append_file(&mut tar, "lib.dylib", bytes)?,
385 CapBinary::Elf(bytes) => append_file(&mut tar, "lib.so", bytes)?,
386 }
387 }
388 append_file(
389 &mut tar,
390 "ident.json",
391 serde_json::to_string(&self.ident)
392 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
393 .as_bytes(),
394 )?;
395 append_file(
396 &mut tar,
397 "interface.json",
398 serde_json::to_string_pretty(&self.interface)
399 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
400 .as_bytes(),
401 )?;
402
403 tar.into_inner()?.finish()
404 }
405
406 fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
407 let tar = GzDecoder::new(bytes);
408 let mut archive = tar::Archive::new(tar);
409
410 let mut libs = Vec::new();
411 let mut ident = None;
412 let mut interface = None;
413
414 for file in archive.entries()? {
415 let mut file = file?;
416 let path = file.path()?.to_path_buf();
417 let mut content = Vec::new();
418 file.read_to_end(&mut content)?;
419
420 match path.to_string_lossy().as_ref() {
421 "lib.dll" => libs.push(CapBinary::Pe(content)),
422 "lib.dylib" => libs.push(CapBinary::MachO(content)),
423 "lib.so" => libs.push(CapBinary::Elf(content)),
424 "ident.json" => {
425 ident = serde_json::from_slice(&content).map_err(|e| {
426 io::Error::new(
427 io::ErrorKind::InvalidData,
428 format!("Unable to deserialize manifest: {}", e),
429 )
430 })?;
431 }
432 "interface.json" => {
433 interface = serde_json::from_slice(&content).map_err(|e| {
434 io::Error::new(
435 io::ErrorKind::InvalidData,
436 format!("Unable to deserialize interface: {}", e),
437 )
438 })?;
439 }
440 _ => {}
441 }
442 }
443
444 if libs.is_empty() {
445 return Err(io::Error::new(io::ErrorKind::NotFound, "Missing library"));
446 }
447
448 Ok(CapabilityBinary {
449 ident: ident
450 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing ident.json"))?,
451 libs,
452 interface: interface.ok_or_else(|| {
453 io::Error::new(io::ErrorKind::InvalidData, "Missing interface.json")
454 })?,
455 })
456 }
457
458 #[tracing::instrument(skip(path), fields(path = %path.display()))]
459 async fn from_dir(path: &Path) -> Result<Self, io::Error> {
460 tracing::debug!("Loading CapabilityBinary from directory");
461 let mut libs = Vec::new();
462 if let Ok(bytes) = fs::read(path.join("lib.dll")).await {
463 libs.push(CapBinary::Pe(bytes));
464 }
465 if let Ok(bytes) = fs::read(path.join("lib.dylib")).await {
466 libs.push(CapBinary::MachO(bytes));
467 }
468 if let Ok(bytes) = fs::read(path.join("lib.so")).await {
469 libs.push(CapBinary::Elf(bytes));
470 }
471
472 if libs.is_empty() {
473 let err = io::Error::new(io::ErrorKind::NotFound, "Missing capability library");
474 tracing::error!(error = ?err, "Failed to load capability library");
475 return Err(err);
476 }
477
478 let ident_string = fs::read(path.join("ident.json")).await?;
479 let ident = serde_json::from_slice(&ident_string).map_err(|e| {
480 let err = io::Error::new(
481 io::ErrorKind::InvalidData,
482 format!("Unable to deserialize ident: {}", e),
483 );
484 tracing::error!(error = ?err, "Failed to deserialize ident.json");
485 err
486 })?;
487
488 let interface_string = fs::read(path.join("interface.json")).await?;
489 let interface = serde_json::from_slice(&interface_string).map_err(|e| {
490 let err = io::Error::new(
491 io::ErrorKind::InvalidData,
492 format!("Unable to deserialize interface: {}", e),
493 );
494 tracing::error!(error = ?err, "Failed to deserialize interface.json");
495 err
496 })?;
497
498 Ok(CapabilityBinary {
499 libs,
500 ident,
501 interface,
502 })
503 }
504}
505
506impl Artifact for CapabilitySource {
507 #[tracing::instrument(skip(self, path), fields(path = %path.display(), manifest = ?self.manifest.capability))]
508 async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
509 tracing::debug!("Writing CapabilitySource to directory");
510 fs::create_dir_all(path).await?;
511 let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
512 let err = io::Error::new(
513 io::ErrorKind::InvalidData,
514 format!("Unable to serialize manifest: {}", e),
515 );
516 tracing::error!(error = ?err, "Failed to serialize Capability.toml");
517 err
518 })?;
519 fs::write(path.join("Capability.toml"), &manifest).await?;
520 fs::write(path.join("Cargo.toml"), &self.cargo_toml).await?;
521 fs::write(path.join("Cargo.lock"), &self.cargo_lock).await?;
522
523 let src_dir = path.join("src");
524 fs::create_dir_all(&src_dir).await?;
525 fs::write(src_dir.join("lib.rs"), &self.src_lib_rs).await?;
526 Ok(())
527 }
528
529 fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
530 let encoder = GzEncoder::new(Vec::new(), Compression::default());
531 let mut tar = Builder::new(encoder);
532
533 let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
534 io::Error::new(
535 io::ErrorKind::InvalidData,
536 format!("Unable to serialize manifest: {}", e),
537 )
538 })?;
539 append_file(&mut tar, "Capability.toml", manifest.as_bytes())?;
540 append_file(&mut tar, "Cargo.toml", self.cargo_toml.as_bytes())?;
541 append_file(&mut tar, "Cargo.lock", self.cargo_lock.as_bytes())?;
542 append_file(&mut tar, "src/lib.rs", self.src_lib_rs.as_bytes())?;
543
544 tar.into_inner()?.finish()
545 }
546
547 fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
548 let tar = GzDecoder::new(bytes);
549 let mut archive = tar::Archive::new(tar);
550
551 let mut manifest = None;
552 let mut cargo_toml = None;
553 let mut cargo_lock = None;
554 let mut src_lib_rs = None;
555
556 for file in archive.entries()? {
557 let mut file = file?;
558 let path = file.path()?.to_path_buf();
559 let mut content = Vec::new();
560 file.read_to_end(&mut content)?;
561
562 match path.to_string_lossy().as_ref() {
563 "Capability.toml" => {
564 manifest = toml::from_slice(&content).map_err(|e| {
565 io::Error::new(
566 io::ErrorKind::InvalidData,
567 format!("Unable to deserialize manifest: {}", e),
568 )
569 })?;
570 }
571 "Cargo.toml" => cargo_toml = String::from_utf8(content).ok(),
572 "Cargo.lock" => cargo_lock = String::from_utf8(content).ok(),
573 "src/lib.rs" => src_lib_rs = String::from_utf8(content).ok(),
574 _ => {}
575 }
576 }
577
578 Ok(CapabilitySource {
579 manifest: manifest.ok_or_else(|| {
580 io::Error::new(io::ErrorKind::InvalidData, "Missing Capability.toml")
581 })?,
582 cargo_toml: cargo_toml
583 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing Cargo.toml"))?,
584 cargo_lock: cargo_lock
585 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing Cargo.lock"))?,
586 src_lib_rs: src_lib_rs
587 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing src/lib.rs"))?,
588 })
589 }
590
591 #[tracing::instrument(skip(path), fields(path = %path.display()))]
592 async fn from_dir(path: &Path) -> Result<Self, io::Error> {
593 tracing::debug!("Loading CapabilitySource from directory");
594 let manifest_string = fs::read(path.join("Capability.toml")).await?;
595 let manifest = toml::from_slice(&manifest_string).map_err(|e| {
596 let err = io::Error::new(
597 io::ErrorKind::InvalidData,
598 format!("Unable to deserialize manifest: {}", e),
599 );
600 tracing::error!(error = ?err, "Failed to deserialize Capability.toml");
601 err
602 })?;
603
604 Ok(CapabilitySource {
605 manifest,
606 cargo_toml: fs::read_to_string(path.join("Cargo.toml")).await?,
607 cargo_lock: fs::read_to_string(path.join("Cargo.lock")).await?,
608 src_lib_rs: fs::read_to_string(path.join("src").join("lib.rs")).await?,
609 })
610 }
611}
612
613impl Artifact for Interface {
614 #[tracing::instrument(skip(self, path), fields(path = %path.display(), manifest = ?self.manifest.capability))]
615 async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
616 tracing::debug!("Writing Interface to directory");
617 let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
618 let err = io::Error::new(
619 io::ErrorKind::InvalidData,
620 format!("Unable to serialize manifest: {}", e),
621 );
622 tracing::error!(error = ?err, "Failed to serialize Capability.toml");
623 err
624 })?;
625 fs::create_dir_all(path).await?;
626 fs::write(path.join("Capability.toml"), &manifest).await?;
627 let interface_str = serde_json::to_string_pretty(&self.interface).map_err(|e| {
628 let err = io::Error::new(
629 io::ErrorKind::InvalidData,
630 format!("Unable to serialize interface: {}", e),
631 );
632 tracing::error!(error = ?err, "Failed to serialize interface.json");
633 err
634 })?;
635 fs::write(path.join("interface.json"), &interface_str).await?;
636
637 let src_dir = path.join("src");
638 fs::create_dir_all(&src_dir).await?;
639 fs::write(src_dir.join("lib.rs"), &self.src_lib_rs).await?;
640 Ok(())
641 }
642
643 fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
644 let encoder = GzEncoder::new(Vec::new(), Compression::default());
645 let mut tar = Builder::new(encoder);
646 let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
647 io::Error::new(
648 io::ErrorKind::InvalidData,
649 format!("Unable to serialize manifest: {}", e),
650 )
651 })?;
652 let interface = serde_json::to_string_pretty(&self.interface).map_err(|e| {
653 io::Error::new(
654 io::ErrorKind::InvalidData,
655 format!("Unable to serialize interface: {}", e),
656 )
657 })?;
658
659 append_file(&mut tar, "Capability.toml", manifest.as_bytes())?;
660 append_file(&mut tar, "interface.json", interface.as_bytes())?;
661 append_file(&mut tar, "src/lib.rs", self.src_lib_rs.as_bytes())?;
662
663 tar.into_inner()?.finish()
664 }
665
666 fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
667 let tar = GzDecoder::new(bytes);
668 let mut archive = tar::Archive::new(tar);
669
670 let mut manifest = None;
671 let mut src_lib_rs = None;
672 let mut interface = None;
673
674 for file in archive.entries()? {
675 let mut file = file?;
676 let path = file.path()?.to_path_buf();
677 let mut content = Vec::new();
678 file.read_to_end(&mut content)?;
679
680 match path.to_string_lossy().as_ref() {
681 "Capability.toml" => {
682 manifest = toml::from_slice(&content).map_err(|e| {
683 io::Error::new(
684 io::ErrorKind::InvalidData,
685 format!("Unable to deserialize manifest: {}", e),
686 )
687 })?;
688 }
689 "interface.json" => {
690 interface = serde_json::from_slice(&content).map_err(|e| {
691 io::Error::new(
692 io::ErrorKind::InvalidData,
693 format!("Unable to deserialize interface: {}", e),
694 )
695 })?;
696 }
697 "src/lib.rs" => src_lib_rs = String::from_utf8(content).ok(),
698 _ => {}
699 }
700 }
701
702 Ok(Interface {
703 manifest: manifest.ok_or_else(|| {
704 io::Error::new(io::ErrorKind::InvalidData, "Missing Capability.toml")
705 })?,
706 src_lib_rs: src_lib_rs
707 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing src/lib.rs"))?,
708 interface: interface.ok_or_else(|| {
709 io::Error::new(io::ErrorKind::InvalidData, "Missing interface.json")
710 })?,
711 })
712 }
713
714 #[tracing::instrument(skip(path), fields(path = %path.display()))]
715 async fn from_dir(path: &Path) -> Result<Self, io::Error> {
716 tracing::debug!("Loading Interface from directory");
717 let manifest_string = fs::read(path.join("Capability.toml")).await?;
718 let manifest = toml::from_slice(&manifest_string).map_err(|e| {
719 let err = io::Error::new(
720 io::ErrorKind::InvalidData,
721 format!("Unable to deserialize manifest: {}", e),
722 );
723 tracing::error!(error = ?err, "Failed to deserialize Capability.toml");
724 err
725 })?;
726
727 let interface_string = fs::read(path.join("interface.json")).await?;
728 let interface = toml::from_slice(&interface_string).map_err(|e| {
729 let err = io::Error::new(
730 io::ErrorKind::InvalidData,
731 format!("Unable to deserialize interface: {}", e),
732 );
733 tracing::error!(error = ?err, "Failed to deserialize interface.json");
734 err
735 })?;
736
737 Ok(Interface {
738 manifest,
739 src_lib_rs: fs::read_to_string(path.join("src").join("lib.rs")).await?,
740 interface,
741 })
742 }
743}
744
745impl Artifact for PlaybookSource {
746 #[tracing::instrument(skip(self, path), fields(path = %path.display(), ident = ?self.ident()))]
747 async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
748 tracing::debug!("Writing PlaybookSource to directory");
749 fs::create_dir_all(path).await?;
750
751 let toml_str = toml::to_string_pretty(&self.manifest).map_err(|e| {
752 let err = io::Error::new(
753 io::ErrorKind::InvalidData,
754 format!("Unable to serialize Module.toml: {}", e),
755 );
756 tracing::error!(error = ?err, "Failed to serialize Module.toml");
757 err
758 })?;
759
760 fs::write(path.join("Module.toml"), &toml_str).await?;
761 fs::write(path.join("source.rs"), &self.source).await?;
762 Ok(())
763 }
764
765 fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
766 let encoder = GzEncoder::new(Vec::new(), Compression::default());
767 let mut tar = Builder::new(encoder);
768
769 let toml_str = toml::to_string_pretty(&self.manifest).map_err(|e| {
770 io::Error::new(
771 io::ErrorKind::InvalidData,
772 format!("Unable to serialize Module.toml: {}", e),
773 )
774 })?;
775
776 append_file(&mut tar, "Module.toml", toml_str.as_bytes())?;
777 append_file(&mut tar, "source.rs", self.source.as_bytes())?;
778 tar.into_inner()?.finish()
779 }
780
781 fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
782 let tar = GzDecoder::new(bytes);
783 let mut archive = tar::Archive::new(tar);
784 let mut source = None;
785
786 let mut legacy_ident = None;
788 let mut legacy_dependencies = None;
789 let mut legacy_configurations = None;
790
791 let mut module_toml: Option<ModuleManifest<toml::Value>> = None;
793
794 for file in archive.entries()? {
795 let mut file = file?;
796 let path = file.path()?.to_path_buf();
797 let mut content = Vec::new();
798 file.read_to_end(&mut content)?;
799
800 match path.to_string_lossy().as_ref() {
801 "Module.toml" => {
802 module_toml = Some(toml::from_slice(&content).map_err(|e| {
803 io::Error::new(
804 io::ErrorKind::InvalidData,
805 format!("Unable to deserialize Module.toml: {}", e),
806 )
807 })?);
808 }
809 "source.rs" => source = String::from_utf8(content).ok(),
810 "ident.json" => {
812 legacy_ident = Some(
813 serde_json::from_slice::<PlaybookIdent>(&content).map_err(|e| {
814 io::Error::new(
815 io::ErrorKind::InvalidData,
816 format!("Unable to deserialize ident: {}", e),
817 )
818 })?,
819 );
820 }
821 "dependencies.json" => {
822 legacy_dependencies = Some(
823 serde_json::from_slice::<ModuleDependencies>(&content).map_err(|e| {
824 io::Error::new(
825 io::ErrorKind::InvalidData,
826 format!("Unable to deserialize dependencies: {}", e),
827 )
828 })?,
829 );
830 }
831 "configurations.json" => {
832 legacy_configurations = Some(
833 serde_json::from_slice::<Vec<ConfiguredCapability>>(&content).map_err(
834 |e| {
835 io::Error::new(
836 io::ErrorKind::InvalidData,
837 format!("Unable to deserialize configurations: {}", e),
838 )
839 },
840 )?,
841 );
842 }
843 _ => {}
844 }
845 }
846
847 let source =
848 source.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Missing source.rs"))?;
849
850 if let Some(mt) = module_toml {
851 Ok(PlaybookSource {
852 manifest: mt,
853 source,
854 })
855 } else {
856 let mut capabilities_map: BTreeMap<String, ConfiguredCapability> = BTreeMap::new();
857 let legacy_config = legacy_configurations.unwrap_or_default();
858 for cap in legacy_config {
859 capabilities_map.insert(cap.package.clone(), cap);
860 }
861 let legacy_dep = legacy_dependencies.ok_or_else(|| {
862 io::Error::new(
863 io::ErrorKind::NotFound,
864 "Missing Module.toml or dependencies.json",
865 )
866 })?;
867 let legacy_id = legacy_ident.ok_or_else(|| {
868 io::Error::new(io::ErrorKind::NotFound, "Missing Module.toml or ident.json")
869 })?;
870
871 let manifest = ModuleManifest::<toml::Value> {
872 module: CapabilityIdent {
873 package: legacy_id.package,
874 version: legacy_id.version,
875 author: legacy_id.author,
876 },
877 workspace: None,
878 pyroduct: cargo_toml::Dependency::Inherited(
879 cargo_toml::InheritedDependencyDetail {
880 workspace: true,
881 ..Default::default()
882 },
883 ),
884 capabilities: capabilities_map,
885 dependencies: legacy_dep.dependencies,
886 dev_dependencies: Default::default(),
887 build_dependencies: Default::default(),
888 target: Default::default(),
889 features: Default::default(),
890 patch: Default::default(),
891 lib: None,
892 profile: Default::default(),
893 badges: Default::default(),
894 bin: Vec::new(),
895 bench: Vec::new(),
896 test: Vec::new(),
897 example: Vec::new(),
898 lints: Default::default(),
899 interconnect: BTreeMap::new(),
900 };
901 Ok(PlaybookSource { manifest, source })
902 }
903 }
904
905 #[tracing::instrument(skip(path), fields(path = %path.display()))]
906 async fn from_dir(path: &Path) -> Result<Self, io::Error> {
907 tracing::debug!("Loading PlaybookSource from directory");
908 let source = fs::read_to_string(path.join("source.rs")).await?;
909
910 if fs::try_exists(path.join("Module.toml"))
911 .await
912 .unwrap_or(false)
913 {
914 let toml_bytes = fs::read(path.join("Module.toml")).await?;
915 let mt: ModuleManifest<toml::Value> = toml::from_slice(&toml_bytes).map_err(|e| {
916 let err = io::Error::new(
917 io::ErrorKind::InvalidData,
918 format!("Failed to parse Module.toml: {e}"),
919 );
920 tracing::error!(error = ?err);
921 err
922 })?;
923 Ok(PlaybookSource {
924 manifest: mt,
925 source,
926 })
927 } else {
928 let ident_string = fs::read(path.join("ident.json")).await?;
929 let ident: PlaybookIdent = serde_json::from_slice(&ident_string).map_err(|e| {
930 let err = io::Error::new(
931 io::ErrorKind::InvalidData,
932 format!("Unable to deserialize ident: {}", e),
933 );
934 tracing::error!(error = ?err, "Failed to deserialize ident.json");
935 err
936 })?;
937 let dependencies_string = fs::read(path.join("dependencies.json")).await?;
938 let dependencies: ModuleDependencies = serde_json::from_slice(&dependencies_string)
939 .map_err(|e| {
940 let err = io::Error::new(
941 io::ErrorKind::InvalidData,
942 format!("Unable to deserialize dependencies: {}", e),
943 );
944 tracing::error!(error = ?err, "Failed to deserialize dependencies.json");
945 err
946 })?;
947 let configurations: Vec<ConfiguredCapability> =
948 if fs::try_exists(path.join("configurations.json"))
949 .await
950 .unwrap_or(false)
951 {
952 let configurations_string = fs::read(path.join("configurations.json")).await?;
953 serde_json::from_slice(&configurations_string).map_err(|e| {
954 let err = io::Error::new(
955 io::ErrorKind::InvalidData,
956 format!("Unable to deserialize configurations: {}", e),
957 );
958 tracing::error!(error = ?err, "Failed to deserialize configurations.json");
959 err
960 })?
961 } else {
962 Vec::new()
963 };
964
965 let mut capabilities_map: BTreeMap<String, ConfiguredCapability> = BTreeMap::new();
966 for cap in configurations {
967 capabilities_map.insert(cap.package.clone(), cap);
968 }
969
970 let manifest = ModuleManifest::<toml::Value> {
971 module: CapabilityIdent {
972 package: ident.package,
973 version: ident.version,
974 author: ident.author,
975 },
976 workspace: None,
977 pyroduct: cargo_toml::Dependency::Inherited(
978 cargo_toml::InheritedDependencyDetail {
979 workspace: true,
980 ..Default::default()
981 },
982 ),
983 capabilities: capabilities_map,
984 dependencies: dependencies.dependencies,
985 dev_dependencies: Default::default(),
986 build_dependencies: Default::default(),
987 target: Default::default(),
988 features: Default::default(),
989 patch: Default::default(),
990 lib: None,
991 profile: Default::default(),
992 badges: Default::default(),
993 bin: Vec::new(),
994 bench: Vec::new(),
995 test: Vec::new(),
996 example: Vec::new(),
997 lints: Default::default(),
998 interconnect: BTreeMap::new(),
999 };
1000 Ok(PlaybookSource { manifest, source })
1001 }
1002 }
1003}
1004
1005impl Artifact for PlaybookBinary {
1006 #[tracing::instrument(skip(self, path), fields(path = %path.display()))]
1007 async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
1008 tracing::debug!("Writing PlaybookBinary to directory");
1009 fs::create_dir_all(path).await?;
1010 fs::write(path.join("mod.wasm"), &self.wasm).await?;
1011 let spec = serde_json::to_string_pretty(&self.spec).map_err(|e| {
1012 let err = io::Error::new(
1013 io::ErrorKind::InvalidData,
1014 format!("Unable to serialize spec: {}", e),
1015 );
1016 tracing::error!(error = ?err, "Failed to serialize spec.json");
1017 err
1018 })?;
1019 fs::write(path.join("spec.json"), spec).await?;
1020
1021 let mut capabilities_map = BTreeMap::new();
1022 for cap in &self.configurations {
1023 capabilities_map.insert(cap.package.clone(), cap.clone());
1024 }
1025 let pyroduct_dep =
1026 cargo_toml::Dependency::Inherited(cargo_toml::InheritedDependencyDetail {
1027 workspace: true,
1028 ..Default::default()
1029 });
1030 let manifest = ModuleManifest::<toml::Value> {
1031 module: CapabilityIdent {
1032 package: self.spec.ident.package.clone(),
1033 version: self.spec.ident.version.clone(),
1034 author: self.spec.ident.author.clone(),
1035 },
1036 workspace: None,
1037 pyroduct: pyroduct_dep,
1038 capabilities: capabilities_map,
1039 dependencies: BTreeMap::new(),
1040 dev_dependencies: Default::default(),
1041 build_dependencies: Default::default(),
1042 target: Default::default(),
1043 features: Default::default(),
1044 patch: Default::default(),
1045 lib: None,
1046 profile: Default::default(),
1047 badges: Default::default(),
1048 bin: Vec::new(),
1049 bench: Vec::new(),
1050 test: Vec::new(),
1051 example: Vec::new(),
1052 lints: Default::default(),
1053 interconnect: self.spec.interconnect.clone(),
1054 };
1055 let toml_str = toml::to_string_pretty(&manifest).map_err(|e| {
1056 let err = io::Error::new(
1057 io::ErrorKind::InvalidData,
1058 format!("Unable to serialize Module.toml: {}", e),
1059 );
1060 tracing::error!(error = ?err, "Failed to serialize Module.toml");
1061 err
1062 })?;
1063 fs::write(path.join("Module.toml"), &toml_str).await?;
1064 Ok(())
1065 }
1066
1067 fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
1068 let encoder = GzEncoder::new(Vec::new(), Compression::default());
1069 let mut tar = Builder::new(encoder);
1070 append_file(&mut tar, "mod.wasm", &self.wasm)?;
1071 let spec = serde_json::to_string_pretty(&self.spec).map_err(|e| {
1072 io::Error::new(
1073 io::ErrorKind::InvalidData,
1074 format!("Unable to serialize spec: {}", e),
1075 )
1076 })?;
1077 append_file(&mut tar, "spec.json", spec.as_bytes())?;
1078
1079 let mut capabilities_map = BTreeMap::new();
1080 for cap in &self.configurations {
1081 capabilities_map.insert(cap.package.clone(), cap.clone());
1082 }
1083 let pyroduct_dep =
1084 cargo_toml::Dependency::Inherited(cargo_toml::InheritedDependencyDetail {
1085 workspace: true,
1086 ..Default::default()
1087 });
1088 let manifest = ModuleManifest::<toml::Value> {
1089 module: CapabilityIdent {
1090 package: self.spec.ident.package.clone(),
1091 version: self.spec.ident.version.clone(),
1092 author: self.spec.ident.author.clone(),
1093 },
1094 workspace: None,
1095 pyroduct: pyroduct_dep,
1096 capabilities: capabilities_map,
1097 dependencies: BTreeMap::new(),
1098 dev_dependencies: Default::default(),
1099 build_dependencies: Default::default(),
1100 target: Default::default(),
1101 features: Default::default(),
1102 patch: Default::default(),
1103 lib: None,
1104 profile: Default::default(),
1105 badges: Default::default(),
1106 bin: Vec::new(),
1107 bench: Vec::new(),
1108 test: Vec::new(),
1109 example: Vec::new(),
1110 lints: Default::default(),
1111 interconnect: self.spec.interconnect.clone(),
1112 };
1113 let toml_str = toml::to_string_pretty(&manifest).map_err(|e| {
1114 io::Error::new(
1115 io::ErrorKind::InvalidData,
1116 format!("Unable to serialize Module.toml: {}", e),
1117 )
1118 })?;
1119 append_file(&mut tar, "Module.toml", toml_str.as_bytes())?;
1120 tar.into_inner()?.finish()
1121 }
1122
1123 fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
1124 let tar = GzDecoder::new(bytes);
1125 let mut archive = tar::Archive::new(tar);
1126 let mut wasm = None;
1127 let mut spec: Option<PlaybookSpec> = None;
1128 let mut configurations = None;
1129 let mut legacy_configurations = None;
1130
1131 for file in archive.entries()? {
1132 let mut file = file?;
1133 let path = file.path()?.to_path_buf();
1134 let mut content = Vec::new();
1135 file.read_to_end(&mut content)?;
1136
1137 match path.to_string_lossy().as_ref() {
1138 "mod.wasm" => wasm = Some(content),
1139 "spec.json" => {
1140 spec = serde_json::from_slice(&content).map_err(|e| {
1141 io::Error::new(
1142 io::ErrorKind::InvalidData,
1143 format!("Unable to deserialize spec: {}", e),
1144 )
1145 })?;
1146 }
1147 "Module.toml" => {
1148 let mt: ModuleManifest<toml::Value> =
1149 toml::from_slice(&content).map_err(|e| {
1150 io::Error::new(
1151 io::ErrorKind::InvalidData,
1152 format!("Unable to deserialize Module.toml: {}", e),
1153 )
1154 })?;
1155 configurations = Some(mt.capabilities.into_values().collect());
1156 }
1157 "configurations.json" => {
1158 legacy_configurations =
1159 Some(serde_json::from_slice(&content).map_err(|e| {
1160 io::Error::new(
1161 io::ErrorKind::InvalidData,
1162 format!("Unable to deserialize configurations: {}", e),
1163 )
1164 })?);
1165 }
1166 _ => {}
1167 }
1168 }
1169
1170 let configurations = configurations.or(legacy_configurations).unwrap_or_default();
1171
1172 Ok(PlaybookBinary {
1173 wasm: wasm
1174 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Missing mod.wasm"))?,
1175 spec: spec
1176 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Missing spec.json"))?,
1177 configurations,
1178 })
1179 }
1180
1181 #[tracing::instrument(skip(path), fields(path = %path.display()))]
1182 async fn from_dir(path: &Path) -> Result<Self, io::Error> {
1183 tracing::debug!("Loading PlaybookBinary from directory");
1184 let spec_string = fs::read(path.join("spec.json")).await?;
1185 let spec: PlaybookSpec = serde_json::from_slice(&spec_string).map_err(|e| {
1186 let err = io::Error::new(
1187 io::ErrorKind::InvalidData,
1188 format!("Unable to deserialize spec: {}", e),
1189 );
1190 tracing::error!(error = ?err, "Failed to deserialize spec.json");
1191 err
1192 })?;
1193
1194 let configurations = if fs::try_exists(path.join("Module.toml"))
1195 .await
1196 .unwrap_or(false)
1197 {
1198 let toml_bytes = fs::read(path.join("Module.toml")).await?;
1199 let mt: ModuleManifest<toml::Value> = toml::from_slice(&toml_bytes).map_err(|e| {
1200 let err = io::Error::new(
1201 io::ErrorKind::InvalidData,
1202 format!("Unable to deserialize Module.toml: {}", e),
1203 );
1204 tracing::error!(error = ?err, "Failed to deserialize Module.toml");
1205 err
1206 })?;
1207 mt.capabilities.into_values().collect()
1208 } else if fs::try_exists(path.join("configurations.json"))
1209 .await
1210 .unwrap_or(false)
1211 {
1212 let configurations_string = fs::read(path.join("configurations.json")).await?;
1213 serde_json::from_slice(&configurations_string).map_err(|e| {
1214 let err = io::Error::new(
1215 io::ErrorKind::InvalidData,
1216 format!("Unable to deserialize configurations: {}", e),
1217 );
1218 tracing::error!(error = ?err, "Failed to deserialize configurations.json");
1219 err
1220 })?
1221 } else {
1222 Vec::new()
1223 };
1224
1225 Ok(PlaybookBinary {
1226 wasm: fs::read(path.join("mod.wasm")).await?,
1227 spec,
1228 configurations,
1229 })
1230 }
1231}
1232
1233impl Artifact for Playbook {
1234 #[tracing::instrument(skip(self, path), fields(path = %path.display()))]
1235 async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
1236 tracing::debug!("Writing Playbook to directory");
1237 match self {
1238 Playbook::Source(module_source) => module_source.write_to_directory(path).await,
1239 Playbook::Binary(module_binary) => module_binary.write_to_directory(path).await,
1240 }
1241 }
1242
1243 fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
1244 match self {
1245 Playbook::Source(module_source) => module_source.to_tarball(),
1246 Playbook::Binary(module_binary) => module_binary.to_tarball(),
1247 }
1248 }
1249
1250 fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
1251 let tar = GzDecoder::new(bytes);
1253 let mut archive = tar::Archive::new(tar);
1254
1255 let mut has_source_rs = false;
1256 let mut has_wasm = false;
1257
1258 for file in archive.entries()? {
1259 let file = file?;
1260 let path_str = file.path()?.to_string_lossy().into_owned();
1261
1262 match path_str.as_ref() {
1263 "source.rs" => has_source_rs = true,
1264 "mod.wasm" => has_wasm = true,
1265 _ => {}
1266 }
1267 }
1268
1269 if has_source_rs {
1270 Ok(Playbook::Source(PlaybookSource::from_tarball(bytes)?))
1271 } else if has_wasm {
1272 Ok(Playbook::Binary(PlaybookBinary::from_tarball(bytes)?))
1273 } else {
1274 Err(io::Error::new(
1275 io::ErrorKind::InvalidData,
1276 "Unknown module format in tarball: missing 'source.rs' or 'mod.wasm'",
1277 ))
1278 }
1279 }
1280
1281 #[tracing::instrument(skip(path), fields(path = %path.display()))]
1282 async fn from_dir(path: &Path) -> Result<Self, io::Error> {
1283 tracing::debug!("Loading Playbook from directory");
1284 if fs::try_exists(path.join("source.rs"))
1285 .await
1286 .unwrap_or(false)
1287 {
1288 Ok(Playbook::Source(PlaybookSource::from_dir(path).await?))
1289 } else if fs::try_exists(path.join("mod.wasm")).await.unwrap_or(false) {
1290 Ok(Playbook::Binary(PlaybookBinary::from_dir(path).await?))
1291 } else {
1292 Err(io::Error::new(
1293 io::ErrorKind::InvalidData,
1294 "Unknown module format in directory: missing 'source.rs' or 'mod.wasm'",
1295 ))
1296 }
1297 }
1298}
1299
1300impl Artifact for Artifacts {
1301 #[tracing::instrument(skip(self, path), fields(path = %path.display()))]
1302 async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
1303 tracing::debug!("Writing Artifacts to directory");
1304 match self {
1305 Artifacts::CapabilityBinary(c) => c.write_to_directory(path).await,
1306 Artifacts::CapabilitySource(c) => c.write_to_directory(path).await,
1307 Artifacts::Interface(i) => i.write_to_directory(path).await,
1308 Artifacts::Playbook(m) => m.write_to_directory(path).await,
1309 }
1310 }
1311
1312 fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
1313 match self {
1314 Artifacts::CapabilityBinary(c) => c.to_tarball(),
1315 Artifacts::CapabilitySource(c) => c.to_tarball(),
1316 Artifacts::Interface(i) => i.to_tarball(),
1317 Artifacts::Playbook(m) => m.to_tarball(),
1318 }
1319 }
1320
1321 fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
1322 let tar = GzDecoder::new(bytes);
1324 let mut archive = tar::Archive::new(tar);
1325
1326 let mut has_source_rs = false;
1327 let mut has_wasm = false;
1328 let mut has_cap_toml = false;
1329 let mut has_lib = false;
1330
1331 for file in archive.entries()? {
1332 let file = file?;
1333 let path_str = file.path()?.to_string_lossy().into_owned();
1334
1335 match path_str.as_ref() {
1336 "source.rs" => has_source_rs = true,
1337 "mod.wasm" => has_wasm = true,
1338 "Capability.toml" => has_cap_toml = true,
1339 "lib.dll" | "lib.dylib" | "lib.so" => has_lib = true,
1340 _ => {}
1341 }
1342 }
1343
1344 if has_source_rs || has_wasm {
1345 Ok(Artifacts::Playbook(Playbook::from_tarball(bytes)?))
1347 } else if has_cap_toml {
1348 if has_lib {
1349 Ok(Artifacts::CapabilitySource(CapabilitySource::from_tarball(
1351 bytes,
1352 )?))
1353 } else {
1354 Ok(Artifacts::Interface(Interface::from_tarball(bytes)?))
1355 }
1356 } else if has_lib {
1357 Ok(Artifacts::CapabilityBinary(CapabilityBinary::from_tarball(
1358 bytes,
1359 )?))
1360 } else {
1361 Err(io::Error::new(
1362 io::ErrorKind::InvalidData,
1363 "Unknown artifact format in tarball",
1364 ))
1365 }
1366 }
1367
1368 #[tracing::instrument(skip(path), fields(path = %path.display()))]
1369 async fn from_dir(path: &Path) -> Result<Self, io::Error> {
1370 tracing::debug!("Loading Artifacts from directory");
1371 let has_source_rs = fs::try_exists(path.join("source.rs"))
1372 .await
1373 .unwrap_or(false);
1374 let has_wasm = fs::try_exists(path.join("mod.wasm")).await.unwrap_or(false);
1375
1376 if has_source_rs || has_wasm {
1377 Ok(Artifacts::Playbook(Playbook::from_dir(path).await?))
1379 } else if fs::try_exists(path.join("Capability.toml"))
1380 .await
1381 .unwrap_or(false)
1382 {
1383 let has_dll = fs::try_exists(path.join("lib.dll")).await.unwrap_or(false);
1384 let has_dylib = fs::try_exists(path.join("lib.dylib"))
1385 .await
1386 .unwrap_or(false);
1387 let has_so = fs::try_exists(path.join("lib.so")).await.unwrap_or(false);
1388
1389 if has_dll || has_dylib || has_so {
1390 Ok(Artifacts::CapabilitySource(
1392 CapabilitySource::from_dir(path).await?,
1393 ))
1394 } else {
1395 Ok(Artifacts::Interface(Interface::from_dir(path).await?))
1396 }
1397 } else if fs::try_exists(path.join("lib.dll")).await.unwrap_or(false)
1398 || fs::try_exists(path.join("lib.dylib"))
1399 .await
1400 .unwrap_or(false)
1401 || fs::try_exists(path.join("lib.so")).await.unwrap_or(false)
1402 {
1403 Ok(Artifacts::CapabilityBinary(
1404 CapabilityBinary::from_dir(path).await?,
1405 ))
1406 } else {
1407 Err(io::Error::new(
1408 io::ErrorKind::InvalidData,
1409 "Unknown artifact format in directory",
1410 ))
1411 }
1412 }
1413}