Skip to content

Commit

Permalink
feat(package): Implement restoring packages from webcs
Browse files Browse the repository at this point in the history
Convert webc images to a package directory with a wasmer.toml
  • Loading branch information
theduke committed Sep 16, 2024
1 parent 64b7559 commit 372a8e2
Show file tree
Hide file tree
Showing 2 changed files with 381 additions and 0 deletions.
14 changes: 14 additions & 0 deletions lib/config/src/package/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ pub struct Package {
}

impl Package {
pub fn new_empty() -> Self {
PackageBuilder::default().build().unwrap()
}

/// Create a [`PackageBuilder`] populated with all mandatory fields.
pub fn builder(
name: impl Into<String>,
Expand Down Expand Up @@ -732,6 +736,16 @@ pub struct Manifest {
}

impl Manifest {
pub fn new_empty() -> Self {
Self {
package: None,
dependencies: HashMap::new(),
fs: IndexMap::new(),
modules: Vec::new(),
commands: Vec::new(),
}
}

/// Create a [`ManifestBuilder`] populated with all mandatory fields.
pub fn builder(package: Package) -> ManifestBuilder {
ManifestBuilder::new(package)
Expand Down
367 changes: 367 additions & 0 deletions lib/package/src/convert.rs
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(),
);
}
}

0 comments on commit 372a8e2

Please sign in to comment.