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