wasmer_package/convert/
webc_to_package.rs

1use std::path::Path;
2
3use wasmer_config::package::ModuleReference;
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 pacakge.
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::with_cause("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::with_cause("could n ot 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            pkg_manifest.modules.push(wasmer_config::package::Module {
154                name: atom_name,
155                source: relative_path.into(),
156                abi: wasmer_config::package::Abi::None,
157                kind: None,
158                interfaces: None,
159                bindings: None,
160            });
161        }
162    }
163
164    // Convert commands.
165    for (name, spec) in &manifest.commands {
166        let mut annotations = toml::Table::new();
167        for (key, value) in &spec.annotations {
168            if key == webc::metadata::annotations::Atom::KEY {
169                continue;
170            }
171
172            let raw_toml = toml::to_string(&value).unwrap();
173            let toml_value = toml::from_str::<toml::Value>(&raw_toml).unwrap();
174            annotations.insert(key.into(), toml_value);
175        }
176
177        let atom_annotation = spec
178            .annotation::<webc::metadata::annotations::Atom>(webc::metadata::annotations::Atom::KEY)
179            .map_err(|err| {
180                ConversionError::with_cause(
181                    format!("could not read atom annotation for command '{name}'"),
182                    err,
183                )
184            })?
185            .ok_or_else(|| {
186                ConversionError::msg(format!(
187                    "Command '{name}' is missing the required atom annotation"
188                ))
189            })?;
190
191        let module = if let Some(dep) = atom_annotation.dependency {
192            ModuleReference::Dependency {
193                dependency: dep,
194                module: atom_annotation.name,
195            }
196        } else {
197            ModuleReference::CurrentPackage {
198                module: atom_annotation.name,
199            }
200        };
201
202        let cmd = wasmer_config::package::Command::V2(wasmer_config::package::CommandV2 {
203            name: name.clone(),
204            module,
205            runner: spec.runner.clone(),
206            annotations: Some(wasmer_config::package::CommandAnnotations::Raw(
207                annotations.into(),
208            )),
209        });
210
211        pkg_manifest.commands.push(cmd);
212    }
213
214    // Write out the manifest.
215    let manifest_toml = toml::to_string(&pkg_manifest)
216        .map_err(|err| ConversionError::with_cause("could not serialize package manifest", err))?;
217    std::fs::write(target_dir.join("wasmer.toml"), manifest_toml)
218        .map_err(|err| ConversionError::with_cause("could not write wasmer.toml", err))?;
219
220    Ok(())
221}
222
223#[cfg(test)]
224mod tests {
225    use std::fs::create_dir_all;
226
227    use pretty_assertions::assert_eq;
228
229    use crate::{package::Package, utils::from_bytes};
230
231    use super::*;
232
233    // Build a webc from a pacakge directory, and then restore the directory
234    // from the webc.
235    #[test]
236    fn test_wasmer_package_webc_roundtrip() {
237        let tmpdir = tempfile::tempdir().unwrap();
238        let dir = tmpdir.path();
239
240        let webc = {
241            let dir_input = dir.join("input");
242            let dir_public = dir_input.join("public");
243
244            create_dir_all(&dir_public).unwrap();
245
246            std::fs::write(dir_public.join("index.html"), "INDEX").unwrap();
247
248            std::fs::write(dir_input.join("mywasm.wasm"), "()").unwrap();
249
250            std::fs::write(
251                dir_input.join("wasmer.toml"),
252                r#"
253[package]
254name = "testns/testpkg"
255version = "0.0.1"
256description = "descr1"
257license = "MIT"
258
259[dependencies]
260"wasmer/python" = "8.12.0"
261
262[fs]
263public = "./public"
264
265[[module]]
266name = "mywasm"
267source = "./mywasm.wasm"
268
269[[command]]
270name = "run"
271module = "mywasm"
272runner = "wasi"
273
274[command.annotations.wasi]
275env =  ["A=B"]
276main-args = ["/mounted/script.py"]
277"#,
278            )
279            .unwrap();
280
281            let pkg = Package::from_manifest(dir_input.join("wasmer.toml")).unwrap();
282            let raw = pkg.serialize().unwrap();
283            from_bytes(raw).unwrap()
284        };
285
286        let dir_output = dir.join("output");
287        webc_to_package_dir(&webc, &dir_output).unwrap();
288
289        assert_eq!(
290            std::fs::read_to_string(dir_output.join("public/index.html")).unwrap(),
291            "INDEX",
292        );
293
294        assert_eq!(
295            std::fs::read_to_string(dir_output.join("modules/mywasm")).unwrap(),
296            "()",
297        );
298
299        assert_eq!(
300            std::fs::read_to_string(dir_output.join("wasmer.toml"))
301                .unwrap()
302                .trim(),
303            r#"
304
305[package]
306license = "MIT"
307entrypoint = "run"
308
309[dependencies]
310"wasmer/python" = "^8.12.0"
311
312[fs]
313"/public" = "./public"
314
315[[module]]
316name = "mywasm"
317source = "./modules/mywasm"
318
319[[command]]
320name = "run"
321module = "mywasm"
322runner = "https://webc.org/runner/wasi"
323
324[command.annotations.wasi]
325atom = "mywasm"
326env = ["A=B"]
327main-args = ["/mounted/script.py"]
328            "#
329            .trim(),
330        );
331    }
332}