wasmtime_cli/commands/
compile.rs

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