1use 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#[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 #[arg(long, value_name = "TARGET")]
44 pub target: Option<String>,
45
46 #[arg(short = 'o', long, value_name = "OUTPUT")]
48 pub output: Option<PathBuf>,
49
50 #[arg(long = "emit-clif", value_name = "PATH")]
52 pub emit_clif: Option<PathBuf>,
53
54 #[arg(index = 1, value_name = "MODULE")]
56 pub module: PathBuf,
57}
58
59impl CompileCommand {
60 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 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 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 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}