1 // Build script. 2 // 3 // This program is run by Cargo when building cranelift-codegen. It is used to generate Rust code from 4 // the language definitions in the cranelift-codegen/meta directory. 5 // 6 // Environment: 7 // 8 // OUT_DIR 9 // Directory where generated files should be placed. 10 // 11 // TARGET 12 // Target triple provided by Cargo. 13 // 14 // The build script expects to be run from the directory where this build.rs file lives. The 15 // current directory is used to find the sources. 16 17 use cranelift_codegen_meta as meta; 18 19 use std::env; 20 use std::io::Read; 21 use std::process; 22 use std::time::Instant; 23 24 fn main() { 25 let start_time = Instant::now(); 26 27 let out_dir = env::var("OUT_DIR").expect("The OUT_DIR environment variable must be set"); 28 let target_triple = env::var("TARGET").expect("The TARGET environment variable must be set"); 29 30 let isa_targets = meta::isa::Isa::all() 31 .iter() 32 .cloned() 33 .filter(|isa| { 34 let env_key = format!("CARGO_FEATURE_{}", isa.to_string().to_uppercase()); 35 env::var(env_key).is_ok() 36 }) 37 .collect::<Vec<_>>(); 38 39 let isas = if isa_targets.is_empty() { 40 // Try to match native target. 41 let target_name = target_triple.split('-').next().unwrap(); 42 let isa = meta::isa_from_arch(&target_name).expect("error when identifying target"); 43 println!("cargo:rustc-cfg=feature=\"{}\"", isa); 44 vec![isa] 45 } else { 46 isa_targets 47 }; 48 49 let cur_dir = env::current_dir().expect("Can't access current working directory"); 50 let crate_dir = cur_dir.as_path(); 51 52 println!("cargo:rerun-if-changed=build.rs"); 53 54 if let Err(err) = meta::generate(&isas, &out_dir, crate_dir) { 55 eprintln!("Error: {}", err); 56 process::exit(1); 57 } 58 59 if env::var("CRANELIFT_VERBOSE").is_ok() { 60 for isa in &isas { 61 println!("cargo:warning=Includes support for {} ISA", isa.to_string()); 62 } 63 println!( 64 "cargo:warning=Build step took {:?}.", 65 Instant::now() - start_time 66 ); 67 println!("cargo:warning=Generated files are in {}", out_dir); 68 } 69 70 // The "Meta deterministic check" CI job runs this build script N 71 // times to ensure it produces the same output 72 // consistently. However, it runs the script in a fresh directory, 73 // without any of the source tree present; this breaks our 74 // manifest check (we need the ISLE source to be present). To keep 75 // things simple, we just disable all ISLE-related logic for this 76 // specific CI job. 77 #[cfg(not(feature = "completely-skip-isle-for-ci-deterministic-check"))] 78 { 79 maybe_rebuild_isle(crate_dir).expect("Unhandled failure in ISLE rebuild"); 80 } 81 82 let pkg_version = env::var("CARGO_PKG_VERSION").unwrap(); 83 let mut cmd = std::process::Command::new("git"); 84 cmd.arg("rev-parse") 85 .arg("HEAD") 86 .stdout(std::process::Stdio::piped()) 87 .current_dir(env::var("CARGO_MANIFEST_DIR").unwrap()); 88 let version = if let Ok(mut child) = cmd.spawn() { 89 let mut git_rev = String::new(); 90 child 91 .stdout 92 .as_mut() 93 .unwrap() 94 .read_to_string(&mut git_rev) 95 .unwrap(); 96 let status = child.wait().unwrap(); 97 if status.success() { 98 let git_rev = git_rev.trim().chars().take(9).collect::<String>(); 99 format!("{}-{}", pkg_version, git_rev) 100 } else { 101 // not a git repo 102 pkg_version 103 } 104 } else { 105 // git not available 106 pkg_version 107 }; 108 std::fs::write( 109 std::path::Path::new(&out_dir).join("version.rs"), 110 format!( 111 "/// Version number of this crate. \n\ 112 pub const VERSION: &str = \"{}\";", 113 version 114 ), 115 ) 116 .unwrap(); 117 } 118 119 /// Strip the current directory from the file paths, because `islec` 120 /// includes them in the generated source, and this helps us maintain 121 /// deterministic builds that don't include those local file paths. 122 fn make_isle_source_path_relative( 123 cur_dir: &std::path::PathBuf, 124 filename: std::path::PathBuf, 125 ) -> std::path::PathBuf { 126 if let Ok(suffix) = filename.strip_prefix(&cur_dir) { 127 suffix.to_path_buf() 128 } else { 129 filename 130 } 131 } 132 133 /// A list of compilations (transformations from ISLE source to 134 /// generated Rust source) that exist in the repository. 135 /// 136 /// This list is used either to regenerate the Rust source in-tree (if 137 /// the `rebuild-isle` feature is enabled), or to verify that the ISLE 138 /// source in-tree corresponds to the ISLE source that was last used 139 /// to rebuild the Rust source (if the `rebuild-isle` feature is not 140 /// enabled). 141 #[derive(Clone, Debug)] 142 struct IsleCompilations { 143 items: Vec<IsleCompilation>, 144 } 145 146 #[derive(Clone, Debug)] 147 struct IsleCompilation { 148 output: std::path::PathBuf, 149 inputs: Vec<std::path::PathBuf>, 150 } 151 152 impl IsleCompilation { 153 /// Compute the manifest filename for the given generated Rust file. 154 fn manifest_filename(&self) -> std::path::PathBuf { 155 self.output.with_extension("manifest") 156 } 157 158 /// Compute the content of the source manifest for all ISLE source 159 /// files that go into the compilation of one Rust file. 160 /// 161 /// We store this alongside the `<generated_filename>.rs` file as 162 /// `<generated_filename>.manifest` and use it to verify that a 163 /// rebuild was done if necessary. 164 fn compute_manifest(&self) -> Result<String, Box<dyn std::error::Error + 'static>> { 165 // We use the deprecated SipHasher from std::hash in order to verify 166 // that ISLE sources haven't changed since the generated source was 167 // last regenerated. 168 // 169 // We use this instead of a stronger and more usual content hash, like 170 // SHA-{160,256,512}, because it's built into the standard library and 171 // we don't want to pull in a separate crate. We try to keep Cranelift 172 // crate dependencies as intentionally small as possible. In fact, we 173 // used to use the `sha2` crate for SHA-512 and this turns out to be 174 // undesirable for downstream consumers (see #3609). 175 // 176 // Why not the recommended replacement 177 // `std::collections::hash_map::DefaultHasher`? Because we need the 178 // hash to be deterministic, both between runs (i.e., not seeded with 179 // random state) and across Rust versions. 180 // 181 // If `SipHasher` is ever actually removed from `std`, we'll need to 182 // find a new option, either a very small crate or something else 183 // that's built-in. 184 #![allow(deprecated)] 185 use std::fmt::Write; 186 use std::hash::{Hasher, SipHasher}; 187 188 let mut manifest = String::new(); 189 190 for filename in &self.inputs { 191 // Our source must be valid UTF-8 for this to work, else user 192 // will get an error on build. This is not expected to be an 193 // issue. 194 let content = std::fs::read_to_string(filename)?; 195 // On Windows, source is checked out with line-endings changed 196 // to `\r\n`; canonicalize the source that we hash to 197 // Unix-style (`\n`) so hashes will match. 198 let content = content.replace("\r\n", "\n"); 199 // One line in the manifest: <filename> <siphash>. 200 let mut hasher = SipHasher::new_with_keys(0, 0); // fixed keys for determinism 201 hasher.write(content.as_bytes()); 202 let filename = format!("{}", filename.display()).replace("\\", "/"); 203 writeln!(&mut manifest, "{} {:x}", filename, hasher.finish())?; 204 } 205 206 Ok(manifest) 207 } 208 } 209 210 /// Construct the list of compilations (transformations from ISLE 211 /// source to generated Rust source) that exist in the repository. 212 fn get_isle_compilations(crate_dir: &std::path::Path) -> Result<IsleCompilations, std::io::Error> { 213 let cur_dir = std::env::current_dir()?; 214 215 let clif_isle = 216 make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("clif.isle")); 217 let prelude_isle = 218 make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("prelude.isle")); 219 let src_isa_x64 = 220 make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("x64")); 221 let src_isa_aarch64 = 222 make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("aarch64")); 223 let src_isa_s390x = 224 make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("s390x")); 225 226 // This is a set of ISLE compilation units. 227 // 228 // The format of each entry is: 229 // 230 // (output Rust code file, input ISLE source files) 231 // 232 // There should be one entry for each backend that uses ISLE for lowering, 233 // and if/when we replace our peephole optimization passes with ISLE, there 234 // should be an entry for each of those as well. 235 // 236 // N.B.: add any new compilation outputs to 237 // `scripts/force-rebuild-isle.sh` if they do not fit the pattern 238 // `cranelift/codegen/src/isa/*/lower/isle/generated_code.rs`! 239 Ok(IsleCompilations { 240 items: vec![ 241 // The x86-64 instruction selector. 242 IsleCompilation { 243 output: src_isa_x64 244 .join("lower") 245 .join("isle") 246 .join("generated_code.rs"), 247 inputs: vec![ 248 clif_isle.clone(), 249 prelude_isle.clone(), 250 src_isa_x64.join("inst.isle"), 251 src_isa_x64.join("lower.isle"), 252 ], 253 }, 254 // The aarch64 instruction selector. 255 IsleCompilation { 256 output: src_isa_aarch64 257 .join("lower") 258 .join("isle") 259 .join("generated_code.rs"), 260 inputs: vec![ 261 clif_isle.clone(), 262 prelude_isle.clone(), 263 src_isa_aarch64.join("inst.isle"), 264 src_isa_aarch64.join("lower.isle"), 265 ], 266 }, 267 // The s390x instruction selector. 268 IsleCompilation { 269 output: src_isa_s390x 270 .join("lower") 271 .join("isle") 272 .join("generated_code.rs"), 273 inputs: vec![ 274 clif_isle.clone(), 275 prelude_isle.clone(), 276 src_isa_s390x.join("inst.isle"), 277 src_isa_s390x.join("lower.isle"), 278 ], 279 }, 280 ], 281 }) 282 } 283 284 /// Check the manifest for the ISLE generated code, which documents 285 /// what ISLE source went into generating the Rust, and if there is a 286 /// mismatch, either invoke the ISLE compiler (if we have the 287 /// `rebuild-isle` feature) or exit with an error (if not). 288 /// 289 /// We do this by computing a hash of the ISLE source and checking it 290 /// against a "manifest" that is also checked into git, alongside the 291 /// generated Rust. 292 /// 293 /// (Why not include the `rebuild-isle` feature by default? Because 294 /// the build process must not modify the checked-in source by 295 /// default; any checked-in source is a human-managed bit of data, and 296 /// we can only act as an agent of the human developer when explicitly 297 /// requested to do so. This manifest check is a middle ground that 298 /// ensures this explicit control while also avoiding the easy footgun 299 /// of "I changed the ISLE, why isn't the compiler updated?!".) 300 fn maybe_rebuild_isle( 301 crate_dir: &std::path::Path, 302 ) -> Result<(), Box<dyn std::error::Error + 'static>> { 303 let isle_compilations = get_isle_compilations(crate_dir)?; 304 let mut rebuild_compilations = vec![]; 305 306 for compilation in &isle_compilations.items { 307 for file in &compilation.inputs { 308 println!("cargo:rerun-if-changed={}", file.display()); 309 } 310 311 let manifest = 312 std::fs::read_to_string(compilation.manifest_filename()).unwrap_or(String::new()); 313 // Canonicalize Windows line-endings into Unix line-endings in 314 // the manifest text itself. 315 let manifest = manifest.replace("\r\n", "\n"); 316 let expected_manifest = compilation.compute_manifest()?.replace("\r\n", "\n"); 317 if manifest != expected_manifest { 318 rebuild_compilations.push((compilation, expected_manifest)); 319 } 320 } 321 322 #[cfg(feature = "rebuild-isle")] 323 { 324 if !rebuild_compilations.is_empty() { 325 set_miette_hook(); 326 } 327 let mut had_error = false; 328 for (compilation, expected_manifest) in rebuild_compilations { 329 if let Err(e) = rebuild_isle(compilation, &expected_manifest) { 330 eprintln!("Error building ISLE files: {:?}", e); 331 let mut source = e.source(); 332 while let Some(e) = source { 333 eprintln!("{:?}", e); 334 source = e.source(); 335 } 336 had_error = true; 337 } 338 } 339 340 if had_error { 341 std::process::exit(1); 342 } 343 } 344 345 #[cfg(not(feature = "rebuild-isle"))] 346 { 347 if !rebuild_compilations.is_empty() { 348 for (compilation, _) in rebuild_compilations { 349 eprintln!(""); 350 eprintln!( 351 "Error: the ISLE source files that resulted in the generated Rust source" 352 ); 353 eprintln!(""); 354 eprintln!(" * {}", compilation.output.display()); 355 eprintln!(""); 356 eprintln!( 357 "have changed but the generated source was not rebuilt! These ISLE source" 358 ); 359 eprintln!("files are:"); 360 eprintln!(""); 361 for file in &compilation.inputs { 362 eprintln!(" * {}", file.display()); 363 } 364 } 365 366 eprintln!(""); 367 eprintln!("Please add `--features rebuild-isle` to your `cargo build` command"); 368 eprintln!("if you wish to rebuild the generated source, then include these changes"); 369 eprintln!("in any git commits you make that include the changes to the ISLE."); 370 eprintln!(""); 371 eprintln!("For example:"); 372 eprintln!(""); 373 eprintln!(" $ cargo build -p cranelift-codegen --features rebuild-isle"); 374 eprintln!(""); 375 eprintln!("(This build script cannot do this for you by default because we cannot"); 376 eprintln!("modify checked-into-git source without your explicit opt-in.)"); 377 eprintln!(""); 378 std::process::exit(1); 379 } 380 } 381 382 Ok(()) 383 } 384 385 #[cfg(feature = "rebuild-isle")] 386 fn set_miette_hook() { 387 use std::sync::Once; 388 static SET_MIETTE_HOOK: Once = Once::new(); 389 SET_MIETTE_HOOK.call_once(|| { 390 let _ = miette::set_hook(Box::new(|_| { 391 Box::new( 392 miette::MietteHandlerOpts::new() 393 // This is necessary for `miette` to properly display errors 394 // until https://github.com/zkat/miette/issues/93 is fixed. 395 .force_graphical(true) 396 .build(), 397 ) 398 })); 399 }); 400 } 401 402 /// Rebuild ISLE DSL source text into generated Rust code. 403 /// 404 /// NB: This must happen *after* the `cranelift-codegen-meta` functions, since 405 /// it consumes files generated by them. 406 #[cfg(feature = "rebuild-isle")] 407 fn rebuild_isle( 408 compilation: &IsleCompilation, 409 manifest: &str, 410 ) -> Result<(), Box<dyn std::error::Error + 'static>> { 411 use cranelift_isle as isle; 412 413 println!("Rebuilding {}", compilation.output.display()); 414 415 // First, remove the manifest, if any; we will recreate it 416 // below if the compilation is successful. Ignore error if no 417 // manifest was present. 418 let manifest_filename = compilation.manifest_filename(); 419 let _ = std::fs::remove_file(&manifest_filename); 420 421 let code = (|| { 422 let lexer = isle::lexer::Lexer::from_files(&compilation.inputs[..])?; 423 let defs = isle::parser::parse(lexer)?; 424 isle::compile::compile(&defs) 425 })() 426 .map_err(|e| { 427 // Make sure to include the source snippets location info along with 428 // the error messages. 429 430 let report = miette::Report::new(e); 431 return DebugReport(report); 432 433 struct DebugReport(miette::Report); 434 435 impl std::fmt::Display for DebugReport { 436 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 437 self.0.handler().debug(&*self.0, f) 438 } 439 } 440 441 impl std::fmt::Debug for DebugReport { 442 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 443 std::fmt::Display::fmt(self, f) 444 } 445 } 446 447 impl std::error::Error for DebugReport {} 448 })?; 449 450 let code = rustfmt(&code).unwrap_or_else(|e| { 451 println!( 452 "cargo:warning=Failed to run `rustfmt` on ISLE-generated code: {:?}", 453 e 454 ); 455 code 456 }); 457 458 println!( 459 "Writing ISLE-generated Rust code to {}", 460 compilation.output.display() 461 ); 462 std::fs::write(&compilation.output, code)?; 463 464 // Write the manifest so that, in the default build configuration 465 // without the `rebuild-isle` feature, we can at least verify that 466 // no changes were made that will not be picked up. Note that we 467 // only write this *after* we write the source above, so no 468 // manifest is produced if there was an error. 469 std::fs::write(&manifest_filename, manifest)?; 470 471 return Ok(()); 472 473 fn rustfmt(code: &str) -> std::io::Result<String> { 474 use std::io::Write; 475 476 let mut rustfmt = std::process::Command::new("rustfmt") 477 .stdin(std::process::Stdio::piped()) 478 .stdout(std::process::Stdio::piped()) 479 .spawn()?; 480 481 let mut stdin = rustfmt.stdin.take().unwrap(); 482 stdin.write_all(code.as_bytes())?; 483 drop(stdin); 484 485 let mut stdout = rustfmt.stdout.take().unwrap(); 486 let mut data = vec![]; 487 stdout.read_to_end(&mut data)?; 488 489 let status = rustfmt.wait()?; 490 if !status.success() { 491 return Err(std::io::Error::new( 492 std::io::ErrorKind::Other, 493 format!("`rustfmt` exited with status {}", status), 494 )); 495 } 496 497 Ok(String::from_utf8(data).expect("rustfmt always writs utf-8 to stdout")) 498 } 499 } 500