wasmer_package/package/
manifest.rs

1use std::{
2    collections::BTreeMap,
3    path::{Path, PathBuf},
4};
5
6use ciborium::{Value, cbor};
7use semver::VersionReq;
8use sha2::Digest;
9use shared_buffer::{MmapError, OwnedBuffer};
10use url::Url;
11#[allow(deprecated)]
12use wasmer_config::package::{CommandV1, CommandV2, Manifest as WasmerManifest, Package};
13use wasmer_config::package::{SuggestedCompilerOptimizations, UserAnnotations};
14use webc::{
15    indexmap::{self, IndexMap},
16    metadata::AtomSignature,
17    sanitize_path,
18};
19
20use crate::utils::features_to_wasm_annotations;
21
22use webc::metadata::{
23    Atom, Binding, Command, Manifest as WebcManifest, UrlOrManifest, WaiBindings, WitBindings,
24    annotations::{
25        Atom as AtomAnnotation, FileSystemMapping, FileSystemMappings, VolumeSpecificPath, Wapm,
26        Wasi,
27    },
28};
29
30use super::{FsVolume, Strictness};
31
32const METADATA_VOLUME: &str = FsVolume::METADATA;
33
34/// Errors that may occur when converting from a `wasmer_config::package::Manifest`
35/// into a WebC manifest.
36#[derive(Debug, thiserror::Error)]
37#[non_exhaustive]
38pub enum ManifestError {
39    /// A dependency specification had a syntax error.
40    #[error("The dependency, \"{_0}\", isn't in the \"namespace/name\" format")]
41    InvalidDependency(String),
42    /// Unable to serialize an annotation.
43    #[error("Unable to serialize the \"{key}\" annotation")]
44    SerializeCborAnnotation {
45        /// Which annotation was being serialized?
46        key: String,
47        /// The underlying error.
48        #[source]
49        error: ciborium::value::Error,
50    },
51    /// Specified an unknown atom kind.
52    #[error("Unknown atom kind, \"{_0}\"")]
53    UnknownAtomKind(String),
54    /// A module was specified more than once.
55    #[error("Duplicate module, \"{_0}\"")]
56    DuplicateModule(String),
57    /// Unable to read a module's `source`.
58    #[error("Unable to read the \"{module}\" module's file from \"{}\"", path.display())]
59    ReadAtomFile {
60        /// The name of the module.
61        module: String,
62        /// The path that was read.
63        path: PathBuf,
64        /// The underlying error.
65        #[source]
66        error: std::io::Error,
67    },
68    /// A command was specified more than once.
69    #[error("Duplicate command, \"{_0}\"")]
70    DuplicateCommand(String),
71    /// An unknown runner kind was specified.
72    #[error("Unknown runner kind, \"{_0}\"")]
73    UnknownRunnerKind(String),
74    /// An error occurred while merging user-defined annotations in with
75    /// automatically generated ones.
76    #[error("Unable to merge in user-defined \"{key}\" annotations for the \"{command}\" command")]
77    #[non_exhaustive]
78    MergeAnnotations {
79        /// The command annotations were being merged for.
80        command: String,
81        /// The annotation that was being merged.
82        key: String,
83    },
84    /// A command uses a non-existent module.
85    #[error("The \"{command}\" command uses a non-existent module, \"{module}\"")]
86    InvalidModuleReference {
87        /// The command name.
88        command: String,
89        /// The module name.
90        module: String,
91    },
92    /// A command references a module from an undeclared dependency.
93    #[error("The \"{command}\" command references the undeclared dependency \"{dependency}\"")]
94    UndeclaredCommandDependency {
95        /// The command name.
96        command: String,
97        /// The dependency name.
98        dependency: String,
99    },
100    /// Unable to deserialize custom annotations from the `wasmer.toml`
101    /// manifest.
102    #[error("Unable to deserialize custom annotations from the wasmer.toml manifest")]
103    WasmerTomlAnnotations {
104        /// The underlying error.
105        #[source]
106        error: Box<dyn std::error::Error + Send + Sync>,
107    },
108    /// The `wasmer.toml` file references a file outside of its base directory.
109    #[error("\"{}\" is outside of \"{}\"", path.display(), base_dir.display())]
110    OutsideBaseDirectory {
111        /// The file that was referenced.
112        path: PathBuf,
113        /// The base directory.
114        base_dir: PathBuf,
115    },
116    /// The manifest references a file that doesn't exist.
117    #[error("The \"{}\" doesn't exist (base dir: {})", path.display(), base_dir.display())]
118    MissingFile {
119        /// The file that was referenced.
120        path: PathBuf,
121        /// The base directory.
122        base_dir: PathBuf,
123    },
124    /// File based commands are not supported for in-memory package creation
125    #[error("File based commands are not supported for in-memory package creation")]
126    FileNotSupported,
127}
128
129/// take a `wasmer.toml` manifest and convert it to the `*.webc` equivalent.
130pub(crate) fn wasmer_manifest_to_webc(
131    manifest: &WasmerManifest,
132    base_dir: &Path,
133    strictness: Strictness,
134) -> Result<(WebcManifest, BTreeMap<String, OwnedBuffer>), ManifestError> {
135    let use_map = transform_dependencies(&manifest.dependencies)?;
136
137    // Note: We need to clone the [fs] table because the wasmer-toml crate has
138    // already upgraded to indexmap v2.0, but the webc crate needs to stay at
139    // 1.9.2 for backwards compatibility reasons.
140    let fs: IndexMap<String, PathBuf> = manifest.fs.clone().into_iter().collect();
141
142    let package =
143        transform_package_annotations(manifest.package.as_ref(), &fs, base_dir, strictness)?;
144    let (atoms, atom_files) = transform_atoms(manifest, base_dir)?;
145    let commands = transform_commands(manifest, base_dir)?;
146    let bindings = transform_bindings(manifest, base_dir)?;
147
148    let manifest = WebcManifest {
149        origin: None,
150        use_map,
151        package,
152        atoms,
153        commands,
154        bindings,
155        entrypoint: entrypoint(manifest),
156    };
157
158    Ok((manifest, atom_files))
159}
160
161/// take a `wasmer.toml` manifest and convert it to the `*.webc` equivalent.
162pub(crate) fn in_memory_wasmer_manifest_to_webc(
163    manifest: &WasmerManifest,
164    atoms: &BTreeMap<String, (Option<String>, OwnedBuffer, Option<&UserAnnotations>)>,
165) -> Result<(WebcManifest, BTreeMap<String, OwnedBuffer>), ManifestError> {
166    let use_map = transform_dependencies(&manifest.dependencies)?;
167
168    // Note: We need to clone the [fs] table because the wasmer-toml crate has
169    // already upgraded to indexmap v2.0, but the webc crate needs to stay at
170    // 1.9.2 for backwards compatibility reasons.
171    let fs: IndexMap<String, PathBuf> = manifest.fs.clone().into_iter().collect();
172
173    let package = transform_in_memory_package_annotations(manifest.package.as_ref(), &fs)?;
174    let (atoms, atom_files) = transform_in_memory_atoms(atoms)?;
175    let commands = transform_in_memory_commands(manifest)?;
176    let bindings = transform_in_memory_bindings(manifest)?;
177
178    let manifest = WebcManifest {
179        origin: None,
180        use_map,
181        package,
182        atoms,
183        commands,
184        bindings,
185        entrypoint: entrypoint(manifest),
186    };
187
188    Ok((manifest, atom_files))
189}
190
191fn transform_package_annotations(
192    package: Option<&wasmer_config::package::Package>,
193    fs: &IndexMap<String, PathBuf>,
194    base_dir: &Path,
195    strictness: Strictness,
196) -> Result<IndexMap<String, Value>, ManifestError> {
197    transform_package_annotations_shared(package, fs, |package| {
198        transform_package_meta_to_annotations(package, base_dir, strictness)
199    })
200}
201
202fn transform_in_memory_package_annotations(
203    package: Option<&wasmer_config::package::Package>,
204    fs: &IndexMap<String, PathBuf>,
205) -> Result<IndexMap<String, Value>, ManifestError> {
206    transform_package_annotations_shared(package, fs, |package| {
207        transform_in_memory_package_meta_to_annotations(package)
208    })
209}
210
211fn transform_package_annotations_shared(
212    package: Option<&wasmer_config::package::Package>,
213    fs: &IndexMap<String, PathBuf>,
214    transform_package_meta_to_annotations: impl Fn(&Package) -> Result<Wapm, ManifestError>,
215) -> Result<IndexMap<String, Value>, ManifestError> {
216    let mut annotations = IndexMap::new();
217
218    if let Some(wasmer_package) = package {
219        let wapm = transform_package_meta_to_annotations(wasmer_package)?;
220        insert_annotation(&mut annotations, Wapm::KEY, wapm)?;
221    }
222
223    let fs = get_fs_table(fs);
224
225    if !fs.is_empty() {
226        insert_annotation(&mut annotations, FileSystemMappings::KEY, fs)?;
227    }
228
229    Ok(annotations)
230}
231
232fn transform_dependencies(
233    original_dependencies: &IndexMap<String, VersionReq>,
234) -> Result<IndexMap<String, UrlOrManifest>, ManifestError> {
235    let mut dependencies = IndexMap::new();
236
237    for (dep, version) in original_dependencies {
238        let (namespace, package_name) = extract_dependency_parts(dep)
239            .ok_or_else(|| ManifestError::InvalidDependency(dep.clone()))?;
240
241        // Note: the wasmer.toml format forces you to go through a registry for
242        // all dependencies. There's no way to specify a URL-based dependency.
243        let dependency_specifier =
244            UrlOrManifest::RegistryDependentUrl(format!("{namespace}/{package_name}@{version}"));
245
246        dependencies.insert(dep.clone(), dependency_specifier);
247    }
248
249    Ok(dependencies)
250}
251
252fn extract_dependency_parts(dep: &str) -> Option<(&str, &str)> {
253    let (namespace, package_name) = dep.split_once('/')?;
254
255    fn invalid_char(c: char) -> bool {
256        !matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '-' | '0'..='9')
257    }
258
259    if namespace.contains(invalid_char) || package_name.contains(invalid_char) {
260        None
261    } else {
262        Some((namespace, package_name))
263    }
264}
265
266type Atoms = BTreeMap<String, OwnedBuffer>;
267
268fn transform_atoms(
269    manifest: &WasmerManifest,
270    base_dir: &Path,
271) -> Result<(IndexMap<String, Atom>, Atoms), ManifestError> {
272    let mut atom_entries = BTreeMap::new();
273
274    for module in &manifest.modules {
275        let name = &module.name;
276        let path = base_dir.join(&module.source);
277        let file = open_file(&path).map_err(|error| ManifestError::ReadAtomFile {
278            module: name.clone(),
279            path,
280            error,
281        })?;
282
283        atom_entries.insert(
284            name.clone(),
285            (module.kind.clone(), file, module.annotations.as_ref()),
286        );
287    }
288
289    transform_atoms_shared(&atom_entries)
290}
291
292fn transform_in_memory_atoms(
293    atoms: &BTreeMap<String, (Option<String>, OwnedBuffer, Option<&UserAnnotations>)>,
294) -> Result<(IndexMap<String, Atom>, Atoms), ManifestError> {
295    transform_atoms_shared(atoms)
296}
297
298fn transform_atoms_shared(
299    atoms: &BTreeMap<String, (Option<String>, OwnedBuffer, Option<&UserAnnotations>)>,
300) -> Result<(IndexMap<String, Atom>, Atoms), ManifestError> {
301    let mut atom_files = BTreeMap::new();
302    let mut metadata = IndexMap::new();
303
304    for (name, (kind, content, misc_annotations)) in atoms.iter() {
305        // Create atom with annotations including Wasm features if available
306        let mut annotations = IndexMap::new();
307        if let Some(misc_annotations) = misc_annotations
308            && let Some(pass_params) = misc_annotations
309                .suggested_compiler_optimizations
310                .pass_params
311        {
312            annotations.insert(
313                SuggestedCompilerOptimizations::KEY.to_string(),
314                cbor!({"pass_params" => pass_params}).unwrap(),
315            );
316        }
317
318        // Detect required WebAssembly features by analyzing the module binary
319        let features_result = wasmer_types::Features::detect_from_wasm(content);
320
321        if let Ok(features) = features_result {
322            // Convert wasmer_types::Features to webc::metadata::annotations::Wasm
323            let feature_strings = features_to_wasm_annotations(&features);
324
325            // Only create annotation if we detected features
326            if !feature_strings.is_empty() {
327                let wasm = webc::metadata::annotations::Wasm::new(feature_strings);
328                match ciborium::value::Value::serialized(&wasm) {
329                    Ok(wasm_value) => {
330                        annotations.insert(
331                            webc::metadata::annotations::Wasm::KEY.to_string(),
332                            wasm_value,
333                        );
334                    }
335                    Err(e) => {
336                        eprintln!("Failed to serialize wasm features: {e}");
337                    }
338                }
339            }
340        }
341
342        let atom = Atom {
343            kind: atom_kind(kind.as_ref().map(|s| s.as_str()))?,
344            signature: atom_signature(content),
345            annotations,
346        };
347
348        if metadata.contains_key(name) {
349            return Err(ManifestError::DuplicateModule(name.clone()));
350        }
351
352        metadata.insert(name.clone(), atom);
353        atom_files.insert(name.clone(), content.clone());
354    }
355
356    Ok((metadata, atom_files))
357}
358
359fn atom_signature(atom: &[u8]) -> String {
360    let hash: [u8; 32] = sha2::Sha256::digest(atom).into();
361    AtomSignature::Sha256(hash).to_string()
362}
363
364/// Map the "kind" field in a `[module]` to the corresponding URI.
365fn atom_kind(kind: Option<&str>) -> Result<Url, ManifestError> {
366    const WASM_ATOM_KIND: &str = "https://webc.org/kind/wasm";
367    const TENSORFLOW_SAVED_MODEL_KIND: &str = "https://webc.org/kind/tensorflow-SavedModel";
368
369    let url = match kind {
370        Some("wasm") | None => WASM_ATOM_KIND.parse().expect("Should never fail"),
371        Some("tensorflow-SavedModel") => TENSORFLOW_SAVED_MODEL_KIND
372            .parse()
373            .expect("Should never fail"),
374        Some(other) => {
375            if let Ok(url) = Url::parse(other) {
376                // if it is a valid URL, pass that through as-is
377                url
378            } else {
379                return Err(ManifestError::UnknownAtomKind(other.to_string()));
380            }
381        }
382    };
383
384    Ok(url)
385}
386
387/// Try to open a file, preferring mmap and falling back to [`std::fs::read()`]
388/// if mapping fails.
389fn open_file(path: &Path) -> Result<OwnedBuffer, std::io::Error> {
390    match OwnedBuffer::mmap(path) {
391        Ok(b) => return Ok(b),
392        Err(MmapError::Map(_)) => {
393            // Unable to mmap the atom file. Falling back to std::fs::read()
394        }
395        Err(MmapError::FileOpen { error, .. }) => {
396            return Err(error);
397        }
398    }
399
400    let bytes = std::fs::read(path)?;
401
402    Ok(OwnedBuffer::from_bytes(bytes))
403}
404
405fn insert_annotation(
406    annotations: &mut IndexMap<String, ciborium::Value>,
407    key: impl Into<String>,
408    value: impl serde::Serialize,
409) -> Result<(), ManifestError> {
410    let key = key.into();
411
412    match ciborium::value::Value::serialized(&value) {
413        Ok(value) => {
414            annotations.insert(key, value);
415            Ok(())
416        }
417        Err(error) => Err(ManifestError::SerializeCborAnnotation { key, error }),
418    }
419}
420
421fn get_fs_table(fs: &IndexMap<String, PathBuf>) -> FileSystemMappings {
422    if fs.is_empty() {
423        return FileSystemMappings::default();
424    }
425
426    // When wapm-targz-to-pirita creates the final webc all files will be
427    // merged into one "atom" volume, but we want to map each directory
428    // separately.
429    let mut entries = Vec::new();
430    for (guest, host) in fs {
431        let volume_name = host
432            .to_str()
433            .expect("failed to convert path to string")
434            .to_string();
435
436        let volume_name = sanitize_path(volume_name);
437
438        let mapping = FileSystemMapping {
439            from: None,
440            volume_name,
441            host_path: None,
442            mount_path: sanitize_path(guest),
443        };
444        entries.push(mapping);
445    }
446
447    FileSystemMappings(entries)
448}
449
450fn transform_package_meta_to_annotations(
451    package: &wasmer_config::package::Package,
452    base_dir: &Path,
453    strictness: Strictness,
454) -> Result<Wapm, ManifestError> {
455    fn metadata_file(
456        path: Option<&PathBuf>,
457        base_dir: &Path,
458        strictness: Strictness,
459    ) -> Result<Option<VolumeSpecificPath>, ManifestError> {
460        let path = match path {
461            Some(p) => p,
462            None => return Ok(None),
463        };
464
465        let absolute_path = base_dir.join(path);
466
467        // Touch the file to make sure it actually exists
468        if !absolute_path.exists() {
469            match strictness.missing_file(path, base_dir) {
470                Ok(_) => return Ok(None),
471                Err(e) => {
472                    return Err(e);
473                }
474            }
475        }
476
477        match base_dir.join(path).strip_prefix(base_dir) {
478            Ok(without_prefix) => Ok(Some(VolumeSpecificPath {
479                volume: METADATA_VOLUME.to_string(),
480                path: sanitize_path(without_prefix),
481            })),
482            Err(_) => match strictness.outside_base_directory(path, base_dir) {
483                Ok(_) => Ok(None),
484                Err(e) => Err(e),
485            },
486        }
487    }
488
489    transform_package_meta_to_annotations_shared(package, |path| {
490        metadata_file(path, base_dir, strictness)
491    })
492}
493
494fn transform_in_memory_package_meta_to_annotations(
495    package: &wasmer_config::package::Package,
496) -> Result<Wapm, ManifestError> {
497    transform_package_meta_to_annotations_shared(package, |path| {
498        Ok(path.map(|readme_file| VolumeSpecificPath {
499            volume: METADATA_VOLUME.to_string(),
500            path: sanitize_path(readme_file),
501        }))
502    })
503}
504
505fn transform_package_meta_to_annotations_shared(
506    package: &wasmer_config::package::Package,
507    volume_specific_path: impl Fn(Option<&PathBuf>) -> Result<Option<VolumeSpecificPath>, ManifestError>,
508) -> Result<Wapm, ManifestError> {
509    let mut wapm = Wapm::new(
510        package.name.clone(),
511        package.version.clone().map(|v| v.to_string()),
512        package.description.clone(),
513    );
514
515    wapm.license = package.license.clone();
516    wapm.license_file = volume_specific_path(package.license_file.as_ref())?;
517    wapm.readme = volume_specific_path(package.readme.as_ref())?;
518    wapm.repository = package.repository.clone();
519    wapm.homepage = package.homepage.clone();
520    wapm.private = package.private;
521
522    Ok(wapm)
523}
524
525fn transform_commands(
526    manifest: &WasmerManifest,
527    base_dir: &Path,
528) -> Result<IndexMap<String, Command>, ManifestError> {
529    trasform_commands_shared(
530        manifest,
531        |cmd| transform_command_v1(cmd, manifest),
532        |cmd| transform_command_v2(cmd, base_dir),
533    )
534}
535
536fn transform_in_memory_commands(
537    manifest: &WasmerManifest,
538) -> Result<IndexMap<String, Command>, ManifestError> {
539    trasform_commands_shared(
540        manifest,
541        |cmd| transform_command_v1(cmd, manifest),
542        transform_in_memory_command_v2,
543    )
544}
545
546#[allow(deprecated)]
547fn trasform_commands_shared(
548    manifest: &WasmerManifest,
549    transform_command_v1: impl Fn(&CommandV1) -> Result<Command, ManifestError>,
550    transform_command_v2: impl Fn(&CommandV2) -> Result<Command, ManifestError>,
551) -> Result<IndexMap<String, Command>, ManifestError> {
552    let mut commands = IndexMap::new();
553
554    for command in &manifest.commands {
555        let cmd = match command {
556            wasmer_config::package::Command::V1(cmd) => transform_command_v1(cmd)?,
557            wasmer_config::package::Command::V2(cmd) => transform_command_v2(cmd)?,
558        };
559
560        // If a command uses a module from a dependency, then ensure that
561        // the dependency is declared.
562        match command.get_module() {
563            wasmer_config::package::ModuleReference::CurrentPackage { .. } => {}
564            wasmer_config::package::ModuleReference::Dependency { dependency, .. } => {
565                if !manifest.dependencies.contains_key(dependency) {
566                    return Err(ManifestError::UndeclaredCommandDependency {
567                        command: command.get_name().to_string(),
568                        dependency: dependency.to_string(),
569                    });
570                }
571            }
572        }
573
574        match commands.entry(command.get_name().to_string()) {
575            indexmap::map::Entry::Occupied(_) => {
576                return Err(ManifestError::DuplicateCommand(
577                    command.get_name().to_string(),
578                ));
579            }
580            indexmap::map::Entry::Vacant(entry) => {
581                entry.insert(cmd);
582            }
583        }
584    }
585
586    Ok(commands)
587}
588
589#[allow(deprecated)]
590fn transform_command_v1(
591    cmd: &wasmer_config::package::CommandV1,
592    manifest: &WasmerManifest,
593) -> Result<Command, ManifestError> {
594    // Note: a key difference between CommandV1 and CommandV2 is that v1 uses
595    // a module's "abi" field to figure out which runner to use, whereas v2 has
596    // a dedicated "runner" field and ignores module.abi.
597    let runner = match &cmd.module {
598        wasmer_config::package::ModuleReference::CurrentPackage { module } => {
599            let module = manifest
600                .modules
601                .iter()
602                .find(|m| m.name == module.as_str())
603                .ok_or_else(|| ManifestError::InvalidModuleReference {
604                    command: cmd.name.clone(),
605                    module: cmd.module.to_string(),
606                })?;
607
608            RunnerKind::from_name(module.abi.to_str())?
609        }
610        wasmer_config::package::ModuleReference::Dependency { .. } => {
611            // Note: We don't have any visibility into dependencies (this code
612            // doesn't do resolution), so we blindly assume it's a WASI command.
613            // That should be fine because people shouldn't use the CommandV1
614            // syntax any more.
615            RunnerKind::Wasi
616        }
617    };
618
619    let mut annotations = IndexMap::new();
620    // Splitting by whitespace isn't really correct, but proper shell splitting
621    // would require a dependency and CommandV1 won't be used any more, anyway.
622    let main_args = cmd
623        .main_args
624        .as_deref()
625        .map(|args| args.split_whitespace().map(String::from).collect());
626    runner.runner_specific_annotations(
627        &mut annotations,
628        &cmd.module,
629        cmd.package.clone(),
630        main_args,
631    )?;
632
633    Ok(Command {
634        runner: runner.uri().to_string(),
635        annotations,
636    })
637}
638
639fn transform_command_v2(
640    cmd: &wasmer_config::package::CommandV2,
641    base_dir: &Path,
642) -> Result<Command, ManifestError> {
643    transform_command_v2_shared(cmd, || {
644        cmd.get_annotations(base_dir)
645            .map_err(|error| ManifestError::WasmerTomlAnnotations {
646                error: error.into(),
647            })
648    })
649}
650
651fn transform_in_memory_command_v2(
652    cmd: &wasmer_config::package::CommandV2,
653) -> Result<Command, ManifestError> {
654    transform_command_v2_shared(cmd, || {
655        cmd.annotations
656            .as_ref()
657            .map(|a| match a {
658                wasmer_config::package::CommandAnnotations::File(_) => {
659                    Err(ManifestError::FileNotSupported)
660                }
661                wasmer_config::package::CommandAnnotations::Raw(v) => Ok(toml_to_cbor_value(v)),
662            })
663            .transpose()
664    })
665}
666
667fn transform_command_v2_shared(
668    cmd: &wasmer_config::package::CommandV2,
669    custom_annotations: impl Fn() -> Result<Option<Value>, ManifestError>,
670) -> Result<Command, ManifestError> {
671    let runner = RunnerKind::from_name(&cmd.runner)?;
672    let mut annotations = IndexMap::new();
673
674    runner.runner_specific_annotations(&mut annotations, &cmd.module, None, None)?;
675
676    let custom_annotations = custom_annotations()?;
677
678    if let Some(ciborium::Value::Map(custom_annotations)) = custom_annotations {
679        for (key, value) in custom_annotations {
680            if let ciborium::Value::Text(key) = key {
681                match annotations.entry(key) {
682                    indexmap::map::Entry::Occupied(mut entry) => {
683                        merge_cbor(entry.get_mut(), value).map_err(|_| {
684                            ManifestError::MergeAnnotations {
685                                command: cmd.name.clone(),
686                                key: entry.key().clone(),
687                            }
688                        })?;
689                    }
690                    indexmap::map::Entry::Vacant(entry) => {
691                        entry.insert(value);
692                    }
693                }
694            }
695        }
696    }
697
698    Ok(Command {
699        runner: runner.uri().to_string(),
700        annotations,
701    })
702}
703
704fn toml_to_cbor_value(val: &toml::value::Value) -> ciborium::Value {
705    match val {
706        toml::Value::String(s) => ciborium::Value::Text(s.clone()),
707        toml::Value::Integer(i) => ciborium::Value::Integer(ciborium::value::Integer::from(*i)),
708        toml::Value::Float(f) => ciborium::Value::Float(*f),
709        toml::Value::Boolean(b) => ciborium::Value::Bool(*b),
710        toml::Value::Datetime(d) => ciborium::Value::Text(format!("{d}")),
711        toml::Value::Array(sq) => {
712            ciborium::Value::Array(sq.iter().map(toml_to_cbor_value).collect())
713        }
714        toml::Value::Table(m) => ciborium::Value::Map(
715            m.iter()
716                .map(|(k, v)| (ciborium::Value::Text(k.clone()), toml_to_cbor_value(v)))
717                .collect(),
718        ),
719    }
720}
721
722fn merge_cbor(original: &mut Value, addition: Value) -> Result<(), ()> {
723    match (original, addition) {
724        (Value::Map(left), Value::Map(right)) => {
725            for (k, v) in right {
726                if let Some(entry) = left.iter_mut().find(|lk| lk.0 == k) {
727                    merge_cbor(&mut entry.1, v)?;
728                } else {
729                    left.push((k, v));
730                }
731            }
732        }
733        (Value::Array(left), Value::Array(right)) => {
734            left.extend(right);
735        }
736        // Primitives that have the same values are fine
737        (Value::Bool(left), Value::Bool(right)) if *left == right => {}
738        (Value::Bytes(left), Value::Bytes(right)) if *left == right => {}
739        (Value::Float(left), Value::Float(right)) if *left == right => {}
740        (Value::Integer(left), Value::Integer(right)) if *left == right => {}
741        (Value::Text(left), Value::Text(right)) if *left == right => {}
742        // null can be overwritten
743        (original @ Value::Null, value) => {
744            *original = value;
745        }
746        (_original, Value::Null) => {}
747        // Oh well, we tried...
748        (_left, _right) => {
749            return Err(());
750        }
751    }
752
753    Ok(())
754}
755
756#[derive(Debug, Clone, PartialEq)]
757enum RunnerKind {
758    Wasi,
759    Wcgi,
760    Wasm4,
761    Other(Url),
762}
763
764impl RunnerKind {
765    fn from_name(name: &str) -> Result<Self, ManifestError> {
766        match name {
767            "wasi" | "wasi@unstable_" | webc::metadata::annotations::WASI_RUNNER_URI => {
768                Ok(RunnerKind::Wasi)
769            }
770            "generic" => {
771                // This is what you get with a CommandV1 and abi = "none"
772                Ok(RunnerKind::Wasi)
773            }
774            "wcgi" | webc::metadata::annotations::WCGI_RUNNER_URI => Ok(RunnerKind::Wcgi),
775            "wasm4" | webc::metadata::annotations::WASM4_RUNNER_URI => Ok(RunnerKind::Wasm4),
776            other => {
777                if let Ok(other) = Url::parse(other) {
778                    Ok(RunnerKind::Other(other))
779                } else if let Ok(other) = format!("https://webc.org/runner/{other}").parse() {
780                    // fall back to something under webc.org
781                    Ok(RunnerKind::Other(other))
782                } else {
783                    Err(ManifestError::UnknownRunnerKind(other.to_string()))
784                }
785            }
786        }
787    }
788
789    fn uri(&self) -> &str {
790        match self {
791            RunnerKind::Wasi => webc::metadata::annotations::WASI_RUNNER_URI,
792            RunnerKind::Wcgi => webc::metadata::annotations::WCGI_RUNNER_URI,
793            RunnerKind::Wasm4 => webc::metadata::annotations::WASM4_RUNNER_URI,
794            RunnerKind::Other(other) => other.as_str(),
795        }
796    }
797
798    #[allow(deprecated)]
799    fn runner_specific_annotations(
800        &self,
801        annotations: &mut IndexMap<String, Value>,
802        module: &wasmer_config::package::ModuleReference,
803        package: Option<String>,
804        main_args: Option<Vec<String>>,
805    ) -> Result<(), ManifestError> {
806        let atom_annotation = match module {
807            wasmer_config::package::ModuleReference::CurrentPackage { module } => {
808                AtomAnnotation::new(module, None)
809            }
810            wasmer_config::package::ModuleReference::Dependency { dependency, module } => {
811                AtomAnnotation::new(module, dependency.to_string())
812            }
813        };
814        insert_annotation(annotations, AtomAnnotation::KEY, atom_annotation)?;
815
816        match self {
817            RunnerKind::Wasi | RunnerKind::Wcgi => {
818                let mut wasi = Wasi::new(module.to_string());
819                wasi.main_args = main_args;
820                wasi.package = package;
821                insert_annotation(annotations, Wasi::KEY, wasi)?;
822            }
823            RunnerKind::Wasm4 | RunnerKind::Other(_) => {
824                // No extra annotations to add
825            }
826        }
827
828        Ok(())
829    }
830}
831
832/// Infer the package's entrypoint.
833fn entrypoint(manifest: &WasmerManifest) -> Option<String> {
834    // check if manifest.package is none
835    if let Some(package) = &manifest.package
836        && let Some(entrypoint) = &package.entrypoint
837    {
838        return Some(entrypoint.clone());
839    }
840
841    if let [only_command] = manifest.commands.as_slice() {
842        // For convenience (and to stay compatible with old docs), if there is
843        // only one command we'll use that as the entrypoint
844        return Some(only_command.get_name().to_string());
845    }
846
847    None
848}
849
850fn transform_bindings(
851    manifest: &WasmerManifest,
852    base_dir: &Path,
853) -> Result<Vec<Binding>, ManifestError> {
854    transform_bindings_shared(
855        manifest,
856        |wit, module| transform_wit_bindings(wit, module, base_dir),
857        |wit, module| transform_wai_bindings(wit, module, base_dir),
858    )
859}
860
861fn transform_in_memory_bindings(manifest: &WasmerManifest) -> Result<Vec<Binding>, ManifestError> {
862    transform_bindings_shared(
863        manifest,
864        transform_in_memory_wit_bindings,
865        transform_in_memory_wai_bindings,
866    )
867}
868
869fn transform_bindings_shared(
870    manifest: &WasmerManifest,
871    wit_binding: impl Fn(
872        &wasmer_config::package::WitBindings,
873        &wasmer_config::package::Module,
874    ) -> Result<Binding, ManifestError>,
875    wai_binding: impl Fn(
876        &wasmer_config::package::WaiBindings,
877        &wasmer_config::package::Module,
878    ) -> Result<Binding, ManifestError>,
879) -> Result<Vec<Binding>, ManifestError> {
880    let mut bindings = Vec::new();
881
882    for module in &manifest.modules {
883        let b = match &module.bindings {
884            Some(wasmer_config::package::Bindings::Wit(wit)) => wit_binding(wit, module)?,
885            Some(wasmer_config::package::Bindings::Wai(wai)) => wai_binding(wai, module)?,
886            None => continue,
887        };
888        bindings.push(b);
889    }
890
891    Ok(bindings)
892}
893
894fn transform_wai_bindings(
895    wai: &wasmer_config::package::WaiBindings,
896    module: &wasmer_config::package::Module,
897    base_dir: &Path,
898) -> Result<Binding, ManifestError> {
899    transform_wai_bindings_shared(wai, module, |path| metadata_volume_uri(path, base_dir))
900}
901
902fn transform_in_memory_wai_bindings(
903    wai: &wasmer_config::package::WaiBindings,
904    module: &wasmer_config::package::Module,
905) -> Result<Binding, ManifestError> {
906    transform_wai_bindings_shared(wai, module, |path| {
907        Ok(format!("{METADATA_VOLUME}:/{}", sanitize_path(path)))
908    })
909}
910
911fn transform_wai_bindings_shared(
912    wai: &wasmer_config::package::WaiBindings,
913    module: &wasmer_config::package::Module,
914    metadata_volume_path: impl Fn(&PathBuf) -> Result<String, ManifestError>,
915) -> Result<Binding, ManifestError> {
916    let wasmer_config::package::WaiBindings {
917        wai_version,
918        exports,
919        imports,
920    } = wai;
921
922    let bindings = WaiBindings {
923        exports: exports.as_ref().map(&metadata_volume_path).transpose()?,
924        module: module.name.clone(),
925        imports: imports
926            .iter()
927            .map(metadata_volume_path)
928            .collect::<Result<Vec<_>, ManifestError>>()?,
929    };
930    let mut annotations = IndexMap::new();
931    insert_annotation(&mut annotations, "wai", bindings)?;
932
933    Ok(Binding {
934        name: "library-bindings".to_string(),
935        kind: format!("wai@{wai_version}"),
936        annotations: Value::Map(
937            annotations
938                .into_iter()
939                .map(|(k, v)| (Value::Text(k), v))
940                .collect(),
941        ),
942    })
943}
944
945fn metadata_volume_uri(path: &Path, base_dir: &Path) -> Result<String, ManifestError> {
946    make_relative_path(path, base_dir)
947        .map(sanitize_path)
948        .map(|p| format!("{METADATA_VOLUME}:/{p}"))
949}
950
951fn transform_wit_bindings(
952    wit: &wasmer_config::package::WitBindings,
953    module: &wasmer_config::package::Module,
954    base_dir: &Path,
955) -> Result<Binding, ManifestError> {
956    transform_wit_bindings_shared(wit, module, |path| metadata_volume_uri(path, base_dir))
957}
958
959fn transform_in_memory_wit_bindings(
960    wit: &wasmer_config::package::WitBindings,
961    module: &wasmer_config::package::Module,
962) -> Result<Binding, ManifestError> {
963    transform_wit_bindings_shared(wit, module, |path| {
964        Ok(format!("{METADATA_VOLUME}:/{}", sanitize_path(path)))
965    })
966}
967
968fn transform_wit_bindings_shared(
969    wit: &wasmer_config::package::WitBindings,
970    module: &wasmer_config::package::Module,
971    metadata_volume_path: impl Fn(&PathBuf) -> Result<String, ManifestError>,
972) -> Result<Binding, ManifestError> {
973    let wasmer_config::package::WitBindings {
974        wit_bindgen,
975        wit_exports,
976    } = wit;
977
978    let bindings = WitBindings {
979        exports: metadata_volume_path(wit_exports)?,
980        module: module.name.clone(),
981    };
982    let mut annotations = IndexMap::new();
983    insert_annotation(&mut annotations, "wit", bindings)?;
984
985    Ok(Binding {
986        name: "library-bindings".to_string(),
987        kind: format!("wit@{wit_bindgen}"),
988        annotations: Value::Map(
989            annotations
990                .into_iter()
991                .map(|(k, v)| (Value::Text(k), v))
992                .collect(),
993        ),
994    })
995}
996
997/// Resolve an item relative to the base directory, returning an error if the
998/// file lies outside of it.
999fn make_relative_path(path: &Path, base_dir: &Path) -> Result<PathBuf, ManifestError> {
1000    let absolute_path = base_dir.join(path);
1001
1002    match absolute_path.strip_prefix(base_dir) {
1003        Ok(p) => Ok(p.into()),
1004        Err(_) => Err(ManifestError::OutsideBaseDirectory {
1005            path: absolute_path,
1006            base_dir: base_dir.to_path_buf(),
1007        }),
1008    }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013    use tempfile::TempDir;
1014    use webc::metadata::annotations::Wasi;
1015
1016    use super::*;
1017
1018    #[test]
1019    fn custom_annotations_are_copied_across_verbatim() {
1020        let temp = TempDir::new().unwrap();
1021        let wasmer_toml = r#"
1022        [package]
1023        name = "test"
1024        version = "0.0.0"
1025        description = "asdf"
1026
1027        [[module]]
1028        name = "module"
1029        source = "file.wasm"
1030        abi = "wasi"
1031
1032        [[command]]
1033        name = "command"
1034        module = "module"
1035        runner = "asdf"
1036        annotations = { first = 42, second = ["a", "b"] }
1037        "#;
1038        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1039        std::fs::write(temp.path().join("file.wasm"), b"\0asm...").unwrap();
1040
1041        let (transformed, _) =
1042            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1043
1044        let command = &transformed.commands["command"];
1045        assert_eq!(command.annotation::<u32>("first").unwrap(), Some(42));
1046        assert_eq!(command.annotation::<String>("non-existent").unwrap(), None);
1047        insta::with_settings! {
1048            { description => wasmer_toml },
1049            { insta::assert_yaml_snapshot!(&transformed); }
1050        }
1051    }
1052
1053    #[test]
1054    fn transform_empty_manifest() {
1055        let temp = TempDir::new().unwrap();
1056        let wasmer_toml = r#"
1057            [package]
1058            name = "some/package"
1059            version = "0.0.0"
1060            description = "My awesome package"
1061        "#;
1062        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1063
1064        let (transformed, atoms) =
1065            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1066
1067        assert!(atoms.is_empty());
1068        insta::with_settings! {
1069            { description => wasmer_toml },
1070            { insta::assert_yaml_snapshot!(&transformed); }
1071        }
1072    }
1073
1074    #[test]
1075    fn transform_manifest_with_single_atom() {
1076        let temp = TempDir::new().unwrap();
1077        let wasmer_toml = r#"
1078            [package]
1079            name = "some/package"
1080            version = "0.0.0"
1081            description = "My awesome package"
1082
1083            [[module]]
1084            name = "first"
1085            source = "./path/to/file.wasm"
1086            abi = "wasi"
1087        "#;
1088        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1089        let dir = temp.path().join("path").join("to");
1090        std::fs::create_dir_all(&dir).unwrap();
1091        std::fs::write(dir.join("file.wasm"), b"\0asm...").unwrap();
1092
1093        let (transformed, atoms) =
1094            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1095
1096        assert_eq!(atoms.len(), 1);
1097        assert_eq!(atoms["first"].as_slice(), b"\0asm...");
1098        insta::with_settings! {
1099            { description => wasmer_toml },
1100            { insta::assert_yaml_snapshot!(&transformed); }
1101        }
1102    }
1103
1104    #[test]
1105    fn transform_manifest_with_atom_and_command() {
1106        let temp = TempDir::new().unwrap();
1107        let wasmer_toml = r#"
1108            [package]
1109            name = "some/package"
1110            version = "0.0.0"
1111            description = "My awesome package"
1112
1113            [[module]]
1114            name = "cpython"
1115            source = "python.wasm"
1116            abi = "wasi"
1117
1118            [[command]]
1119            name = "python"
1120            module = "cpython"
1121            runner = "wasi"
1122        "#;
1123        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1124        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1125
1126        let (transformed, _) =
1127            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1128
1129        assert_eq!(transformed.commands.len(), 1);
1130        let python = &transformed.commands["python"];
1131        assert_eq!(&python.runner, webc::metadata::annotations::WASI_RUNNER_URI);
1132        assert_eq!(python.wasi().unwrap().unwrap(), Wasi::new("cpython"));
1133        insta::with_settings! {
1134            { description => wasmer_toml },
1135            { insta::assert_yaml_snapshot!(&transformed); }
1136        }
1137    }
1138
1139    #[test]
1140    fn transform_manifest_with_multiple_commands() {
1141        let temp = TempDir::new().unwrap();
1142        let wasmer_toml = r#"
1143            [package]
1144            name = "some/package"
1145            version = "0.0.0"
1146            description = "My awesome package"
1147
1148            [[module]]
1149            name = "cpython"
1150            source = "python.wasm"
1151            abi = "wasi"
1152
1153            [[command]]
1154            name = "first"
1155            module = "cpython"
1156            runner = "wasi"
1157
1158            [[command]]
1159            name = "second"
1160            module = "cpython"
1161            runner = "wasi"
1162
1163            [[command]]
1164            name = "third"
1165            module = "cpython"
1166            runner = "wasi"
1167        "#;
1168        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1169        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1170
1171        let (transformed, _) =
1172            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1173
1174        assert_eq!(transformed.commands.len(), 3);
1175        assert!(transformed.commands.contains_key("first"));
1176        assert!(transformed.commands.contains_key("second"));
1177        assert!(transformed.commands.contains_key("third"));
1178        insta::with_settings! {
1179            { description => wasmer_toml },
1180            { insta::assert_yaml_snapshot!(&transformed); }
1181        }
1182    }
1183
1184    #[test]
1185    fn merge_custom_attributes_with_builtin_ones() {
1186        let temp = TempDir::new().unwrap();
1187        let wasmer_toml = r#"
1188            [package]
1189            name = "some/package"
1190            version = "0.0.0"
1191            description = "My awesome package"
1192
1193            [[module]]
1194            name = "cpython"
1195            source = "python.wasm"
1196            abi = "wasi"
1197
1198            [[command]]
1199            name = "python"
1200            module = "cpython"
1201            runner = "wasi"
1202            annotations = { wasi = { env = ["KEY=val"]} }
1203        "#;
1204        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1205        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1206
1207        let (transformed, _) =
1208            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1209
1210        assert_eq!(transformed.commands.len(), 1);
1211        let cmd = &transformed.commands["python"];
1212        assert_eq!(
1213            &cmd.wasi().unwrap().unwrap(),
1214            Wasi::new("cpython").with_env("KEY", "val")
1215        );
1216        insta::with_settings! {
1217            { description => wasmer_toml },
1218            { insta::assert_yaml_snapshot!(&transformed); }
1219        }
1220    }
1221
1222    #[test]
1223    fn transform_bash_manifest() {
1224        let temp = TempDir::new().unwrap();
1225        let wasmer_toml = r#"
1226            [package]
1227            name = "sharrattj/bash"
1228            version = "1.0.17"
1229            description = "Bash is a modern POSIX-compliant implementation of /bin/sh."
1230            license = "GNU"
1231            wasmer-extra-flags = "--enable-threads --enable-bulk-memory"
1232
1233            [dependencies]
1234            "sharrattj/coreutils" = "1.0.16"
1235
1236            [[module]]
1237            name = "bash"
1238            source = "bash.wasm"
1239            abi = "wasi"
1240
1241            [[command]]
1242            name = "bash"
1243            module = "bash"
1244            runner = "wasi@unstable_"
1245        "#;
1246        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1247        std::fs::write(temp.path().join("bash.wasm"), b"\0asm...").unwrap();
1248
1249        let (transformed, _) =
1250            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1251
1252        insta::with_settings! {
1253            { description => wasmer_toml },
1254            { insta::assert_yaml_snapshot!(&transformed); }
1255        }
1256    }
1257
1258    #[test]
1259    fn transform_wasmer_pack_manifest() {
1260        let temp = TempDir::new().unwrap();
1261        let wasmer_toml = r#"
1262            [package]
1263            name = "wasmer/wasmer-pack"
1264            version = "0.7.0"
1265            description = "The WebAssembly interface to wasmer-pack."
1266            license = "MIT"
1267            readme = "README.md"
1268            repository = "https://github.com/wasmerio/wasmer-pack"
1269            homepage = "https://wasmer.io/"
1270
1271            [[module]]
1272            name = "wasmer-pack-wasm"
1273            source = "wasmer_pack_wasm.wasm"
1274
1275            [module.bindings]
1276            wai-version = "0.2.0"
1277            exports = "wasmer-pack.exports.wai"
1278            imports = []
1279        "#;
1280        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1281        std::fs::write(temp.path().join("wasmer_pack_wasm.wasm"), b"\0asm...").unwrap();
1282        std::fs::write(temp.path().join("wasmer-pack.exports.wai"), b"").unwrap();
1283        std::fs::write(temp.path().join("README.md"), b"").unwrap();
1284
1285        let (transformed, _) =
1286            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1287
1288        insta::with_settings! {
1289            { description => wasmer_toml },
1290            { insta::assert_yaml_snapshot!(&transformed); }
1291        }
1292    }
1293
1294    #[test]
1295    fn transform_python_manifest() {
1296        let temp = TempDir::new().unwrap();
1297        let wasmer_toml = r#"
1298            [package]
1299            name = "python"
1300            version = "0.1.0"
1301            description = "Python is an interpreted, high-level, general-purpose programming language"
1302            license = "ISC"
1303            repository = "https://github.com/wapm-packages/python"
1304
1305            [[module]]
1306            name = "python"
1307            source = "bin/python.wasm"
1308            abi = "wasi"
1309
1310            [module.interfaces]
1311            wasi = "0.0.0-unstable"
1312
1313            [[command]]
1314            name = "python"
1315            module = "python"
1316
1317            [fs]
1318            lib = "lib"
1319        "#;
1320        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1321        let bin = temp.path().join("bin");
1322        std::fs::create_dir_all(&bin).unwrap();
1323        std::fs::write(bin.join("python.wasm"), b"\0asm...").unwrap();
1324
1325        let (transformed, _) =
1326            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1327
1328        insta::with_settings! {
1329            { description => wasmer_toml },
1330            { insta::assert_yaml_snapshot!(&transformed); }
1331        }
1332    }
1333
1334    #[test]
1335    fn transform_manifest_with_fs_table() {
1336        let temp = TempDir::new().unwrap();
1337        let wasmer_toml = r#"
1338            [package]
1339            name = "some/package"
1340            version = "0.0.0"
1341            description = "This is a package"
1342
1343            [fs]
1344            lib = "lib"
1345            "/public" = "out"
1346        "#;
1347        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1348        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1349
1350        let (transformed, _) =
1351            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1352
1353        let fs = transformed.filesystem().unwrap().unwrap();
1354        assert_eq!(
1355            fs,
1356            [
1357                FileSystemMapping {
1358                    from: None,
1359                    volume_name: "/lib".to_string(),
1360                    host_path: None,
1361                    mount_path: "/lib".to_string(),
1362                },
1363                FileSystemMapping {
1364                    from: None,
1365                    volume_name: "/out".to_string(),
1366                    host_path: None,
1367                    mount_path: "/public".to_string(),
1368                }
1369            ]
1370        );
1371        insta::with_settings! {
1372            { description => wasmer_toml },
1373            { insta::assert_yaml_snapshot!(&transformed); }
1374        }
1375    }
1376
1377    #[test]
1378    fn missing_command_dependency() {
1379        let temp = TempDir::new().unwrap();
1380        let wasmer_toml = r#"
1381            [[command]]
1382            name = "python"
1383            module = "test/python:python"
1384        "#;
1385        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1386        let bin = temp.path().join("bin");
1387        std::fs::create_dir_all(&bin).unwrap();
1388        let res = wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict);
1389
1390        assert!(matches!(
1391            res,
1392            Err(ManifestError::UndeclaredCommandDependency { .. })
1393        ));
1394    }
1395
1396    #[test]
1397    fn issue_124_command_runner_is_swallowed() {
1398        let temp = TempDir::new().unwrap();
1399        let wasmer_toml = r#"
1400            [package]
1401            name = "wasmer-tests/wcgi-always-panic"
1402            version = "0.1.0"
1403            description = "wasmer-tests/wcgi-always-panic website"
1404
1405            [[module]]
1406            name = "wcgi-always-panic"
1407            source = "./wcgi-always-panic.wasm"
1408            abi = "wasi"
1409
1410            [[command]]
1411            name = "wcgi"
1412            module = "wcgi-always-panic"
1413            runner = "https://webc.org/runner/wcgi"
1414        "#;
1415        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1416        std::fs::write(temp.path().join("wcgi-always-panic.wasm"), b"\0asm...").unwrap();
1417
1418        let (transformed, _) =
1419            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1420
1421        let cmd = &transformed.commands["wcgi"];
1422        assert_eq!(cmd.runner, webc::metadata::annotations::WCGI_RUNNER_URI);
1423        assert_eq!(cmd.wasi().unwrap().unwrap(), Wasi::new("wcgi-always-panic"));
1424        insta::with_settings! {
1425            { description => wasmer_toml },
1426            { insta::assert_yaml_snapshot!(&transformed); }
1427        }
1428    }
1429}