wasmer_config/package/
mod.rs

1//! Wasmer package definitions.
2//!
3//! Describes the contents of a `wasmer.toml` file.
4
5#![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/// The ABI is a hint to WebAssembly runtimes about what additional imports to
37/// insert and how a module may be run.
38///
39/// If not specified, [`Abi::None`] is the default.
40#[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    /// Get the ABI's human-friendly name.
54    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    /// Is this a [`Abi::None`]?
63    pub fn is_none(&self) -> bool {
64        matches!(self, Abi::None)
65    }
66
67    /// Create an [`Abi`] from its human-friendly name.
68    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
92/// The default name for the manifest file.
93pub 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/// Package definition for a Wasmer package.
106///
107/// Usually stored in a `wasmer.toml` file.
108#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)]
109#[non_exhaustive]
110pub struct Package {
111    /// The package's name in the form `namespace/name`.
112    #[builder(setter(into, strip_option), default)]
113    pub name: Option<String>,
114    /// The package's version number.
115    #[builder(setter(into, strip_option), default)]
116    pub version: Option<Version>,
117    /// A brief description of the package.
118    #[builder(setter(into, strip_option), default)]
119    pub description: Option<String>,
120    /// A SPDX license specifier for this package.
121    #[builder(setter(into, strip_option), default)]
122    pub license: Option<String>,
123    /// The location of the license file, useful for non-standard licenses
124    #[serde(rename = "license-file")]
125    #[builder(setter(into, strip_option), default)]
126    pub license_file: Option<PathBuf>,
127    /// The package's README file.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    #[builder(setter(into, strip_option), default)]
130    pub readme: Option<PathBuf>,
131    /// A URL pointing to the package's source code.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    #[builder(setter(into, strip_option), default)]
134    pub repository: Option<String>,
135    /// The website used as the package's homepage.
136    #[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    /// Unlike, `disable-command-rename` which prevents `wasmer run <Module name>`,
158    /// this flag enables the command rename of `wasmer run <COMMAND_NAME>` into
159    /// just `<COMMAND_NAME>`. This is useful for programs that need to inspect
160    /// their `argv[0]` names and when the command name matches their executable
161    /// name.
162    #[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    /// The name of the command that should be used by `wasmer run` by default.
174    #[serde(skip_serializing_if = "Option::is_none")]
175    #[builder(setter(into, strip_option), default)]
176    pub entrypoint: Option<String>,
177    /// Mark this as a private package
178    #[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    /// Create a [`PackageBuilder`] populated with all mandatory fields.
189    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    /// Get the command's name.
215    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    /// Get the module this [`Command`] refers to.
223    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/// Describes a command for a wasmer module.
232///
233/// When a command is deserialized using [`CommandV1`], the runner is inferred
234/// by looking at the [`Abi`] from the [`Module`] it refers to.
235///
236/// If possible, prefer to use the [`CommandV2`] format.
237#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
238#[serde(deny_unknown_fields)] // Note: needed to prevent accidentally parsing
239// a CommandV2 as a CommandV1
240#[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/// An executable command.
249#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
250pub struct CommandV2 {
251    /// The name of the command.
252    pub name: String,
253    /// The module containing this command's executable.
254    pub module: ModuleReference,
255    /// The runner to use when running this command.
256    ///
257    /// This may be a URL, or the well-known runners `wasi` or `wcgi`
258    pub runner: String,
259    /// Extra annotations that will be consumed by the runner.
260    pub annotations: Option<CommandAnnotations>,
261}
262
263impl CommandV2 {
264    /// Get annotations, automatically loading them from a file relative to the
265    /// `wasmer.toml`'s directory, if necessary.
266    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/// A reference to a module which may or may not come from another package.
309///
310/// # Serialization
311///
312/// A [`ModuleReference`] is serialized via its [`String`] representation.
313#[derive(Clone, Debug, PartialEq)]
314pub enum ModuleReference {
315    /// A module in the current package.
316    CurrentPackage {
317        /// The name of the module.
318        module: String,
319    },
320    /// A module that will be provided by a dependency, in `dependency:module`
321    /// form.
322    Dependency {
323        /// The name of the dependency the module comes from.
324        dependency: String,
325        /// The name of the module.
326        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/// Annotations for a command.
456#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
457#[serde(untagged)]
458#[repr(C)]
459pub enum CommandAnnotations {
460    /// Annotations that will be read from a file on disk.
461    File(FileCommandAnnotations),
462    /// Annotations that are specified inline.
463    Raw(toml::Value),
464}
465
466/// Annotations on disk.
467#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
468pub struct FileCommandAnnotations {
469    /// The path to the annotations file.
470    pub file: PathBuf,
471    /// Which format are the annotations saved in?
472    pub kind: FileKind,
473}
474
475/// The different formats that [`FileCommandAnnotations`] can be saved in.
476#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize)]
477pub enum FileKind {
478    /// A `*.yaml` file that will be deserialized using [`serde_yaml`].
479    #[serde(rename = "yaml")]
480    Yaml,
481    /// A `*.json` file that will be deserialized using [`serde_json`].
482    #[serde(rename = "json")]
483    Json,
484}
485
486/// A file which may be executed by a [`Command`]. Sometimes also referred to as
487/// an "atom".
488#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
489pub struct Module {
490    /// The name used to refer to this module.
491    pub name: String,
492    /// The location of the module file on disk, relative to the manifest
493    /// directory.
494    pub source: PathBuf,
495    /// The ABI this module satisfies.
496    #[serde(default = "Abi::default", skip_serializing_if = "Abi::is_none")]
497    pub abi: Abi,
498    #[serde(default)]
499    pub kind: Option<String>,
500    /// WebAssembly interfaces this module requires.
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub interfaces: Option<IndexMap<String, String>>,
503    /// Interface definitions that can be used to generate bindings to this
504    /// module.
505    pub bindings: Option<Bindings>,
506    /// Miscellaneous annotations from the user.
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub annotations: Option<UserAnnotations>,
509}
510
511/// Miscellaneous annotations specified by the user.
512#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
513pub struct UserAnnotations {
514    pub suggested_compiler_optimizations: SuggestedCompilerOptimizations,
515}
516
517/// Suggested optimization that might be operated on the module when (and if) compiled.
518#[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/// The interface exposed by a [`Module`].
529#[derive(Clone, Debug, PartialEq, Eq)]
530pub enum Bindings {
531    Wit(WitBindings),
532    Wai(WaiBindings),
533}
534
535impl Bindings {
536    /// Get all files that make up this interface.
537    ///
538    /// For all binding types except [`WitBindings`], this will recursively
539    /// look for any files that are imported.
540    ///
541    /// The caller can assume that any path that was referenced exists.
542    pub fn referenced_files(&self, base_directory: &Path) -> Result<Vec<PathBuf>, ImportsError> {
543        match self {
544            Bindings::Wit(WitBindings { wit_exports, .. }) => {
545                // Note: we explicitly don't support imported files with WIT
546                // because wit-bindgen's wit-parser crate isn't on crates.io.
547
548                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    /// The version of the WIT format being used.
605    pub wit_bindgen: Version,
606    /// The `*.wit` file's location on disk.
607    pub wit_exports: PathBuf,
608}
609
610#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
611#[serde(rename_all = "kebab-case")]
612pub struct WaiBindings {
613    /// The version of the WAI format being used.
614    pub wai_version: Version,
615    /// The `*.wai` file defining the interface this package exposes.
616    pub exports: Option<PathBuf>,
617    /// The `*.wai` files for any functionality this package imports from the
618    /// host.
619    #[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        // Note: WAI files may import other WAI files, so we start with all
630        // WAI files mentioned in the wasmer.toml then recursively add their
631        // imports.
632
633        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
662/// Parse a `*.wai` file to find the absolute path for any other `*.wai` files
663/// it may import, relative to the original `*.wai` file.
664///
665/// This function makes sure any imported files exist.
666fn 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    // TODO(Michael-F-Bryan): update the wai-parser crate to give you access to
676    // the imported interfaces. For now, we just pretend there are no import
677    // statements in the *.wai file.
678    let raw_imports: Vec<String> = Vec::new();
679
680    // Note: imported paths in a *.wai file are all relative, so we need to
681    // resolve their absolute path relative to the original *.wai file.
682    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/// Errors that may occur when resolving [`Bindings`] imports.
701#[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/// The manifest represents the file used to describe a Wasm package.
729#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)]
730#[non_exhaustive]
731pub struct Manifest {
732    /// Metadata about the package itself.
733    pub package: Option<Package>,
734    /// The package's dependencies.
735    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
736    #[builder(default)]
737    pub dependencies: IndexMap<String, VersionReq>,
738    /// The mappings used when making bundled assets available to WebAssembly
739    /// instances, in the form guest -> host.
740    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
741    #[builder(default)]
742    pub fs: IndexMap<String, PathBuf>,
743    /// WebAssembly modules to be published.
744    #[serde(default, rename = "module", skip_serializing_if = "Vec::is_empty")]
745    #[builder(default)]
746    pub modules: Vec<Module>,
747    /// Commands the package makes available to users.
748    #[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    /// Create a [`ManifestBuilder`] populated with all mandatory fields.
765    pub fn builder(package: Package) -> ManifestBuilder {
766        ManifestBuilder::new(package)
767    }
768
769    /// Parse a [`Manifest`] from its TOML representation.
770    pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
771        toml::from_str(s)
772    }
773
774    /// Construct a manifest by searching in the specified directory for a
775    /// manifest file.
776    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    /// Validate this [`Manifest`] to check for common semantic errors.
802    ///
803    /// Some common error cases are:
804    ///
805    /// - Having multiple modules with the same name
806    /// - Having multiple commands with the same name
807    /// - A [`Command`] that references a non-existent [`Module`] in the current
808    ///   package
809    /// - A [`Package::entrypoint`] which points to a non-existent [`Command`]
810    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                    // We don't have access to the dependency so just assume
853                    // the module is correct.
854                    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    /// add a dependency
880    pub fn add_dependency(&mut self, dependency_name: String, dependency_version: VersionReq) {
881        self.dependencies
882            .insert(dependency_name, dependency_version);
883    }
884
885    /// remove dependency by package name
886    pub fn remove_dependency(&mut self, dependency_name: &str) -> Option<VersionReq> {
887        self.dependencies.remove(dependency_name)
888    }
889
890    /// Convert a [`Manifest`] to its TOML representation.
891    pub fn to_string(&self) -> anyhow::Result<String> {
892        let repr = toml::to_string_pretty(&self)?;
893        Ok(repr)
894    }
895
896    /// Write the manifest to permanent storage
897    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    /// Include a directory on the host in the package and make it available to
922    /// a WebAssembly guest at the `guest` path.
923    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    /// Add a dependency to the [`Manifest`].
931    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    /// Add a [`Module`] to the [`Manifest`].
939    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    /// Add a [`Command`] to the [`Manifest`].
945    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/// Errors that may occur while working with a [`Manifest`].
952#[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/// Errors that may be returned by [`Manifest::validate()`].
966#[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        // adding the same dependency twice changes nothing
1322        manifest.add_dependency(dependency_name.to_string(), dependency_version);
1323        assert_eq!(1, manifest.dependencies.len());
1324
1325        // adding a second different dependency will increase the count
1326        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}