Skip to main content

wasmtime_cli/commands/
compile.rs

1//! The module that implements the `wasmtime compile` command.
2
3use clap::Parser;
4use std::fs;
5use std::path::PathBuf;
6use wasmtime::{CodeBuilder, CodeHint, Engine, Result, bail, error::Context as _};
7use wasmtime_cli_flags::CommonOptions;
8
9const AFTER_HELP: &str =
10    "By default, no CPU features or presets will be enabled for the compilation.\n\
11        \n\
12        Usage examples:\n\
13        \n\
14        Compiling a WebAssembly module for the current platform:\n\
15        \n  \
16        wasmtime compile example.wasm
17        \n\
18        Specifying the output file:\n\
19        \n  \
20        wasmtime compile -o output.cwasm input.wasm\n\
21        \n\
22        Compiling for a specific platform (Linux) and CPU preset (Skylake):\n\
23        \n  \
24        wasmtime compile --target x86_64-unknown-linux -Ccranelift-skylake foo.wasm\n";
25
26/// Compiles a WebAssembly module.
27#[derive(Parser)]
28#[command(
29    version,
30    after_help = AFTER_HELP,
31)]
32pub struct CompileCommand {
33    #[command(flatten)]
34    #[expect(missing_docs, reason = "don't want to mess with clap doc-strings")]
35    pub common: CommonOptions,
36
37    /// The path of the output compiled module; defaults to `<MODULE>.cwasm`
38    #[arg(short = 'o', long, value_name = "OUTPUT")]
39    pub output: Option<PathBuf>,
40
41    /// The directory path to write clif files into, one clif file per wasm function.
42    #[arg(long = "emit-clif", value_name = "PATH")]
43    pub emit_clif: Option<PathBuf>,
44
45    /// The path of the WebAssembly to compile
46    #[arg(index = 1, value_name = "MODULE")]
47    pub module: PathBuf,
48}
49
50impl CompileCommand {
51    /// Executes the command.
52    pub fn execute(mut self) -> Result<()> {
53        self.common.init_logging()?;
54
55        let mut config = self.common.config(None)?;
56
57        if let Some(path) = self.emit_clif {
58            if !path.exists() {
59                std::fs::create_dir(&path)?;
60            }
61
62            if !path.is_dir() {
63                bail!(
64                    "the path passed for '--emit-clif' ({}) must be a directory",
65                    path.display()
66                );
67            }
68
69            config.emit_clif(&path);
70        }
71
72        let engine = Engine::new(&config)?;
73
74        if self.module.file_name().is_none() {
75            bail!(
76                "'{}' is not a valid input module path",
77                self.module.display()
78            );
79        }
80
81        let mut code = CodeBuilder::new(&engine);
82        code.wasm_binary_or_text_file(&self.module)?;
83
84        let output = self.output.take().unwrap_or_else(|| {
85            let mut output: PathBuf = self.module.file_name().unwrap().into();
86            output.set_extension("cwasm");
87            output
88        });
89
90        let output_bytes = match code.hint() {
91            #[cfg(feature = "component-model")]
92            Some(CodeHint::Component) => code.compile_component_serialized()?,
93            #[cfg(not(feature = "component-model"))]
94            Some(CodeHint::Component) => {
95                bail!("component model support was disabled at compile time")
96            }
97            Some(CodeHint::Module) | None => code.compile_module_serialized()?,
98        };
99        fs::write(&output, output_bytes)
100            .with_context(|| format!("failed to write output: {}", output.display()))?;
101
102        Ok(())
103    }
104}
105
106#[cfg(all(test, not(miri)))]
107mod test {
108    use super::*;
109    use std::io::Write;
110    use tempfile::NamedTempFile;
111    use wasmtime::{Instance, Module, Store};
112
113    #[test]
114    fn test_successful_compile() -> Result<()> {
115        let (mut input, input_path) = NamedTempFile::new()?.into_parts();
116        input.write_all(
117            "(module (func (export \"f\") (param i32) (result i32) local.get 0))".as_bytes(),
118        )?;
119        drop(input);
120
121        let output_path = NamedTempFile::new()?.into_temp_path();
122
123        let command = CompileCommand::try_parse_from(vec![
124            "compile",
125            "-Dlogging=n",
126            "-o",
127            output_path.to_str().unwrap(),
128            input_path.to_str().unwrap(),
129        ])?;
130
131        command.execute()?;
132
133        let engine = Engine::default();
134        let contents = std::fs::read(output_path)?;
135        let module = unsafe { Module::deserialize(&engine, contents)? };
136        let mut store = Store::new(&engine, ());
137        let instance = Instance::new(&mut store, &module, &[])?;
138        let f = instance.get_typed_func::<i32, i32>(&mut store, "f")?;
139        assert_eq!(f.call(&mut store, 1234).unwrap(), 1234);
140
141        Ok(())
142    }
143
144    #[cfg(target_arch = "x86_64")]
145    #[test]
146    fn test_x64_flags_compile() -> Result<()> {
147        let (mut input, input_path) = NamedTempFile::new()?.into_parts();
148        input.write_all("(module)".as_bytes())?;
149        drop(input);
150
151        let output_path = NamedTempFile::new()?.into_temp_path();
152
153        // Set all the x64 flags to make sure they work
154        let command = CompileCommand::try_parse_from(vec![
155            "compile",
156            "-Dlogging=n",
157            "-Ccranelift-has-sse3",
158            "-Ccranelift-has-ssse3",
159            "-Ccranelift-has-sse41",
160            "-Ccranelift-has-sse42",
161            "-Ccranelift-has-avx",
162            "-Ccranelift-has-avx2",
163            "-Ccranelift-has-fma",
164            "-Ccranelift-has-avx512dq",
165            "-Ccranelift-has-avx512vl",
166            "-Ccranelift-has-avx512f",
167            "-Ccranelift-has-popcnt",
168            "-Ccranelift-has-bmi1",
169            "-Ccranelift-has-bmi2",
170            "-Ccranelift-has-lzcnt",
171            "-o",
172            output_path.to_str().unwrap(),
173            input_path.to_str().unwrap(),
174        ])?;
175
176        command.execute()?;
177
178        Ok(())
179    }
180
181    #[cfg(target_arch = "aarch64")]
182    #[test]
183    fn test_aarch64_flags_compile() -> Result<()> {
184        let (mut input, input_path) = NamedTempFile::new()?.into_parts();
185        input.write_all("(module)".as_bytes())?;
186        drop(input);
187
188        let output_path = NamedTempFile::new()?.into_temp_path();
189
190        // Set all the aarch64 flags to make sure they work
191        let command = CompileCommand::try_parse_from(vec![
192            "compile",
193            "-Dlogging=n",
194            "-Ccranelift-has-lse",
195            "-Ccranelift-has-pauth",
196            "-Ccranelift-has-fp16",
197            "-Ccranelift-sign-return-address",
198            "-Ccranelift-sign-return-address-all",
199            "-Ccranelift-sign-return-address-with-bkey",
200            "-o",
201            output_path.to_str().unwrap(),
202            input_path.to_str().unwrap(),
203        ])?;
204
205        command.execute()?;
206
207        Ok(())
208    }
209
210    #[cfg(target_arch = "x86_64")]
211    #[test]
212    fn test_unsupported_flags_compile() -> Result<()> {
213        let (mut input, input_path) = NamedTempFile::new()?.into_parts();
214        input.write_all("(module)".as_bytes())?;
215        drop(input);
216
217        let output_path = NamedTempFile::new()?.into_temp_path();
218
219        // aarch64 flags should not be supported
220        let command = CompileCommand::try_parse_from(vec![
221            "compile",
222            "-Dlogging=n",
223            "-Ccranelift-has-lse",
224            "-o",
225            output_path.to_str().unwrap(),
226            input_path.to_str().unwrap(),
227        ])?;
228
229        assert_eq!(
230            command.execute().unwrap_err().to_string(),
231            "No existing setting named 'has_lse'"
232        );
233
234        Ok(())
235    }
236
237    #[cfg(target_arch = "x86_64")]
238    #[test]
239    fn test_x64_presets_compile() -> Result<()> {
240        let (mut input, input_path) = NamedTempFile::new()?.into_parts();
241        input.write_all("(module)".as_bytes())?;
242        drop(input);
243
244        let output_path = NamedTempFile::new()?.into_temp_path();
245
246        for preset in &[
247            "nehalem",
248            "haswell",
249            "broadwell",
250            "skylake",
251            "cannonlake",
252            "icelake",
253            "znver1",
254        ] {
255            let flag = format!("-Ccranelift-{preset}");
256            let command = CompileCommand::try_parse_from(vec![
257                "compile",
258                "-Dlogging=n",
259                flag.as_str(),
260                "-o",
261                output_path.to_str().unwrap(),
262                input_path.to_str().unwrap(),
263            ])?;
264
265            command.execute()?;
266        }
267
268        Ok(())
269    }
270}