wasmer_toml/
lib.rs

1//! The `wasmer.toml` file format.
2//!
3//! You'll typically start by deserializing into a [`Manifest`] and inspecting
4//! its properties.
5
6#![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/// The ABI is a hint to WebAssembly runtimes about what additional imports to
27/// insert and how a module may be run.
28///
29/// If not specified, [`Abi::None`] is the default.
30#[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    /// Get the ABI's human-friendly name.
46    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    /// Is this a [`Abi::None`]?
56    pub fn is_none(&self) -> bool {
57        matches!(self, Abi::None)
58    }
59
60    /// Create an [`Abi`] from its human-friendly name.
61    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
86/// The default name for the manifest file.
87pub 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/// Metadata about the package.
100#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)]
101#[non_exhaustive]
102pub struct Package {
103    /// The package's name in the form `namespace/name`.
104    #[builder(setter(into))]
105    pub name: String,
106    /// The package's version number.
107    pub version: Version,
108    /// A brief description of the package.
109    #[builder(setter(into))]
110    pub description: String,
111    /// A SPDX license specifier for this package.
112    #[builder(setter(into, strip_option), default)]
113    pub license: Option<String>,
114    /// The location of the license file, useful for non-standard licenses
115    #[serde(rename = "license-file")]
116    #[builder(setter(into, strip_option), default)]
117    pub license_file: Option<PathBuf>,
118    /// The package's README file.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    #[builder(setter(into, strip_option), default)]
121    pub readme: Option<PathBuf>,
122    /// A URL pointing to the package's source code.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    #[builder(setter(into, strip_option), default)]
125    pub repository: Option<String>,
126    /// The website used as the package's homepage.
127    #[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    /// Unlike, `disable-command-rename` which prevents `wasmer run <Module name>`,
149    /// this flag enables the command rename of `wasmer run <COMMAND_NAME>` into
150    /// just `<COMMAND_NAME>`. This is useful for programs that need to inspect
151    /// their `argv[0]` names and when the command name matches their executable
152    /// name.
153    #[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    /// The name of the command that should be used by `wasmer run` by default.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    #[builder(setter(into, strip_option), default)]
167    pub entrypoint: Option<String>,
168    /// Mark this as a private package
169    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
170    #[builder(default)]
171    pub private: bool,
172}
173
174impl Package {
175    /// Create a [`PackageBuilder`] populated with all mandatory fields.
176    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    /// Get the command's name.
202    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    /// Get the module this [`Command`] refers to.
210    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/// Describes a command for a wasmer module.
219///
220/// When a command is deserialized using [`CommandV1`], the runner is inferred
221/// by looking at the [`Abi`] from the [`Module`] it refers to.
222///
223/// If possible, prefer to use the [`CommandV2`] format.
224#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
225#[serde(deny_unknown_fields)] // Note: needed to prevent accidentally parsing
226// a CommandV2 as a CommandV1
227#[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/// An executable command.
236#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
237pub struct CommandV2 {
238    /// The name of the command.
239    pub name: String,
240    /// The module containing this command's executable.
241    pub module: ModuleReference,
242    /// The runner to use when running this command.
243    ///
244    /// This may be a URL, or the well-known runners `wasi`, `wcgi`, or
245    /// `emscripten`.
246    pub runner: String,
247    /// Extra annotations that will be consumed by the runner.
248    pub annotations: Option<CommandAnnotations>,
249}
250
251impl CommandV2 {
252    /// Get annotations, automatically loading them from a file relative to the
253    /// `wasmer.toml`'s directory, if necessary.
254    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/// A reference to a module which may or may not come from another package.
297///
298/// # Serialization
299///
300/// A [`ModuleReference`] is serialized via its [`String`] representation.
301#[derive(Clone, Debug, PartialEq)]
302pub enum ModuleReference {
303    /// A module in the current package.
304    CurrentPackage {
305        /// The name of the module.
306        module: String,
307    },
308    /// A module that will be provided by a dependency, in `dependency:module`
309    /// form.
310    Dependency {
311        /// The name of the dependency the module comes from.
312        dependency: String,
313        /// The name of the module.
314        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/// Annotations for a command.
444#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
445#[serde(untagged)]
446#[repr(C)]
447pub enum CommandAnnotations {
448    /// Annotations that will be read from a file on disk.
449    File(FileCommandAnnotations),
450    /// Annotations that are specified inline.
451    Raw(toml::Value),
452}
453
454/// Annotations on disk.
455#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
456pub struct FileCommandAnnotations {
457    /// The path to the annotations file.
458    pub file: PathBuf,
459    /// Which format are the annotations saved in?
460    pub kind: FileKind,
461}
462
463/// The different formats that [`FileCommandAnnotations`] can be saved in.
464#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize)]
465pub enum FileKind {
466    /// A `*.yaml` file that will be deserialized using [`serde_yaml`].
467    #[serde(rename = "yaml")]
468    Yaml,
469    /// A `*.json` file that will be deserialized using [`serde_json`].
470    #[serde(rename = "json")]
471    Json,
472}
473
474/// A file which may be executed by a [`Command`]. Sometimes also referred to as
475/// an "atom".
476#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
477pub struct Module {
478    /// The name used to refer to this module.
479    pub name: String,
480    /// The location of the module file on disk, relative to the manifest
481    /// directory.
482    pub source: PathBuf,
483    /// The ABI this module satisfies.
484    #[serde(default = "Abi::default", skip_serializing_if = "Abi::is_none")]
485    pub abi: Abi,
486    #[serde(default)]
487    pub kind: Option<String>,
488    /// WebAssembly interfaces this module requires.
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub interfaces: Option<HashMap<String, String>>,
491    /// Interface definitions that can be used to generate bindings to this
492    /// module.
493    pub bindings: Option<Bindings>,
494}
495
496/// The interface exposed by a [`Module`].
497#[derive(Clone, Debug, PartialEq, Eq)]
498pub enum Bindings {
499    Wit(WitBindings),
500    Wai(WaiBindings),
501}
502
503impl Bindings {
504    /// Get all files that make up this interface.
505    ///
506    /// For all binding types except [`WitBindings`], this will recursively
507    /// look for any files that are imported.
508    ///
509    /// The caller can assume that any path that was referenced exists.
510    pub fn referenced_files(&self, base_directory: &Path) -> Result<Vec<PathBuf>, ImportsError> {
511        match self {
512            Bindings::Wit(WitBindings { wit_exports, .. }) => {
513                // Note: we explicitly don't support imported files with WIT
514                // because wit-bindgen's wit-parser crate isn't on crates.io.
515
516                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    /// The version of the WIT format being used.
573    pub wit_bindgen: Version,
574    /// The `*.wit` file's location on disk.
575    pub wit_exports: PathBuf,
576}
577
578#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
579#[serde(rename_all = "kebab-case")]
580pub struct WaiBindings {
581    /// The version of the WAI format being used.
582    pub wai_version: Version,
583    /// The `*.wai` file defining the interface this package exposes.
584    pub exports: Option<PathBuf>,
585    /// The `*.wai` files for any functionality this package imports from the
586    /// host.
587    #[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        // Note: WAI files may import other WAI files, so we start with all
598        // WAI files mentioned in the wasmer.toml then recursively add their
599        // imports.
600
601        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
630/// Parse a `*.wai` file to find the absolute path for any other `*.wai` files
631/// it may import, relative to the original `*.wai` file.
632///
633/// This function makes sure any imported files exist.
634fn 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    // TODO(Michael-F-Bryan): update the wai-parser crate to give you access to
644    // the imported interfaces. For now, we just pretend there are no import
645    // statements in the *.wai file.
646    let raw_imports: Vec<String> = Vec::new();
647
648    // Note: imported paths in a *.wai file are all relative, so we need to
649    // resolve their absolute path relative to the original *.wai file.
650    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/// Errors that may occur when resolving [`Bindings`] imports.
669#[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/// The manifest represents the file used to describe a Wasm package.
697#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)]
698#[non_exhaustive]
699pub struct Manifest {
700    /// Metadata about the package itself.
701    pub package: Option<Package>,
702    /// The package's dependencies.
703    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
704    #[builder(default)]
705    pub dependencies: HashMap<String, VersionReq>,
706    /// The mappings used when making bundled assets available to WebAssembly
707    /// instances, in the form guest -> host.
708    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
709    #[builder(default)]
710    pub fs: IndexMap<String, PathBuf>,
711    /// WebAssembly modules to be published.
712    #[serde(default, rename = "module", skip_serializing_if = "Vec::is_empty")]
713    #[builder(default)]
714    pub modules: Vec<Module>,
715    /// Commands the package makes available to users.
716    #[serde(default, rename = "command", skip_serializing_if = "Vec::is_empty")]
717    #[builder(default)]
718    pub commands: Vec<Command>,
719}
720
721impl Manifest {
722    /// Create a [`ManifestBuilder`] populated with all mandatory fields.
723    pub fn builder(package: Package) -> ManifestBuilder {
724        ManifestBuilder::new(package)
725    }
726
727    /// Parse a [`Manifest`] from its TOML representation.
728    pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
729        toml::from_str(s)
730    }
731
732    /// Construct a manifest by searching in the specified directory for a
733    /// manifest file.
734    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    /// Validate this [`Manifest`] to check for common semantic errors.
760    ///
761    /// Some common error cases are:
762    ///
763    /// - Having multiple modules with the same name
764    /// - Having multiple commands with the same name
765    /// - A [`Command`] that references a non-existent [`Module`] in the current
766    ///   package
767    /// - A [`Package::entrypoint`] which points to a non-existent [`Command`]
768    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                    // We don't have access to the dependency so just assume
811                    // the module is correct.
812                    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    /// add a dependency
838    pub fn add_dependency(&mut self, dependency_name: String, dependency_version: VersionReq) {
839        self.dependencies
840            .insert(dependency_name, dependency_version);
841    }
842
843    /// remove dependency by package name
844    pub fn remove_dependency(&mut self, dependency_name: &str) -> Option<VersionReq> {
845        self.dependencies.remove(dependency_name)
846    }
847
848    /// Convert a [`Manifest`] to its TOML representation.
849    pub fn to_string(&self) -> anyhow::Result<String> {
850        let repr = toml::to_string_pretty(&self)?;
851        Ok(repr)
852    }
853
854    /// Write the manifest to permanent storage
855    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    /// Include a directory on the host in the package and make it available to
880    /// a WebAssembly guest at the `guest` path.
881    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    /// Add a dependency to the [`Manifest`].
889    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    /// Add a [`Module`] to the [`Manifest`].
897    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    /// Add a [`Command`] to the [`Manifest`].
903    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/// Errors that may occur while working with a [`Manifest`].
910#[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/// Errors that may be returned by [`Manifest::validate()`].
924#[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        // adding the same dependency twice changes nothing
1276        manifest.add_dependency(dependency_name.to_string(), dependency_version);
1277        assert_eq!(1, manifest.dependencies.len());
1278
1279        // adding a second different dependency will increase the count
1280        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}