1use crate::config::WasmerEnv;
2use anyhow::Context;
3use cargo_metadata::{CargoOpt, MetadataCommand};
4use clap::Parser;
5use indexmap::IndexMap;
6use semver::VersionReq;
7use std::path::{Path, PathBuf};
8
9use super::AsyncCliCommand;
10
11static NOTE: &str = "# See more keys and definitions at https://docs.wasmer.io/registry/manifest";
12
13const NEWLINE: &str = if cfg!(windows) { "\r\n" } else { "\n" };
14
15#[derive(Debug, Parser)]
17pub struct Init {
18 #[clap(flatten)]
19 env: WasmerEnv,
20
21 #[clap(long, group = "crate-type")]
23 pub lib: bool,
24 #[clap(long, group = "crate-type")]
26 pub bin: bool,
27 #[clap(long, group = "crate-type")]
29 pub empty: bool,
30 #[clap(long)]
32 pub overwrite: bool,
33 #[clap(long)]
35 pub quiet: bool,
36 #[clap(long)]
38 pub namespace: Option<String>,
39 #[clap(long)]
41 pub package_name: Option<String>,
42 #[clap(long)]
44 pub version: Option<semver::Version>,
45 #[clap(long)]
47 pub manifest_path: Option<PathBuf>,
48 #[clap(long, value_enum)]
50 pub template: Option<Template>,
51 #[clap(long)]
53 pub include: Vec<String>,
54 #[clap(name = "PACKAGE_PATH")]
57 pub out: Option<PathBuf>,
58}
59
60#[derive(Debug, PartialEq, Eq, Copy, Clone, clap::ValueEnum)]
62pub enum Template {
63 Python,
65 Js,
67}
68
69#[derive(Debug, PartialEq, Copy, Clone)]
70enum BinOrLib {
71 Bin,
72 Lib,
73 Empty,
74}
75
76#[derive(Debug, Clone)]
78struct MiniCargoTomlPackage {
79 cargo_toml_path: PathBuf,
80 name: String,
81 version: semver::Version,
82 description: Option<String>,
83 homepage: Option<String>,
84 repository: Option<String>,
85 license: Option<String>,
86 readme: Option<PathBuf>,
87 license_file: Option<PathBuf>,
88 #[allow(dead_code)]
89 workspace_root: PathBuf,
90 #[allow(dead_code)]
91 build_dir: PathBuf,
92}
93
94static WASMER_TOML_NAME: &str = "wasmer.toml";
95
96#[async_trait::async_trait]
97impl AsyncCliCommand for Init {
98 type Output = ();
99
100 async fn run_async(self) -> Result<(), anyhow::Error> {
101 let bin_or_lib = self.get_bin_or_lib()?;
102
103 let manifest_path = match self.manifest_path.as_ref() {
105 Some(s) => s.clone(),
106 None => {
107 let cargo_toml_path = self
108 .out
109 .clone()
110 .unwrap_or_else(|| std::env::current_dir().unwrap())
111 .join("Cargo.toml");
112 cargo_toml_path
113 .canonicalize()
114 .unwrap_or_else(|_| cargo_toml_path.clone())
115 }
116 };
117
118 let cargo_toml = if manifest_path.exists() {
119 parse_cargo_toml(&manifest_path).ok()
120 } else {
121 None
122 };
123
124 let (fallback_package_name, target_file) = self.target_file()?;
125
126 if target_file.exists() && !self.overwrite {
127 anyhow::bail!(
128 "wasmer project already initialized in {}",
129 target_file.display(),
130 );
131 }
132
133 let constructed_manifest = construct_manifest(
134 &self.env,
135 cargo_toml.as_ref(),
136 &fallback_package_name,
137 self.package_name.as_deref(),
138 &target_file,
139 &manifest_path,
140 bin_or_lib,
141 self.namespace.clone(),
142 self.version.clone(),
143 self.template.as_ref(),
144 self.include.as_slice(),
145 self.quiet,
146 )
147 .await?;
148
149 if let Some(parent) = target_file.parent() {
150 let _ = std::fs::create_dir_all(parent);
151 }
152
153 Self::write_wasmer_toml(&target_file, &constructed_manifest)
155 }
156}
157
158impl Init {
159 fn write_wasmer_toml(
162 path: &PathBuf,
163 toml: &wasmer_config::package::Manifest,
164 ) -> Result<(), anyhow::Error> {
165 let toml_string = toml::to_string_pretty(&toml)?;
166
167 let mut resulting_string = String::new();
168 let mut note_inserted = false;
169
170 for line in toml_string.lines() {
171 resulting_string.push_str(line);
172
173 if !note_inserted && line.is_empty() {
174 resulting_string.push_str(NEWLINE);
177 resulting_string.push_str(NOTE);
178 resulting_string.push_str(NEWLINE);
179 note_inserted = true;
180 }
181 resulting_string.push_str(NEWLINE);
182 }
183
184 if !note_inserted {
185 resulting_string.push_str(NEWLINE);
187 resulting_string.push_str(NOTE);
188 resulting_string.push_str(NEWLINE);
189 resulting_string.push_str(NEWLINE);
190 }
191
192 std::fs::write(path, resulting_string)
193 .with_context(|| format!("Unable to write to \"{}\"", path.display()))?;
194
195 Ok(())
196 }
197
198 fn target_file(&self) -> Result<(String, PathBuf), anyhow::Error> {
199 match self.out.as_ref() {
200 None => {
201 let current_dir = std::env::current_dir()?;
202 let package_name = self
203 .package_name
204 .clone()
205 .or_else(|| {
206 current_dir
207 .canonicalize()
208 .ok()?
209 .file_stem()
210 .and_then(|s| s.to_str())
211 .map(|s| s.to_string())
212 })
213 .ok_or_else(|| anyhow::anyhow!("no current dir name"))?;
214 Ok((package_name, current_dir.join(WASMER_TOML_NAME)))
215 }
216 Some(s) => {
217 std::fs::create_dir_all(s)
218 .map_err(|e| anyhow::anyhow!("{e}"))
219 .with_context(|| anyhow::anyhow!("{}", s.display()))?;
220 let package_name = self
221 .package_name
222 .clone()
223 .or_else(|| {
224 s.canonicalize()
225 .ok()?
226 .file_stem()
227 .and_then(|s| s.to_str())
228 .map(|s| s.to_string())
229 })
230 .ok_or_else(|| anyhow::anyhow!("no dir name"))?;
231 Ok((package_name, s.join(WASMER_TOML_NAME)))
232 }
233 }
234 }
235
236 fn get_filesystem_mapping(include: &[String]) -> impl Iterator<Item = (String, PathBuf)> + '_ {
237 include.iter().map(|path| {
238 if path == "." || path == "/" {
239 return ("/".to_string(), Path::new("/").to_path_buf());
240 }
241
242 let key = format!("./{path}");
243 let value = PathBuf::from(format!("/{path}"));
244
245 (key, value)
246 })
247 }
248
249 fn get_command(
250 modules: &[wasmer_config::package::Module],
251 bin_or_lib: BinOrLib,
252 ) -> Vec<wasmer_config::package::Command> {
253 match bin_or_lib {
254 BinOrLib::Bin => modules
255 .iter()
256 .map(|m| {
257 wasmer_config::package::Command::V2(wasmer_config::package::CommandV2 {
258 name: m.name.clone(),
259 module: wasmer_config::package::ModuleReference::CurrentPackage {
260 module: m.name.clone(),
261 },
262 runner: "wasi".to_string(),
263 annotations: None,
264 })
265 })
266 .collect(),
267 BinOrLib::Lib | BinOrLib::Empty => Vec::new(),
268 }
269 }
270
271 fn get_dependencies(template: Option<&Template>) -> IndexMap<String, VersionReq> {
273 let mut map = IndexMap::default();
274
275 match template {
276 Some(Template::Js) => {
277 map.insert("quickjs/quickjs".to_string(), VersionReq::STAR);
278 }
279 Some(Template::Python) => {
280 map.insert("python/python".to_string(), VersionReq::STAR);
281 }
282 _ => {}
283 }
284
285 map
286 }
287
288 fn get_bin_or_lib(&self) -> Result<BinOrLib, anyhow::Error> {
290 match (self.empty, self.bin, self.lib) {
291 (true, false, false) => Ok(BinOrLib::Empty),
292 (false, true, false) => Ok(BinOrLib::Bin),
293 (false, false, true) => Ok(BinOrLib::Lib),
294 (false, false, false) => Ok(BinOrLib::Bin),
295 _ => anyhow::bail!("Only one of --bin, --lib, or --empty can be provided"),
296 }
297 }
298
299 fn get_bindings(target_file: &Path, bin_or_lib: BinOrLib) -> Option<GetBindingsResult> {
302 match bin_or_lib {
303 BinOrLib::Bin | BinOrLib::Empty => None,
304 BinOrLib::Lib => target_file.parent().and_then(|parent| {
305 let all_bindings = walkdir::WalkDir::new(parent)
306 .min_depth(1)
307 .max_depth(3)
308 .follow_links(false)
309 .into_iter()
310 .filter_map(|e| e.ok())
311 .filter_map(|e| {
312 let is_wit = e.path().extension().and_then(|s| s.to_str()) == Some(".wit");
313 let is_wai = e.path().extension().and_then(|s| s.to_str()) == Some(".wai");
314 if is_wit {
315 Some(wasmer_config::package::Bindings::Wit(
316 wasmer_config::package::WitBindings {
317 wit_exports: e.path().to_path_buf(),
318 wit_bindgen: semver::Version::parse("0.1.0").unwrap(),
319 },
320 ))
321 } else if is_wai {
322 Some(wasmer_config::package::Bindings::Wai(
323 wasmer_config::package::WaiBindings {
324 exports: None,
325 imports: vec![e.path().to_path_buf()],
326 wai_version: semver::Version::parse("0.2.0").unwrap(),
327 },
328 ))
329 } else {
330 None
331 }
332 })
333 .collect::<Vec<_>>();
334
335 if all_bindings.is_empty() {
336 None
337 } else if all_bindings.len() == 1 {
338 Some(GetBindingsResult::OneBinding(all_bindings[0].clone()))
339 } else {
340 Some(GetBindingsResult::MultiBindings(all_bindings))
341 }
342 }),
343 }
344 }
345}
346
347enum GetBindingsResult {
348 OneBinding(wasmer_config::package::Bindings),
349 MultiBindings(Vec<wasmer_config::package::Bindings>),
350}
351
352impl GetBindingsResult {
353 fn first_binding(&self) -> Option<wasmer_config::package::Bindings> {
354 match self {
355 Self::OneBinding(s) => Some(s.clone()),
356 Self::MultiBindings(s) => s.first().cloned(),
357 }
358 }
359}
360
361#[allow(clippy::too_many_arguments)]
362async fn construct_manifest(
363 env: &WasmerEnv,
364 cargo_toml: Option<&MiniCargoTomlPackage>,
365 fallback_package_name: &String,
366 package_name: Option<&str>,
367 target_file: &Path,
368 manifest_path: &Path,
369 bin_or_lib: BinOrLib,
370 namespace: Option<String>,
371 version: Option<semver::Version>,
372 template: Option<&Template>,
373 include_fs: &[String],
374 quiet: bool,
375) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
376 if let Some(ct) = cargo_toml.as_ref() {
377 let msg = format!(
378 "NOTE: Initializing wasmer.toml file with metadata from Cargo.toml{NEWLINE} -> {}",
379 ct.cargo_toml_path.display()
380 );
381 if !quiet {
382 println!("{msg}");
383 }
384 log::warn!("{msg}");
385 }
386
387 let package_name = package_name.unwrap_or_else(|| {
388 cargo_toml
389 .as_ref()
390 .map(|p| &p.name)
391 .unwrap_or(fallback_package_name)
392 });
393 let namespace = match namespace {
394 Some(n) => Some(n),
395 None => {
396 if let Ok(client) = env.client() {
397 if let Ok(Some(u)) = wasmer_backend_api::query::current_user(&client).await {
398 Some(u.username)
399 } else {
400 None
401 }
402 } else {
403 None
404 }
405 }
406 };
407 let version = version.unwrap_or_else(|| {
408 cargo_toml
409 .as_ref()
410 .map(|t| t.version.clone())
411 .unwrap_or_else(|| semver::Version::parse("0.1.0").unwrap())
412 });
413 let license = cargo_toml.as_ref().and_then(|t| t.license.clone());
414 let license_file = cargo_toml.as_ref().and_then(|t| t.license_file.clone());
415 let readme = cargo_toml.as_ref().and_then(|t| t.readme.clone());
416 let repository = cargo_toml.as_ref().and_then(|t| t.repository.clone());
417 let homepage = cargo_toml.as_ref().and_then(|t| t.homepage.clone());
418 let description = cargo_toml
419 .as_ref()
420 .and_then(|t| t.description.clone())
421 .unwrap_or_else(|| format!("Description for package {package_name}"));
422
423 let default_abi = wasmer_config::package::Abi::Wasi;
424 let bindings = Init::get_bindings(target_file, bin_or_lib);
425
426 if let Some(GetBindingsResult::MultiBindings(m)) = bindings.as_ref() {
427 let found = m
428 .iter()
429 .map(|m| match m {
430 wasmer_config::package::Bindings::Wit(wb) => {
431 format!("found: {}", serde_json::to_string(wb).unwrap_or_default())
432 }
433 wasmer_config::package::Bindings::Wai(wb) => {
434 format!("found: {}", serde_json::to_string(wb).unwrap_or_default())
435 }
436 })
437 .collect::<Vec<_>>()
438 .join("\r\n");
439
440 let msg = [
441 String::new(),
442 " It looks like your project contains multiple *.wai files.".to_string(),
443 " Make sure you update the [[module.bindings]] appropriately".to_string(),
444 String::new(),
445 found,
446 ];
447 let msg = msg.join("\r\n");
448 if !quiet {
449 println!("{msg}");
450 }
451 log::warn!("{msg}");
452 }
453
454 let module_source = cargo_toml
455 .as_ref()
456 .map(|p| {
457 let outpath = p
459 .build_dir
460 .join("release")
461 .join(format!("{package_name}.wasm"));
462 let canonicalized_outpath = outpath.canonicalize().unwrap_or(outpath);
463 let outpath_str =
464 crate::common::normalize_path(&canonicalized_outpath.display().to_string());
465 let manifest_canonicalized = crate::common::normalize_path(
466 &manifest_path
467 .parent()
468 .and_then(|p| p.canonicalize().ok())
469 .unwrap_or_else(|| manifest_path.to_path_buf())
470 .display()
471 .to_string(),
472 );
473 let diff = outpath_str
474 .strip_prefix(&manifest_canonicalized)
475 .unwrap_or(&outpath_str)
476 .replace('\\', "/");
477 let relative_str = diff.strip_prefix('/').unwrap_or(&diff);
479 Path::new(&relative_str).to_path_buf()
480 })
481 .unwrap_or_else(|| Path::new(&format!("{package_name}.wasm")).to_path_buf());
482
483 let modules = vec![wasmer_config::package::Module {
484 name: package_name.to_string(),
485 source: module_source,
486 kind: None,
487 abi: default_abi,
488 bindings: bindings.as_ref().and_then(|b| b.first_binding()),
489 interfaces: Some({
490 let mut map = IndexMap::new();
491 map.insert("wasi".to_string(), "0.1.0-unstable".to_string());
492 map
493 }),
494 annotations: None,
495 }];
496
497 let mut pkg = wasmer_config::package::Package::builder(
498 if let Some(s) = namespace {
499 format!("{s}/{package_name}")
500 } else {
501 package_name.to_string()
502 },
503 version,
504 description,
505 );
506
507 if let Some(license) = license {
508 pkg.license(license);
509 }
510 if let Some(license_file) = license_file {
511 pkg.license_file(license_file);
512 }
513 if let Some(readme) = readme {
514 pkg.readme(readme);
515 }
516 if let Some(repository) = repository {
517 pkg.repository(repository);
518 }
519 if let Some(homepage) = homepage {
520 pkg.homepage(homepage);
521 }
522 let pkg = pkg.build()?;
523
524 let mut manifest = wasmer_config::package::Manifest::builder(pkg);
525 manifest
526 .dependencies(Init::get_dependencies(template))
527 .commands(Init::get_command(&modules, bin_or_lib))
528 .fs(Init::get_filesystem_mapping(include_fs).collect());
529 match bin_or_lib {
530 BinOrLib::Bin | BinOrLib::Lib => {
531 manifest.modules(modules);
532 }
533 BinOrLib::Empty => {}
534 }
535 let manifest = manifest.build()?;
536
537 Ok(manifest)
538}
539fn parse_cargo_toml(manifest_path: &PathBuf) -> Result<MiniCargoTomlPackage, anyhow::Error> {
540 let mut metadata = MetadataCommand::new();
541 metadata.manifest_path(manifest_path);
542 metadata.no_deps();
543 metadata.features(CargoOpt::AllFeatures);
544
545 let metadata = metadata.exec().with_context(|| {
546 format!(
547 "Unable to load metadata from \"{}\"",
548 manifest_path.display()
549 )
550 })?;
551
552 let package = metadata
553 .root_package()
554 .ok_or_else(|| anyhow::anyhow!("no root package found in cargo metadata"))
555 .context(anyhow::anyhow!("{}", manifest_path.display()))?;
556
557 Ok(MiniCargoTomlPackage {
558 cargo_toml_path: manifest_path.clone(),
559 name: package.name.clone(),
560 version: package.version.clone(),
561 description: package.description.clone(),
562 homepage: package.homepage.clone(),
563 repository: package.repository.clone(),
564 license: package.license.clone(),
565 readme: package.readme.clone().map(|s| s.into_std_path_buf()),
566 license_file: package.license_file.clone().map(|f| f.into_std_path_buf()),
567 workspace_root: metadata.workspace_root.into_std_path_buf(),
568 build_dir: metadata
569 .target_directory
570 .into_std_path_buf()
571 .join("wasm32-wasip1"),
572 })
573}