#![allow(deprecated)]
pub extern crate serde_cbor;
pub extern crate toml;
pub mod rust;
use std::{
borrow::Cow,
collections::{hash_map::HashMap, BTreeMap, BTreeSet},
fmt::{self, Display},
path::{Path, PathBuf},
str::FromStr,
};
use indexmap::IndexMap;
use semver::{Version, VersionReq};
use serde::{de::Error as _, Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Copy, Default, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum Abi {
#[serde(rename = "emscripten")]
Emscripten,
#[default]
#[serde(rename = "none")]
None,
#[serde(rename = "wasi")]
Wasi,
#[serde(rename = "wasm4")]
WASM4,
}
impl Abi {
pub fn to_str(&self) -> &str {
match self {
Abi::Emscripten => "emscripten",
Abi::Wasi => "wasi",
Abi::WASM4 => "wasm4",
Abi::None => "generic",
}
}
pub fn is_none(&self) -> bool {
matches!(self, Abi::None)
}
pub fn from_name(name: &str) -> Self {
name.parse().unwrap_or(Abi::None)
}
}
impl fmt::Display for Abi {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_str())
}
}
impl FromStr for Abi {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"emscripten" => Ok(Abi::Emscripten),
"wasi" => Ok(Abi::Wasi),
"wasm4" => Ok(Abi::WASM4),
"generic" => Ok(Abi::None),
_ => Err(format!("Unknown ABI, \"{s}\"").into()),
}
}
}
pub static MANIFEST_FILE_NAME: &str = "wasmer.toml";
const README_PATHS: &[&str; 5] = &[
"README",
"README.md",
"README.markdown",
"README.mdown",
"README.mkdn",
];
const LICENSE_PATHS: &[&str; 3] = &["LICENSE", "LICENSE.md", "COPYING"];
#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)]
#[non_exhaustive]
pub struct Package {
#[builder(setter(into))]
pub name: String,
pub version: Version,
#[builder(setter(into))]
pub description: String,
#[builder(setter(into, strip_option), default)]
pub license: Option<String>,
#[serde(rename = "license-file")]
#[builder(setter(into, strip_option), default)]
pub license_file: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(setter(into, strip_option), default)]
pub readme: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(setter(into, strip_option), default)]
pub repository: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(setter(into, strip_option), default)]
pub homepage: Option<String>,
#[serde(rename = "wasmer-extra-flags")]
#[builder(setter(into, strip_option), default)]
#[deprecated(
since = "0.9.2",
note = "Use runner-specific command attributes instead"
)]
pub wasmer_extra_flags: Option<String>,
#[serde(
rename = "disable-command-rename",
default,
skip_serializing_if = "std::ops::Not::not"
)]
#[builder(default)]
#[deprecated(
since = "0.9.2",
note = "Does nothing. Prefer a runner-specific command attribute instead"
)]
pub disable_command_rename: bool,
#[serde(
rename = "rename-commands-to-raw-command-name",
default,
skip_serializing_if = "std::ops::Not::not"
)]
#[builder(default)]
#[deprecated(
since = "0.9.2",
note = "Does nothing. Prefer a runner-specific command attribute instead"
)]
pub rename_commands_to_raw_command_name: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(setter(into, strip_option), default)]
pub entrypoint: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[builder(default)]
pub private: bool,
}
impl Package {
pub fn builder(
name: impl Into<String>,
version: Version,
description: impl Into<String>,
) -> PackageBuilder {
PackageBuilder::new(name, version, description)
}
}
impl PackageBuilder {
pub fn new(name: impl Into<String>, version: Version, description: impl Into<String>) -> Self {
let mut builder = PackageBuilder::default();
builder.name(name).version(version).description(description);
builder
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Command {
V1(CommandV1),
V2(CommandV2),
}
impl Command {
pub fn get_name(&self) -> &str {
match self {
Self::V1(c) => &c.name,
Self::V2(c) => &c.name,
}
}
pub fn get_module(&self) -> &ModuleReference {
match self {
Self::V1(c) => &c.module,
Self::V2(c) => &c.module,
}
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] #[deprecated(since = "0.9.2", note = "Prefer the CommandV2 syntax")]
pub struct CommandV1 {
pub name: String,
pub module: ModuleReference,
pub main_args: Option<String>,
pub package: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct CommandV2 {
pub name: String,
pub module: ModuleReference,
pub runner: String,
pub annotations: Option<CommandAnnotations>,
}
impl CommandV2 {
pub fn get_annotations(&self, basepath: &Path) -> Result<Option<serde_cbor::Value>, String> {
match self.annotations.as_ref() {
Some(CommandAnnotations::Raw(v)) => Ok(Some(toml_to_cbor_value(v))),
Some(CommandAnnotations::File(FileCommandAnnotations { file, kind })) => {
let path = basepath.join(file.clone());
let file = std::fs::read_to_string(&path).map_err(|e| {
format!(
"Error reading {:?}.annotation ({:?}): {e}",
self.name,
path.display()
)
})?;
match kind {
FileKind::Json => {
let value: serde_json::Value =
serde_json::from_str(&file).map_err(|e| {
format!(
"Error reading {:?}.annotation ({:?}): {e}",
self.name,
path.display()
)
})?;
Ok(Some(json_to_cbor_value(&value)))
}
FileKind::Yaml => {
let value: serde_yaml::Value =
serde_yaml::from_str(&file).map_err(|e| {
format!(
"Error reading {:?}.annotation ({:?}): {e}",
self.name,
path.display()
)
})?;
Ok(Some(yaml_to_cbor_value(&value)))
}
}
}
None => Ok(None),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ModuleReference {
CurrentPackage {
module: String,
},
Dependency {
dependency: String,
module: String,
},
}
impl Serialize for ModuleReference {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ModuleReference {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let repr: Cow<'de, str> = Cow::deserialize(deserializer)?;
repr.parse().map_err(D::Error::custom)
}
}
impl FromStr for ModuleReference {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once(':') {
Some((dependency, module)) => {
if module.contains(':') {
return Err("Invalid format".into());
}
Ok(ModuleReference::Dependency {
dependency: dependency.to_string(),
module: module.to_string(),
})
}
None => Ok(ModuleReference::CurrentPackage {
module: s.to_string(),
}),
}
}
}
impl Display for ModuleReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ModuleReference::CurrentPackage { module } => Display::fmt(module, f),
ModuleReference::Dependency { dependency, module } => {
write!(f, "{dependency}:{module}")
}
}
}
}
fn toml_to_cbor_value(val: &toml::Value) -> serde_cbor::Value {
match val {
toml::Value::String(s) => serde_cbor::Value::Text(s.clone()),
toml::Value::Integer(i) => serde_cbor::Value::Integer(*i as i128),
toml::Value::Float(f) => serde_cbor::Value::Float(*f),
toml::Value::Boolean(b) => serde_cbor::Value::Bool(*b),
toml::Value::Datetime(d) => serde_cbor::Value::Text(format!("{}", d)),
toml::Value::Array(sq) => {
serde_cbor::Value::Array(sq.iter().map(toml_to_cbor_value).collect())
}
toml::Value::Table(m) => serde_cbor::Value::Map(
m.iter()
.map(|(k, v)| (serde_cbor::Value::Text(k.clone()), toml_to_cbor_value(v)))
.collect(),
),
}
}
fn json_to_cbor_value(val: &serde_json::Value) -> serde_cbor::Value {
match val {
serde_json::Value::Null => serde_cbor::Value::Null,
serde_json::Value::Bool(b) => serde_cbor::Value::Bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
serde_cbor::Value::Integer(i as i128)
} else if let Some(u) = n.as_u64() {
serde_cbor::Value::Integer(u as i128)
} else if let Some(f) = n.as_f64() {
serde_cbor::Value::Float(f)
} else {
serde_cbor::Value::Null
}
}
serde_json::Value::String(s) => serde_cbor::Value::Text(s.clone()),
serde_json::Value::Array(sq) => {
serde_cbor::Value::Array(sq.iter().map(json_to_cbor_value).collect())
}
serde_json::Value::Object(m) => serde_cbor::Value::Map(
m.iter()
.map(|(k, v)| (serde_cbor::Value::Text(k.clone()), json_to_cbor_value(v)))
.collect(),
),
}
}
fn yaml_to_cbor_value(val: &serde_yaml::Value) -> serde_cbor::Value {
match val {
serde_yaml::Value::Null => serde_cbor::Value::Null,
serde_yaml::Value::Bool(b) => serde_cbor::Value::Bool(*b),
serde_yaml::Value::Number(n) => {
if let Some(i) = n.as_i64() {
serde_cbor::Value::Integer(i as i128)
} else if let Some(u) = n.as_u64() {
serde_cbor::Value::Integer(u as i128)
} else if let Some(f) = n.as_f64() {
serde_cbor::Value::Float(f)
} else {
serde_cbor::Value::Null
}
}
serde_yaml::Value::String(s) => serde_cbor::Value::Text(s.clone()),
serde_yaml::Value::Sequence(sq) => {
serde_cbor::Value::Array(sq.iter().map(yaml_to_cbor_value).collect())
}
serde_yaml::Value::Mapping(m) => serde_cbor::Value::Map(
m.iter()
.map(|(k, v)| (yaml_to_cbor_value(k), yaml_to_cbor_value(v)))
.collect(),
),
serde_yaml::Value::Tagged(tag) => yaml_to_cbor_value(&tag.value),
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
#[repr(C)]
pub enum CommandAnnotations {
File(FileCommandAnnotations),
Raw(toml::Value),
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct FileCommandAnnotations {
pub file: PathBuf,
pub kind: FileKind,
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize)]
pub enum FileKind {
#[serde(rename = "yaml")]
Yaml,
#[serde(rename = "json")]
Json,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct Module {
pub name: String,
pub source: PathBuf,
#[serde(default = "Abi::default", skip_serializing_if = "Abi::is_none")]
pub abi: Abi,
#[serde(default)]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub interfaces: Option<HashMap<String, String>>,
pub bindings: Option<Bindings>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Bindings {
Wit(WitBindings),
Wai(WaiBindings),
}
impl Bindings {
pub fn referenced_files(&self, base_directory: &Path) -> Result<Vec<PathBuf>, ImportsError> {
match self {
Bindings::Wit(WitBindings { wit_exports, .. }) => {
let path = base_directory.join(wit_exports);
if path.exists() {
Ok(vec![path])
} else {
Err(ImportsError::FileNotFound(path))
}
}
Bindings::Wai(wai) => wai.referenced_files(base_directory),
}
}
}
impl Serialize for Bindings {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Bindings::Wit(w) => w.serialize(serializer),
Bindings::Wai(w) => w.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for Bindings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = toml::Value::deserialize(deserializer)?;
let keys = ["wit-bindgen", "wai-version"];
let [wit_bindgen, wai_version] = keys.map(|key| value.get(key).is_some());
match (wit_bindgen, wai_version) {
(true, false) => WitBindings::deserialize(value)
.map(Bindings::Wit)
.map_err(D::Error::custom),
(false, true) => WaiBindings::deserialize(value)
.map(Bindings::Wai)
.map_err(D::Error::custom),
(true, true) | (false, false) => {
let msg = format!(
"expected one of \"{}\" to be provided, but not both",
keys.join("\" or \""),
);
Err(D::Error::custom(msg))
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct WitBindings {
pub wit_bindgen: Version,
pub wit_exports: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct WaiBindings {
pub wai_version: Version,
pub exports: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub imports: Vec<PathBuf>,
}
impl WaiBindings {
fn referenced_files(&self, base_directory: &Path) -> Result<Vec<PathBuf>, ImportsError> {
let WaiBindings {
exports, imports, ..
} = self;
let initial_paths = exports
.iter()
.chain(imports)
.map(|relative_path| base_directory.join(relative_path));
let mut to_check: Vec<PathBuf> = Vec::new();
for path in initial_paths {
if !path.exists() {
return Err(ImportsError::FileNotFound(path));
}
to_check.push(path);
}
let mut files = BTreeSet::new();
while let Some(path) = to_check.pop() {
if files.contains(&path) {
continue;
}
to_check.extend(get_imported_wai_files(&path)?);
files.insert(path);
}
Ok(files.into_iter().collect())
}
}
fn get_imported_wai_files(path: &Path) -> Result<Vec<PathBuf>, ImportsError> {
let _wai_src = std::fs::read_to_string(path).map_err(|error| ImportsError::Read {
path: path.to_path_buf(),
error,
})?;
let parent_dir = path.parent()
.expect("All paths should have a parent directory because we joined them relative to the base directory");
let raw_imports: Vec<String> = Vec::new();
let mut resolved_paths = Vec::new();
for imported in raw_imports {
let absolute_path = parent_dir.join(imported);
if !absolute_path.exists() {
return Err(ImportsError::ImportedFileNotFound {
path: absolute_path,
referenced_by: path.to_path_buf(),
});
}
resolved_paths.push(absolute_path);
}
Ok(resolved_paths)
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ImportsError {
#[error(
"The \"{}\" mentioned in the manifest doesn't exist",
_0.display(),
)]
FileNotFound(PathBuf),
#[error(
"The \"{}\" imported by \"{}\" doesn't exist",
path.display(),
referenced_by.display(),
)]
ImportedFileNotFound {
path: PathBuf,
referenced_by: PathBuf,
},
#[error("Unable to parse \"{}\" as a WAI file", path.display())]
WaiParse { path: PathBuf },
#[error("Unable to read \"{}\"", path.display())]
Read {
path: PathBuf,
#[source]
error: std::io::Error,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)]
#[non_exhaustive]
pub struct Manifest {
pub package: Package,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
#[builder(default)]
pub dependencies: HashMap<String, VersionReq>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
#[builder(default)]
pub fs: IndexMap<String, PathBuf>,
#[serde(default, rename = "module", skip_serializing_if = "Vec::is_empty")]
#[builder(default)]
pub modules: Vec<Module>,
#[serde(default, rename = "command", skip_serializing_if = "Vec::is_empty")]
#[builder(default)]
pub commands: Vec<Command>,
}
impl Manifest {
pub fn builder(package: Package) -> ManifestBuilder {
ManifestBuilder::new(package)
}
pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
toml::from_str(s)
}
pub fn find_in_directory<T: AsRef<Path>>(path: T) -> Result<Self, ManifestError> {
let path = path.as_ref();
if !path.is_dir() {
return Err(ManifestError::MissingManifest(path.to_path_buf()));
}
let manifest_path_buf = path.join(MANIFEST_FILE_NAME);
let contents = std::fs::read_to_string(&manifest_path_buf)
.map_err(|_e| ManifestError::MissingManifest(manifest_path_buf))?;
let mut manifest: Self = toml::from_str(contents.as_str())?;
if manifest.package.readme.is_none() {
manifest.package.readme = locate_file(path, README_PATHS);
}
if manifest.package.license_file.is_none() {
manifest.package.license_file = locate_file(path, LICENSE_PATHS);
}
manifest.validate()?;
Ok(manifest)
}
pub fn validate(&self) -> Result<(), ValidationError> {
let mut modules = BTreeMap::new();
for module in &self.modules {
let is_duplicate = modules.insert(&module.name, module).is_some();
if is_duplicate {
return Err(ValidationError::DuplicateModule {
name: module.name.clone(),
});
}
}
let mut commands = BTreeMap::new();
for command in &self.commands {
let is_duplicate = commands.insert(command.get_name(), command).is_some();
if is_duplicate {
return Err(ValidationError::DuplicateCommand {
name: command.get_name().to_string(),
});
}
let module_reference = command.get_module();
match &module_reference {
ModuleReference::CurrentPackage { module } => {
if let Some(module) = modules.get(&module) {
if module.abi == Abi::None && module.interfaces.is_none() {
return Err(ValidationError::MissingABI {
command: command.get_name().to_string(),
module: module.name.clone(),
});
}
} else {
return Err(ValidationError::MissingModuleForCommand {
command: command.get_name().to_string(),
module: command.get_module().clone(),
});
}
}
ModuleReference::Dependency { dependency, .. } => {
if !self.dependencies.contains_key(dependency) {
return Err(ValidationError::MissingDependency {
command: command.get_name().to_string(),
dependency: dependency.clone(),
module_ref: module_reference.clone(),
});
}
}
}
}
if let Some(entrypoint) = self.package.entrypoint.as_deref() {
if !commands.contains_key(entrypoint) {
return Err(ValidationError::InvalidEntrypoint {
entrypoint: entrypoint.to_string(),
available_commands: commands.keys().map(ToString::to_string).collect(),
});
}
}
Ok(())
}
pub fn add_dependency(&mut self, dependency_name: String, dependency_version: VersionReq) {
self.dependencies
.insert(dependency_name, dependency_version);
}
pub fn remove_dependency(&mut self, dependency_name: &str) -> Option<VersionReq> {
self.dependencies.remove(dependency_name)
}
pub fn to_string(&self) -> anyhow::Result<String> {
let repr = toml::to_string_pretty(&self)?;
Ok(repr)
}
pub fn save(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
let manifest = toml::to_string_pretty(self)?;
std::fs::write(path, manifest).map_err(ManifestError::CannotSaveManifest)?;
Ok(())
}
}
fn locate_file(path: &Path, candidates: &[&str]) -> Option<PathBuf> {
for filename in candidates {
let path_buf = path.join(filename);
if path_buf.exists() {
return Some(filename.into());
}
}
None
}
impl ManifestBuilder {
pub fn new(package: Package) -> Self {
let mut builder = ManifestBuilder::default();
builder.package(package);
builder
}
pub fn map_fs(&mut self, guest: impl Into<String>, host: impl Into<PathBuf>) -> &mut Self {
self.fs
.get_or_insert_with(IndexMap::new)
.insert(guest.into(), host.into());
self
}
pub fn with_dependency(&mut self, name: impl Into<String>, version: VersionReq) -> &mut Self {
self.dependencies
.get_or_insert_with(HashMap::new)
.insert(name.into(), version);
self
}
pub fn with_module(&mut self, module: Module) -> &mut Self {
self.modules.get_or_insert_with(Vec::new).push(module);
self
}
pub fn with_command(&mut self, command: Command) -> &mut Self {
self.commands.get_or_insert_with(Vec::new).push(command);
self
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ManifestError {
#[error("Manifest file not found at \"{}\"", _0.display())]
MissingManifest(PathBuf),
#[error("Could not save manifest file: {0}.")]
CannotSaveManifest(#[source] std::io::Error),
#[error("Could not parse manifest because {0}.")]
TomlParseError(#[from] toml::de::Error),
#[error("There was an error validating the manifest")]
ValidationError(#[from] ValidationError),
}
#[derive(Debug, PartialEq, Error)]
#[non_exhaustive]
pub enum ValidationError {
#[error(
"missing ABI field on module, \"{module}\", used by command, \"{command}\"; an ABI of `wasi` or `emscripten` is required",
)]
MissingABI { command: String, module: String },
#[error("missing module, \"{module}\", in manifest used by command, \"{command}\"")]
MissingModuleForCommand {
command: String,
module: ModuleReference,
},
#[error("The \"{command}\" command refers to a nonexistent dependency, \"{dependency}\" in \"{module_ref}\"")]
MissingDependency {
command: String,
dependency: String,
module_ref: ModuleReference,
},
#[error("The entrypoint, \"{entrypoint}\", isn't a valid command (commands: {})", available_commands.join(", "))]
InvalidEntrypoint {
entrypoint: String,
available_commands: Vec<String>,
},
#[error("Duplicate module, \"{name}\"")]
DuplicateModule { name: String },
#[error("Duplicate command, \"{name}\"")]
DuplicateCommand { name: String },
}
#[cfg(test)]
mod tests {
use std::fmt::Debug;
use serde::{de::DeserializeOwned, Deserialize};
use toml::toml;
use super::*;
#[test]
fn test_to_string() {
Manifest {
package: Package {
name: "package/name".to_string(),
version: Version::parse("1.0.0").unwrap(),
description: "test".to_string(),
license: None,
license_file: None,
readme: None,
repository: None,
homepage: None,
wasmer_extra_flags: None,
disable_command_rename: false,
rename_commands_to_raw_command_name: false,
entrypoint: None,
private: false,
},
dependencies: HashMap::new(),
modules: vec![Module {
name: "test".to_string(),
abi: Abi::Wasi,
bindings: None,
interfaces: None,
kind: Some("https://webc.org/kind/wasi".to_string()),
source: Path::new("test.wasm").to_path_buf(),
}],
commands: Vec::new(),
fs: vec![
("a".to_string(), Path::new("/a").to_path_buf()),
("b".to_string(), Path::new("/b").to_path_buf()),
]
.into_iter()
.collect(),
}
.to_string()
.unwrap();
}
#[test]
fn interface_test() {
let manifest_str = r#"
[package]
name = "test"
version = "0.0.0"
description = "This is a test package"
license = "MIT"
[[module]]
name = "mod"
source = "target/wasm32-wasi/release/mod.wasm"
interfaces = {"wasi" = "0.0.0-unstable"}
[[module]]
name = "mod-with-exports"
source = "target/wasm32-wasi/release/mod-with-exports.wasm"
bindings = { wit-exports = "exports.wit", wit-bindgen = "0.0.0" }
[[command]]
name = "command"
module = "mod"
"#;
let manifest: Manifest = Manifest::parse(manifest_str).unwrap();
let modules = &manifest.modules;
assert_eq!(
modules[0].interfaces.as_ref().unwrap().get("wasi"),
Some(&"0.0.0-unstable".to_string())
);
assert_eq!(
modules[1],
Module {
name: "mod-with-exports".to_string(),
source: PathBuf::from("target/wasm32-wasi/release/mod-with-exports.wasm"),
abi: Abi::None,
kind: None,
interfaces: None,
bindings: Some(Bindings::Wit(WitBindings {
wit_exports: PathBuf::from("exports.wit"),
wit_bindgen: "0.0.0".parse().unwrap()
})),
},
);
}
#[test]
fn parse_wit_bindings() {
let table = toml! {
name = "..."
source = "..."
bindings = { wit-bindgen = "0.1.0", wit-exports = "./file.wit" }
};
let module = Module::deserialize(table).unwrap();
assert_eq!(
module.bindings.as_ref().unwrap(),
&Bindings::Wit(WitBindings {
wit_bindgen: "0.1.0".parse().unwrap(),
wit_exports: PathBuf::from("./file.wit"),
}),
);
assert_round_trippable(&module);
}
#[test]
fn parse_wai_bindings() {
let table = toml! {
name = "..."
source = "..."
bindings = { wai-version = "0.1.0", exports = "./file.wai", imports = ["a.wai", "../b.wai"] }
};
let module = Module::deserialize(table).unwrap();
assert_eq!(
module.bindings.as_ref().unwrap(),
&Bindings::Wai(WaiBindings {
wai_version: "0.1.0".parse().unwrap(),
exports: Some(PathBuf::from("./file.wai")),
imports: vec![PathBuf::from("a.wai"), PathBuf::from("../b.wai")],
}),
);
assert_round_trippable(&module);
}
#[track_caller]
fn assert_round_trippable<T>(value: &T)
where
T: Serialize + DeserializeOwned + PartialEq + Debug,
{
let repr = toml::to_string(value).unwrap();
let round_tripped: T = toml::from_str(&repr).unwrap();
assert_eq!(
round_tripped, *value,
"The value should convert to/from TOML losslessly"
);
}
#[test]
fn imports_and_exports_are_optional_with_wai() {
let table = toml! {
name = "..."
source = "..."
bindings = { wai-version = "0.1.0" }
};
let module = Module::deserialize(table).unwrap();
assert_eq!(
module.bindings.as_ref().unwrap(),
&Bindings::Wai(WaiBindings {
wai_version: "0.1.0".parse().unwrap(),
exports: None,
imports: Vec::new(),
}),
);
assert_round_trippable(&module);
}
#[test]
fn ambiguous_bindings_table() {
let table = toml! {
wai-version = "0.2.0"
wit-bindgen = "0.1.0"
};
let err = Bindings::deserialize(table).unwrap_err();
assert_eq!(
err.to_string(),
"expected one of \"wit-bindgen\" or \"wai-version\" to be provided, but not both\n"
);
}
#[test]
fn bindings_table_that_is_neither_wit_nor_wai() {
let table = toml! {
wai-bindgen = "lol, this should have been wai-version"
exports = "./file.wai"
};
let err = Bindings::deserialize(table).unwrap_err();
assert_eq!(
err.to_string(),
"expected one of \"wit-bindgen\" or \"wai-version\" to be provided, but not both\n"
);
}
#[test]
fn command_v2_isnt_ambiguous_with_command_v1() {
let src = r#"
[package]
name = "hotg-ai/sine"
version = "0.12.0"
description = "sine"
[dependencies]
"hotg-ai/train_test_split" = "0.12.1"
"hotg-ai/elastic_net" = "0.12.1"
[[module]] # This is the same as atoms
name = "sine"
kind = "tensorflow-SavedModel" # It can also be "wasm" (default)
source = "models/sine"
[[command]]
name = "run"
runner = "rune"
module = "sine"
annotations = { file = "Runefile.yml", kind = "yaml" }
"#;
let manifest: Manifest = toml::from_str(src).unwrap();
let commands = &manifest.commands;
assert_eq!(commands.len(), 1);
assert_eq!(
commands[0],
Command::V2(CommandV2 {
name: "run".into(),
module: "sine".parse().unwrap(),
runner: "rune".into(),
annotations: Some(CommandAnnotations::File(FileCommandAnnotations {
file: "Runefile.yml".into(),
kind: FileKind::Yaml,
}))
})
);
}
#[test]
fn get_manifest() {
let wasmer_toml = toml! {
[package]
name = "test"
version = "1.0.0"
repository = "test.git"
homepage = "test.com"
description = "The best package."
};
let manifest: Manifest = wasmer_toml.try_into().unwrap();
assert!(!manifest.package.disable_command_rename);
}
#[test]
fn get_commands() {
let wasmer_toml = toml! {
[package]
name = "test"
version = "1.0.0"
repository = "test.git"
homepage = "test.com"
description = "The best package."
[[module]]
name = "test-pkg"
module = "target.wasm"
source = "source.wasm"
description = "description"
interfaces = {"wasi" = "0.0.0-unstable"}
[[command]]
name = "foo"
module = "test"
[[command]]
name = "baz"
module = "test"
main_args = "$@"
};
let manifest: Manifest = wasmer_toml.try_into().unwrap();
let commands = &manifest.commands;
assert_eq!(2, commands.len());
}
#[test]
fn add_new_dependency() {
let tmp_dir = tempfile::tempdir().unwrap();
let tmp_dir_path: &std::path::Path = tmp_dir.as_ref();
let manifest_path = tmp_dir_path.join(MANIFEST_FILE_NAME);
let wasmer_toml = toml! {
[package]
name = "_/test"
version = "1.0.0"
description = "description"
[[module]]
name = "test"
source = "test.wasm"
interfaces = {}
};
let toml_string = toml::to_string(&wasmer_toml).unwrap();
std::fs::write(manifest_path, toml_string).unwrap();
let mut manifest = Manifest::find_in_directory(tmp_dir).unwrap();
let dependency_name = "dep_pkg";
let dependency_version: VersionReq = "0.1.0".parse().unwrap();
manifest.add_dependency(dependency_name.to_string(), dependency_version.clone());
assert_eq!(1, manifest.dependencies.len());
manifest.add_dependency(dependency_name.to_string(), dependency_version);
assert_eq!(1, manifest.dependencies.len());
let dependency_name_2 = "dep_pkg_2";
let dependency_version_2: VersionReq = "0.2.0".parse().unwrap();
manifest.add_dependency(dependency_name_2.to_string(), dependency_version_2);
assert_eq!(2, manifest.dependencies.len());
}
#[test]
fn duplicate_modules_are_invalid() {
let wasmer_toml = toml! {
[package]
name = "some/package"
version = "0.0.0"
description = ""
[[module]]
name = "test"
source = "test.wasm"
[[module]]
name = "test"
source = "test.wasm"
};
let manifest = Manifest::deserialize(wasmer_toml).unwrap();
let error = manifest.validate().unwrap_err();
assert_eq!(
error,
ValidationError::DuplicateModule {
name: "test".to_string()
}
);
}
#[test]
fn duplicate_commands_are_invalid() {
let wasmer_toml = toml! {
[package]
name = "some/package"
version = "0.0.0"
description = ""
[[module]]
name = "test"
source = "test.wasm"
abi = "wasi"
[[command]]
name = "cmd"
module = "test"
[[command]]
name = "cmd"
module = "test"
};
let manifest = Manifest::deserialize(wasmer_toml).unwrap();
let error = manifest.validate().unwrap_err();
assert_eq!(
error,
ValidationError::DuplicateCommand {
name: "cmd".to_string()
}
);
}
#[test]
fn nonexistent_entrypoint() {
let wasmer_toml = toml! {
[package]
name = "some/package"
version = "0.0.0"
description = ""
entrypoint = "this-doesnt-exist"
[[module]]
name = "test"
source = "test.wasm"
abi = "wasi"
[[command]]
name = "cmd"
module = "test"
};
let manifest = Manifest::deserialize(wasmer_toml).unwrap();
let error = manifest.validate().unwrap_err();
assert_eq!(
error,
ValidationError::InvalidEntrypoint {
entrypoint: "this-doesnt-exist".to_string(),
available_commands: vec!["cmd".to_string()]
}
);
}
#[test]
fn command_with_nonexistent_module() {
let wasmer_toml = toml! {
[package]
name = "some/package"
version = "0.0.0"
description = ""
[[command]]
name = "cmd"
module = "this-doesnt-exist"
};
let manifest = Manifest::deserialize(wasmer_toml).unwrap();
let error = manifest.validate().unwrap_err();
assert_eq!(
error,
ValidationError::MissingModuleForCommand {
command: "cmd".to_string(),
module: "this-doesnt-exist".parse().unwrap()
}
);
}
#[test]
fn use_builder_api_to_create_simplest_manifest() {
let package =
Package::builder("my/package", "1.0.0".parse().unwrap(), "My awesome package")
.build()
.unwrap();
let manifest = Manifest::builder(package).build().unwrap();
manifest.validate().unwrap();
}
#[test]
fn deserialize_command_referring_to_module_from_dependency() {
let wasmer_toml = toml! {
[package]
name = "some/package"
version = "0.0.0"
description = ""
[dependencies]
dep = "1.2.3"
[[command]]
name = "cmd"
module = "dep:module"
};
let manifest = Manifest::deserialize(wasmer_toml).unwrap();
let command = manifest
.commands
.iter()
.find(|cmd| cmd.get_name() == "cmd")
.unwrap();
assert_eq!(
command.get_module(),
&ModuleReference::Dependency {
dependency: "dep".to_string(),
module: "module".to_string()
}
);
}
#[test]
fn command_with_module_from_nonexistent_dependency() {
let wasmer_toml = toml! {
[package]
name = "some/package"
version = "0.0.0"
description = ""
[[command]]
name = "cmd"
module = "dep:module"
};
let manifest = Manifest::deserialize(wasmer_toml).unwrap();
let error = manifest.validate().unwrap_err();
assert_eq!(
error,
ValidationError::MissingDependency {
command: "cmd".to_string(),
dependency: "dep".to_string(),
module_ref: ModuleReference::Dependency {
dependency: "dep".to_string(),
module: "module".to_string()
}
}
);
}
#[test]
fn round_trip_dependency_module_ref() {
let original = ModuleReference::Dependency {
dependency: "my/dep".to_string(),
module: "module".to_string(),
};
let repr = original.to_string();
let round_tripped: ModuleReference = repr.parse().unwrap();
assert_eq!(round_tripped, original);
}
}