wasmer_package/convert/
webc_to_package.rs

1use std::path::Path;
2
3use wasmer_config::package::{ModuleReference, SuggestedCompilerOptimizations, UserAnnotations};
4
5use webc::Container;
6
7use super::ConversionError;
8
9/// Convert a webc image into a directory with a wasmer.toml file that can
10/// be used for generating a new package.
11pub fn webc_to_package_dir(webc: &Container, target_dir: &Path) -> Result<(), ConversionError> {
12    let mut pkg_manifest = wasmer_config::package::Manifest::new_empty();
13
14    let manifest = webc.manifest();
15    // Convert the package annotation.
16
17    let pkg_annotation = manifest
18        .wapm()
19        .map_err(|err| ConversionError::msg(format!("could not read package annotation: {err}")))?;
20    if let Some(ann) = pkg_annotation {
21        let mut pkg = wasmer_config::package::Package::new_empty();
22
23        pkg.name = ann.name;
24        pkg.version = if let Some(raw) = ann.version {
25            let v = raw
26                .parse()
27                .map_err(|e| ConversionError::with_cause("invalid package version", e))?;
28            Some(v)
29        } else {
30            None
31        };
32
33        pkg.description = ann.description;
34        pkg.license = ann.license;
35
36        // TODO: map license_file and README (paths!)
37
38        pkg.homepage = ann.homepage;
39        pkg.repository = ann.repository;
40        pkg.private = ann.private;
41        pkg.entrypoint = manifest.entrypoint.clone();
42
43        pkg_manifest.package = Some(pkg);
44    }
45
46    // Map dependencies.
47    for (_name, target) in &manifest.use_map {
48        match target {
49            webc::metadata::UrlOrManifest::Url(_url) => {
50                // Not supported.
51            }
52            webc::metadata::UrlOrManifest::Manifest(_) => {
53                // Not supported.
54            }
55            webc::metadata::UrlOrManifest::RegistryDependentUrl(raw) => {
56                let (name, version) = if let Some((name, version_raw)) = raw.split_once('@') {
57                    let version = version_raw.parse().map_err(|err| {
58                        ConversionError::with_cause(
59                            format!("Could not parse version of dependency: '{raw}'"),
60                            err,
61                        )
62                    })?;
63                    (name.to_string(), version)
64                } else {
65                    (raw.to_string(), "*".parse().unwrap())
66                };
67
68                pkg_manifest.dependencies.insert(name, version);
69            }
70        }
71    }
72
73    // Convert filesystem mappings.
74
75    let fs_annotation = manifest
76        .filesystem()
77        .map_err(|err| ConversionError::msg(format!("could not read fs annotation: {err}")))?;
78    if let Some(ann) = fs_annotation {
79        for mapping in ann.0 {
80            if mapping.from.is_some() {
81                // wasmer.toml does not allow specifying dependency mounts.
82                continue;
83            }
84
85            // Extract the volume to "<target-dir>/<volume-name>".
86            let volume = webc.get_volume(&mapping.volume_name).ok_or_else(|| {
87                ConversionError::msg(format!(
88                    "Package annotations specify a volume that does not exist: '{}'",
89                    mapping.volume_name
90                ))
91            })?;
92
93            let volume_path = target_dir.join(mapping.volume_name.trim_start_matches('/'));
94
95            std::fs::create_dir_all(&volume_path).map_err(|err| {
96                ConversionError::with_cause(
97                    format!(
98                        "could not create volume directory '{}'",
99                        volume_path.display()
100                    ),
101                    err,
102                )
103            })?;
104
105            volume.unpack("/", &volume_path).map_err(|err| {
106                ConversionError::with_cause("could not unpack volume to filesystemt", err)
107            })?;
108
109            let mut source_path = mapping
110                .volume_name
111                .trim_start_matches('/')
112                .trim_end_matches('/')
113                .to_string();
114            if let Some(subpath) = mapping.host_path {
115                if !source_path.ends_with('/') {
116                    source_path.push('/');
117                }
118                source_path.push_str(&subpath);
119            }
120            source_path.insert_str(0, "./");
121
122            pkg_manifest
123                .fs
124                .insert(mapping.mount_path, source_path.into());
125        }
126    }
127
128    // Convert modules.
129
130    let module_dir_name = "modules";
131    let module_dir = target_dir.join(module_dir_name);
132
133    let atoms = webc.atoms();
134    if !atoms.is_empty() {
135        std::fs::create_dir_all(&module_dir).map_err(|err| {
136            ConversionError::with_cause(
137                format!("Could not create directory '{}'", module_dir.display()),
138                err,
139            )
140        })?;
141        for (atom_name, data) in atoms {
142            let atom_path = module_dir.join(&atom_name);
143
144            std::fs::write(&atom_path, &data).map_err(|err| {
145                ConversionError::with_cause(
146                    format!("Could not write atom to path '{}'", atom_path.display()),
147                    err,
148                )
149            })?;
150
151            let relative_path = format!("./{module_dir_name}/{atom_name}");
152
153            let mut annotations = None;
154
155            if let Some(manifest_atom) = manifest.atoms.get(&atom_name)
156                && let Some(sco) = manifest_atom
157                    .annotations
158                    .get(SuggestedCompilerOptimizations::KEY)
159                && let Some((_, v)) = sco.as_map().and_then(|v| {
160                    v.iter().find(|(k, _)| {
161                        k.as_text()
162                            .is_some_and(|v| v == SuggestedCompilerOptimizations::PASS_PARAMS_KEY)
163                    })
164                })
165            {
166                annotations = Some(UserAnnotations {
167                    suggested_compiler_optimizations: SuggestedCompilerOptimizations {
168                        pass_params: Some(v.as_bool().unwrap_or_default()),
169                    },
170                });
171            }
172
173            pkg_manifest.modules.push(wasmer_config::package::Module {
174                name: atom_name,
175                source: relative_path.into(),
176                abi: wasmer_config::package::Abi::None,
177                kind: None,
178                interfaces: None,
179                bindings: None,
180                annotations,
181            });
182        }
183    }
184
185    // Convert commands.
186    for (name, spec) in &manifest.commands {
187        let mut annotations = toml::Table::new();
188        for (key, value) in &spec.annotations {
189            if key == webc::metadata::annotations::Atom::KEY {
190                continue;
191            }
192
193            let raw_toml = toml::to_string(&value).unwrap();
194            let toml_value = toml::from_str::<toml::Value>(&raw_toml).unwrap();
195            annotations.insert(key.into(), toml_value);
196        }
197
198        let atom_annotation = spec
199            .annotation::<webc::metadata::annotations::Atom>(webc::metadata::annotations::Atom::KEY)
200            .map_err(|err| {
201                ConversionError::msg(format!(
202                    "could not read atom annotation for command '{name}': {err}"
203                ))
204            })?
205            .ok_or_else(|| {
206                ConversionError::msg(format!(
207                    "Command '{name}' is missing the required atom annotation"
208                ))
209            })?;
210
211        let module = if let Some(dep) = atom_annotation.dependency {
212            ModuleReference::Dependency {
213                dependency: dep,
214                module: atom_annotation.name,
215            }
216        } else {
217            ModuleReference::CurrentPackage {
218                module: atom_annotation.name,
219            }
220        };
221
222        let cmd = wasmer_config::package::Command::V2(wasmer_config::package::CommandV2 {
223            name: name.clone(),
224            module,
225            runner: spec.runner.clone(),
226            annotations: Some(wasmer_config::package::CommandAnnotations::Raw(
227                annotations.into(),
228            )),
229        });
230
231        pkg_manifest.commands.push(cmd);
232    }
233
234    // Write out the manifest.
235    let manifest_toml = toml::to_string(&pkg_manifest)
236        .map_err(|err| ConversionError::with_cause("could not serialize package manifest", err))?;
237    std::fs::write(target_dir.join("wasmer.toml"), manifest_toml)
238        .map_err(|err| ConversionError::with_cause("could not write wasmer.toml", err))?;
239
240    Ok(())
241}
242
243#[cfg(test)]
244mod tests {
245    use std::fs::create_dir_all;
246
247    use pretty_assertions::assert_eq;
248
249    use crate::{package::Package, utils::from_bytes};
250
251    use super::*;
252
253    // Build a webc from a package directory, and then restore the directory
254    // from the webc.
255    #[test]
256    fn test_wasmer_package_webc_roundtrip() {
257        let tmpdir = tempfile::tempdir().unwrap();
258        let dir = tmpdir.path();
259
260        let webc = {
261            let dir_input = dir.join("input");
262            let dir_public = dir_input.join("public");
263
264            create_dir_all(&dir_public).unwrap();
265
266            std::fs::write(dir_public.join("index.html"), "INDEX").unwrap();
267
268            std::fs::write(dir_input.join("mywasm.wasm"), "()").unwrap();
269
270            std::fs::write(
271                dir_input.join("wasmer.toml"),
272                r#"
273[package]
274name = "testns/testpkg"
275version = "0.0.1"
276description = "descr1"
277license = "MIT"
278
279[dependencies]
280"wasmer/python" = "8.12.0"
281
282[fs]
283public = "./public"
284
285[[module]]
286name = "mywasm"
287source = "./mywasm.wasm"
288
289[[command]]
290name = "run"
291module = "mywasm"
292runner = "wasi"
293
294[command.annotations.wasi]
295env =  ["A=B"]
296main-args = ["/mounted/script.py"]
297"#,
298            )
299            .unwrap();
300
301            let pkg = Package::from_manifest(dir_input.join("wasmer.toml")).unwrap();
302            let raw = pkg.serialize().unwrap();
303            from_bytes(raw).unwrap()
304        };
305
306        let dir_output = dir.join("output");
307        webc_to_package_dir(&webc, &dir_output).unwrap();
308
309        assert_eq!(
310            std::fs::read_to_string(dir_output.join("public/index.html")).unwrap(),
311            "INDEX",
312        );
313
314        assert_eq!(
315            std::fs::read_to_string(dir_output.join("modules/mywasm")).unwrap(),
316            "()",
317        );
318
319        assert_eq!(
320            std::fs::read_to_string(dir_output.join("wasmer.toml"))
321                .unwrap()
322                .trim(),
323            r#"
324
325[package]
326license = "MIT"
327entrypoint = "run"
328
329[dependencies]
330"wasmer/python" = "^8.12.0"
331
332[fs]
333"/public" = "./public"
334
335[[module]]
336name = "mywasm"
337source = "./modules/mywasm"
338
339[[command]]
340name = "run"
341module = "mywasm"
342runner = "https://webc.org/runner/wasi"
343
344[command.annotations.wasi]
345atom = "mywasm"
346env = ["A=B"]
347main-args = ["/mounted/script.py"]
348            "#
349            .trim(),
350        );
351    }
352}