wasmer_cli/commands/
init.rs

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/// CLI args for the `wasmer init` command
16#[derive(Debug, Parser)]
17pub struct Init {
18    #[clap(flatten)]
19    env: WasmerEnv,
20
21    /// Initialize wasmer.toml for a library package
22    #[clap(long, group = "crate-type")]
23    pub lib: bool,
24    /// Initialize wasmer.toml for a binary package
25    #[clap(long, group = "crate-type")]
26    pub bin: bool,
27    /// Initialize an empty wasmer.toml
28    #[clap(long, group = "crate-type")]
29    pub empty: bool,
30    /// Force overwriting the wasmer.toml, even if it already exists
31    #[clap(long)]
32    pub overwrite: bool,
33    /// Don't display debug output
34    #[clap(long)]
35    pub quiet: bool,
36    /// Namespace to init with, default = current logged in user or _
37    #[clap(long)]
38    pub namespace: Option<String>,
39    /// Package name to init with, default = Cargo.toml name or current directory name
40    #[clap(long)]
41    pub package_name: Option<String>,
42    /// Version of the initialized package
43    #[clap(long)]
44    pub version: Option<semver::Version>,
45    /// If the `manifest-path` is a Cargo.toml, use that file to initialize the wasmer.toml
46    #[clap(long)]
47    pub manifest_path: Option<PathBuf>,
48    /// Add default dependencies for common packages
49    #[clap(long, value_enum)]
50    pub template: Option<Template>,
51    /// Include file paths into the target container filesystem
52    #[clap(long)]
53    pub include: Vec<String>,
54    /// Directory of the output file name. wasmer init will error if the target dir
55    /// already contains a wasmer.toml. Also sets the package name.
56    #[clap(name = "PACKAGE_PATH")]
57    pub out: Option<PathBuf>,
58}
59
60/// What template to use for the initialized wasmer.toml
61#[derive(Debug, PartialEq, Eq, Copy, Clone, clap::ValueEnum)]
62pub enum Template {
63    /// Add dependency on Python
64    Python,
65    /// Add dependency on JS
66    Js,
67}
68
69#[derive(Debug, PartialEq, Copy, Clone)]
70enum BinOrLib {
71    Bin,
72    Lib,
73    Empty,
74}
75
76// minimal version of the Cargo.toml [package] section
77#[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        // See if the directory has a Cargo.toml file, if yes, copy the license / readme, etc.
104        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        // generate the wasmer.toml and exit
154        Self::write_wasmer_toml(&target_file, &constructed_manifest)
155    }
156}
157
158impl Init {
159    /// Writes the metadata to a wasmer.toml file, making sure we include the
160    /// [`NOTE`] so people get a link to the registry docs.
161    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                // We've found an empty line after the initial [package]
175                // section. Let's add our note here.
176                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            // Make sure the note still ends up at the end of the file.
186            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    /// Returns the dependencies based on the `--template` flag
272    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    // Returns whether the template for the wasmer.toml should be a binary, a library or an empty file
289    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    /// Get bindings returns the first .wai / .wit file found and
300    /// optionally takes a warning callback that is triggered when > 1 .wai files are found
301    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            // Normalize the path to /target/release to be relative to the parent of the Cargo.toml
458            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            // Format in UNIX fashion (forward slashes)
478            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}