| 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"); |
| } |