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