1use 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#[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 #[arg(short = 'o', long, value_name = "OUTPUT")]
39 pub output: Option<PathBuf>,
40
41 #[arg(long = "emit-clif", value_name = "PATH")]
43 pub emit_clif: Option<PathBuf>,
44
45 #[arg(index = 1, value_name = "MODULE")]
47 pub module: PathBuf,
48}
49
50impl CompileCommand {
51 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 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 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 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}