xref: /wasmtime-44.0.1/src/commands/compile.rs (revision 94740588)
1 //! The module that implements the `wasmtime compile` command.
2 
3 use clap::Parser;
4 use std::fs;
5 use std::path::PathBuf;
6 use wasmtime::{CodeBuilder, CodeHint, Engine, Result, bail, error::Context as _};
7 use wasmtime_cli_flags::CommonOptions;
8 
9 const 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 /// Compiles a WebAssembly module.
27 #[derive(Parser)]
28 #[command(
29     version,
30     after_help = AFTER_HELP,
31 )]
32 pub 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     /// The path of the output compiled module; defaults to `<MODULE>.cwasm`
38     #[arg(short = 'o', long, value_name = "OUTPUT")]
39     pub output: Option<PathBuf>,
40 
41     /// The directory path to write clif files into, one clif file per wasm function.
42     #[arg(long = "emit-clif", value_name = "PATH")]
43     pub emit_clif: Option<PathBuf>,
44 
45     /// The path of the WebAssembly to compile
46     #[arg(index = 1, value_name = "MODULE")]
47     pub module: PathBuf,
48 }
49 
50 impl CompileCommand {
51     /// Executes the command.
execute(mut self) -> Result<()>52     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)))]
107 mod test {
108     use super::*;
109     use std::io::Write;
110     use tempfile::NamedTempFile;
111     use wasmtime::{Instance, Module, Store};
112 
113     #[test]
test_successful_compile() -> Result<()>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]
test_x64_flags_compile() -> Result<()>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         // Set all the x64 flags to make sure they work
154         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]
test_aarch64_flags_compile() -> Result<()>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         // Set all the aarch64 flags to make sure they work
191         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]
test_unsupported_flags_compile() -> Result<()>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         // aarch64 flags should not be supported
220         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]
test_x64_presets_compile() -> Result<()>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 }
271