blob: 8d437c20a0926a8b194aac948b849c951ecb1d4f [file] [log] [blame]
use std::path::{Path, PathBuf};
use std::str::FromStr;
// Build script for running Miri with GenMC.
// Check out doc/genmc.md for more info.
/// Path where the downloaded GenMC repository will be stored (relative to the `genmc-sys` directory).
/// Note that this directory is *not* cleaned up automatically by `cargo clean`.
const GENMC_DOWNLOAD_PATH: &str = "./genmc-src/";
/// Name of the library of the GenMC model checker.
const GENMC_MODEL_CHECKER: &str = "genmc_lib";
/// Path where the `cxx_bridge!` macro is used to define the Rust-C++ interface.
const RUST_CXX_BRIDGE_FILE_PATH: &str = "src/lib.rs";
/// The profile with which to build GenMC.
const GENMC_CMAKE_PROFILE: &str = "RelWithDebInfo";
mod downloading {
use std::path::PathBuf;
use std::str::FromStr;
use git2::{Commit, Oid, Remote, Repository, StatusOptions};
use super::GENMC_DOWNLOAD_PATH;
/// The GenMC repository the we get our commit from.
pub(crate) const GENMC_GITHUB_URL: &str = "https://gitlab.inf.ethz.ch/public-plf/genmc.git";
/// The GenMC commit we depend on. It must be available on the specified GenMC repository.
pub(crate) const GENMC_COMMIT: &str = "af9cc9ccd5d412b16defc35dbf36571c63a19c76";
/// Ensure that a local GenMC repo is present and set to the correct commit.
/// Return the path of the GenMC repo and whether the checked out commit was changed.
pub(crate) fn download_genmc() -> (PathBuf, bool) {
let Ok(genmc_download_path) = PathBuf::from_str(GENMC_DOWNLOAD_PATH);
let commit_oid = Oid::from_str(GENMC_COMMIT).expect("Commit should be valid.");
match Repository::open(&genmc_download_path) {
Ok(repo) => {
assert_repo_unmodified(&repo);
if let Ok(head) = repo.head()
&& let Ok(head_commit) = head.peel_to_commit()
&& head_commit.id() == commit_oid
{
// Fast path: The expected commit is already checked out.
return (genmc_download_path, false);
}
// Check if the local repository already contains the commit we need, download it otherwise.
let commit = update_local_repo(&repo, commit_oid);
checkout_commit(&repo, &commit);
}
Err(_) => {
let repo = clone_remote_repo(&genmc_download_path);
let Ok(commit) = repo.find_commit(commit_oid) else {
panic!(
"Cloned GenMC repository does not contain required commit '{GENMC_COMMIT}'"
);
};
checkout_commit(&repo, &commit);
}
};
(genmc_download_path, true)
}
fn get_remote(repo: &Repository) -> Remote<'_> {
let remote = repo.find_remote("origin").unwrap_or_else(|e| {
panic!(
"Could not load commit ({GENMC_COMMIT}) from remote repository '{GENMC_GITHUB_URL}'. Error: {e}"
);
});
// Ensure that the correct remote URL is set.
let remote_url = remote.url();
if let Some(remote_url) = remote_url
&& remote_url == GENMC_GITHUB_URL
{
return remote;
}
// Update remote URL.
println!(
"cargo::warning=GenMC repository remote URL has changed from '{}' to '{GENMC_GITHUB_URL}'",
remote_url.unwrap_or_default()
);
repo.remote_set_url("origin", GENMC_GITHUB_URL)
.expect("cannot rename url of remote 'origin'");
// Reacquire the `Remote`, since `remote_set_url` doesn't update Remote objects already in memory.
repo.find_remote("origin").unwrap()
}
// Check if the required commit exists already, otherwise try fetching it.
fn update_local_repo(repo: &Repository, commit_oid: Oid) -> Commit<'_> {
repo.find_commit(commit_oid).unwrap_or_else(|_find_error| {
println!("GenMC repository at path '{GENMC_DOWNLOAD_PATH}' does not contain commit '{GENMC_COMMIT}'.");
// The commit is not in the checkout. Try `git fetch` and hope that we find the commit then.
let mut remote = get_remote(repo);
remote.fetch(&[GENMC_COMMIT], None, None).expect("Failed to fetch from remote.");
repo.find_commit(commit_oid)
.expect("Remote repository should contain expected commit")
})
}
fn clone_remote_repo(genmc_download_path: &PathBuf) -> Repository {
Repository::clone(GENMC_GITHUB_URL, &genmc_download_path).unwrap_or_else(|e| {
panic!("Cannot clone GenMC repo from '{GENMC_GITHUB_URL}': {e:?}");
})
}
/// Set the state of the repo to a specific commit
fn checkout_commit(repo: &Repository, commit: &Commit<'_>) {
repo.checkout_tree(commit.as_object(), None).expect("Failed to checkout");
repo.set_head_detached(commit.id()).expect("Failed to set HEAD");
println!("Successfully set checked out commit {commit:?}");
}
/// Check that the downloaded repository is unmodified.
/// If it is modified, explain that it shouldn't be, and hint at how to do local development with GenMC.
/// We don't overwrite any changes made to the directory, to prevent data loss.
fn assert_repo_unmodified(repo: &Repository) {
let statuses = repo
.statuses(Some(
StatusOptions::new()
.include_untracked(true)
.include_ignored(false)
.include_unmodified(false),
))
.expect("should be able to get repository status");
if statuses.is_empty() {
return;
}
panic!(
"Downloaded GenMC repository at path '{GENMC_DOWNLOAD_PATH}' has been modified. Please undo any changes made, or delete the '{GENMC_DOWNLOAD_PATH}' directory to have it downloaded again.\n\
HINT: For local development, set the environment variable 'GENMC_SRC_PATH' to the path of a GenMC repository."
);
}
}
// FIXME(genmc,llvm): Remove once the LLVM dependency of the GenMC model checker is removed.
/// The linked LLVM version is in the generated `config.h`` file, which we parse and use to link to LLVM.
/// Returns c++ compiler definitions required for building with/including LLVM, and the include path for LLVM headers.
fn link_to_llvm(config_file: &Path) -> (String, String) {
/// Search a string for a line matching `//@VARIABLE_NAME: VARIABLE CONTENT`
fn extract_value<'a>(input: &'a str, name: &str) -> Option<&'a str> {
input
.lines()
.find_map(|line| line.strip_prefix("//@")?.strip_prefix(name)?.strip_prefix(": "))
}
let file_content = std::fs::read_to_string(&config_file).unwrap_or_else(|err| {
panic!("GenMC config file ({}) should exist, but got errror {err:?}", config_file.display())
});
let llvm_definitions = extract_value(&file_content, "LLVM_DEFINITIONS")
.expect("Config file should contain LLVM_DEFINITIONS");
let llvm_include_dirs = extract_value(&file_content, "LLVM_INCLUDE_DIRS")
.expect("Config file should contain LLVM_INCLUDE_DIRS");
let llvm_library_dir = extract_value(&file_content, "LLVM_LIBRARY_DIR")
.expect("Config file should contain LLVM_LIBRARY_DIR");
let llvm_config_path = extract_value(&file_content, "LLVM_CONFIG_PATH")
.expect("Config file should contain LLVM_CONFIG_PATH");
// Add linker search path.
let lib_dir = PathBuf::from_str(llvm_library_dir).unwrap();
println!("cargo::rustc-link-search=native={}", lib_dir.display());
// Add libraries to link.
let output = std::process::Command::new(llvm_config_path)
.arg("--libs") // Print the libraries to link to (space-separated list)
.output()
.expect("failed to execute llvm-config");
let llvm_link_libs =
String::try_from(output.stdout).expect("llvm-config output should be a valid string");
for link_lib in llvm_link_libs.trim().split(" ") {
let link_lib =
link_lib.strip_prefix("-l").expect("Linker parameter should start with \"-l\"");
println!("cargo::rustc-link-lib=dylib={link_lib}");
}
(llvm_definitions.to_string(), llvm_include_dirs.to_string())
}
/// Build the GenMC model checker library and the Rust-C++ interop library with cxx.rs
fn compile_cpp_dependencies(genmc_path: &Path, always_configure: bool) {
// Give each step a separate build directory to prevent interference.
let out_dir = PathBuf::from(std::env::var("OUT_DIR").as_deref().unwrap());
let genmc_build_dir = out_dir.join("genmc");
let interface_build_dir = out_dir.join("miri_genmc");
// Part 1:
// Compile the GenMC library using cmake.
// FIXME(genmc,cargo): Switch to using `CARGO_CFG_DEBUG_ASSERTIONS` once https://github.com/rust-lang/cargo/issues/15760 is completed.
// Enable/disable additional debug checks, prints and options for GenMC, based on the Rust profile (debug/release)
let enable_genmc_debug = matches!(std::env::var("PROFILE").as_deref().unwrap(), "debug");
let mut config = cmake::Config::new(genmc_path);
config
.always_configure(always_configure) // We force running the configure step when the GenMC commit changed.
.out_dir(genmc_build_dir)
.profile(GENMC_CMAKE_PROFILE)
.define("GENMC_DEBUG", if enable_genmc_debug { "ON" } else { "OFF" });
// The actual compilation happens here:
let genmc_install_dir = config.build();
// Add the model checker library to be linked and tell rustc where to find it:
let cmake_lib_dir = genmc_install_dir.join("lib").join("genmc");
println!("cargo::rustc-link-search=native={}", cmake_lib_dir.display());
println!("cargo::rustc-link-lib=static={GENMC_MODEL_CHECKER}");
// FIXME(genmc,llvm): Remove once the LLVM dependency of the GenMC model checker is removed.
let config_file = genmc_install_dir.join("include").join("genmc").join("config.h");
let (llvm_definitions, llvm_include_dirs) = link_to_llvm(&config_file);
// Part 2:
// Compile the cxx_bridge (the link between the Rust and C++ code).
let genmc_include_dir = genmc_install_dir.join("include").join("genmc");
// FIXME(genmc,llvm): remove once LLVM dependency is removed.
// These definitions are parsed into a cmake list and then printed to the config.h file, so they are ';' separated.
let definitions = llvm_definitions.split(";");
let cpp_files = [
"./cpp/src/MiriInterface/EventHandling.cpp",
"./cpp/src/MiriInterface/Exploration.cpp",
"./cpp/src/MiriInterface/Setup.cpp",
"./cpp/src/MiriInterface/ThreadManagement.cpp",
];
let mut bridge = cxx_build::bridge("src/lib.rs");
// FIXME(genmc,cmake): Remove once the GenMC debug setting is available in the config.h file.
if enable_genmc_debug {
bridge.define("ENABLE_GENMC_DEBUG", None);
}
for definition in definitions {
bridge.flag(definition);
}
bridge
.opt_level(2)
.debug(true) // Same settings that GenMC uses (default for cmake `RelWithDebInfo`)
.warnings(false) // NOTE: enabling this produces a lot of warnings.
.std("c++23")
.include(genmc_include_dir)
.include(llvm_include_dirs)
.include("./cpp/include")
.files(&cpp_files)
.out_dir(interface_build_dir)
.compile("genmc_interop");
// Link the Rust-C++ interface library generated by cxx_build:
println!("cargo::rustc-link-lib=static=genmc_interop");
}
fn main() {
// Select which path to use for the GenMC repo:
let (genmc_path, always_configure) = if let Some(genmc_src_path) = option_env!("GENMC_SRC_PATH")
{
let genmc_src_path =
PathBuf::from_str(&genmc_src_path).expect("GENMC_SRC_PATH should contain a valid path");
assert!(
genmc_src_path.exists(),
"GENMC_SRC_PATH={} does not exist!",
genmc_src_path.display()
);
// Rebuild files in the given path change.
println!("cargo::rerun-if-changed={}", genmc_src_path.display());
// We disable `always_configure` when working with a local repository,
// since it increases compile times when working on `genmc-sys`.
(genmc_src_path, false)
} else {
// Download GenMC if required and ensure that the correct commit is checked out.
// If anything changed in the downloaded repository (e.g., the commit),
// we set `always_configure` to ensure there are no weird configs from previous builds.
downloading::download_genmc()
};
// Build all required components:
compile_cpp_dependencies(&genmc_path, always_configure);
// Only rebuild if anything changes:
// Note that we don't add the downloaded GenMC repo, since that should never be modified
// manually. Adding that path here would also trigger an unnecessary rebuild after the repo is
// cloned (since cargo detects that as a file modification).
println!("cargo::rerun-if-changed={RUST_CXX_BRIDGE_FILE_PATH}");
println!("cargo::rerun-if-changed=./src");
println!("cargo::rerun-if-changed=./cpp");
}