|
|
@ -16,6 +16,7 @@ |
|
|
|
|
|
|
|
use cranelift_codegen_meta as meta; |
|
|
|
|
|
|
|
use sha2::{Digest, Sha512}; |
|
|
|
use std::env; |
|
|
|
use std::io::Read; |
|
|
|
use std::process; |
|
|
@ -77,17 +78,16 @@ fn main() { |
|
|
|
.unwrap() |
|
|
|
} |
|
|
|
|
|
|
|
#[cfg(feature = "rebuild-isle")] |
|
|
|
// The "Meta deterministic check" CI job runs this build script N
|
|
|
|
// times to ensure it produces the same output
|
|
|
|
// consistently. However, it runs the script in a fresh directory,
|
|
|
|
// without any of the source tree present; this breaks our
|
|
|
|
// manifest check (we need the ISLE source to be present). To keep
|
|
|
|
// things simple, we just disable all ISLE-related logic for this
|
|
|
|
// specific CI job.
|
|
|
|
#[cfg(not(feature = "completely-skip-isle-for-ci-deterministic-check"))] |
|
|
|
{ |
|
|
|
if let Err(e) = rebuild_isle(crate_dir) { |
|
|
|
eprintln!("Error building ISLE files: {:?}", e); |
|
|
|
let mut source = e.source(); |
|
|
|
while let Some(e) = source { |
|
|
|
eprintln!("{:?}", e); |
|
|
|
source = e.source(); |
|
|
|
} |
|
|
|
std::process::abort(); |
|
|
|
} |
|
|
|
maybe_rebuild_isle(crate_dir).expect("Unhandled failure in ISLE rebuild"); |
|
|
|
} |
|
|
|
|
|
|
|
let pkg_version = env::var("CARGO_PKG_VERSION").unwrap(); |
|
|
@ -127,29 +127,87 @@ fn main() { |
|
|
|
.unwrap(); |
|
|
|
} |
|
|
|
|
|
|
|
/// Rebuild ISLE DSL source text into generated Rust code.
|
|
|
|
/// Strip the current directory from the file paths, because `islec`
|
|
|
|
/// includes them in the generated source, and this helps us maintain
|
|
|
|
/// deterministic builds that don't include those local file paths.
|
|
|
|
fn make_isle_source_path_relative( |
|
|
|
cur_dir: &std::path::PathBuf, |
|
|
|
filename: std::path::PathBuf, |
|
|
|
) -> std::path::PathBuf { |
|
|
|
if let Ok(suffix) = filename.strip_prefix(&cur_dir) { |
|
|
|
suffix.to_path_buf() |
|
|
|
} else { |
|
|
|
filename |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// A list of compilations (transformations from ISLE source to
|
|
|
|
/// generated Rust source) that exist in the repository.
|
|
|
|
///
|
|
|
|
/// NB: This must happen *after* the `cranelift-codegen-meta` functions, since
|
|
|
|
/// it consumes files generated by them.
|
|
|
|
#[cfg(feature = "rebuild-isle")] |
|
|
|
fn rebuild_isle(crate_dir: &std::path::Path) -> Result<(), Box<dyn std::error::Error + 'static>> { |
|
|
|
use std::sync::Once; |
|
|
|
static SET_MIETTE_HOOK: Once = Once::new(); |
|
|
|
SET_MIETTE_HOOK.call_once(|| { |
|
|
|
let _ = miette::set_hook(Box::new(|_| { |
|
|
|
Box::new( |
|
|
|
miette::MietteHandlerOpts::new() |
|
|
|
// This is necessary for `miette` to properly display errors
|
|
|
|
// until https://github.com/zkat/miette/issues/93 is fixed.
|
|
|
|
.force_graphical(true) |
|
|
|
.build(), |
|
|
|
) |
|
|
|
})); |
|
|
|
}); |
|
|
|
/// This list is used either to regenerate the Rust source in-tree (if
|
|
|
|
/// the `rebuild-isle` feature is enabled), or to verify that the ISLE
|
|
|
|
/// source in-tree corresponds to the ISLE source that was last used
|
|
|
|
/// to rebuild the Rust source (if the `rebuild-isle` feature is not
|
|
|
|
/// enabled).
|
|
|
|
#[derive(Clone, Debug)] |
|
|
|
struct IsleCompilations { |
|
|
|
items: Vec<IsleCompilation>, |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Clone, Debug)] |
|
|
|
struct IsleCompilation { |
|
|
|
output: std::path::PathBuf, |
|
|
|
inputs: Vec<std::path::PathBuf>, |
|
|
|
} |
|
|
|
|
|
|
|
impl IsleCompilation { |
|
|
|
/// Compute the manifest filename for the given generated Rust file.
|
|
|
|
fn manifest_filename(&self) -> std::path::PathBuf { |
|
|
|
self.output.with_extension("manifest") |
|
|
|
} |
|
|
|
|
|
|
|
/// Compute the content of the source manifest for all ISLE source
|
|
|
|
/// files that go into the compilation of one Rust file.
|
|
|
|
///
|
|
|
|
/// We store this alongside the `<generated_filename>.rs` file as
|
|
|
|
/// `<generated_filename>.manifest` and use it to verify that a
|
|
|
|
/// rebuild was done if necessary.
|
|
|
|
fn compute_manifest(&self) -> Result<String, Box<dyn std::error::Error + 'static>> { |
|
|
|
use std::fmt::Write; |
|
|
|
|
|
|
|
let mut manifest = String::new(); |
|
|
|
|
|
|
|
for filename in &self.inputs { |
|
|
|
// Our source must be valid UTF-8 for this to work, else user
|
|
|
|
// will get an error on build. This is not expected to be an
|
|
|
|
// issue.
|
|
|
|
let content = std::fs::read_to_string(filename)?; |
|
|
|
// On Windows, source is checked out with line-endings changed
|
|
|
|
// to `\r\n`; canonicalize the source that we hash to
|
|
|
|
// Unix-style (`\n`) so hashes will match.
|
|
|
|
let content = content.replace("\r\n", "\n"); |
|
|
|
// One line in the manifest: <filename> <sha-512 hash>.
|
|
|
|
let mut hasher = Sha512::default(); |
|
|
|
hasher.update(content.as_bytes()); |
|
|
|
let filename = format!("{}", filename.display()).replace("\\", "/"); |
|
|
|
writeln!(&mut manifest, "{} {:x}", filename, hasher.finalize())?; |
|
|
|
} |
|
|
|
|
|
|
|
Ok(manifest) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// Construct the list of compilations (transformations from ISLE
|
|
|
|
/// source to generated Rust source) that exist in the repository.
|
|
|
|
fn get_isle_compilations(crate_dir: &std::path::Path) -> Result<IsleCompilations, std::io::Error> { |
|
|
|
let cur_dir = std::env::current_dir()?; |
|
|
|
|
|
|
|
let clif_isle = crate_dir.join("src").join("clif.isle"); |
|
|
|
let prelude_isle = crate_dir.join("src").join("prelude.isle"); |
|
|
|
let src_isa_x64 = crate_dir.join("src").join("isa").join("x64"); |
|
|
|
let clif_isle = |
|
|
|
make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("clif.isle")); |
|
|
|
let prelude_isle = |
|
|
|
make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("prelude.isle")); |
|
|
|
let src_isa_x64 = |
|
|
|
make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("x64")); |
|
|
|
|
|
|
|
// This is a set of ISLE compilation units.
|
|
|
|
//
|
|
|
@ -160,75 +218,206 @@ fn rebuild_isle(crate_dir: &std::path::Path) -> Result<(), Box<dyn std::error::E |
|
|
|
// There should be one entry for each backend that uses ISLE for lowering,
|
|
|
|
// and if/when we replace our peephole optimization passes with ISLE, there
|
|
|
|
// should be an entry for each of those as well.
|
|
|
|
let isle_compilations = vec![ |
|
|
|
// The x86-64 instruction selector.
|
|
|
|
( |
|
|
|
src_isa_x64 |
|
|
|
.join("lower") |
|
|
|
.join("isle") |
|
|
|
.join("generated_code.rs"), |
|
|
|
vec![ |
|
|
|
clif_isle, |
|
|
|
prelude_isle, |
|
|
|
src_isa_x64.join("inst.isle"), |
|
|
|
src_isa_x64.join("lower.isle"), |
|
|
|
], |
|
|
|
), |
|
|
|
]; |
|
|
|
Ok(IsleCompilations { |
|
|
|
items: vec![ |
|
|
|
// The x86-64 instruction selector.
|
|
|
|
IsleCompilation { |
|
|
|
output: src_isa_x64 |
|
|
|
.join("lower") |
|
|
|
.join("isle") |
|
|
|
.join("generated_code.rs"), |
|
|
|
inputs: vec![ |
|
|
|
clif_isle, |
|
|
|
prelude_isle, |
|
|
|
src_isa_x64.join("inst.isle"), |
|
|
|
src_isa_x64.join("lower.isle"), |
|
|
|
], |
|
|
|
}, |
|
|
|
], |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
let cur_dir = std::env::current_dir()?; |
|
|
|
for (out_file, mut files) in isle_compilations { |
|
|
|
for file in files.iter_mut() { |
|
|
|
/// Check the manifest for the ISLE generated code, which documents
|
|
|
|
/// what ISLE source went into generating the Rust, and if there is a
|
|
|
|
/// mismatch, either invoke the ISLE compiler (if we have the
|
|
|
|
/// `rebuild-isle` feature) or exit with an error (if not).
|
|
|
|
///
|
|
|
|
/// We do this by computing a hash of the ISLE source and checking it
|
|
|
|
/// against a "manifest" that is also checked into git, alongside the
|
|
|
|
/// generated Rust.
|
|
|
|
///
|
|
|
|
/// (Why not include the `rebuild-isle` feature by default? Because
|
|
|
|
/// the build process must not modify the checked-in source by
|
|
|
|
/// default; any checked-in source is a human-managed bit of data, and
|
|
|
|
/// we can only act as an agent of the human developer when explicitly
|
|
|
|
/// requested to do so. This manifest check is a middle ground that
|
|
|
|
/// ensures this explicit control while also avoiding the easy footgun
|
|
|
|
/// of "I changed the ISLE, why isn't the compiler updated?!".)
|
|
|
|
fn maybe_rebuild_isle( |
|
|
|
crate_dir: &std::path::Path, |
|
|
|
) -> Result<(), Box<dyn std::error::Error + 'static>> { |
|
|
|
let isle_compilations = get_isle_compilations(crate_dir)?; |
|
|
|
let mut rebuild_compilations = vec![]; |
|
|
|
|
|
|
|
for compilation in &isle_compilations.items { |
|
|
|
for file in &compilation.inputs { |
|
|
|
println!("cargo:rerun-if-changed={}", file.display()); |
|
|
|
} |
|
|
|
|
|
|
|
// Strip the current directory from the file paths, because `islec`
|
|
|
|
// includes them in the generated source, and this helps us maintain
|
|
|
|
// deterministic builds that don't include those local file paths.
|
|
|
|
if let Ok(suffix) = file.strip_prefix(&cur_dir) { |
|
|
|
*file = suffix.to_path_buf(); |
|
|
|
let manifest = std::fs::read_to_string(compilation.manifest_filename())?; |
|
|
|
// Canonicalize Windows line-endings into Unix line-endings in
|
|
|
|
// the manifest text itself.
|
|
|
|
let manifest = manifest.replace("\r\n", "\n"); |
|
|
|
let expected_manifest = compilation.compute_manifest()?.replace("\r\n", "\n"); |
|
|
|
if manifest != expected_manifest { |
|
|
|
rebuild_compilations.push((compilation, expected_manifest)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[cfg(feature = "rebuild-isle")] |
|
|
|
{ |
|
|
|
if !rebuild_compilations.is_empty() { |
|
|
|
set_miette_hook(); |
|
|
|
} |
|
|
|
let mut had_error = false; |
|
|
|
for (compilation, expected_manifest) in rebuild_compilations { |
|
|
|
if let Err(e) = rebuild_isle(compilation, &expected_manifest) { |
|
|
|
eprintln!("Error building ISLE files: {:?}", e); |
|
|
|
let mut source = e.source(); |
|
|
|
while let Some(e) = source { |
|
|
|
eprintln!("{:?}", e); |
|
|
|
source = e.source(); |
|
|
|
} |
|
|
|
had_error = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let code = (|| { |
|
|
|
let lexer = isle::lexer::Lexer::from_files(files)?; |
|
|
|
let defs = isle::parser::parse(lexer)?; |
|
|
|
isle::compile::compile(&defs) |
|
|
|
})() |
|
|
|
.map_err(|e| { |
|
|
|
// Make sure to include the source snippets location info along with
|
|
|
|
// the error messages.
|
|
|
|
if had_error { |
|
|
|
std::process::exit(1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[cfg(not(feature = "rebuild-isle"))] |
|
|
|
{ |
|
|
|
if !rebuild_compilations.is_empty() { |
|
|
|
for (compilation, _) in rebuild_compilations { |
|
|
|
eprintln!(""); |
|
|
|
eprintln!( |
|
|
|
"Error: the ISLE source files that resulted in the generated Rust source" |
|
|
|
); |
|
|
|
eprintln!(""); |
|
|
|
eprintln!(" * {}", compilation.output.display()); |
|
|
|
eprintln!(""); |
|
|
|
eprintln!( |
|
|
|
"have changed but the generated source was not rebuilt! These ISLE source" |
|
|
|
); |
|
|
|
eprintln!("files are:"); |
|
|
|
eprintln!(""); |
|
|
|
for file in &compilation.inputs { |
|
|
|
eprintln!(" * {}", file.display()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let report = miette::Report::new(e); |
|
|
|
return DebugReport(report); |
|
|
|
eprintln!(""); |
|
|
|
eprintln!("Please add `--features rebuild-isle` to your `cargo build` command"); |
|
|
|
eprintln!("if you wish to rebuild the generated source, then include these changes"); |
|
|
|
eprintln!("in any git commits you make that include the changes to the ISLE."); |
|
|
|
eprintln!(""); |
|
|
|
eprintln!("For example:"); |
|
|
|
eprintln!(""); |
|
|
|
eprintln!(" $ cargo build -p cranelift-codegen --features rebuild-isle"); |
|
|
|
eprintln!(""); |
|
|
|
eprintln!("(This build script cannot do this for you by default because we cannot"); |
|
|
|
eprintln!("modify checked-into-git source without your explicit opt-in.)"); |
|
|
|
eprintln!(""); |
|
|
|
std::process::exit(1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
struct DebugReport(miette::Report); |
|
|
|
Ok(()) |
|
|
|
} |
|
|
|
|
|
|
|
impl std::fmt::Display for DebugReport { |
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
|
|
|
self.0.handler().debug(&*self.0, f) |
|
|
|
} |
|
|
|
#[cfg(feature = "rebuild-isle")] |
|
|
|
fn set_miette_hook() { |
|
|
|
use std::sync::Once; |
|
|
|
static SET_MIETTE_HOOK: Once = Once::new(); |
|
|
|
SET_MIETTE_HOOK.call_once(|| { |
|
|
|
let _ = miette::set_hook(Box::new(|_| { |
|
|
|
Box::new( |
|
|
|
miette::MietteHandlerOpts::new() |
|
|
|
// This is necessary for `miette` to properly display errors
|
|
|
|
// until https://github.com/zkat/miette/issues/93 is fixed.
|
|
|
|
.force_graphical(true) |
|
|
|
.build(), |
|
|
|
) |
|
|
|
})); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
/// Rebuild ISLE DSL source text into generated Rust code.
|
|
|
|
///
|
|
|
|
/// NB: This must happen *after* the `cranelift-codegen-meta` functions, since
|
|
|
|
/// it consumes files generated by them.
|
|
|
|
#[cfg(feature = "rebuild-isle")] |
|
|
|
fn rebuild_isle( |
|
|
|
compilation: &IsleCompilation, |
|
|
|
manifest: &str, |
|
|
|
) -> Result<(), Box<dyn std::error::Error + 'static>> { |
|
|
|
// First, remove the manifest, if any; we will recreate it
|
|
|
|
// below if the compilation is successful. Ignore error if no
|
|
|
|
// manifest was present.
|
|
|
|
let manifest_filename = compilation.manifest_filename(); |
|
|
|
let _ = std::fs::remove_file(&manifest_filename); |
|
|
|
|
|
|
|
let code = (|| { |
|
|
|
let lexer = isle::lexer::Lexer::from_files(&compilation.inputs[..])?; |
|
|
|
let defs = isle::parser::parse(lexer)?; |
|
|
|
isle::compile::compile(&defs) |
|
|
|
})() |
|
|
|
.map_err(|e| { |
|
|
|
// Make sure to include the source snippets location info along with
|
|
|
|
// the error messages.
|
|
|
|
|
|
|
|
let report = miette::Report::new(e); |
|
|
|
return DebugReport(report); |
|
|
|
|
|
|
|
struct DebugReport(miette::Report); |
|
|
|
|
|
|
|
impl std::fmt::Display for DebugReport { |
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
|
|
|
self.0.handler().debug(&*self.0, f) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
impl std::fmt::Debug for DebugReport { |
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
|
|
|
std::fmt::Display::fmt(self, f) |
|
|
|
} |
|
|
|
impl std::fmt::Debug for DebugReport { |
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
|
|
|
std::fmt::Display::fmt(self, f) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
impl std::error::Error for DebugReport {} |
|
|
|
})?; |
|
|
|
impl std::error::Error for DebugReport {} |
|
|
|
})?; |
|
|
|
|
|
|
|
let code = rustfmt(&code).unwrap_or_else(|e| { |
|
|
|
println!( |
|
|
|
"cargo:warning=Failed to run `rustfmt` on ISLE-generated code: {:?}", |
|
|
|
e |
|
|
|
); |
|
|
|
code |
|
|
|
}); |
|
|
|
let code = rustfmt(&code).unwrap_or_else(|e| { |
|
|
|
println!( |
|
|
|
"cargo:warning=Failed to run `rustfmt` on ISLE-generated code: {:?}", |
|
|
|
e |
|
|
|
); |
|
|
|
code |
|
|
|
}); |
|
|
|
|
|
|
|
println!("Writing ISLE-generated Rust code to {}", out_file.display()); |
|
|
|
std::fs::write(out_file, code)?; |
|
|
|
} |
|
|
|
println!( |
|
|
|
"Writing ISLE-generated Rust code to {}", |
|
|
|
compilation.output.display() |
|
|
|
); |
|
|
|
std::fs::write(&compilation.output, code)?; |
|
|
|
|
|
|
|
// Write the manifest so that, in the default build configuration
|
|
|
|
// without the `rebuild-isle` feature, we can at least verify that
|
|
|
|
// no changes were made that will not be picked up. Note that we
|
|
|
|
// only write this *after* we write the source above, so no
|
|
|
|
// manifest is produced if there was an error.
|
|
|
|
std::fs::write(&manifest_filename, manifest)?; |
|
|
|
|
|
|
|
return Ok(()); |
|
|
|
|
|
|
|