xref: /wasmtime-44.0.1/src/commands/compile.rs (revision fee9be21)
1 //! The module that implements the `wasmtime compile` command.
2 
3 use anyhow::{Context, Result, bail};
4 use clap::Parser;
5 use std::fs;
6 use std::path::PathBuf;
7 use wasmtime::{CodeBuilder, CodeHint, Engine};
8 use wasmtime_cli_flags::CommonOptions;
9 
10 const 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 /// Compiles a WebAssembly module.
28 #[derive(Parser)]
29 #[command(
30     version,
31     after_help = AFTER_HELP,
32 )]
33 pub 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     /// The path of the output compiled module; defaults to `<MODULE>.cwasm`
39     #[arg(short = 'o', long, value_name = "OUTPUT")]
40     pub output: Option<PathBuf>,
41 
42     /// The directory path to write clif files into, one clif file per wasm function.
43     #[arg(long = "emit-clif", value_name = "PATH")]
44     pub emit_clif: Option<PathBuf>,
45 
46     /// The path of the WebAssembly to compile
47     #[arg(index = 1, value_name = "MODULE")]
48     pub module: PathBuf,
49 }
50 
51 impl CompileCommand {
52     /// Executes the command.
53     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)))]
108 mod 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         // Set all the x64 flags to make sure they work
155         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         // Set all the aarch64 flags to make sure they work
192         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         // aarch64 flags should not be supported
221         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 }
272