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