1 //! The module that implements the `wasmtime compile` command. 2 3 use anyhow::{bail, Context, Result}; 4 use clap::Parser; 5 use once_cell::sync::Lazy; 6 use std::fs; 7 use std::path::PathBuf; 8 use wasmtime::Engine; 9 use wasmtime_cli_flags::CommonOptions; 10 11 static 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 {}\ 16 \n\ 17 Usage examples:\n\ 18 \n\ 19 Compiling a WebAssembly module for the current platform:\n\ 20 \n \ 21 wasmtime compile example.wasm 22 \n\ 23 Specifying the output file:\n\ 24 \n \ 25 wasmtime compile -o output.cwasm input.wasm\n\ 26 \n\ 27 Compiling for a specific platform (Linux) and CPU preset (Skylake):\n\ 28 \n \ 29 wasmtime compile --target x86_64-unknown-linux --cranelift-enable skylake foo.wasm\n", 30 crate::FLAG_EXPLANATIONS.as_str() 31 ) 32 }); 33 34 /// Compiles a WebAssembly module. 35 #[derive(Parser)] 36 #[structopt( 37 name = "compile", 38 version, 39 after_help = AFTER_HELP.as_str() 40 )] 41 pub struct CompileCommand { 42 #[clap(flatten)] 43 common: CommonOptions, 44 45 /// The target triple; default is the host triple 46 #[clap(long, value_name = "TARGET")] 47 target: Option<String>, 48 49 /// The path of the output compiled module; defaults to <MODULE>.cwasm 50 #[clap(short = 'o', long, value_name = "OUTPUT", parse(from_os_str))] 51 output: Option<PathBuf>, 52 53 /// The directory path to write clif files into, one clif file per wasm function. 54 #[clap(long = "emit-clif", value_name = "PATH", parse(from_os_str))] 55 emit_clif: Option<PathBuf>, 56 57 /// The path of the WebAssembly to compile 58 #[clap(index = 1, value_name = "MODULE", parse(from_os_str))] 59 module: PathBuf, 60 } 61 62 impl CompileCommand { 63 /// Executes the command. 64 pub fn execute(mut self) -> Result<()> { 65 self.common.init_logging(); 66 67 let mut config = self.common.config(self.target.as_deref())?; 68 69 if let Some(path) = self.emit_clif { 70 if !path.exists() { 71 std::fs::create_dir(&path)?; 72 } 73 74 if !path.is_dir() { 75 bail!( 76 "the path passed for '--emit-clif' ({}) must be a directory", 77 path.display() 78 ); 79 } 80 81 config.emit_clif(&path); 82 } 83 84 let engine = Engine::new(&config)?; 85 86 if self.module.file_name().is_none() { 87 bail!( 88 "'{}' is not a valid input module path", 89 self.module.display() 90 ); 91 } 92 93 let input = wat::parse_file(&self.module).with_context(|| "failed to read input file")?; 94 95 let output = self.output.take().unwrap_or_else(|| { 96 let mut output: PathBuf = self.module.file_name().unwrap().into(); 97 output.set_extension("cwasm"); 98 output 99 }); 100 101 // If the component-model proposal is enabled and the binary we're 102 // compiling looks like a component, tested by sniffing the first 8 103 // bytes with the current component model proposal. 104 #[cfg(feature = "component-model")] 105 { 106 if let Ok(wasmparser::Chunk::Parsed { 107 payload: 108 wasmparser::Payload::Version { 109 encoding: wasmparser::Encoding::Component, 110 .. 111 }, 112 .. 113 }) = wasmparser::Parser::new(0).parse(&input, true) 114 { 115 fs::write(output, engine.precompile_component(&input)?)?; 116 return Ok(()); 117 } 118 } 119 fs::write(output, engine.precompile_module(&input)?)?; 120 121 Ok(()) 122 } 123 } 124 125 #[cfg(all(test, not(miri)))] 126 mod test { 127 use super::*; 128 use std::io::Write; 129 use tempfile::NamedTempFile; 130 use wasmtime::{Instance, Module, Store}; 131 132 #[test] 133 fn test_successful_compile() -> Result<()> { 134 let (mut input, input_path) = NamedTempFile::new()?.into_parts(); 135 input.write_all( 136 "(module (func (export \"f\") (param i32) (result i32) local.get 0))".as_bytes(), 137 )?; 138 drop(input); 139 140 let output_path = NamedTempFile::new()?.into_temp_path(); 141 142 let command = CompileCommand::try_parse_from(vec![ 143 "compile", 144 "--disable-logging", 145 "-o", 146 output_path.to_str().unwrap(), 147 input_path.to_str().unwrap(), 148 ])?; 149 150 command.execute()?; 151 152 let engine = Engine::default(); 153 let contents = std::fs::read(output_path)?; 154 let module = unsafe { Module::deserialize(&engine, contents)? }; 155 let mut store = Store::new(&engine, ()); 156 let instance = Instance::new(&mut store, &module, &[])?; 157 let f = instance.get_typed_func::<i32, i32>(&mut store, "f")?; 158 assert_eq!(f.call(&mut store, 1234).unwrap(), 1234); 159 160 Ok(()) 161 } 162 163 #[cfg(target_arch = "x86_64")] 164 #[test] 165 fn test_x64_flags_compile() -> Result<()> { 166 let (mut input, input_path) = NamedTempFile::new()?.into_parts(); 167 input.write_all("(module)".as_bytes())?; 168 drop(input); 169 170 let output_path = NamedTempFile::new()?.into_temp_path(); 171 172 // Set all the x64 flags to make sure they work 173 let command = CompileCommand::try_parse_from(vec![ 174 "compile", 175 "--disable-logging", 176 "--cranelift-enable", 177 "has_sse3", 178 "--cranelift-enable", 179 "has_ssse3", 180 "--cranelift-enable", 181 "has_sse41", 182 "--cranelift-enable", 183 "has_sse42", 184 "--cranelift-enable", 185 "has_avx", 186 "--cranelift-enable", 187 "has_avx2", 188 "--cranelift-enable", 189 "has_fma", 190 "--cranelift-enable", 191 "has_avx512dq", 192 "--cranelift-enable", 193 "has_avx512vl", 194 "--cranelift-enable", 195 "has_avx512f", 196 "--cranelift-enable", 197 "has_popcnt", 198 "--cranelift-enable", 199 "has_bmi1", 200 "--cranelift-enable", 201 "has_bmi2", 202 "--cranelift-enable", 203 "has_lzcnt", 204 "-o", 205 output_path.to_str().unwrap(), 206 input_path.to_str().unwrap(), 207 ])?; 208 209 command.execute()?; 210 211 Ok(()) 212 } 213 214 #[cfg(target_arch = "aarch64")] 215 #[test] 216 fn test_aarch64_flags_compile() -> Result<()> { 217 let (mut input, input_path) = NamedTempFile::new()?.into_parts(); 218 input.write_all("(module)".as_bytes())?; 219 drop(input); 220 221 let output_path = NamedTempFile::new()?.into_temp_path(); 222 223 // Set all the aarch64 flags to make sure they work 224 let command = CompileCommand::try_parse_from(vec![ 225 "compile", 226 "--disable-logging", 227 "--cranelift-enable", 228 "has_lse", 229 "--cranelift-enable", 230 "has_pauth", 231 "--cranelift-enable", 232 "sign_return_address", 233 "--cranelift-enable", 234 "sign_return_address_all", 235 "--cranelift-enable", 236 "sign_return_address_with_bkey", 237 "-o", 238 output_path.to_str().unwrap(), 239 input_path.to_str().unwrap(), 240 ])?; 241 242 command.execute()?; 243 244 Ok(()) 245 } 246 247 #[cfg(target_arch = "x86_64")] 248 #[test] 249 fn test_unsupported_flags_compile() -> Result<()> { 250 let (mut input, input_path) = NamedTempFile::new()?.into_parts(); 251 input.write_all("(module)".as_bytes())?; 252 drop(input); 253 254 let output_path = NamedTempFile::new()?.into_temp_path(); 255 256 // aarch64 flags should not be supported 257 let command = CompileCommand::try_parse_from(vec![ 258 "compile", 259 "--disable-logging", 260 "--cranelift-enable", 261 "has_lse", 262 "-o", 263 output_path.to_str().unwrap(), 264 input_path.to_str().unwrap(), 265 ])?; 266 267 assert_eq!( 268 command.execute().unwrap_err().to_string(), 269 "No existing setting named 'has_lse'" 270 ); 271 272 Ok(()) 273 } 274 275 #[cfg(target_arch = "x86_64")] 276 #[test] 277 fn test_x64_presets_compile() -> Result<()> { 278 let (mut input, input_path) = NamedTempFile::new()?.into_parts(); 279 input.write_all("(module)".as_bytes())?; 280 drop(input); 281 282 let output_path = NamedTempFile::new()?.into_temp_path(); 283 284 for preset in &[ 285 "nehalem", 286 "haswell", 287 "broadwell", 288 "skylake", 289 "cannonlake", 290 "icelake", 291 "znver1", 292 ] { 293 let command = CompileCommand::try_parse_from(vec![ 294 "compile", 295 "--disable-logging", 296 "--cranelift-enable", 297 preset, 298 "-o", 299 output_path.to_str().unwrap(), 300 input_path.to_str().unwrap(), 301 ])?; 302 303 command.execute()?; 304 } 305 306 Ok(()) 307 } 308 } 309