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