use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use anyhow::Context;
use wasmer_toml::{Command, Manifest, Package, MANIFEST_FILE_NAME};
use webc::metadata::{
annotations::{self, FileSystemMapping, FileSystemMappings, VolumeSpecificPath},
BindingsExtended, WaiBindings, WitBindings,
};
const README_PATHS: &[&str; 5] = &[
"README",
"README.md",
"README.markdown",
"README.mdown",
"README.mkdn",
];
const LICENSE_PATHS: &[&str; 3] = &["LICENSE", "LICENSE.md", "COPYING"];
pub trait Transform {
fn get_atoms_wapm_toml(
&self,
files: &BTreeMap<&Path, &Vec<u8>>,
) -> Result<Vec<(String, PathBuf)>, anyhow::Error>;
fn get_dependencies(&self, wapm_toml: &str) -> Vec<(String, String)>;
fn get_package_annotations(
&self,
wapm_toml: &str,
) -> Result<webc::metadata::annotations::Wapm, anyhow::Error>;
fn get_fs_table(&self, wapm_toml: &str) -> Result<FileSystemMappings, anyhow::Error>;
fn get_modules(&self, wapm_toml: &str) -> Vec<(String, String, String)>;
fn get_commands(
&self,
wapm_toml: &str,
base_path: &Path,
atom_kinds: &BTreeMap<String, String>,
) -> Result<BTreeMap<String, WebcCommand>, anyhow::Error>;
fn get_bindings(
&self,
wapm_toml: &str,
base_path: &Path,
atom_kinds: &BTreeMap<String, String>,
) -> Result<Vec<(String, String, serde_cbor::Value)>, anyhow::Error>;
fn get_manifest_file_names(&self) -> Vec<PathBuf>;
fn get_metadata_paths(&self, bindings: &[serde_cbor::Value]) -> Vec<PathBuf>;
fn get_wapm_manifest_file_name(&self) -> PathBuf;
}
#[derive(Copy, Clone, Default)]
pub struct TransformManifestFunctions {}
impl Transform for TransformManifestFunctions {
fn get_atoms_wapm_toml(
&self,
files: &BTreeMap<&Path, &Vec<u8>>,
) -> Result<Vec<(String, PathBuf)>, anyhow::Error> {
{
let paths = files;
let wapm_toml = paths
.get(Path::new(MANIFEST_FILE_NAME))
.or_else(|| paths.get(Path::new("wapm.toml")))
.with_context(|| {
anyhow::anyhow!(
"Could not find {MANIFEST_FILE_NAME} in FileMap: {:#?}",
paths.keys().collect::<Vec<_>>()
)
})?;
let wapm_toml =
std::str::from_utf8(wapm_toml).context("toml file contains invalid UTF-8")?;
let wapm_toml: Manifest =
toml::from_str(wapm_toml).context("Could not parse wasmer.toml")?;
Ok(wapm_toml
.modules
.into_iter()
.map(|m| (m.name.clone(), Path::new(&m.source).to_path_buf()))
.collect())
}
}
fn get_dependencies(&self, wapm_toml: &str) -> Vec<(String, String)> {
{
let wapm: Manifest = match toml::from_str(wapm_toml) {
Ok(o) => o,
Err(_) => {
return Vec::new();
}
};
wapm.dependencies
.into_iter()
.map(|(k, v)| (k, v.to_string()))
.collect()
}
}
fn get_package_annotations(
&self,
wapm_toml: &str,
) -> Result<webc::metadata::annotations::Wapm, anyhow::Error> {
{
let wapm: Manifest =
toml::from_str(wapm_toml).context("Unable to deserialize the manifest")?;
Ok(transform_package_meta_to_annotations(&wapm.package))
}
}
fn get_fs_table(&self, wapm_toml: &str) -> Result<FileSystemMappings, anyhow::Error> {
{
let wapm: Manifest =
toml::from_str(wapm_toml).context("Unable to deserialize the manifest")?;
let fs = wapm.fs;
if fs.is_empty() {
Ok(FileSystemMappings::default())
} else {
let mut entries = Vec::new();
for (guest, host) in fs {
let mapping = FileSystemMapping {
from: None,
volume_name: "atom".to_string(),
original_path: sanitize_path(host.display().to_string()),
mount_path: sanitize_path(guest),
};
entries.push(mapping);
}
Ok(FileSystemMappings(entries))
}
}
}
fn get_modules(&self, wapm_toml: &str) -> Vec<(String, String, String)> {
{
let wapm: Manifest = match toml::from_str(wapm_toml) {
Ok(o) => o,
Err(_) => {
return Vec::new();
}
};
wapm.modules
.iter()
.map(|m| {
(
m.name.to_string(),
m.abi.to_string(),
m.kind.as_deref().unwrap_or("wasm").to_string(),
)
})
.collect()
}
}
fn get_commands(
&self,
wapm_toml: &str,
base_path: &Path,
atom_kinds: &BTreeMap<String, String>,
) -> Result<BTreeMap<String, WebcCommand>, anyhow::Error> {
{
let wapm: Manifest = toml::from_str(wapm_toml)?;
let mut commands = BTreeMap::new();
for command in &wapm.commands {
match command {
Command::V1(command) => {
let name = &command.name;
let module = command.module.to_string();
let main_args = command.main_args.as_ref();
let package = command.package.as_ref();
if commands.iter().any(|(k, _)| k == name) {
return Err(anyhow::anyhow!(
"Command {name} is defined more than once"
));
}
let abi = atom_kinds.get(&module).map(|s| s.as_str());
let runner = match abi {
Some("emscripten") => annotations::EMSCRIPTEN_RUNNER_URI,
Some("wasm4") => annotations::WASM4_RUNNER_URI,
Some("wcgi") => annotations::WCGI_RUNNER_URI,
Some("wasi") | Some("generic") => annotations::WASI_RUNNER_URI,
_ => {
return Err(anyhow::anyhow!(
"Unknown ABI in command {name:?}: {:?}",
abi.unwrap_or("")
));
}
};
let annotations_str = match abi {
Some("emscripten") => annotations::Emscripten::KEY,
Some("wasm4") => "wasm4",
Some("wcgi") => annotations::Wcgi::KEY,
Some("wasi") | Some("generic") => annotations::Wasi::KEY,
_ => {
return Err(anyhow::anyhow!(
"Unknown ABI in command {name:?}: {:?}",
abi.unwrap_or("")
));
}
};
let annotations = vec![(
annotations_str.to_string(),
serde_cbor::value::to_value(TransformCmdArgs {
atom: module,
main_args: main_args.cloned(),
package: package.cloned(),
})
.unwrap(),
)]
.into_iter()
.collect();
commands.insert(
name.clone(),
WebcCommand {
runner: runner.to_string(),
annotations,
},
);
}
Command::V2(command) => {
let runner = runner_uri_from_name(&command.runner);
let annotations = command
.get_annotations(base_path)
.map_err(|e| anyhow::anyhow!("command {}: {e}", command.name))?;
let annotations = match annotations {
Some(serde_cbor::Value::Map(annotations)) => annotations
.into_iter()
.filter_map(|(k, v)| match k {
serde_cbor::Value::Text(key) => Some((key, v)),
_ => None,
})
.collect(),
_ => BTreeMap::default(),
};
commands.insert(
command.name.clone(),
WebcCommand {
runner,
annotations,
},
);
}
}
}
Ok(commands)
}
}
fn get_bindings(
&self,
wapm_toml: &str,
base_path: &Path,
atom_kinds: &BTreeMap<String, String>,
) -> Result<Vec<(String, String, serde_cbor::Value)>, anyhow::Error> {
{
let _base_path = base_path;
let _atom_kinds = atom_kinds;
let wapm: Manifest = toml::from_str(wapm_toml)?;
let mut bindings = Vec::new();
for module in &wapm.modules {
if let Some(b) = module.bindings.as_ref() {
bindings.push(bindings_to_webc(b, &module.name)?);
}
}
Ok(bindings)
}
}
fn get_manifest_file_names(&self) -> Vec<PathBuf> {
vec![Path::new(MANIFEST_FILE_NAME).to_path_buf()]
}
fn get_metadata_paths(&self, bindings: &[serde_cbor::Value]) -> Vec<PathBuf> {
{
let mut paths = Vec::new();
for b in bindings {
if let Ok(wit) =
serde_cbor::from_slice::<BindingsExtended>(&serde_cbor::to_vec(b).unwrap())
{
paths.extend(
wit.metadata_paths()
.into_iter()
.map(|p| p.replacen("metadata://", "", 1))
.map(PathBuf::from),
);
}
}
for p in README_PATHS.iter() {
paths.push(Path::new(p).to_path_buf());
}
for p in LICENSE_PATHS.iter() {
paths.push(Path::new(p).to_path_buf());
}
paths
}
}
fn get_wapm_manifest_file_name(&self) -> PathBuf {
Path::new(MANIFEST_FILE_NAME).to_path_buf()
}
}
fn runner_uri_from_name(runner: &str) -> String {
match runner {
"wasi" => return webc::metadata::annotations::WASI_RUNNER_URI.to_string(),
"wcgi" => return webc::metadata::annotations::WCGI_RUNNER_URI.to_string(),
"emscripten" => return webc::metadata::annotations::EMSCRIPTEN_RUNNER_URI.to_string(),
"wasm4" => return webc::metadata::annotations::WASM4_RUNNER_URI.to_string(),
_ => {}
}
if let Ok(url) = url::Url::parse(runner) {
if url.has_host() {
return runner.to_string();
}
}
format!("https://webc.org/runner/{}", runner)
}
fn sanitize_path(path: impl AsRef<Path>) -> String {
let path = path.as_ref();
let mut segments = Vec::new();
for component in path.components() {
match component {
std::path::Component::Prefix(_) | std::path::Component::RootDir => {}
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
segments.pop();
}
std::path::Component::Normal(segment) => {
segments.push(segment.to_string_lossy());
}
}
}
let mut sanitized = String::new();
for segment in segments {
sanitized.push('/');
sanitized.push_str(&segment);
}
sanitized
}
#[derive(Debug, Clone)]
pub struct WebcCommand {
pub runner: String,
pub annotations: BTreeMap<String, serde_cbor::Value>,
}
type WebcBinding = (String, String, serde_cbor::Value);
fn bindings_to_webc(
b: &wasmer_toml::Bindings,
module_name: &str,
) -> Result<WebcBinding, anyhow::Error> {
match b {
wasmer_toml::Bindings::Wit(wit) => wit_bindings_to_webc(wit, module_name),
wasmer_toml::Bindings::Wai(wai) => wai_bindings_to_webc(wai, module_name),
}
}
fn wai_bindings_to_webc(
wai: &wasmer_toml::WaiBindings,
module_name: &str,
) -> Result<WebcBinding, anyhow::Error> {
let exports = wai
.exports
.as_ref()
.map(|exports| format!("metadata://{}", exports.display()));
let imports = wai
.imports
.iter()
.map(|import| format!("metadata://{}", import.display()))
.collect();
let value = serde_cbor::value::to_value(BindingsExtended::Wai(WaiBindings {
module: format!("atoms://{module_name}"),
exports,
imports,
}))?;
Ok((
"library-bindings".to_string(),
format!("wai@{}", wai.wai_version),
value,
))
}
fn wit_bindings_to_webc(
wit: &wasmer_toml::WitBindings,
module_name: &str,
) -> Result<WebcBinding, anyhow::Error> {
let value = serde_cbor::value::to_value(BindingsExtended::Wit(WitBindings {
exports: format!("metadata://{}", wit.wit_exports.display()),
module: format!("atoms://{module_name}"),
}))?;
Ok((
"library-bindings".to_string(),
format!("wit@{}", wit.wit_bindgen),
value,
))
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TransformCmdArgs {
atom: String,
main_args: Option<String>,
package: Option<String>,
}
fn transform_package_meta_to_annotations(package: &Package) -> webc::metadata::annotations::Wapm {
let mut wapm = webc::metadata::annotations::Wapm::new(
&package.name,
package.version.to_string(),
&package.description,
);
wapm.license = package.license.clone();
wapm.license_file = package
.license_file
.as_ref()
.map(|path| VolumeSpecificPath {
volume: "metadata".to_string(),
path: path.display().to_string(),
});
wapm.readme = package.readme.as_ref().map(|path| VolumeSpecificPath {
volume: "metadata".to_string(),
path: path.display().to_string(),
});
wapm.repository = package.repository.clone();
wapm.homepage = package.homepage.clone();
wapm
}