use crate::config::WasmerEnv;
use anyhow::Context;
use cargo_metadata::{CargoOpt, MetadataCommand};
use clap::Parser;
use indexmap::IndexMap;
use semver::VersionReq;
use std::path::{Path, PathBuf};
use super::AsyncCliCommand;
static NOTE: &str = "# See more keys and definitions at https://docs.wasmer.io/registry/manifest";
const NEWLINE: &str = if cfg!(windows) { "\r\n" } else { "\n" };
#[derive(Debug, Parser)]
pub struct Init {
#[clap(flatten)]
env: WasmerEnv,
#[clap(long, group = "crate-type")]
pub lib: bool,
#[clap(long, group = "crate-type")]
pub bin: bool,
#[clap(long, group = "crate-type")]
pub empty: bool,
#[clap(long)]
pub overwrite: bool,
#[clap(long)]
pub quiet: bool,
#[clap(long)]
pub namespace: Option<String>,
#[clap(long)]
pub package_name: Option<String>,
#[clap(long)]
pub version: Option<semver::Version>,
#[clap(long)]
pub manifest_path: Option<PathBuf>,
#[clap(long, value_enum)]
pub template: Option<Template>,
#[clap(long)]
pub include: Vec<String>,
#[clap(name = "PACKAGE_PATH")]
pub out: Option<PathBuf>,
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, clap::ValueEnum)]
pub enum Template {
Python,
Js,
}
#[derive(Debug, PartialEq, Copy, Clone)]
enum BinOrLib {
Bin,
Lib,
Empty,
}
#[derive(Debug, Clone)]
struct MiniCargoTomlPackage {
cargo_toml_path: PathBuf,
name: String,
version: semver::Version,
description: Option<String>,
homepage: Option<String>,
repository: Option<String>,
license: Option<String>,
readme: Option<PathBuf>,
license_file: Option<PathBuf>,
#[allow(dead_code)]
workspace_root: PathBuf,
#[allow(dead_code)]
build_dir: PathBuf,
}
static WASMER_TOML_NAME: &str = "wasmer.toml";
#[async_trait::async_trait]
impl AsyncCliCommand for Init {
type Output = ();
async fn run_async(self) -> Result<(), anyhow::Error> {
let bin_or_lib = self.get_bin_or_lib()?;
let manifest_path = match self.manifest_path.as_ref() {
Some(s) => s.clone(),
None => {
let cargo_toml_path = self
.out
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap())
.join("Cargo.toml");
cargo_toml_path
.canonicalize()
.unwrap_or_else(|_| cargo_toml_path.clone())
}
};
let cargo_toml = if manifest_path.exists() {
parse_cargo_toml(&manifest_path).ok()
} else {
None
};
let (fallback_package_name, target_file) = self.target_file()?;
if target_file.exists() && !self.overwrite {
anyhow::bail!(
"wasmer project already initialized in {}",
target_file.display(),
);
}
let constructed_manifest = construct_manifest(
&self.env,
cargo_toml.as_ref(),
&fallback_package_name,
self.package_name.as_deref(),
&target_file,
&manifest_path,
bin_or_lib,
self.namespace.clone(),
self.version.clone(),
self.template.as_ref(),
self.include.as_slice(),
self.quiet,
)
.await?;
if let Some(parent) = target_file.parent() {
let _ = std::fs::create_dir_all(parent);
}
Self::write_wasmer_toml(&target_file, &constructed_manifest)
}
}
impl Init {
fn write_wasmer_toml(
path: &PathBuf,
toml: &wasmer_config::package::Manifest,
) -> Result<(), anyhow::Error> {
let toml_string = toml::to_string_pretty(&toml)?;
let mut resulting_string = String::new();
let mut note_inserted = false;
for line in toml_string.lines() {
resulting_string.push_str(line);
if !note_inserted && line.is_empty() {
resulting_string.push_str(NEWLINE);
resulting_string.push_str(NOTE);
resulting_string.push_str(NEWLINE);
note_inserted = true;
}
resulting_string.push_str(NEWLINE);
}
if !note_inserted {
resulting_string.push_str(NEWLINE);
resulting_string.push_str(NOTE);
resulting_string.push_str(NEWLINE);
resulting_string.push_str(NEWLINE);
}
std::fs::write(path, resulting_string)
.with_context(|| format!("Unable to write to \"{}\"", path.display()))?;
Ok(())
}
fn target_file(&self) -> Result<(String, PathBuf), anyhow::Error> {
match self.out.as_ref() {
None => {
let current_dir = std::env::current_dir()?;
let package_name = self
.package_name
.clone()
.or_else(|| {
current_dir
.canonicalize()
.ok()?
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
})
.ok_or_else(|| anyhow::anyhow!("no current dir name"))?;
Ok((package_name, current_dir.join(WASMER_TOML_NAME)))
}
Some(s) => {
std::fs::create_dir_all(s)
.map_err(|e| anyhow::anyhow!("{e}"))
.with_context(|| anyhow::anyhow!("{}", s.display()))?;
let package_name = self
.package_name
.clone()
.or_else(|| {
s.canonicalize()
.ok()?
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
})
.ok_or_else(|| anyhow::anyhow!("no dir name"))?;
Ok((package_name, s.join(WASMER_TOML_NAME)))
}
}
}
fn get_filesystem_mapping(include: &[String]) -> impl Iterator<Item = (String, PathBuf)> + '_ {
include.iter().map(|path| {
if path == "." || path == "/" {
return ("/".to_string(), Path::new("/").to_path_buf());
}
let key = format!("./{path}");
let value = PathBuf::from(format!("/{path}"));
(key, value)
})
}
fn get_command(
modules: &[wasmer_config::package::Module],
bin_or_lib: BinOrLib,
) -> Vec<wasmer_config::package::Command> {
match bin_or_lib {
BinOrLib::Bin => modules
.iter()
.map(|m| {
wasmer_config::package::Command::V2(wasmer_config::package::CommandV2 {
name: m.name.clone(),
module: wasmer_config::package::ModuleReference::CurrentPackage {
module: m.name.clone(),
},
runner: "wasi".to_string(),
annotations: None,
})
})
.collect(),
BinOrLib::Lib | BinOrLib::Empty => Vec::new(),
}
}
fn get_dependencies(template: Option<&Template>) -> IndexMap<String, VersionReq> {
let mut map = IndexMap::default();
match template {
Some(Template::Js) => {
map.insert("quickjs/quickjs".to_string(), VersionReq::STAR);
}
Some(Template::Python) => {
map.insert("python/python".to_string(), VersionReq::STAR);
}
_ => {}
}
map
}
fn get_bin_or_lib(&self) -> Result<BinOrLib, anyhow::Error> {
match (self.empty, self.bin, self.lib) {
(true, false, false) => Ok(BinOrLib::Empty),
(false, true, false) => Ok(BinOrLib::Bin),
(false, false, true) => Ok(BinOrLib::Lib),
(false, false, false) => Ok(BinOrLib::Bin),
_ => anyhow::bail!("Only one of --bin, --lib, or --empty can be provided"),
}
}
fn get_bindings(target_file: &Path, bin_or_lib: BinOrLib) -> Option<GetBindingsResult> {
match bin_or_lib {
BinOrLib::Bin | BinOrLib::Empty => None,
BinOrLib::Lib => target_file.parent().and_then(|parent| {
let all_bindings = walkdir::WalkDir::new(parent)
.min_depth(1)
.max_depth(3)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
.filter_map(|e| {
let is_wit = e.path().extension().and_then(|s| s.to_str()) == Some(".wit");
let is_wai = e.path().extension().and_then(|s| s.to_str()) == Some(".wai");
if is_wit {
Some(wasmer_config::package::Bindings::Wit(
wasmer_config::package::WitBindings {
wit_exports: e.path().to_path_buf(),
wit_bindgen: semver::Version::parse("0.1.0").unwrap(),
},
))
} else if is_wai {
Some(wasmer_config::package::Bindings::Wai(
wasmer_config::package::WaiBindings {
exports: None,
imports: vec![e.path().to_path_buf()],
wai_version: semver::Version::parse("0.2.0").unwrap(),
},
))
} else {
None
}
})
.collect::<Vec<_>>();
if all_bindings.is_empty() {
None
} else if all_bindings.len() == 1 {
Some(GetBindingsResult::OneBinding(all_bindings[0].clone()))
} else {
Some(GetBindingsResult::MultiBindings(all_bindings))
}
}),
}
}
}
enum GetBindingsResult {
OneBinding(wasmer_config::package::Bindings),
MultiBindings(Vec<wasmer_config::package::Bindings>),
}
impl GetBindingsResult {
fn first_binding(&self) -> Option<wasmer_config::package::Bindings> {
match self {
Self::OneBinding(s) => Some(s.clone()),
Self::MultiBindings(s) => s.first().cloned(),
}
}
}
#[allow(clippy::too_many_arguments)]
async fn construct_manifest(
env: &WasmerEnv,
cargo_toml: Option<&MiniCargoTomlPackage>,
fallback_package_name: &String,
package_name: Option<&str>,
target_file: &Path,
manifest_path: &Path,
bin_or_lib: BinOrLib,
namespace: Option<String>,
version: Option<semver::Version>,
template: Option<&Template>,
include_fs: &[String],
quiet: bool,
) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
if let Some(ct) = cargo_toml.as_ref() {
let msg = format!(
"NOTE: Initializing wasmer.toml file with metadata from Cargo.toml{NEWLINE} -> {}",
ct.cargo_toml_path.display()
);
if !quiet {
println!("{msg}");
}
log::warn!("{msg}");
}
let package_name = package_name.unwrap_or_else(|| {
cargo_toml
.as_ref()
.map(|p| &p.name)
.unwrap_or(fallback_package_name)
});
let namespace = match namespace {
Some(n) => Some(n),
None => {
if let Ok(client) = env.client() {
if let Ok(Some(u)) = wasmer_backend_api::query::current_user(&client).await {
Some(u.username)
} else {
None
}
} else {
None
}
}
};
let version = version.unwrap_or_else(|| {
cargo_toml
.as_ref()
.map(|t| t.version.clone())
.unwrap_or_else(|| semver::Version::parse("0.1.0").unwrap())
});
let license = cargo_toml.as_ref().and_then(|t| t.license.clone());
let license_file = cargo_toml.as_ref().and_then(|t| t.license_file.clone());
let readme = cargo_toml.as_ref().and_then(|t| t.readme.clone());
let repository = cargo_toml.as_ref().and_then(|t| t.repository.clone());
let homepage = cargo_toml.as_ref().and_then(|t| t.homepage.clone());
let description = cargo_toml
.as_ref()
.and_then(|t| t.description.clone())
.unwrap_or_else(|| format!("Description for package {package_name}"));
let default_abi = wasmer_config::package::Abi::Wasi;
let bindings = Init::get_bindings(target_file, bin_or_lib);
if let Some(GetBindingsResult::MultiBindings(m)) = bindings.as_ref() {
let found = m
.iter()
.map(|m| match m {
wasmer_config::package::Bindings::Wit(wb) => {
format!("found: {}", serde_json::to_string(wb).unwrap_or_default())
}
wasmer_config::package::Bindings::Wai(wb) => {
format!("found: {}", serde_json::to_string(wb).unwrap_or_default())
}
})
.collect::<Vec<_>>()
.join("\r\n");
let msg = [
String::new(),
" It looks like your project contains multiple *.wai files.".to_string(),
" Make sure you update the [[module.bindings]] appropriately".to_string(),
String::new(),
found,
];
let msg = msg.join("\r\n");
if !quiet {
println!("{msg}");
}
log::warn!("{msg}");
}
let module_source = cargo_toml
.as_ref()
.map(|p| {
let outpath = p
.build_dir
.join("release")
.join(format!("{package_name}.wasm"));
let canonicalized_outpath = outpath.canonicalize().unwrap_or(outpath);
let outpath_str =
crate::common::normalize_path(&canonicalized_outpath.display().to_string());
let manifest_canonicalized = crate::common::normalize_path(
&manifest_path
.parent()
.and_then(|p| p.canonicalize().ok())
.unwrap_or_else(|| manifest_path.to_path_buf())
.display()
.to_string(),
);
let diff = outpath_str
.strip_prefix(&manifest_canonicalized)
.unwrap_or(&outpath_str)
.replace('\\', "/");
let relative_str = diff.strip_prefix('/').unwrap_or(&diff);
Path::new(&relative_str).to_path_buf()
})
.unwrap_or_else(|| Path::new(&format!("{package_name}.wasm")).to_path_buf());
let modules = vec![wasmer_config::package::Module {
name: package_name.to_string(),
source: module_source,
kind: None,
abi: default_abi,
bindings: bindings.as_ref().and_then(|b| b.first_binding()),
interfaces: Some({
let mut map = IndexMap::new();
map.insert("wasi".to_string(), "0.1.0-unstable".to_string());
map
}),
}];
let mut pkg = wasmer_config::package::Package::builder(
if let Some(s) = namespace {
format!("{s}/{package_name}")
} else {
package_name.to_string()
},
version,
description,
);
if let Some(license) = license {
pkg.license(license);
}
if let Some(license_file) = license_file {
pkg.license_file(license_file);
}
if let Some(readme) = readme {
pkg.readme(readme);
}
if let Some(repository) = repository {
pkg.repository(repository);
}
if let Some(homepage) = homepage {
pkg.homepage(homepage);
}
let pkg = pkg.build()?;
let mut manifest = wasmer_config::package::Manifest::builder(pkg);
manifest
.dependencies(Init::get_dependencies(template))
.commands(Init::get_command(&modules, bin_or_lib))
.fs(Init::get_filesystem_mapping(include_fs).collect());
match bin_or_lib {
BinOrLib::Bin | BinOrLib::Lib => {
manifest.modules(modules);
}
BinOrLib::Empty => {}
}
let manifest = manifest.build()?;
Ok(manifest)
}
fn parse_cargo_toml(manifest_path: &PathBuf) -> Result<MiniCargoTomlPackage, anyhow::Error> {
let mut metadata = MetadataCommand::new();
metadata.manifest_path(manifest_path);
metadata.no_deps();
metadata.features(CargoOpt::AllFeatures);
let metadata = metadata.exec().with_context(|| {
format!(
"Unable to load metadata from \"{}\"",
manifest_path.display()
)
})?;
let package = metadata
.root_package()
.ok_or_else(|| anyhow::anyhow!("no root package found in cargo metadata"))
.context(anyhow::anyhow!("{}", manifest_path.display()))?;
Ok(MiniCargoTomlPackage {
cargo_toml_path: manifest_path.clone(),
name: package.name.clone(),
version: package.version.clone(),
description: package.description.clone(),
homepage: package.homepage.clone(),
repository: package.repository.clone(),
license: package.license.clone(),
readme: package.readme.clone().map(|s| s.into_std_path_buf()),
license_file: package.license_file.clone().map(|f| f.into_std_path_buf()),
workspace_root: metadata.workspace_root.into_std_path_buf(),
build_dir: metadata
.target_directory
.into_std_path_buf()
.join("wasm32-wasi"),
})
}