1#![allow(deprecated)]
7
8pub extern crate serde_cbor;
9pub extern crate toml;
10
11pub mod rust;
12
13use std::{
14 borrow::Cow,
15 collections::{hash_map::HashMap, BTreeMap, BTreeSet},
16 fmt::{self, Display},
17 path::{Path, PathBuf},
18 str::FromStr,
19};
20
21use indexmap::IndexMap;
22use semver::{Version, VersionReq};
23use serde::{de::Error as _, Deserialize, Serialize};
24use thiserror::Error;
25
26#[derive(Clone, Copy, Default, Debug, Deserialize, Serialize, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Abi {
33 #[serde(rename = "emscripten")]
34 Emscripten,
35 #[default]
36 #[serde(rename = "none")]
37 None,
38 #[serde(rename = "wasi")]
39 Wasi,
40 #[serde(rename = "wasm4")]
41 WASM4,
42}
43
44impl Abi {
45 pub fn to_str(&self) -> &str {
47 match self {
48 Abi::Emscripten => "emscripten",
49 Abi::Wasi => "wasi",
50 Abi::WASM4 => "wasm4",
51 Abi::None => "generic",
52 }
53 }
54
55 pub fn is_none(&self) -> bool {
57 matches!(self, Abi::None)
58 }
59
60 pub fn from_name(name: &str) -> Self {
62 name.parse().unwrap_or(Abi::None)
63 }
64}
65
66impl fmt::Display for Abi {
67 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
68 write!(f, "{}", self.to_str())
69 }
70}
71
72impl FromStr for Abi {
73 type Err = Box<dyn std::error::Error + Send + Sync>;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 match s.to_lowercase().as_str() {
77 "emscripten" => Ok(Abi::Emscripten),
78 "wasi" => Ok(Abi::Wasi),
79 "wasm4" => Ok(Abi::WASM4),
80 "generic" => Ok(Abi::None),
81 _ => Err(format!("Unknown ABI, \"{s}\"").into()),
82 }
83 }
84}
85
86pub static MANIFEST_FILE_NAME: &str = "wasmer.toml";
88
89const README_PATHS: &[&str; 5] = &[
90 "README",
91 "README.md",
92 "README.markdown",
93 "README.mdown",
94 "README.mkdn",
95];
96
97const LICENSE_PATHS: &[&str; 3] = &["LICENSE", "LICENSE.md", "COPYING"];
98
99#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)]
101#[non_exhaustive]
102pub struct Package {
103 #[builder(setter(into))]
105 pub name: String,
106 pub version: Version,
108 #[builder(setter(into))]
110 pub description: String,
111 #[builder(setter(into, strip_option), default)]
113 pub license: Option<String>,
114 #[serde(rename = "license-file")]
116 #[builder(setter(into, strip_option), default)]
117 pub license_file: Option<PathBuf>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 #[builder(setter(into, strip_option), default)]
121 pub readme: Option<PathBuf>,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 #[builder(setter(into, strip_option), default)]
125 pub repository: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 #[builder(setter(into, strip_option), default)]
129 pub homepage: Option<String>,
130 #[serde(rename = "wasmer-extra-flags")]
131 #[builder(setter(into, strip_option), default)]
132 #[deprecated(
133 since = "0.9.2",
134 note = "Use runner-specific command attributes instead"
135 )]
136 pub wasmer_extra_flags: Option<String>,
137 #[serde(
138 rename = "disable-command-rename",
139 default,
140 skip_serializing_if = "std::ops::Not::not"
141 )]
142 #[builder(default)]
143 #[deprecated(
144 since = "0.9.2",
145 note = "Does nothing. Prefer a runner-specific command attribute instead"
146 )]
147 pub disable_command_rename: bool,
148 #[serde(
154 rename = "rename-commands-to-raw-command-name",
155 default,
156 skip_serializing_if = "std::ops::Not::not"
157 )]
158 #[builder(default)]
159 #[deprecated(
160 since = "0.9.2",
161 note = "Does nothing. Prefer a runner-specific command attribute instead"
162 )]
163 pub rename_commands_to_raw_command_name: bool,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 #[builder(setter(into, strip_option), default)]
167 pub entrypoint: Option<String>,
168 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
170 #[builder(default)]
171 pub private: bool,
172}
173
174impl Package {
175 pub fn builder(
177 name: impl Into<String>,
178 version: Version,
179 description: impl Into<String>,
180 ) -> PackageBuilder {
181 PackageBuilder::new(name, version, description)
182 }
183}
184
185impl PackageBuilder {
186 pub fn new(name: impl Into<String>, version: Version, description: impl Into<String>) -> Self {
187 let mut builder = PackageBuilder::default();
188 builder.name(name).version(version).description(description);
189 builder
190 }
191}
192
193#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
194#[serde(untagged)]
195pub enum Command {
196 V1(CommandV1),
197 V2(CommandV2),
198}
199
200impl Command {
201 pub fn get_name(&self) -> &str {
203 match self {
204 Self::V1(c) => &c.name,
205 Self::V2(c) => &c.name,
206 }
207 }
208
209 pub fn get_module(&self) -> &ModuleReference {
211 match self {
212 Self::V1(c) => &c.module,
213 Self::V2(c) => &c.module,
214 }
215 }
216}
217
218#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
225#[serde(deny_unknown_fields)] #[deprecated(since = "0.9.2", note = "Prefer the CommandV2 syntax")]
228pub struct CommandV1 {
229 pub name: String,
230 pub module: ModuleReference,
231 pub main_args: Option<String>,
232 pub package: Option<String>,
233}
234
235#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
237pub struct CommandV2 {
238 pub name: String,
240 pub module: ModuleReference,
242 pub runner: String,
247 pub annotations: Option<CommandAnnotations>,
249}
250
251impl CommandV2 {
252 pub fn get_annotations(&self, basepath: &Path) -> Result<Option<serde_cbor::Value>, String> {
255 match self.annotations.as_ref() {
256 Some(CommandAnnotations::Raw(v)) => Ok(Some(toml_to_cbor_value(v))),
257 Some(CommandAnnotations::File(FileCommandAnnotations { file, kind })) => {
258 let path = basepath.join(file.clone());
259 let file = std::fs::read_to_string(&path).map_err(|e| {
260 format!(
261 "Error reading {:?}.annotation ({:?}): {e}",
262 self.name,
263 path.display()
264 )
265 })?;
266 match kind {
267 FileKind::Json => {
268 let value: serde_json::Value =
269 serde_json::from_str(&file).map_err(|e| {
270 format!(
271 "Error reading {:?}.annotation ({:?}): {e}",
272 self.name,
273 path.display()
274 )
275 })?;
276 Ok(Some(json_to_cbor_value(&value)))
277 }
278 FileKind::Yaml => {
279 let value: serde_yaml::Value =
280 serde_yaml::from_str(&file).map_err(|e| {
281 format!(
282 "Error reading {:?}.annotation ({:?}): {e}",
283 self.name,
284 path.display()
285 )
286 })?;
287 Ok(Some(yaml_to_cbor_value(&value)))
288 }
289 }
290 }
291 None => Ok(None),
292 }
293 }
294}
295
296#[derive(Clone, Debug, PartialEq)]
302pub enum ModuleReference {
303 CurrentPackage {
305 module: String,
307 },
308 Dependency {
311 dependency: String,
313 module: String,
315 },
316}
317
318impl Serialize for ModuleReference {
319 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
320 where
321 S: serde::Serializer,
322 {
323 self.to_string().serialize(serializer)
324 }
325}
326
327impl<'de> Deserialize<'de> for ModuleReference {
328 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
329 where
330 D: serde::Deserializer<'de>,
331 {
332 let repr: Cow<'de, str> = Cow::deserialize(deserializer)?;
333 repr.parse().map_err(D::Error::custom)
334 }
335}
336
337impl FromStr for ModuleReference {
338 type Err = Box<dyn std::error::Error + Send + Sync>;
339
340 fn from_str(s: &str) -> Result<Self, Self::Err> {
341 match s.split_once(':') {
342 Some((dependency, module)) => {
343 if module.contains(':') {
344 return Err("Invalid format".into());
345 }
346
347 Ok(ModuleReference::Dependency {
348 dependency: dependency.to_string(),
349 module: module.to_string(),
350 })
351 }
352 None => Ok(ModuleReference::CurrentPackage {
353 module: s.to_string(),
354 }),
355 }
356 }
357}
358
359impl Display for ModuleReference {
360 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 match self {
362 ModuleReference::CurrentPackage { module } => Display::fmt(module, f),
363 ModuleReference::Dependency { dependency, module } => {
364 write!(f, "{dependency}:{module}")
365 }
366 }
367 }
368}
369
370fn toml_to_cbor_value(val: &toml::Value) -> serde_cbor::Value {
371 match val {
372 toml::Value::String(s) => serde_cbor::Value::Text(s.clone()),
373 toml::Value::Integer(i) => serde_cbor::Value::Integer(*i as i128),
374 toml::Value::Float(f) => serde_cbor::Value::Float(*f),
375 toml::Value::Boolean(b) => serde_cbor::Value::Bool(*b),
376 toml::Value::Datetime(d) => serde_cbor::Value::Text(format!("{}", d)),
377 toml::Value::Array(sq) => {
378 serde_cbor::Value::Array(sq.iter().map(toml_to_cbor_value).collect())
379 }
380 toml::Value::Table(m) => serde_cbor::Value::Map(
381 m.iter()
382 .map(|(k, v)| (serde_cbor::Value::Text(k.clone()), toml_to_cbor_value(v)))
383 .collect(),
384 ),
385 }
386}
387
388fn json_to_cbor_value(val: &serde_json::Value) -> serde_cbor::Value {
389 match val {
390 serde_json::Value::Null => serde_cbor::Value::Null,
391 serde_json::Value::Bool(b) => serde_cbor::Value::Bool(*b),
392 serde_json::Value::Number(n) => {
393 if let Some(i) = n.as_i64() {
394 serde_cbor::Value::Integer(i as i128)
395 } else if let Some(u) = n.as_u64() {
396 serde_cbor::Value::Integer(u as i128)
397 } else if let Some(f) = n.as_f64() {
398 serde_cbor::Value::Float(f)
399 } else {
400 serde_cbor::Value::Null
401 }
402 }
403 serde_json::Value::String(s) => serde_cbor::Value::Text(s.clone()),
404 serde_json::Value::Array(sq) => {
405 serde_cbor::Value::Array(sq.iter().map(json_to_cbor_value).collect())
406 }
407 serde_json::Value::Object(m) => serde_cbor::Value::Map(
408 m.iter()
409 .map(|(k, v)| (serde_cbor::Value::Text(k.clone()), json_to_cbor_value(v)))
410 .collect(),
411 ),
412 }
413}
414
415fn yaml_to_cbor_value(val: &serde_yaml::Value) -> serde_cbor::Value {
416 match val {
417 serde_yaml::Value::Null => serde_cbor::Value::Null,
418 serde_yaml::Value::Bool(b) => serde_cbor::Value::Bool(*b),
419 serde_yaml::Value::Number(n) => {
420 if let Some(i) = n.as_i64() {
421 serde_cbor::Value::Integer(i as i128)
422 } else if let Some(u) = n.as_u64() {
423 serde_cbor::Value::Integer(u as i128)
424 } else if let Some(f) = n.as_f64() {
425 serde_cbor::Value::Float(f)
426 } else {
427 serde_cbor::Value::Null
428 }
429 }
430 serde_yaml::Value::String(s) => serde_cbor::Value::Text(s.clone()),
431 serde_yaml::Value::Sequence(sq) => {
432 serde_cbor::Value::Array(sq.iter().map(yaml_to_cbor_value).collect())
433 }
434 serde_yaml::Value::Mapping(m) => serde_cbor::Value::Map(
435 m.iter()
436 .map(|(k, v)| (yaml_to_cbor_value(k), yaml_to_cbor_value(v)))
437 .collect(),
438 ),
439 serde_yaml::Value::Tagged(tag) => yaml_to_cbor_value(&tag.value),
440 }
441}
442
443#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
445#[serde(untagged)]
446#[repr(C)]
447pub enum CommandAnnotations {
448 File(FileCommandAnnotations),
450 Raw(toml::Value),
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
456pub struct FileCommandAnnotations {
457 pub file: PathBuf,
459 pub kind: FileKind,
461}
462
463#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize)]
465pub enum FileKind {
466 #[serde(rename = "yaml")]
468 Yaml,
469 #[serde(rename = "json")]
471 Json,
472}
473
474#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
477pub struct Module {
478 pub name: String,
480 pub source: PathBuf,
483 #[serde(default = "Abi::default", skip_serializing_if = "Abi::is_none")]
485 pub abi: Abi,
486 #[serde(default)]
487 pub kind: Option<String>,
488 #[serde(skip_serializing_if = "Option::is_none")]
490 pub interfaces: Option<HashMap<String, String>>,
491 pub bindings: Option<Bindings>,
494}
495
496#[derive(Clone, Debug, PartialEq, Eq)]
498pub enum Bindings {
499 Wit(WitBindings),
500 Wai(WaiBindings),
501}
502
503impl Bindings {
504 pub fn referenced_files(&self, base_directory: &Path) -> Result<Vec<PathBuf>, ImportsError> {
511 match self {
512 Bindings::Wit(WitBindings { wit_exports, .. }) => {
513 let path = base_directory.join(wit_exports);
517
518 if path.exists() {
519 Ok(vec![path])
520 } else {
521 Err(ImportsError::FileNotFound(path))
522 }
523 }
524 Bindings::Wai(wai) => wai.referenced_files(base_directory),
525 }
526 }
527}
528
529impl Serialize for Bindings {
530 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
531 where
532 S: serde::Serializer,
533 {
534 match self {
535 Bindings::Wit(w) => w.serialize(serializer),
536 Bindings::Wai(w) => w.serialize(serializer),
537 }
538 }
539}
540
541impl<'de> Deserialize<'de> for Bindings {
542 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
543 where
544 D: serde::Deserializer<'de>,
545 {
546 let value = toml::Value::deserialize(deserializer)?;
547
548 let keys = ["wit-bindgen", "wai-version"];
549 let [wit_bindgen, wai_version] = keys.map(|key| value.get(key).is_some());
550
551 match (wit_bindgen, wai_version) {
552 (true, false) => WitBindings::deserialize(value)
553 .map(Bindings::Wit)
554 .map_err(D::Error::custom),
555 (false, true) => WaiBindings::deserialize(value)
556 .map(Bindings::Wai)
557 .map_err(D::Error::custom),
558 (true, true) | (false, false) => {
559 let msg = format!(
560 "expected one of \"{}\" to be provided, but not both",
561 keys.join("\" or \""),
562 );
563 Err(D::Error::custom(msg))
564 }
565 }
566 }
567}
568
569#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
570#[serde(rename_all = "kebab-case")]
571pub struct WitBindings {
572 pub wit_bindgen: Version,
574 pub wit_exports: PathBuf,
576}
577
578#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
579#[serde(rename_all = "kebab-case")]
580pub struct WaiBindings {
581 pub wai_version: Version,
583 pub exports: Option<PathBuf>,
585 #[serde(default, skip_serializing_if = "Vec::is_empty")]
588 pub imports: Vec<PathBuf>,
589}
590
591impl WaiBindings {
592 fn referenced_files(&self, base_directory: &Path) -> Result<Vec<PathBuf>, ImportsError> {
593 let WaiBindings {
594 exports, imports, ..
595 } = self;
596
597 let initial_paths = exports
602 .iter()
603 .chain(imports)
604 .map(|relative_path| base_directory.join(relative_path));
605
606 let mut to_check: Vec<PathBuf> = Vec::new();
607
608 for path in initial_paths {
609 if !path.exists() {
610 return Err(ImportsError::FileNotFound(path));
611 }
612 to_check.push(path);
613 }
614
615 let mut files = BTreeSet::new();
616
617 while let Some(path) = to_check.pop() {
618 if files.contains(&path) {
619 continue;
620 }
621
622 to_check.extend(get_imported_wai_files(&path)?);
623 files.insert(path);
624 }
625
626 Ok(files.into_iter().collect())
627 }
628}
629
630fn get_imported_wai_files(path: &Path) -> Result<Vec<PathBuf>, ImportsError> {
635 let _wai_src = std::fs::read_to_string(path).map_err(|error| ImportsError::Read {
636 path: path.to_path_buf(),
637 error,
638 })?;
639
640 let parent_dir = path.parent()
641 .expect("All paths should have a parent directory because we joined them relative to the base directory");
642
643 let raw_imports: Vec<String> = Vec::new();
647
648 let mut resolved_paths = Vec::new();
651
652 for imported in raw_imports {
653 let absolute_path = parent_dir.join(imported);
654
655 if !absolute_path.exists() {
656 return Err(ImportsError::ImportedFileNotFound {
657 path: absolute_path,
658 referenced_by: path.to_path_buf(),
659 });
660 }
661
662 resolved_paths.push(absolute_path);
663 }
664
665 Ok(resolved_paths)
666}
667
668#[derive(Debug, thiserror::Error)]
670#[non_exhaustive]
671pub enum ImportsError {
672 #[error(
673 "The \"{}\" mentioned in the manifest doesn't exist",
674 _0.display(),
675 )]
676 FileNotFound(PathBuf),
677 #[error(
678 "The \"{}\" imported by \"{}\" doesn't exist",
679 path.display(),
680 referenced_by.display(),
681 )]
682 ImportedFileNotFound {
683 path: PathBuf,
684 referenced_by: PathBuf,
685 },
686 #[error("Unable to parse \"{}\" as a WAI file", path.display())]
687 WaiParse { path: PathBuf },
688 #[error("Unable to read \"{}\"", path.display())]
689 Read {
690 path: PathBuf,
691 #[source]
692 error: std::io::Error,
693 },
694}
695
696#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)]
698#[non_exhaustive]
699pub struct Manifest {
700 pub package: Option<Package>,
702 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
704 #[builder(default)]
705 pub dependencies: HashMap<String, VersionReq>,
706 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
709 #[builder(default)]
710 pub fs: IndexMap<String, PathBuf>,
711 #[serde(default, rename = "module", skip_serializing_if = "Vec::is_empty")]
713 #[builder(default)]
714 pub modules: Vec<Module>,
715 #[serde(default, rename = "command", skip_serializing_if = "Vec::is_empty")]
717 #[builder(default)]
718 pub commands: Vec<Command>,
719}
720
721impl Manifest {
722 pub fn builder(package: Package) -> ManifestBuilder {
724 ManifestBuilder::new(package)
725 }
726
727 pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
729 toml::from_str(s)
730 }
731
732 pub fn find_in_directory<T: AsRef<Path>>(path: T) -> Result<Self, ManifestError> {
735 let path = path.as_ref();
736
737 if !path.is_dir() {
738 return Err(ManifestError::MissingManifest(path.to_path_buf()));
739 }
740 let manifest_path_buf = path.join(MANIFEST_FILE_NAME);
741 let contents = std::fs::read_to_string(&manifest_path_buf)
742 .map_err(|_e| ManifestError::MissingManifest(manifest_path_buf))?;
743 let mut manifest: Self = toml::from_str(contents.as_str())?;
744
745 if let Some(mut package) = manifest.package.as_mut() {
746 if package.readme.is_none() {
747 package.readme = locate_file(path, README_PATHS);
748 }
749
750 if package.license_file.is_none() {
751 package.license_file = locate_file(path, LICENSE_PATHS);
752 }
753 }
754 manifest.validate()?;
755
756 Ok(manifest)
757 }
758
759 pub fn validate(&self) -> Result<(), ValidationError> {
769 let mut modules = BTreeMap::new();
770
771 for module in &self.modules {
772 let is_duplicate = modules.insert(&module.name, module).is_some();
773
774 if is_duplicate {
775 return Err(ValidationError::DuplicateModule {
776 name: module.name.clone(),
777 });
778 }
779 }
780
781 let mut commands = BTreeMap::new();
782
783 for command in &self.commands {
784 let is_duplicate = commands.insert(command.get_name(), command).is_some();
785
786 if is_duplicate {
787 return Err(ValidationError::DuplicateCommand {
788 name: command.get_name().to_string(),
789 });
790 }
791
792 let module_reference = command.get_module();
793 match &module_reference {
794 ModuleReference::CurrentPackage { module } => {
795 if let Some(module) = modules.get(&module) {
796 if module.abi == Abi::None && module.interfaces.is_none() {
797 return Err(ValidationError::MissingABI {
798 command: command.get_name().to_string(),
799 module: module.name.clone(),
800 });
801 }
802 } else {
803 return Err(ValidationError::MissingModuleForCommand {
804 command: command.get_name().to_string(),
805 module: command.get_module().clone(),
806 });
807 }
808 }
809 ModuleReference::Dependency { dependency, .. } => {
810 if !self.dependencies.contains_key(dependency) {
813 return Err(ValidationError::MissingDependency {
814 command: command.get_name().to_string(),
815 dependency: dependency.clone(),
816 module_ref: module_reference.clone(),
817 });
818 }
819 }
820 }
821 }
822
823 if let Some(package) = &self.package {
824 if let Some(entrypoint) = package.entrypoint.as_deref() {
825 if !commands.contains_key(entrypoint) {
826 return Err(ValidationError::InvalidEntrypoint {
827 entrypoint: entrypoint.to_string(),
828 available_commands: commands.keys().map(ToString::to_string).collect(),
829 });
830 }
831 }
832 }
833
834 Ok(())
835 }
836
837 pub fn add_dependency(&mut self, dependency_name: String, dependency_version: VersionReq) {
839 self.dependencies
840 .insert(dependency_name, dependency_version);
841 }
842
843 pub fn remove_dependency(&mut self, dependency_name: &str) -> Option<VersionReq> {
845 self.dependencies.remove(dependency_name)
846 }
847
848 pub fn to_string(&self) -> anyhow::Result<String> {
850 let repr = toml::to_string_pretty(&self)?;
851 Ok(repr)
852 }
853
854 pub fn save(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
856 let manifest = toml::to_string_pretty(self)?;
857 std::fs::write(path, manifest).map_err(ManifestError::CannotSaveManifest)?;
858 Ok(())
859 }
860}
861
862fn locate_file(path: &Path, candidates: &[&str]) -> Option<PathBuf> {
863 for filename in candidates {
864 let path_buf = path.join(filename);
865 if path_buf.exists() {
866 return Some(filename.into());
867 }
868 }
869 None
870}
871
872impl ManifestBuilder {
873 pub fn new(package: Package) -> Self {
874 let mut builder = ManifestBuilder::default();
875 builder.package(Some(package));
876 builder
877 }
878
879 pub fn map_fs(&mut self, guest: impl Into<String>, host: impl Into<PathBuf>) -> &mut Self {
882 self.fs
883 .get_or_insert_with(IndexMap::new)
884 .insert(guest.into(), host.into());
885 self
886 }
887
888 pub fn with_dependency(&mut self, name: impl Into<String>, version: VersionReq) -> &mut Self {
890 self.dependencies
891 .get_or_insert_with(HashMap::new)
892 .insert(name.into(), version);
893 self
894 }
895
896 pub fn with_module(&mut self, module: Module) -> &mut Self {
898 self.modules.get_or_insert_with(Vec::new).push(module);
899 self
900 }
901
902 pub fn with_command(&mut self, command: Command) -> &mut Self {
904 self.commands.get_or_insert_with(Vec::new).push(command);
905 self
906 }
907}
908
909#[derive(Debug, Error)]
911#[non_exhaustive]
912pub enum ManifestError {
913 #[error("Manifest file not found at \"{}\"", _0.display())]
914 MissingManifest(PathBuf),
915 #[error("Could not save manifest file: {0}.")]
916 CannotSaveManifest(#[source] std::io::Error),
917 #[error("Could not parse manifest because {0}.")]
918 TomlParseError(#[from] toml::de::Error),
919 #[error("There was an error validating the manifest")]
920 ValidationError(#[from] ValidationError),
921}
922
923#[derive(Debug, PartialEq, Error)]
925#[non_exhaustive]
926pub enum ValidationError {
927 #[error(
928 "missing ABI field on module, \"{module}\", used by command, \"{command}\"; an ABI of `wasi` or `emscripten` is required",
929 )]
930 MissingABI { command: String, module: String },
931 #[error("missing module, \"{module}\", in manifest used by command, \"{command}\"")]
932 MissingModuleForCommand {
933 command: String,
934 module: ModuleReference,
935 },
936 #[error("The \"{command}\" command refers to a nonexistent dependency, \"{dependency}\" in \"{module_ref}\"")]
937 MissingDependency {
938 command: String,
939 dependency: String,
940 module_ref: ModuleReference,
941 },
942 #[error("The entrypoint, \"{entrypoint}\", isn't a valid command (commands: {})", available_commands.join(", "))]
943 InvalidEntrypoint {
944 entrypoint: String,
945 available_commands: Vec<String>,
946 },
947 #[error("Duplicate module, \"{name}\"")]
948 DuplicateModule { name: String },
949 #[error("Duplicate command, \"{name}\"")]
950 DuplicateCommand { name: String },
951}
952
953#[cfg(test)]
954mod tests {
955 use std::fmt::Debug;
956
957 use serde::{de::DeserializeOwned, Deserialize};
958 use toml::toml;
959
960 use super::*;
961
962 #[test]
963 fn test_to_string() {
964 Manifest {
965 package: Some(Package {
966 name: "package/name".to_string(),
967 version: Version::parse("1.0.0").unwrap(),
968 description: "test".to_string(),
969 license: None,
970 license_file: None,
971 readme: None,
972 repository: None,
973 homepage: None,
974 wasmer_extra_flags: None,
975 disable_command_rename: false,
976 rename_commands_to_raw_command_name: false,
977 entrypoint: None,
978 private: false,
979 }),
980 dependencies: HashMap::new(),
981 modules: vec![Module {
982 name: "test".to_string(),
983 abi: Abi::Wasi,
984 bindings: None,
985 interfaces: None,
986 kind: Some("https://webc.org/kind/wasi".to_string()),
987 source: Path::new("test.wasm").to_path_buf(),
988 }],
989 commands: Vec::new(),
990 fs: vec![
991 ("a".to_string(), Path::new("/a").to_path_buf()),
992 ("b".to_string(), Path::new("/b").to_path_buf()),
993 ]
994 .into_iter()
995 .collect(),
996 }
997 .to_string()
998 .unwrap();
999 }
1000
1001 #[test]
1002 fn interface_test() {
1003 let manifest_str = r#"
1004[package]
1005name = "test"
1006version = "0.0.0"
1007description = "This is a test package"
1008license = "MIT"
1009
1010[[module]]
1011name = "mod"
1012source = "target/wasm32-wasi/release/mod.wasm"
1013interfaces = {"wasi" = "0.0.0-unstable"}
1014
1015[[module]]
1016name = "mod-with-exports"
1017source = "target/wasm32-wasi/release/mod-with-exports.wasm"
1018bindings = { wit-exports = "exports.wit", wit-bindgen = "0.0.0" }
1019
1020[[command]]
1021name = "command"
1022module = "mod"
1023"#;
1024 let manifest: Manifest = Manifest::parse(manifest_str).unwrap();
1025 let modules = &manifest.modules;
1026 assert_eq!(
1027 modules[0].interfaces.as_ref().unwrap().get("wasi"),
1028 Some(&"0.0.0-unstable".to_string())
1029 );
1030
1031 assert_eq!(
1032 modules[1],
1033 Module {
1034 name: "mod-with-exports".to_string(),
1035 source: PathBuf::from("target/wasm32-wasi/release/mod-with-exports.wasm"),
1036 abi: Abi::None,
1037 kind: None,
1038 interfaces: None,
1039 bindings: Some(Bindings::Wit(WitBindings {
1040 wit_exports: PathBuf::from("exports.wit"),
1041 wit_bindgen: "0.0.0".parse().unwrap()
1042 })),
1043 },
1044 );
1045 }
1046
1047 #[test]
1048 fn parse_wit_bindings() {
1049 let table = toml! {
1050 name = "..."
1051 source = "..."
1052 bindings = { wit-bindgen = "0.1.0", wit-exports = "./file.wit" }
1053 };
1054
1055 let module = Module::deserialize(table).unwrap();
1056
1057 assert_eq!(
1058 module.bindings.as_ref().unwrap(),
1059 &Bindings::Wit(WitBindings {
1060 wit_bindgen: "0.1.0".parse().unwrap(),
1061 wit_exports: PathBuf::from("./file.wit"),
1062 }),
1063 );
1064 assert_round_trippable(&module);
1065 }
1066
1067 #[test]
1068 fn parse_wai_bindings() {
1069 let table = toml! {
1070 name = "..."
1071 source = "..."
1072 bindings = { wai-version = "0.1.0", exports = "./file.wai", imports = ["a.wai", "../b.wai"] }
1073 };
1074
1075 let module = Module::deserialize(table).unwrap();
1076
1077 assert_eq!(
1078 module.bindings.as_ref().unwrap(),
1079 &Bindings::Wai(WaiBindings {
1080 wai_version: "0.1.0".parse().unwrap(),
1081 exports: Some(PathBuf::from("./file.wai")),
1082 imports: vec![PathBuf::from("a.wai"), PathBuf::from("../b.wai")],
1083 }),
1084 );
1085 assert_round_trippable(&module);
1086 }
1087
1088 #[track_caller]
1089 fn assert_round_trippable<T>(value: &T)
1090 where
1091 T: Serialize + DeserializeOwned + PartialEq + Debug,
1092 {
1093 let repr = toml::to_string(value).unwrap();
1094 let round_tripped: T = toml::from_str(&repr).unwrap();
1095 assert_eq!(
1096 round_tripped, *value,
1097 "The value should convert to/from TOML losslessly"
1098 );
1099 }
1100
1101 #[test]
1102 fn imports_and_exports_are_optional_with_wai() {
1103 let table = toml! {
1104 name = "..."
1105 source = "..."
1106 bindings = { wai-version = "0.1.0" }
1107 };
1108
1109 let module = Module::deserialize(table).unwrap();
1110
1111 assert_eq!(
1112 module.bindings.as_ref().unwrap(),
1113 &Bindings::Wai(WaiBindings {
1114 wai_version: "0.1.0".parse().unwrap(),
1115 exports: None,
1116 imports: Vec::new(),
1117 }),
1118 );
1119 assert_round_trippable(&module);
1120 }
1121
1122 #[test]
1123 fn ambiguous_bindings_table() {
1124 let table = toml! {
1125 wai-version = "0.2.0"
1126 wit-bindgen = "0.1.0"
1127 };
1128
1129 let err = Bindings::deserialize(table).unwrap_err();
1130
1131 assert_eq!(
1132 err.to_string(),
1133 "expected one of \"wit-bindgen\" or \"wai-version\" to be provided, but not both\n"
1134 );
1135 }
1136
1137 #[test]
1138 fn bindings_table_that_is_neither_wit_nor_wai() {
1139 let table = toml! {
1140 wai-bindgen = "lol, this should have been wai-version"
1141 exports = "./file.wai"
1142 };
1143
1144 let err = Bindings::deserialize(table).unwrap_err();
1145
1146 assert_eq!(
1147 err.to_string(),
1148 "expected one of \"wit-bindgen\" or \"wai-version\" to be provided, but not both\n"
1149 );
1150 }
1151
1152 #[test]
1153 fn command_v2_isnt_ambiguous_with_command_v1() {
1154 let src = r#"
1155[package]
1156name = "hotg-ai/sine"
1157version = "0.12.0"
1158description = "sine"
1159
1160[dependencies]
1161"hotg-ai/train_test_split" = "0.12.1"
1162"hotg-ai/elastic_net" = "0.12.1"
1163
1164[[module]] # This is the same as atoms
1165name = "sine"
1166kind = "tensorflow-SavedModel" # It can also be "wasm" (default)
1167source = "models/sine"
1168
1169[[command]]
1170name = "run"
1171runner = "rune"
1172module = "sine"
1173annotations = { file = "Runefile.yml", kind = "yaml" }
1174"#;
1175
1176 let manifest: Manifest = toml::from_str(src).unwrap();
1177
1178 let commands = &manifest.commands;
1179 assert_eq!(commands.len(), 1);
1180 assert_eq!(
1181 commands[0],
1182 Command::V2(CommandV2 {
1183 name: "run".into(),
1184 module: "sine".parse().unwrap(),
1185 runner: "rune".into(),
1186 annotations: Some(CommandAnnotations::File(FileCommandAnnotations {
1187 file: "Runefile.yml".into(),
1188 kind: FileKind::Yaml,
1189 }))
1190 })
1191 );
1192 }
1193
1194 #[test]
1195 fn get_manifest() {
1196 let wasmer_toml = toml! {
1197 [package]
1198 name = "test"
1199 version = "1.0.0"
1200 repository = "test.git"
1201 homepage = "test.com"
1202 description = "The best package."
1203 };
1204 let manifest: Manifest = wasmer_toml.try_into().unwrap();
1205 if let Some(package) = manifest.package {
1206 assert!(!package.disable_command_rename);
1207 }
1208 }
1209
1210 #[test]
1211 fn parse_manifest_without_package_section() {
1212 let wasmer_toml = toml! {
1213 [[module]]
1214 name = "test-module"
1215 source = "data.wasm"
1216 abi = "wasi"
1217 };
1218 let manifest: Manifest = wasmer_toml.try_into().unwrap();
1219 assert!(manifest.package.is_none());
1220 }
1221
1222 #[test]
1223 fn get_commands() {
1224 let wasmer_toml = toml! {
1225 [package]
1226 name = "test"
1227 version = "1.0.0"
1228 repository = "test.git"
1229 homepage = "test.com"
1230 description = "The best package."
1231 [[module]]
1232 name = "test-pkg"
1233 module = "target.wasm"
1234 source = "source.wasm"
1235 description = "description"
1236 interfaces = {"wasi" = "0.0.0-unstable"}
1237 [[command]]
1238 name = "foo"
1239 module = "test"
1240 [[command]]
1241 name = "baz"
1242 module = "test"
1243 main_args = "$@"
1244 };
1245 let manifest: Manifest = wasmer_toml.try_into().unwrap();
1246 let commands = &manifest.commands;
1247 assert_eq!(2, commands.len());
1248 }
1249
1250 #[test]
1251 fn add_new_dependency() {
1252 let tmp_dir = tempfile::tempdir().unwrap();
1253 let tmp_dir_path: &std::path::Path = tmp_dir.as_ref();
1254 let manifest_path = tmp_dir_path.join(MANIFEST_FILE_NAME);
1255 let wasmer_toml = toml! {
1256 [package]
1257 name = "_/test"
1258 version = "1.0.0"
1259 description = "description"
1260 [[module]]
1261 name = "test"
1262 source = "test.wasm"
1263 interfaces = {}
1264 };
1265 let toml_string = toml::to_string(&wasmer_toml).unwrap();
1266 std::fs::write(manifest_path, toml_string).unwrap();
1267 let mut manifest = Manifest::find_in_directory(tmp_dir).unwrap();
1268
1269 let dependency_name = "dep_pkg";
1270 let dependency_version: VersionReq = "0.1.0".parse().unwrap();
1271
1272 manifest.add_dependency(dependency_name.to_string(), dependency_version.clone());
1273 assert_eq!(1, manifest.dependencies.len());
1274
1275 manifest.add_dependency(dependency_name.to_string(), dependency_version);
1277 assert_eq!(1, manifest.dependencies.len());
1278
1279 let dependency_name_2 = "dep_pkg_2";
1281 let dependency_version_2: VersionReq = "0.2.0".parse().unwrap();
1282 manifest.add_dependency(dependency_name_2.to_string(), dependency_version_2);
1283 assert_eq!(2, manifest.dependencies.len());
1284 }
1285
1286 #[test]
1287 fn duplicate_modules_are_invalid() {
1288 let wasmer_toml = toml! {
1289 [package]
1290 name = "some/package"
1291 version = "0.0.0"
1292 description = ""
1293 [[module]]
1294 name = "test"
1295 source = "test.wasm"
1296 [[module]]
1297 name = "test"
1298 source = "test.wasm"
1299 };
1300 let manifest = Manifest::deserialize(wasmer_toml).unwrap();
1301
1302 let error = manifest.validate().unwrap_err();
1303
1304 assert_eq!(
1305 error,
1306 ValidationError::DuplicateModule {
1307 name: "test".to_string()
1308 }
1309 );
1310 }
1311
1312 #[test]
1313 fn duplicate_commands_are_invalid() {
1314 let wasmer_toml = toml! {
1315 [package]
1316 name = "some/package"
1317 version = "0.0.0"
1318 description = ""
1319 [[module]]
1320 name = "test"
1321 source = "test.wasm"
1322 abi = "wasi"
1323 [[command]]
1324 name = "cmd"
1325 module = "test"
1326 [[command]]
1327 name = "cmd"
1328 module = "test"
1329 };
1330 let manifest = Manifest::deserialize(wasmer_toml).unwrap();
1331
1332 let error = manifest.validate().unwrap_err();
1333
1334 assert_eq!(
1335 error,
1336 ValidationError::DuplicateCommand {
1337 name: "cmd".to_string()
1338 }
1339 );
1340 }
1341
1342 #[test]
1343 fn nonexistent_entrypoint() {
1344 let wasmer_toml = toml! {
1345 [package]
1346 name = "some/package"
1347 version = "0.0.0"
1348 description = ""
1349 entrypoint = "this-doesnt-exist"
1350 [[module]]
1351 name = "test"
1352 source = "test.wasm"
1353 abi = "wasi"
1354 [[command]]
1355 name = "cmd"
1356 module = "test"
1357 };
1358 let manifest = Manifest::deserialize(wasmer_toml).unwrap();
1359
1360 let error = manifest.validate().unwrap_err();
1361
1362 assert_eq!(
1363 error,
1364 ValidationError::InvalidEntrypoint {
1365 entrypoint: "this-doesnt-exist".to_string(),
1366 available_commands: vec!["cmd".to_string()]
1367 }
1368 );
1369 }
1370
1371 #[test]
1372 fn command_with_nonexistent_module() {
1373 let wasmer_toml = toml! {
1374 [package]
1375 name = "some/package"
1376 version = "0.0.0"
1377 description = ""
1378 [[command]]
1379 name = "cmd"
1380 module = "this-doesnt-exist"
1381 };
1382 let manifest = Manifest::deserialize(wasmer_toml).unwrap();
1383
1384 let error = manifest.validate().unwrap_err();
1385
1386 assert_eq!(
1387 error,
1388 ValidationError::MissingModuleForCommand {
1389 command: "cmd".to_string(),
1390 module: "this-doesnt-exist".parse().unwrap()
1391 }
1392 );
1393 }
1394
1395 #[test]
1396 fn use_builder_api_to_create_simplest_manifest() {
1397 let package =
1398 Package::builder("my/package", "1.0.0".parse().unwrap(), "My awesome package")
1399 .build()
1400 .unwrap();
1401 let manifest = Manifest::builder(package).build().unwrap();
1402
1403 manifest.validate().unwrap();
1404 }
1405
1406 #[test]
1407 fn deserialize_command_referring_to_module_from_dependency() {
1408 let wasmer_toml = toml! {
1409 [package]
1410 name = "some/package"
1411 version = "0.0.0"
1412 description = ""
1413
1414 [dependencies]
1415 dep = "1.2.3"
1416
1417 [[command]]
1418 name = "cmd"
1419 module = "dep:module"
1420 };
1421 let manifest = Manifest::deserialize(wasmer_toml).unwrap();
1422
1423 let command = manifest
1424 .commands
1425 .iter()
1426 .find(|cmd| cmd.get_name() == "cmd")
1427 .unwrap();
1428
1429 assert_eq!(
1430 command.get_module(),
1431 &ModuleReference::Dependency {
1432 dependency: "dep".to_string(),
1433 module: "module".to_string()
1434 }
1435 );
1436 }
1437
1438 #[test]
1439 fn command_with_module_from_nonexistent_dependency() {
1440 let wasmer_toml = toml! {
1441 [package]
1442 name = "some/package"
1443 version = "0.0.0"
1444 description = ""
1445 [[command]]
1446 name = "cmd"
1447 module = "dep:module"
1448 };
1449 let manifest = Manifest::deserialize(wasmer_toml).unwrap();
1450
1451 let error = manifest.validate().unwrap_err();
1452
1453 assert_eq!(
1454 error,
1455 ValidationError::MissingDependency {
1456 command: "cmd".to_string(),
1457 dependency: "dep".to_string(),
1458 module_ref: ModuleReference::Dependency {
1459 dependency: "dep".to_string(),
1460 module: "module".to_string()
1461 }
1462 }
1463 );
1464 }
1465
1466 #[test]
1467 fn round_trip_dependency_module_ref() {
1468 let original = ModuleReference::Dependency {
1469 dependency: "my/dep".to_string(),
1470 module: "module".to_string(),
1471 };
1472
1473 let repr = original.to_string();
1474 let round_tripped: ModuleReference = repr.parse().unwrap();
1475
1476 assert_eq!(round_tripped, original);
1477 }
1478}