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