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            if 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
319        // Detect required WebAssembly features by analyzing the module binary
320        let features_result = wasmer_types::Features::detect_from_wasm(content);
321
322        if let Ok(features) = features_result {
323            // Convert wasmer_types::Features to webc::metadata::annotations::Wasm
324            let feature_strings = features_to_wasm_annotations(&features);
325
326            // Only create annotation if we detected features
327            if !feature_strings.is_empty() {
328                let wasm = webc::metadata::annotations::Wasm::new(feature_strings);
329                match ciborium::value::Value::serialized(&wasm) {
330                    Ok(wasm_value) => {
331                        annotations.insert(
332                            webc::metadata::annotations::Wasm::KEY.to_string(),
333                            wasm_value,
334                        );
335                    }
336                    Err(e) => {
337                        eprintln!("Failed to serialize wasm features: {e}");
338                    }
339                }
340            }
341        }
342
343        let atom = Atom {
344            kind: atom_kind(kind.as_ref().map(|s| s.as_str()))?,
345            signature: atom_signature(content),
346            annotations,
347        };
348
349        if metadata.contains_key(name) {
350            return Err(ManifestError::DuplicateModule(name.clone()));
351        }
352
353        metadata.insert(name.clone(), atom);
354        atom_files.insert(name.clone(), content.clone());
355    }
356
357    Ok((metadata, atom_files))
358}
359
360fn atom_signature(atom: &[u8]) -> String {
361    let hash: [u8; 32] = sha2::Sha256::digest(atom).into();
362    AtomSignature::Sha256(hash).to_string()
363}
364
365/// Map the "kind" field in a `[module]` to the corresponding URI.
366fn atom_kind(kind: Option<&str>) -> Result<Url, ManifestError> {
367    const WASM_ATOM_KIND: &str = "https://webc.org/kind/wasm";
368    const TENSORFLOW_SAVED_MODEL_KIND: &str = "https://webc.org/kind/tensorflow-SavedModel";
369
370    let url = match kind {
371        Some("wasm") | None => WASM_ATOM_KIND.parse().expect("Should never fail"),
372        Some("tensorflow-SavedModel") => TENSORFLOW_SAVED_MODEL_KIND
373            .parse()
374            .expect("Should never fail"),
375        Some(other) => {
376            if let Ok(url) = Url::parse(other) {
377                // if it is a valid URL, pass that through as-is
378                url
379            } else {
380                return Err(ManifestError::UnknownAtomKind(other.to_string()));
381            }
382        }
383    };
384
385    Ok(url)
386}
387
388/// Try to open a file, preferring mmap and falling back to [`std::fs::read()`]
389/// if mapping fails.
390fn open_file(path: &Path) -> Result<OwnedBuffer, std::io::Error> {
391    match OwnedBuffer::mmap(path) {
392        Ok(b) => return Ok(b),
393        Err(MmapError::Map(_)) => {
394            // Unable to mmap the atom file. Falling back to std::fs::read()
395        }
396        Err(MmapError::FileOpen { error, .. }) => {
397            return Err(error);
398        }
399    }
400
401    let bytes = std::fs::read(path)?;
402
403    Ok(OwnedBuffer::from_bytes(bytes))
404}
405
406fn insert_annotation(
407    annotations: &mut IndexMap<String, ciborium::Value>,
408    key: impl Into<String>,
409    value: impl serde::Serialize,
410) -> Result<(), ManifestError> {
411    let key = key.into();
412
413    match ciborium::value::Value::serialized(&value) {
414        Ok(value) => {
415            annotations.insert(key, value);
416            Ok(())
417        }
418        Err(error) => Err(ManifestError::SerializeCborAnnotation { key, error }),
419    }
420}
421
422fn get_fs_table(fs: &IndexMap<String, PathBuf>) -> FileSystemMappings {
423    if fs.is_empty() {
424        return FileSystemMappings::default();
425    }
426
427    // When wapm-targz-to-pirita creates the final webc all files will be
428    // merged into one "atom" volume, but we want to map each directory
429    // separately.
430    let mut entries = Vec::new();
431    for (guest, host) in fs {
432        let volume_name = host
433            .to_str()
434            .expect("failed to convert path to string")
435            .to_string();
436
437        let volume_name = sanitize_path(volume_name);
438
439        let mapping = FileSystemMapping {
440            from: None,
441            volume_name,
442            host_path: None,
443            mount_path: sanitize_path(guest),
444        };
445        entries.push(mapping);
446    }
447
448    FileSystemMappings(entries)
449}
450
451fn transform_package_meta_to_annotations(
452    package: &wasmer_config::package::Package,
453    base_dir: &Path,
454    strictness: Strictness,
455) -> Result<Wapm, ManifestError> {
456    fn metadata_file(
457        path: Option<&PathBuf>,
458        base_dir: &Path,
459        strictness: Strictness,
460    ) -> Result<Option<VolumeSpecificPath>, ManifestError> {
461        let path = match path {
462            Some(p) => p,
463            None => return Ok(None),
464        };
465
466        let absolute_path = base_dir.join(path);
467
468        // Touch the file to make sure it actually exists
469        if !absolute_path.exists() {
470            match strictness.missing_file(path, base_dir) {
471                Ok(_) => return Ok(None),
472                Err(e) => {
473                    return Err(e);
474                }
475            }
476        }
477
478        match base_dir.join(path).strip_prefix(base_dir) {
479            Ok(without_prefix) => Ok(Some(VolumeSpecificPath {
480                volume: METADATA_VOLUME.to_string(),
481                path: sanitize_path(without_prefix),
482            })),
483            Err(_) => match strictness.outside_base_directory(path, base_dir) {
484                Ok(_) => Ok(None),
485                Err(e) => Err(e),
486            },
487        }
488    }
489
490    transform_package_meta_to_annotations_shared(package, |path| {
491        metadata_file(path, base_dir, strictness)
492    })
493}
494
495fn transform_in_memory_package_meta_to_annotations(
496    package: &wasmer_config::package::Package,
497) -> Result<Wapm, ManifestError> {
498    transform_package_meta_to_annotations_shared(package, |path| {
499        Ok(path.map(|readme_file| VolumeSpecificPath {
500            volume: METADATA_VOLUME.to_string(),
501            path: sanitize_path(readme_file),
502        }))
503    })
504}
505
506fn transform_package_meta_to_annotations_shared(
507    package: &wasmer_config::package::Package,
508    volume_specific_path: impl Fn(Option<&PathBuf>) -> Result<Option<VolumeSpecificPath>, ManifestError>,
509) -> Result<Wapm, ManifestError> {
510    let mut wapm = Wapm::new(
511        package.name.clone(),
512        package.version.clone().map(|v| v.to_string()),
513        package.description.clone(),
514    );
515
516    wapm.license = package.license.clone();
517    wapm.license_file = volume_specific_path(package.license_file.as_ref())?;
518    wapm.readme = volume_specific_path(package.readme.as_ref())?;
519    wapm.repository = package.repository.clone();
520    wapm.homepage = package.homepage.clone();
521    wapm.private = package.private;
522
523    Ok(wapm)
524}
525
526fn transform_commands(
527    manifest: &WasmerManifest,
528    base_dir: &Path,
529) -> Result<IndexMap<String, Command>, ManifestError> {
530    trasform_commands_shared(
531        manifest,
532        |cmd| transform_command_v1(cmd, manifest),
533        |cmd| transform_command_v2(cmd, base_dir),
534    )
535}
536
537fn transform_in_memory_commands(
538    manifest: &WasmerManifest,
539) -> Result<IndexMap<String, Command>, ManifestError> {
540    trasform_commands_shared(
541        manifest,
542        |cmd| transform_command_v1(cmd, manifest),
543        transform_in_memory_command_v2,
544    )
545}
546
547#[allow(deprecated)]
548fn trasform_commands_shared(
549    manifest: &WasmerManifest,
550    transform_command_v1: impl Fn(&CommandV1) -> Result<Command, ManifestError>,
551    transform_command_v2: impl Fn(&CommandV2) -> Result<Command, ManifestError>,
552) -> Result<IndexMap<String, Command>, ManifestError> {
553    let mut commands = IndexMap::new();
554
555    for command in &manifest.commands {
556        let cmd = match command {
557            wasmer_config::package::Command::V1(cmd) => transform_command_v1(cmd)?,
558            wasmer_config::package::Command::V2(cmd) => transform_command_v2(cmd)?,
559        };
560
561        // If a command uses a module from a dependency, then ensure that
562        // the dependency is declared.
563        match command.get_module() {
564            wasmer_config::package::ModuleReference::CurrentPackage { .. } => {}
565            wasmer_config::package::ModuleReference::Dependency { dependency, .. } => {
566                if !manifest.dependencies.contains_key(dependency) {
567                    return Err(ManifestError::UndeclaredCommandDependency {
568                        command: command.get_name().to_string(),
569                        dependency: dependency.to_string(),
570                    });
571                }
572            }
573        }
574
575        match commands.entry(command.get_name().to_string()) {
576            indexmap::map::Entry::Occupied(_) => {
577                return Err(ManifestError::DuplicateCommand(
578                    command.get_name().to_string(),
579                ));
580            }
581            indexmap::map::Entry::Vacant(entry) => {
582                entry.insert(cmd);
583            }
584        }
585    }
586
587    Ok(commands)
588}
589
590#[allow(deprecated)]
591fn transform_command_v1(
592    cmd: &wasmer_config::package::CommandV1,
593    manifest: &WasmerManifest,
594) -> Result<Command, ManifestError> {
595    // Note: a key difference between CommandV1 and CommandV2 is that v1 uses
596    // a module's "abi" field to figure out which runner to use, whereas v2 has
597    // a dedicated "runner" field and ignores module.abi.
598    let runner = match &cmd.module {
599        wasmer_config::package::ModuleReference::CurrentPackage { module } => {
600            let module = manifest
601                .modules
602                .iter()
603                .find(|m| m.name == module.as_str())
604                .ok_or_else(|| ManifestError::InvalidModuleReference {
605                    command: cmd.name.clone(),
606                    module: cmd.module.to_string(),
607                })?;
608
609            RunnerKind::from_name(module.abi.to_str())?
610        }
611        wasmer_config::package::ModuleReference::Dependency { .. } => {
612            // Note: We don't have any visibility into dependencies (this code
613            // doesn't do resolution), so we blindly assume it's a WASI command.
614            // That should be fine because people shouldn't use the CommandV1
615            // syntax any more.
616            RunnerKind::Wasi
617        }
618    };
619
620    let mut annotations = IndexMap::new();
621    // Splitting by whitespace isn't really correct, but proper shell splitting
622    // would require a dependency and CommandV1 won't be used any more, anyway.
623    let main_args = cmd
624        .main_args
625        .as_deref()
626        .map(|args| args.split_whitespace().map(String::from).collect());
627    runner.runner_specific_annotations(
628        &mut annotations,
629        &cmd.module,
630        cmd.package.clone(),
631        main_args,
632    )?;
633
634    Ok(Command {
635        runner: runner.uri().to_string(),
636        annotations,
637    })
638}
639
640fn transform_command_v2(
641    cmd: &wasmer_config::package::CommandV2,
642    base_dir: &Path,
643) -> Result<Command, ManifestError> {
644    transform_command_v2_shared(cmd, || {
645        cmd.get_annotations(base_dir)
646            .map_err(|error| ManifestError::WasmerTomlAnnotations {
647                error: error.into(),
648            })
649    })
650}
651
652fn transform_in_memory_command_v2(
653    cmd: &wasmer_config::package::CommandV2,
654) -> Result<Command, ManifestError> {
655    transform_command_v2_shared(cmd, || {
656        cmd.annotations
657            .as_ref()
658            .map(|a| match a {
659                wasmer_config::package::CommandAnnotations::File(_) => {
660                    Err(ManifestError::FileNotSupported)
661                }
662                wasmer_config::package::CommandAnnotations::Raw(v) => Ok(toml_to_cbor_value(v)),
663            })
664            .transpose()
665    })
666}
667
668fn transform_command_v2_shared(
669    cmd: &wasmer_config::package::CommandV2,
670    custom_annotations: impl Fn() -> Result<Option<Value>, ManifestError>,
671) -> Result<Command, ManifestError> {
672    let runner = RunnerKind::from_name(&cmd.runner)?;
673    let mut annotations = IndexMap::new();
674
675    runner.runner_specific_annotations(&mut annotations, &cmd.module, None, None)?;
676
677    let custom_annotations = custom_annotations()?;
678
679    if let Some(ciborium::Value::Map(custom_annotations)) = custom_annotations {
680        for (key, value) in custom_annotations {
681            if let ciborium::Value::Text(key) = key {
682                match annotations.entry(key) {
683                    indexmap::map::Entry::Occupied(mut entry) => {
684                        merge_cbor(entry.get_mut(), value).map_err(|_| {
685                            ManifestError::MergeAnnotations {
686                                command: cmd.name.clone(),
687                                key: entry.key().clone(),
688                            }
689                        })?;
690                    }
691                    indexmap::map::Entry::Vacant(entry) => {
692                        entry.insert(value);
693                    }
694                }
695            }
696        }
697    }
698
699    Ok(Command {
700        runner: runner.uri().to_string(),
701        annotations,
702    })
703}
704
705fn toml_to_cbor_value(val: &toml::value::Value) -> ciborium::Value {
706    match val {
707        toml::Value::String(s) => ciborium::Value::Text(s.clone()),
708        toml::Value::Integer(i) => ciborium::Value::Integer(ciborium::value::Integer::from(*i)),
709        toml::Value::Float(f) => ciborium::Value::Float(*f),
710        toml::Value::Boolean(b) => ciborium::Value::Bool(*b),
711        toml::Value::Datetime(d) => ciborium::Value::Text(format!("{d}")),
712        toml::Value::Array(sq) => {
713            ciborium::Value::Array(sq.iter().map(toml_to_cbor_value).collect())
714        }
715        toml::Value::Table(m) => ciborium::Value::Map(
716            m.iter()
717                .map(|(k, v)| (ciborium::Value::Text(k.clone()), toml_to_cbor_value(v)))
718                .collect(),
719        ),
720    }
721}
722
723fn merge_cbor(original: &mut Value, addition: Value) -> Result<(), ()> {
724    match (original, addition) {
725        (Value::Map(left), Value::Map(right)) => {
726            for (k, v) in right {
727                if let Some(entry) = left.iter_mut().find(|lk| lk.0 == k) {
728                    merge_cbor(&mut entry.1, v)?;
729                } else {
730                    left.push((k, v));
731                }
732            }
733        }
734        (Value::Array(left), Value::Array(right)) => {
735            left.extend(right);
736        }
737        // Primitives that have the same values are fine
738        (Value::Bool(left), Value::Bool(right)) if *left == right => {}
739        (Value::Bytes(left), Value::Bytes(right)) if *left == right => {}
740        (Value::Float(left), Value::Float(right)) if *left == right => {}
741        (Value::Integer(left), Value::Integer(right)) if *left == right => {}
742        (Value::Text(left), Value::Text(right)) if *left == right => {}
743        // null can be overwritten
744        (original @ Value::Null, value) => {
745            *original = value;
746        }
747        (_original, Value::Null) => {}
748        // Oh well, we tried...
749        (_left, _right) => {
750            return Err(());
751        }
752    }
753
754    Ok(())
755}
756
757#[derive(Debug, Clone, PartialEq)]
758enum RunnerKind {
759    Wasi,
760    Wcgi,
761    Wasm4,
762    Other(Url),
763}
764
765impl RunnerKind {
766    fn from_name(name: &str) -> Result<Self, ManifestError> {
767        match name {
768            "wasi" | "wasi@unstable_" | webc::metadata::annotations::WASI_RUNNER_URI => {
769                Ok(RunnerKind::Wasi)
770            }
771            "generic" => {
772                // This is what you get with a CommandV1 and abi = "none"
773                Ok(RunnerKind::Wasi)
774            }
775            "wcgi" | webc::metadata::annotations::WCGI_RUNNER_URI => Ok(RunnerKind::Wcgi),
776            "wasm4" | webc::metadata::annotations::WASM4_RUNNER_URI => Ok(RunnerKind::Wasm4),
777            other => {
778                if let Ok(other) = Url::parse(other) {
779                    Ok(RunnerKind::Other(other))
780                } else if let Ok(other) = format!("https://webc.org/runner/{other}").parse() {
781                    // fall back to something under webc.org
782                    Ok(RunnerKind::Other(other))
783                } else {
784                    Err(ManifestError::UnknownRunnerKind(other.to_string()))
785                }
786            }
787        }
788    }
789
790    fn uri(&self) -> &str {
791        match self {
792            RunnerKind::Wasi => webc::metadata::annotations::WASI_RUNNER_URI,
793            RunnerKind::Wcgi => webc::metadata::annotations::WCGI_RUNNER_URI,
794            RunnerKind::Wasm4 => webc::metadata::annotations::WASM4_RUNNER_URI,
795            RunnerKind::Other(other) => other.as_str(),
796        }
797    }
798
799    #[allow(deprecated)]
800    fn runner_specific_annotations(
801        &self,
802        annotations: &mut IndexMap<String, Value>,
803        module: &wasmer_config::package::ModuleReference,
804        package: Option<String>,
805        main_args: Option<Vec<String>>,
806    ) -> Result<(), ManifestError> {
807        let atom_annotation = match module {
808            wasmer_config::package::ModuleReference::CurrentPackage { module } => {
809                AtomAnnotation::new(module, None)
810            }
811            wasmer_config::package::ModuleReference::Dependency { dependency, module } => {
812                AtomAnnotation::new(module, dependency.to_string())
813            }
814        };
815        insert_annotation(annotations, AtomAnnotation::KEY, atom_annotation)?;
816
817        match self {
818            RunnerKind::Wasi | RunnerKind::Wcgi => {
819                let mut wasi = Wasi::new(module.to_string());
820                wasi.main_args = main_args;
821                wasi.package = package;
822                insert_annotation(annotations, Wasi::KEY, wasi)?;
823            }
824            RunnerKind::Wasm4 | RunnerKind::Other(_) => {
825                // No extra annotations to add
826            }
827        }
828
829        Ok(())
830    }
831}
832
833/// Infer the package's entrypoint.
834fn entrypoint(manifest: &WasmerManifest) -> Option<String> {
835    // check if manifest.package is none
836    if let Some(package) = &manifest.package {
837        if let Some(entrypoint) = &package.entrypoint {
838            return Some(entrypoint.clone());
839        }
840    }
841
842    if let [only_command] = manifest.commands.as_slice() {
843        // For convenience (and to stay compatible with old docs), if there is
844        // only one command we'll use that as the entrypoint
845        return Some(only_command.get_name().to_string());
846    }
847
848    None
849}
850
851fn transform_bindings(
852    manifest: &WasmerManifest,
853    base_dir: &Path,
854) -> Result<Vec<Binding>, ManifestError> {
855    transform_bindings_shared(
856        manifest,
857        |wit, module| transform_wit_bindings(wit, module, base_dir),
858        |wit, module| transform_wai_bindings(wit, module, base_dir),
859    )
860}
861
862fn transform_in_memory_bindings(manifest: &WasmerManifest) -> Result<Vec<Binding>, ManifestError> {
863    transform_bindings_shared(
864        manifest,
865        transform_in_memory_wit_bindings,
866        transform_in_memory_wai_bindings,
867    )
868}
869
870fn transform_bindings_shared(
871    manifest: &WasmerManifest,
872    wit_binding: impl Fn(
873        &wasmer_config::package::WitBindings,
874        &wasmer_config::package::Module,
875    ) -> Result<Binding, ManifestError>,
876    wai_binding: impl Fn(
877        &wasmer_config::package::WaiBindings,
878        &wasmer_config::package::Module,
879    ) -> Result<Binding, ManifestError>,
880) -> Result<Vec<Binding>, ManifestError> {
881    let mut bindings = Vec::new();
882
883    for module in &manifest.modules {
884        let b = match &module.bindings {
885            Some(wasmer_config::package::Bindings::Wit(wit)) => wit_binding(wit, module)?,
886            Some(wasmer_config::package::Bindings::Wai(wai)) => wai_binding(wai, module)?,
887            None => continue,
888        };
889        bindings.push(b);
890    }
891
892    Ok(bindings)
893}
894
895fn transform_wai_bindings(
896    wai: &wasmer_config::package::WaiBindings,
897    module: &wasmer_config::package::Module,
898    base_dir: &Path,
899) -> Result<Binding, ManifestError> {
900    transform_wai_bindings_shared(wai, module, |path| metadata_volume_uri(path, base_dir))
901}
902
903fn transform_in_memory_wai_bindings(
904    wai: &wasmer_config::package::WaiBindings,
905    module: &wasmer_config::package::Module,
906) -> Result<Binding, ManifestError> {
907    transform_wai_bindings_shared(wai, module, |path| {
908        Ok(format!("{METADATA_VOLUME}:/{}", sanitize_path(path)))
909    })
910}
911
912fn transform_wai_bindings_shared(
913    wai: &wasmer_config::package::WaiBindings,
914    module: &wasmer_config::package::Module,
915    metadata_volume_path: impl Fn(&PathBuf) -> Result<String, ManifestError>,
916) -> Result<Binding, ManifestError> {
917    let wasmer_config::package::WaiBindings {
918        wai_version,
919        exports,
920        imports,
921    } = wai;
922
923    let bindings = WaiBindings {
924        exports: exports.as_ref().map(&metadata_volume_path).transpose()?,
925        module: module.name.clone(),
926        imports: imports
927            .iter()
928            .map(metadata_volume_path)
929            .collect::<Result<Vec<_>, ManifestError>>()?,
930    };
931    let mut annotations = IndexMap::new();
932    insert_annotation(&mut annotations, "wai", bindings)?;
933
934    Ok(Binding {
935        name: "library-bindings".to_string(),
936        kind: format!("wai@{wai_version}"),
937        annotations: Value::Map(
938            annotations
939                .into_iter()
940                .map(|(k, v)| (Value::Text(k), v))
941                .collect(),
942        ),
943    })
944}
945
946fn metadata_volume_uri(path: &Path, base_dir: &Path) -> Result<String, ManifestError> {
947    make_relative_path(path, base_dir)
948        .map(sanitize_path)
949        .map(|p| format!("{METADATA_VOLUME}:/{p}"))
950}
951
952fn transform_wit_bindings(
953    wit: &wasmer_config::package::WitBindings,
954    module: &wasmer_config::package::Module,
955    base_dir: &Path,
956) -> Result<Binding, ManifestError> {
957    transform_wit_bindings_shared(wit, module, |path| metadata_volume_uri(path, base_dir))
958}
959
960fn transform_in_memory_wit_bindings(
961    wit: &wasmer_config::package::WitBindings,
962    module: &wasmer_config::package::Module,
963) -> Result<Binding, ManifestError> {
964    transform_wit_bindings_shared(wit, module, |path| {
965        Ok(format!("{METADATA_VOLUME}:/{}", sanitize_path(path)))
966    })
967}
968
969fn transform_wit_bindings_shared(
970    wit: &wasmer_config::package::WitBindings,
971    module: &wasmer_config::package::Module,
972    metadata_volume_path: impl Fn(&PathBuf) -> Result<String, ManifestError>,
973) -> Result<Binding, ManifestError> {
974    let wasmer_config::package::WitBindings {
975        wit_bindgen,
976        wit_exports,
977    } = wit;
978
979    let bindings = WitBindings {
980        exports: metadata_volume_path(wit_exports)?,
981        module: module.name.clone(),
982    };
983    let mut annotations = IndexMap::new();
984    insert_annotation(&mut annotations, "wit", bindings)?;
985
986    Ok(Binding {
987        name: "library-bindings".to_string(),
988        kind: format!("wit@{wit_bindgen}"),
989        annotations: Value::Map(
990            annotations
991                .into_iter()
992                .map(|(k, v)| (Value::Text(k), v))
993                .collect(),
994        ),
995    })
996}
997
998/// Resolve an item relative to the base directory, returning an error if the
999/// file lies outside of it.
1000fn make_relative_path(path: &Path, base_dir: &Path) -> Result<PathBuf, ManifestError> {
1001    let absolute_path = base_dir.join(path);
1002
1003    match absolute_path.strip_prefix(base_dir) {
1004        Ok(p) => Ok(p.into()),
1005        Err(_) => Err(ManifestError::OutsideBaseDirectory {
1006            path: absolute_path,
1007            base_dir: base_dir.to_path_buf(),
1008        }),
1009    }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014    use tempfile::TempDir;
1015    use webc::metadata::annotations::Wasi;
1016
1017    use super::*;
1018
1019    #[test]
1020    fn custom_annotations_are_copied_across_verbatim() {
1021        let temp = TempDir::new().unwrap();
1022        let wasmer_toml = r#"
1023        [package]
1024        name = "test"
1025        version = "0.0.0"
1026        description = "asdf"
1027
1028        [[module]]
1029        name = "module"
1030        source = "file.wasm"
1031        abi = "wasi"
1032
1033        [[command]]
1034        name = "command"
1035        module = "module"
1036        runner = "asdf"
1037        annotations = { first = 42, second = ["a", "b"] }
1038        "#;
1039        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1040        std::fs::write(temp.path().join("file.wasm"), b"\0asm...").unwrap();
1041
1042        let (transformed, _) =
1043            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1044
1045        let command = &transformed.commands["command"];
1046        assert_eq!(command.annotation::<u32>("first").unwrap(), Some(42));
1047        assert_eq!(command.annotation::<String>("non-existent").unwrap(), None);
1048        insta::with_settings! {
1049            { description => wasmer_toml },
1050            { insta::assert_yaml_snapshot!(&transformed); }
1051        }
1052    }
1053
1054    #[test]
1055    fn transform_empty_manifest() {
1056        let temp = TempDir::new().unwrap();
1057        let wasmer_toml = r#"
1058            [package]
1059            name = "some/package"
1060            version = "0.0.0"
1061            description = "My awesome package"
1062        "#;
1063        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1064
1065        let (transformed, atoms) =
1066            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1067
1068        assert!(atoms.is_empty());
1069        insta::with_settings! {
1070            { description => wasmer_toml },
1071            { insta::assert_yaml_snapshot!(&transformed); }
1072        }
1073    }
1074
1075    #[test]
1076    fn transform_manifest_with_single_atom() {
1077        let temp = TempDir::new().unwrap();
1078        let wasmer_toml = r#"
1079            [package]
1080            name = "some/package"
1081            version = "0.0.0"
1082            description = "My awesome package"
1083
1084            [[module]]
1085            name = "first"
1086            source = "./path/to/file.wasm"
1087            abi = "wasi"
1088        "#;
1089        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1090        let dir = temp.path().join("path").join("to");
1091        std::fs::create_dir_all(&dir).unwrap();
1092        std::fs::write(dir.join("file.wasm"), b"\0asm...").unwrap();
1093
1094        let (transformed, atoms) =
1095            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1096
1097        assert_eq!(atoms.len(), 1);
1098        assert_eq!(atoms["first"].as_slice(), b"\0asm...");
1099        insta::with_settings! {
1100            { description => wasmer_toml },
1101            { insta::assert_yaml_snapshot!(&transformed); }
1102        }
1103    }
1104
1105    #[test]
1106    fn transform_manifest_with_atom_and_command() {
1107        let temp = TempDir::new().unwrap();
1108        let wasmer_toml = r#"
1109            [package]
1110            name = "some/package"
1111            version = "0.0.0"
1112            description = "My awesome package"
1113
1114            [[module]]
1115            name = "cpython"
1116            source = "python.wasm"
1117            abi = "wasi"
1118
1119            [[command]]
1120            name = "python"
1121            module = "cpython"
1122            runner = "wasi"
1123        "#;
1124        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1125        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1126
1127        let (transformed, _) =
1128            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1129
1130        assert_eq!(transformed.commands.len(), 1);
1131        let python = &transformed.commands["python"];
1132        assert_eq!(&python.runner, webc::metadata::annotations::WASI_RUNNER_URI);
1133        assert_eq!(python.wasi().unwrap().unwrap(), Wasi::new("cpython"));
1134        insta::with_settings! {
1135            { description => wasmer_toml },
1136            { insta::assert_yaml_snapshot!(&transformed); }
1137        }
1138    }
1139
1140    #[test]
1141    fn transform_manifest_with_multiple_commands() {
1142        let temp = TempDir::new().unwrap();
1143        let wasmer_toml = r#"
1144            [package]
1145            name = "some/package"
1146            version = "0.0.0"
1147            description = "My awesome package"
1148
1149            [[module]]
1150            name = "cpython"
1151            source = "python.wasm"
1152            abi = "wasi"
1153
1154            [[command]]
1155            name = "first"
1156            module = "cpython"
1157            runner = "wasi"
1158
1159            [[command]]
1160            name = "second"
1161            module = "cpython"
1162            runner = "wasi"
1163
1164            [[command]]
1165            name = "third"
1166            module = "cpython"
1167            runner = "wasi"
1168        "#;
1169        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1170        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1171
1172        let (transformed, _) =
1173            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1174
1175        assert_eq!(transformed.commands.len(), 3);
1176        assert!(transformed.commands.contains_key("first"));
1177        assert!(transformed.commands.contains_key("second"));
1178        assert!(transformed.commands.contains_key("third"));
1179        insta::with_settings! {
1180            { description => wasmer_toml },
1181            { insta::assert_yaml_snapshot!(&transformed); }
1182        }
1183    }
1184
1185    #[test]
1186    fn merge_custom_attributes_with_builtin_ones() {
1187        let temp = TempDir::new().unwrap();
1188        let wasmer_toml = r#"
1189            [package]
1190            name = "some/package"
1191            version = "0.0.0"
1192            description = "My awesome package"
1193
1194            [[module]]
1195            name = "cpython"
1196            source = "python.wasm"
1197            abi = "wasi"
1198
1199            [[command]]
1200            name = "python"
1201            module = "cpython"
1202            runner = "wasi"
1203            annotations = { wasi = { env = ["KEY=val"]} }
1204        "#;
1205        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1206        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1207
1208        let (transformed, _) =
1209            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1210
1211        assert_eq!(transformed.commands.len(), 1);
1212        let cmd = &transformed.commands["python"];
1213        assert_eq!(
1214            &cmd.wasi().unwrap().unwrap(),
1215            Wasi::new("cpython").with_env("KEY", "val")
1216        );
1217        insta::with_settings! {
1218            { description => wasmer_toml },
1219            { insta::assert_yaml_snapshot!(&transformed); }
1220        }
1221    }
1222
1223    #[test]
1224    fn transform_bash_manifest() {
1225        let temp = TempDir::new().unwrap();
1226        let wasmer_toml = r#"
1227            [package]
1228            name = "sharrattj/bash"
1229            version = "1.0.17"
1230            description = "Bash is a modern POSIX-compliant implementation of /bin/sh."
1231            license = "GNU"
1232            wasmer-extra-flags = "--enable-threads --enable-bulk-memory"
1233
1234            [dependencies]
1235            "sharrattj/coreutils" = "1.0.16"
1236
1237            [[module]]
1238            name = "bash"
1239            source = "bash.wasm"
1240            abi = "wasi"
1241
1242            [[command]]
1243            name = "bash"
1244            module = "bash"
1245            runner = "wasi@unstable_"
1246        "#;
1247        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1248        std::fs::write(temp.path().join("bash.wasm"), b"\0asm...").unwrap();
1249
1250        let (transformed, _) =
1251            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1252
1253        insta::with_settings! {
1254            { description => wasmer_toml },
1255            { insta::assert_yaml_snapshot!(&transformed); }
1256        }
1257    }
1258
1259    #[test]
1260    fn transform_wasmer_pack_manifest() {
1261        let temp = TempDir::new().unwrap();
1262        let wasmer_toml = r#"
1263            [package]
1264            name = "wasmer/wasmer-pack"
1265            version = "0.7.0"
1266            description = "The WebAssembly interface to wasmer-pack."
1267            license = "MIT"
1268            readme = "README.md"
1269            repository = "https://github.com/wasmerio/wasmer-pack"
1270            homepage = "https://wasmer.io/"
1271
1272            [[module]]
1273            name = "wasmer-pack-wasm"
1274            source = "wasmer_pack_wasm.wasm"
1275
1276            [module.bindings]
1277            wai-version = "0.2.0"
1278            exports = "wasmer-pack.exports.wai"
1279            imports = []
1280        "#;
1281        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1282        std::fs::write(temp.path().join("wasmer_pack_wasm.wasm"), b"\0asm...").unwrap();
1283        std::fs::write(temp.path().join("wasmer-pack.exports.wai"), b"").unwrap();
1284        std::fs::write(temp.path().join("README.md"), b"").unwrap();
1285
1286        let (transformed, _) =
1287            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1288
1289        insta::with_settings! {
1290            { description => wasmer_toml },
1291            { insta::assert_yaml_snapshot!(&transformed); }
1292        }
1293    }
1294
1295    #[test]
1296    fn transform_python_manifest() {
1297        let temp = TempDir::new().unwrap();
1298        let wasmer_toml = r#"
1299            [package]
1300            name = "python"
1301            version = "0.1.0"
1302            description = "Python is an interpreted, high-level, general-purpose programming language"
1303            license = "ISC"
1304            repository = "https://github.com/wapm-packages/python"
1305
1306            [[module]]
1307            name = "python"
1308            source = "bin/python.wasm"
1309            abi = "wasi"
1310
1311            [module.interfaces]
1312            wasi = "0.0.0-unstable"
1313
1314            [[command]]
1315            name = "python"
1316            module = "python"
1317
1318            [fs]
1319            lib = "lib"
1320        "#;
1321        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1322        let bin = temp.path().join("bin");
1323        std::fs::create_dir_all(&bin).unwrap();
1324        std::fs::write(bin.join("python.wasm"), b"\0asm...").unwrap();
1325
1326        let (transformed, _) =
1327            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1328
1329        insta::with_settings! {
1330            { description => wasmer_toml },
1331            { insta::assert_yaml_snapshot!(&transformed); }
1332        }
1333    }
1334
1335    #[test]
1336    fn transform_manifest_with_fs_table() {
1337        let temp = TempDir::new().unwrap();
1338        let wasmer_toml = r#"
1339            [package]
1340            name = "some/package"
1341            version = "0.0.0"
1342            description = "This is a package"
1343
1344            [fs]
1345            lib = "lib"
1346            "/public" = "out"
1347        "#;
1348        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1349        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
1350
1351        let (transformed, _) =
1352            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1353
1354        let fs = transformed.filesystem().unwrap().unwrap();
1355        assert_eq!(
1356            fs,
1357            [
1358                FileSystemMapping {
1359                    from: None,
1360                    volume_name: "/lib".to_string(),
1361                    host_path: None,
1362                    mount_path: "/lib".to_string(),
1363                },
1364                FileSystemMapping {
1365                    from: None,
1366                    volume_name: "/out".to_string(),
1367                    host_path: None,
1368                    mount_path: "/public".to_string(),
1369                }
1370            ]
1371        );
1372        insta::with_settings! {
1373            { description => wasmer_toml },
1374            { insta::assert_yaml_snapshot!(&transformed); }
1375        }
1376    }
1377
1378    #[test]
1379    fn missing_command_dependency() {
1380        let temp = TempDir::new().unwrap();
1381        let wasmer_toml = r#"
1382            [[command]]
1383            name = "python"
1384            module = "test/python:python"
1385        "#;
1386        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1387        let bin = temp.path().join("bin");
1388        std::fs::create_dir_all(&bin).unwrap();
1389        let res = wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict);
1390
1391        assert!(matches!(
1392            res,
1393            Err(ManifestError::UndeclaredCommandDependency { .. })
1394        ));
1395    }
1396
1397    #[test]
1398    fn issue_124_command_runner_is_swallowed() {
1399        let temp = TempDir::new().unwrap();
1400        let wasmer_toml = r#"
1401            [package]
1402            name = "wasmer-tests/wcgi-always-panic"
1403            version = "0.1.0"
1404            description = "wasmer-tests/wcgi-always-panic website"
1405
1406            [[module]]
1407            name = "wcgi-always-panic"
1408            source = "./wcgi-always-panic.wasm"
1409            abi = "wasi"
1410
1411            [[command]]
1412            name = "wcgi"
1413            module = "wcgi-always-panic"
1414            runner = "https://webc.org/runner/wcgi"
1415        "#;
1416        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
1417        std::fs::write(temp.path().join("wcgi-always-panic.wasm"), b"\0asm...").unwrap();
1418
1419        let (transformed, _) =
1420            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
1421
1422        let cmd = &transformed.commands["wcgi"];
1423        assert_eq!(cmd.runner, webc::metadata::annotations::WCGI_RUNNER_URI);
1424        assert_eq!(cmd.wasi().unwrap().unwrap(), Wasi::new("wcgi-always-panic"));
1425        insta::with_settings! {
1426            { description => wasmer_toml },
1427            { insta::assert_yaml_snapshot!(&transformed); }
1428        }
1429    }
1430}