-
Notifications
You must be signed in to change notification settings - Fork 825
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(package): Implement restoring packages from webcs
Convert webc images to a package directory with a wasmer.toml
- Loading branch information
Showing
2 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
use std::{path::Path, sync::Arc}; | ||
|
||
use wasmer_config::package::ModuleReference; | ||
|
||
/// Convert a webc image into a directory with a wasmer.toml file that can | ||
/// be used for generating a new pacakge. | ||
pub fn webc_to_package_dir( | ||
webc: &webc::Container, | ||
target_dir: &Path, | ||
) -> Result<(), ManifestConversionError> { | ||
let mut pkg_manifest = wasmer_config::package::Manifest::new_empty(); | ||
|
||
let manifest = webc.manifest(); | ||
// Convert the package annotation. | ||
|
||
let pkg_annotation = manifest.wapm().map_err(|err| { | ||
ManifestConversionError::with_cause("could not read package annotation", err) | ||
})?; | ||
if let Some(ann) = pkg_annotation { | ||
let mut pkg = wasmer_config::package::Package::new_empty(); | ||
|
||
pkg.name = ann.name; | ||
pkg.version = if let Some(raw) = ann.version { | ||
let v = raw | ||
.parse() | ||
.map_err(|e| ManifestConversionError::with_cause("invalid package version", e))?; | ||
Some(v) | ||
} else { | ||
None | ||
}; | ||
|
||
pkg.description = ann.description; | ||
pkg.license = ann.license; | ||
|
||
// TODO: map license_file and README (paths!) | ||
|
||
pkg.homepage = ann.homepage; | ||
pkg.repository = ann.repository; | ||
pkg.private = ann.private; | ||
pkg.entrypoint = manifest.entrypoint.clone(); | ||
|
||
pkg_manifest.package = Some(pkg); | ||
} | ||
|
||
// Map dependencies. | ||
for (_name, target) in &manifest.use_map { | ||
match target { | ||
webc::metadata::UrlOrManifest::Url(_url) => { | ||
// Not supported. | ||
} | ||
webc::metadata::UrlOrManifest::Manifest(_) => { | ||
// Not supported. | ||
} | ||
webc::metadata::UrlOrManifest::RegistryDependentUrl(raw) => { | ||
let (name, version) = if let Some((name, version_raw)) = raw.split_once('@') { | ||
let version = version_raw.parse().map_err(|err| { | ||
ManifestConversionError::with_cause( | ||
format!("Could not parse version of dependency: '{}'", raw), | ||
err, | ||
) | ||
})?; | ||
(name.to_string(), version) | ||
} else { | ||
(raw.to_string(), "*".parse().unwrap()) | ||
}; | ||
|
||
pkg_manifest.dependencies.insert(name, version); | ||
} | ||
} | ||
} | ||
|
||
// Convert filesystem mappings. | ||
|
||
let fs_annotation = manifest | ||
.filesystem() | ||
.map_err(|err| ManifestConversionError::with_cause("could n ot read fs annotation", err))?; | ||
if let Some(ann) = fs_annotation { | ||
for mapping in ann.0 { | ||
if mapping.from.is_some() { | ||
// wasmer.toml does not allow specifying dependency mounts. | ||
continue; | ||
} | ||
|
||
// Extract the volume to "<target-dir>/<volume-name>". | ||
let volume = webc.get_volume(&mapping.volume_name).ok_or_else(|| { | ||
ManifestConversionError::msg(format!( | ||
"Package annotations specify a volume that does not exist: '{}'", | ||
mapping.volume_name | ||
)) | ||
})?; | ||
|
||
let volume_path = target_dir.join(mapping.volume_name.trim_start_matches('/')); | ||
|
||
std::fs::create_dir_all(&volume_path).map_err(|err| { | ||
ManifestConversionError::with_cause( | ||
format!( | ||
"could not create volume directory '{}'", | ||
volume_path.display() | ||
), | ||
err, | ||
) | ||
})?; | ||
|
||
volume.unpack("/", &volume_path).map_err(|err| { | ||
ManifestConversionError::with_cause("could not unpack volume to filesystemt", err) | ||
})?; | ||
|
||
let mut source_path = mapping | ||
.volume_name | ||
.trim_start_matches('/') | ||
.trim_end_matches('/') | ||
.to_string(); | ||
if let Some(subpath) = mapping.host_path { | ||
if !source_path.ends_with('/') { | ||
source_path.push('/'); | ||
} | ||
source_path.push_str(&subpath); | ||
} | ||
source_path.insert_str(0, "./"); | ||
|
||
pkg_manifest | ||
.fs | ||
.insert(mapping.mount_path, source_path.into()); | ||
} | ||
} | ||
|
||
// Convert modules. | ||
|
||
let module_dir_name = "modules"; | ||
let module_dir = target_dir.join(module_dir_name); | ||
|
||
for (atom_name, data) in webc.atoms() { | ||
let atom_path = module_dir.join(&atom_name); | ||
|
||
std::fs::create_dir_all(&module_dir).map_err(|err| { | ||
ManifestConversionError::with_cause( | ||
format!("Could not create directory '{}'", module_dir.display(),), | ||
err, | ||
) | ||
})?; | ||
|
||
std::fs::write(&atom_path, &data).map_err(|err| { | ||
ManifestConversionError::with_cause( | ||
format!("Could not write atom to path '{}'", atom_path.display()), | ||
err, | ||
) | ||
})?; | ||
|
||
let relative_path = format!("./{module_dir_name}/{atom_name}"); | ||
|
||
pkg_manifest.modules.push(wasmer_config::package::Module { | ||
name: atom_name, | ||
source: relative_path.into(), | ||
abi: wasmer_config::package::Abi::None, | ||
kind: None, | ||
interfaces: None, | ||
bindings: None, | ||
}); | ||
} | ||
|
||
// Convert commands. | ||
for (name, spec) in &manifest.commands { | ||
let mut annotations = toml::Table::new(); | ||
for (key, value) in &spec.annotations { | ||
if key == webc::metadata::annotations::Atom::KEY { | ||
continue; | ||
} | ||
|
||
let raw_toml = toml::to_string(&value).unwrap(); | ||
let toml_value = toml::from_str::<toml::Value>(&raw_toml).unwrap(); | ||
annotations.insert(key.into(), toml_value); | ||
} | ||
|
||
let atom_annotation = spec | ||
.annotation::<webc::metadata::annotations::Atom>(webc::metadata::annotations::Atom::KEY) | ||
.map_err(|err| { | ||
ManifestConversionError::with_cause( | ||
format!("could not read atom annotation for command '{}'", name), | ||
err, | ||
) | ||
})? | ||
.ok_or_else(|| { | ||
ManifestConversionError::msg(format!( | ||
"Command '{name}' is missing the required atom annotation" | ||
)) | ||
})?; | ||
|
||
let module = if let Some(dep) = atom_annotation.dependency { | ||
ModuleReference::Dependency { | ||
dependency: dep, | ||
module: atom_annotation.name, | ||
} | ||
} else { | ||
ModuleReference::CurrentPackage { | ||
module: atom_annotation.name, | ||
} | ||
}; | ||
|
||
let cmd = wasmer_config::package::Command::V2(wasmer_config::package::CommandV2 { | ||
name: name.clone(), | ||
module, | ||
runner: spec.runner.clone(), | ||
annotations: Some(wasmer_config::package::CommandAnnotations::Raw( | ||
annotations.into(), | ||
)), | ||
}); | ||
|
||
pkg_manifest.commands.push(cmd); | ||
} | ||
|
||
// Write out the manifest. | ||
let manifest_toml = toml::to_string(&pkg_manifest).map_err(|err| { | ||
ManifestConversionError::with_cause("could not serialize package manifest", err) | ||
})?; | ||
std::fs::write(target_dir.join("wasmer.toml"), manifest_toml) | ||
.map_err(|err| ManifestConversionError::with_cause("could not write wasmer.toml", err))?; | ||
|
||
Ok(()) | ||
} | ||
|
||
#[derive(Clone, Debug)] | ||
pub struct ManifestConversionError { | ||
message: String, | ||
cause: Option<Arc<dyn std::error::Error + Send + Sync>>, | ||
} | ||
|
||
impl ManifestConversionError { | ||
pub fn msg(msg: impl Into<String>) -> Self { | ||
Self { | ||
message: msg.into(), | ||
cause: None, | ||
} | ||
} | ||
|
||
pub fn with_cause( | ||
msg: impl Into<String>, | ||
cause: impl std::error::Error + Send + Sync + 'static, | ||
) -> Self { | ||
Self { | ||
message: msg.into(), | ||
cause: Some(Arc::new(cause)), | ||
} | ||
} | ||
} | ||
|
||
impl std::fmt::Display for ManifestConversionError { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "could not convert manifest: {}", self.message)?; | ||
if let Some(cause) = &self.cause { | ||
write!(f, " (cause: {})", cause)?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
impl std::error::Error for ManifestConversionError {} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::fs::create_dir_all; | ||
|
||
use pretty_assertions::assert_eq; | ||
|
||
use super::*; | ||
|
||
// Build a webc from a pacakge directory, and then restore the directory | ||
// from the webc. | ||
#[test] | ||
fn test_wasmer_package_webc_roundtrip() { | ||
let tmpdir = tempfile::tempdir().unwrap(); | ||
let dir = tmpdir.path(); | ||
|
||
let webc = { | ||
let dir_input = dir.join("input"); | ||
let dir_public = dir_input.join("public"); | ||
|
||
create_dir_all(&dir_public).unwrap(); | ||
|
||
std::fs::write(dir_public.join("index.html"), "INDEX").unwrap(); | ||
|
||
std::fs::write(dir_input.join("mywasm.wasm"), "()").unwrap(); | ||
|
||
std::fs::write( | ||
dir_input.join("wasmer.toml"), | ||
r#" | ||
[package] | ||
name = "testns/testpkg" | ||
version = "0.0.1" | ||
description = "descr1" | ||
license = "MIT" | ||
[dependencies] | ||
"wasmer/python" = "8.12.0" | ||
[fs] | ||
public = "./public" | ||
[[module]] | ||
name = "mywasm" | ||
source = "./mywasm.wasm" | ||
[[command]] | ||
name = "run" | ||
module = "mywasm" | ||
runner = "wasi" | ||
[command.annotations.wasi] | ||
env = ["A=B"] | ||
main-args = ["/mounted/script.py"] | ||
"#, | ||
) | ||
.unwrap(); | ||
|
||
let pkg = webc::wasmer_package::Package::from_manifest(dir_input.join("wasmer.toml")) | ||
.unwrap(); | ||
let raw = pkg.serialize().unwrap(); | ||
webc::Container::from_bytes(raw).unwrap() | ||
}; | ||
|
||
let dir_output = dir.join("output"); | ||
webc_to_package_dir(&webc, &dir_output).unwrap(); | ||
|
||
assert_eq!( | ||
std::fs::read_to_string(dir_output.join("public/index.html")).unwrap(), | ||
"INDEX", | ||
); | ||
|
||
assert_eq!( | ||
std::fs::read_to_string(dir_output.join("modules/mywasm")).unwrap(), | ||
"()", | ||
); | ||
|
||
assert_eq!( | ||
std::fs::read_to_string(dir_output.join("wasmer.toml")) | ||
.unwrap() | ||
.trim(), | ||
r#" | ||
[package] | ||
license = "MIT" | ||
entrypoint = "run" | ||
[dependencies] | ||
"wasmer/python" = "^8.12.0" | ||
[fs] | ||
"/public" = "./public" | ||
[[module]] | ||
name = "mywasm" | ||
source = "./modules/mywasm" | ||
[[command]] | ||
name = "run" | ||
module = "mywasm" | ||
runner = "https://webc.org/runner/wasi" | ||
[command.annotations.wasi] | ||
atom = "mywasm" | ||
env = ["A=B"] | ||
main-args = ["/mounted/script.py"] | ||
"# | ||
.trim(), | ||
); | ||
} | ||
} |