simics-build-utils 0.2.6

Intel® Simics® Simulator build and linking utilities
// Copyright (C) 2024 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

use anyhow::{anyhow, ensure, Result};
use ispm_wrapper::ispm::{self, GlobalOptions};
use simics_python_utils::discover_python_environment_from_base;
use std::{
    env::var,
    fs::read_dir,
    path::{Path, PathBuf},
};
use versions;

/// Get the only subdirectory of a directory, if only one exists. If zero or more than one subdirectories
/// exist, returns an error
pub fn subdir<P>(dir: P) -> Result<PathBuf>
where
    P: AsRef<Path>,
{
    let subdirs = read_dir(dir)?
        .filter_map(|p| p.ok())
        .map(|p| p.path())
        .filter(|p| p.is_dir())
        .collect::<Vec<_>>();
    ensure!(
        subdirs.len() == 1,
        "Expected exactly 1 sub-directory, found {}",
        subdirs.len()
    );

    subdirs
        .first()
        .cloned()
        .ok_or_else(|| anyhow!("No sub-directories found"))
}

/// Emit expected CFG directives for check-cfg feature tests
pub fn emit_expected_cfg_directives() {
    let values = (600_000..799_999)
        .map(|v| format!(r#""{}.{}.{}""#, v / 100_000, v / 1_000 % 100, v % 1_000))
        .collect::<Vec<_>>()
        .join(", ");
    println!(
        "cargo:rustc-check-cfg=cfg(simics_version, values({}))",
        values
    );
    println!(r#"cargo:rustc-check-cfg=cfg(simics_version, values("6", "7"))"#,);
}

/// Emit CFG directives for the version of the Simics API being compiled against.
///
/// For example, simics_version = "6.0.185" and simics_version = "6". Both a full triple version
/// and a major version directive is emitted.
///
/// This function can be used in the `build.rs` script of a crate that depends on the `simics`
/// crate to conditionally enable experimental features in its own code.
pub fn emit_cfg_directives() -> Result<()> {
    // Set configurations to conditionally enable experimental features that aren't
    // compatible with all supported SIMICS versions, based on the SIMICS version of the
    // low level bindings.

    emit_expected_cfg_directives();

    // Detect Simics version using the same logic as emit_link_info()
    let simics_version = if let Ok(simics_base) = var("SIMICS_BASE") {
        // Extract version from SIMICS_BASE path
        let base_path = PathBuf::from(simics_base);
        if let Some(dir_name) = base_path.file_name().and_then(|n| n.to_str()) {
            if let Some(version_part) = dir_name.strip_prefix("simics-") {
                version_part.to_string()
            } else {
                anyhow::bail!(
                    "Could not extract version from SIMICS_BASE path: {}",
                    base_path.display()
                );
            }
        } else {
            anyhow::bail!(
                "Could not get directory name from SIMICS_BASE path: {}",
                base_path.display()
            );
        }
    } else {
        // Use ISPM to find base package version
        let mut packages = ispm::packages::list(&GlobalOptions::default())?;
        packages.sort();

        let Some(installed) = packages.installed_packages.as_ref() else {
            anyhow::bail!("No SIMICS_BASE variable set and did not get any installed packages");
        };
        let Some(base) = installed.iter().find(|p| p.package_number == 1000) else {
            anyhow::bail!(
                "No SIMICS_BASE variable set and did not find a package with package number 1000"
            );
        };
        base.version.clone()
    };

    // Parse version using the versions crate for proper handling
    let simics_api_version = versions::Versioning::new(&simics_version)
        .ok_or_else(|| anyhow!("Invalid version {}", simics_version))?;

    // Emit full version cfg directive (e.g., simics_version = "6.0.185")
    println!(r#"cargo:rustc-cfg=simics_version="{}""#, simics_api_version);

    // Emit major version cfg directive (e.g., simics_version = "6")
    println!(
        r#"cargo:rustc-cfg=simics_version="{}""#,
        simics_api_version
            .to_string()
            .split('.')
            .next()
            .ok_or_else(|| anyhow!("No major version found"))?
    );

    Ok(())
}

pub fn emit_link_info() -> Result<()> {
    #[cfg(unix)]
    const HOST_DIRNAME: &str = "linux64";

    #[cfg(not(unix))]
    const HOST_DIRNAME: &'static str = "win64";

    let base_dir_path = if let Ok(simics_base) = var("SIMICS_BASE") {
        PathBuf::from(simics_base)
    } else {
        println!("cargo:warning=No SIMICS_BASE environment variable found, using ispm to find installed packages and using latest base version");

        let mut packages = ispm::packages::list(&GlobalOptions::default())?;

        packages.sort();

        let Some(installed) = packages.installed_packages.as_ref() else {
            anyhow::bail!("No SIMICS_BASE variable set and did not get any installed packages");
        };
        let Some(base) = installed.iter().find(|p| p.package_number == 1000) else {
            anyhow::bail!(
                "No SIMICS_BASE variable set and did not find a package with package number 1000"
            );
        };
        println!("cargo:warning=Using Simics base version {}", base.version);
        let base_path = base
            .paths
            .first()
            .ok_or_else(|| anyhow!("No paths found for package with package number 1000"))?
            .clone();
        base_path
    };

    #[cfg(unix)]
    {
        // Link `libsimics-common.so`, `libvtutils.so`, and `libpythonX.XX.so.X.X` if they exist
        let bin_dir = base_dir_path
            .join(HOST_DIRNAME)
            .join("bin")
            .canonicalize()?;
        let libsimics_common = bin_dir.join("libsimics-common.so").canonicalize()?;

        let libvtutils = bin_dir.join("libvtutils.so").canonicalize()?;

        // Discover Python environment using unified detection
        let python_env = discover_python_environment_from_base(&base_dir_path)?;
        println!(
            "cargo:warning=Using Python environment for linking: {}",
            python_env
        );

        println!(
            "cargo:rustc-link-lib=dylib:+verbatim={}",
            libsimics_common
                .file_name()
                .ok_or_else(|| anyhow!("No file name found for {}", libsimics_common.display()))?
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?
        );
        println!(
            "cargo:rustc-link-lib=dylib:+verbatim={}",
            libvtutils
                .file_name()
                .ok_or_else(|| anyhow!("No file name found for {}", libvtutils.display()))?
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?
        );
        println!(
            "cargo:rustc-link-lib=dylib:+verbatim={}",
            python_env.lib_filename()?
        );
        println!(
            "cargo:rustc-link-search=native={}",
            bin_dir
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?
        );
        println!(
            "cargo:rustc-link-search=native={}",
            python_env
                .lib_dir
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?
        );
        let ld_library_path = [
            bin_dir
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?,
            python_env
                .lib_dir
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?,
        ]
        .join(":");

        println!("cargo:rustc-env=LD_LIBRARY_PATH={}", ld_library_path);
    }

    #[cfg(windows)]
    {
        // Link `libsimics-common.dll`, `libvtutils.dll`, and `pythonX.XX.dll`
        let bin_dir = base_dir_path
            .join(HOST_DIRNAME)
            .join("bin")
            .canonicalize()
            .map_err(|e| {
                anyhow!(
                    "Could not find bin dir {:?}: {}",
                    base_dir_path.join(HOST_DIRNAME).join("bin"),
                    e
                )
            })?;

        let libsimics_common = bin_dir
            .join("libsimics-common.dll")
            .canonicalize()
            .map_err(|e| {
                anyhow!(
                    "Could not find libsimics-common {:?}: {}",
                    bin_dir.join("libsimics-common.dll"),
                    e
                )
            })?;

        let libvtutils = bin_dir.join("libvtutils.dll").canonicalize().map_err(|e| {
            anyhow!(
                "Could not find libvtutils {:?}: {}",
                bin_dir.join("libvtutils.dll"),
                e
            )
        })?;

        // Discover Python environment using unified detection
        let python_env = discover_python_environment_from_base(&base_dir_path)?;
        println!(
            "cargo:warning=Using Python environment for linking: {}",
            python_env
        );

        println!(
            "cargo:rustc-link-lib=dylib:+verbatim={}",
            libsimics_common
                .file_name()
                .ok_or_else(|| anyhow!("No file name found for {}", libsimics_common.display()))?
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?
        );
        println!(
            "cargo:rustc-link-lib=dylib:+verbatim={}",
            libvtutils
                .file_name()
                .ok_or_else(|| anyhow!("No file name found for {}", libvtutils.display()))?
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?
        );
        println!(
            "cargo:rustc-link-lib=dylib:+verbatim={}",
            python_env.lib_filename()?
        );
        println!(
            "cargo:rustc-link-search=native={}",
            bin_dir
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?
        );
        println!(
            "cargo:rustc-link-search=native={}",
            python_env
                .lib_dir
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?
        );
        let ld_library_path = vec![
            bin_dir
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?,
            python_env
                .lib_dir
                .to_str()
                .ok_or_else(|| anyhow!("Could not convert path to string"))?,
        ]
        .join(":");
        println!("cargo:rustc-env=LD_LIBRARY_PATH={}", ld_library_path);
    }

    // NOTE: EVEN with all of the above, a binary built using `cargo build` will not
    // be able to find libsimics-common.so. Instead, when we build a binary that
    // transitively depends on this -sys crate, we compile it with `cargo rustc`,
    // passing the `-rpath` link argument like so. Note `--disable-new-dtags`,
    // otherwise `libsimics-common.so` cannot find `libpython3.9.so.1.0` because it
    // will be missing the recursive rpath lookup.

    // SIMICS_BASE=/home/rhart/simics-public/simics-6.0.174
    // PYTHON3_INCLUDE=-I/home/rhart/simics-public/simics-6.0.174/linux64/include/python3.9
    // INCLUDE_PATHS=/home/rhart/simics-public/simics-6.0.174/src/include
    // PYTHON3_LDFLAGS=/home/rhart/simics-public/simics-6.0.174/linux64/sys/lib/libpython3.so
    // LDFLAGS="-L/home/rhart/simics-public/simics-6.0.174/linux64/bin -z noexecstack -z relro -z now" LIBS="-lsimics-common -lvtutils"
    // cargo --features=auto,link --example simple-simics -- -C link-args="-Wl,--disable-new-dtags -Wl,-rpath,/home/rhart/simics-public/simics-6.0.174/linux64/bin;/home/rhart/simics-public/simics-6.0.174/linux64/sys/lib/"
    //
    // This command (the environment variables can be left out) can be
    // auto-generated in the SIMICS makefile build system.

    Ok(())
}