use anyhow::{anyhow, ensure, Context, Result};
use std::env;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
const ENV_LLVM_PREFIX: &str = "NYXSTONE_LLVM_PREFIX";
const ENV_FORCE_FFI_LINKING: &str = "NYXSTONE_LINK_FFI";
fn main() {
let headers = [
"nyxstone/include/nyxstone.h",
"nyxstone/src/ELFStreamerWrapper.h",
"nyxstone/src/ObjectWriterWrapper.h",
"nyxstone/src/Target/AArch64/MCTargetDesc/AArch64FixupKinds.h",
"nyxstone/src/Target/AArch64/MCTargetDesc/AArch64MCExpr.h",
"src/nyxstone_ffi.hpp",
];
let sources = [
"nyxstone/src/nyxstone.cpp",
"nyxstone/src/ObjectWriterWrapper.cpp",
"nyxstone/src/ELFStreamerWrapper.cpp",
"src/nyxstone_ffi.cpp",
];
println!("cargo:rerun-if-env-changed={}", ENV_LLVM_PREFIX);
if let Ok(path) = env::var(ENV_LLVM_PREFIX) {
println!("cargo:rerun-if-changed={}", path);
}
if std::env::var("DOCS_RS").is_ok() {
return;
}
let llvm_config_path = match search_llvm_config() {
Ok(config) => config,
Err(e) => panic!("{e} Please either install LLVM version >= 15 into your PATH or supply the location via $NYXSTONE_LLVM_PREFIX"),
};
let libdir = llvm_config(&llvm_config_path, ["--libdir"]);
println!("cargo:config_path={}", llvm_config_path.display()); println!("cargo:libdir={}", libdir);
println!("cargo:rustc-link-search=native={}", libdir);
for link_search_dir in get_system_library_dirs() {
println!("cargo:rustc-link-search=native={}", link_search_dir);
}
let (kind, libs) = get_link_libraries(&llvm_config_path);
for name in libs {
println!("cargo:rustc-link-lib={}={}", kind.string(), name);
}
let sys_lib_kind = LibraryKind::Dynamic;
for name in get_system_libraries(&llvm_config_path, kind) {
println!("cargo:rustc-link-lib={}={}", sys_lib_kind.string(), name);
}
if target_env_is("msvc") && is_llvm_debug(&llvm_config_path) {
println!("cargo:rustc-link-lib=msvcrtd");
}
if env::var(ENV_FORCE_FFI_LINKING).is_ok() {
println!("cargo:rustc-link-lib=dylib=ffi");
}
let llvm_include_dir = llvm_config(&llvm_config_path, ["--includedir"]);
cxx_build::bridge("src/lib.rs")
.std("c++17")
.include("nyxstone/include")
.include("nyxstone/vendor")
.include(llvm_include_dir.trim())
.files(sources)
.compile("nyxstone_wrap");
for file in headers.iter().chain(sources.iter()) {
println!("cargo:rerun-if-changed={}", file);
}
}
fn search_llvm_config() -> Result<PathBuf> {
let prefix = env::var(ENV_LLVM_PREFIX)
.and_then(|p| {
if p.is_empty() {
return Err(std::env::VarError::NotPresent);
}
Ok(PathBuf::from(p).join("bin"))
})
.unwrap_or_else(|_| PathBuf::new());
for name in llvm_config_binary_names() {
let llvm_config = Path::new(&prefix).join(name);
let Ok(version) = get_major_version(&llvm_config) else {
continue;
};
let version = version.parse::<u32>().context("Parsing LLVM version")?;
ensure!(
(15..=18).contains(&version),
"LLVM major version is {}, must be 15-18.",
version
);
return Ok(llvm_config);
}
Err(anyhow!(
"No llvm-config found in {}",
if prefix == PathBuf::new() {
"$PATH"
} else {
"$NYXSTONE_LLVM_PREFIX"
}
))
}
fn get_major_version(binary: &Path) -> Result<String> {
Ok(llvm_config_ex(binary, ["--version"])
.context("Extracting LLVM major version")?
.split('.')
.next()
.expect("Unexpected llvm-config output.")
.to_owned())
}
fn target_env_is(name: &str) -> bool {
match env::var_os("CARGO_CFG_TARGET_ENV") {
Some(s) => s == name,
None => false,
}
}
fn target_os_is(name: &str) -> bool {
match env::var_os("CARGO_CFG_TARGET_OS") {
Some(s) => s == name,
None => false,
}
}
fn llvm_config_binary_names() -> impl Iterator<Item = String> {
let base_names = (15..=18)
.flat_map(|version| {
[
format!("llvm-config-{}", version),
format!("llvm-config{}", version),
format!("llvm{}-config", version),
]
})
.chain(["llvm-config".into()])
.collect::<Vec<_>>();
if target_os_is("windows") {
IntoIterator::into_iter(base_names)
.flat_map(|name| [format!("{}.exe", name), name])
.collect::<Vec<_>>()
} else {
base_names.to_vec()
}
.into_iter()
}
fn llvm_config<I, S>(binary: &Path, args: I) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
llvm_config_ex(binary, args).expect("Surprising failure from llvm-config")
}
fn llvm_config_ex<I, S>(binary: &Path, args: I) -> anyhow::Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = Command::new(binary);
(|| {
let Output { status, stdout, stderr } = cmd.args(args).output()?;
let stdout = String::from_utf8(stdout).context("stdout")?;
let stderr = String::from_utf8(stderr).context("stderr")?;
if status.success() {
Ok(stdout)
} else {
Err(anyhow::anyhow!(
"status={status}\nstdout={}\nstderr={}",
stdout.trim(),
stderr.trim()
))
}
})()
.with_context(|| format!("{cmd:?}"))
}
fn get_system_libraries(llvm_config_path: &Path, kind: LibraryKind) -> Vec<String> {
let link_arg = match kind {
LibraryKind::Static => "--link-static",
LibraryKind::Dynamic => "--link-shared",
};
llvm_config(llvm_config_path, ["--system-libs", link_arg])
.split(&[' ', '\n'] as &[char])
.filter(|s| !s.is_empty())
.map(|flag| {
if target_env_is("msvc") {
flag.strip_suffix(".lib")
.unwrap_or_else(|| panic!("system library '{}' does not appear to be a MSVC library file", flag))
} else {
if let Some(flag) = flag.strip_prefix("-l") {
if target_os_is("macos") {
if let Some(flag) = flag.strip_prefix("lib").and_then(|flag| flag.strip_suffix(".tbd")) {
return flag;
}
}
if let Some(i) = flag.find(".so.") {
return &flag[..i];
}
return flag;
}
let maybe_lib = Path::new(flag);
if maybe_lib.is_file() {
println!("cargo:rustc-link-search={}", maybe_lib.parent().unwrap().display());
let soname = maybe_lib
.file_name()
.unwrap()
.to_str()
.expect("Shared library path must be a valid string");
let (stem, _rest) = soname
.rsplit_once(target_dylib_extension())
.expect("Shared library should be a .so file");
stem.strip_prefix("lib")
.unwrap_or_else(|| panic!("system library '{}' does not have a 'lib' prefix", soname))
} else {
panic!("Unable to parse result of llvm-config --system-libs: {}", flag)
}
}
})
.chain(get_system_libcpp())
.map(str::to_owned)
.collect()
}
fn get_system_library_dirs() -> impl IntoIterator<Item = &'static str> {
if target_os_is("openbsd") {
Some("/usr/local/lib")
} else {
None
}
}
fn target_dylib_extension() -> &'static str {
if target_os_is("macos") {
".dylib"
} else {
".so"
}
}
fn get_system_libcpp() -> Option<&'static str> {
if target_env_is("msvc") {
None
} else if target_os_is("macos") {
Some("c++")
} else if target_os_is("freebsd") || target_os_is("openbsd") {
Some("c++")
} else if target_env_is("musl") {
Some("c++")
} else {
Some("stdc++")
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LibraryKind {
Static,
Dynamic,
}
impl LibraryKind {
pub fn string(&self) -> &'static str {
match self {
LibraryKind::Static => "static",
LibraryKind::Dynamic => "dylib",
}
}
}
fn get_link_libraries(llvm_config_path: &Path) -> (LibraryKind, Vec<String>) {
fn get_link_libraries_impl(llvm_config_path: &Path, kind: LibraryKind) -> anyhow::Result<String> {
if target_env_is("msvc") && kind == LibraryKind::Dynamic {
anyhow::bail!("Dynamic linking to LLVM is not supported on Windows");
}
let link_arg = match kind {
LibraryKind::Static => "--link-static",
LibraryKind::Dynamic => "--link-shared",
};
llvm_config_ex(llvm_config_path, ["--libnames", link_arg])
}
let preferences = [LibraryKind::Static, LibraryKind::Dynamic];
for kind in preferences {
match get_link_libraries_impl(llvm_config_path, kind) {
Ok(s) => return (kind, extract_library(&s, kind)),
Err(err) => {
println!("failed to get {} libraries from llvm-config: {err:?}", kind.string())
}
}
}
panic!("failed to get linking libraries from llvm-config",);
}
fn extract_library(s: &str, kind: LibraryKind) -> Vec<String> {
s.split(&[' ', '\n'] as &[char])
.filter(|s| !s.is_empty())
.map(|name| {
match kind {
LibraryKind::Static => {
if let Some(name) = name.strip_prefix("lib").and_then(|name| name.strip_suffix(".a")) {
name
} else if let Some(name) = name.strip_suffix(".lib") {
name
} else {
panic!("'{}' does not look like a static library name", name)
}
}
LibraryKind::Dynamic => {
if let Some(name) = name.strip_prefix("lib").and_then(|name| name.strip_suffix(".dylib")) {
name
} else if let Some(name) = name.strip_prefix("lib").and_then(|name| name.strip_suffix(".so")) {
name
} else if let Some(name) =
IntoIterator::into_iter([".dll", ".lib"]).find_map(|suffix| name.strip_suffix(suffix))
{
name
} else {
panic!("'{}' does not look like a shared library name", name)
}
}
}
.to_string()
})
.collect::<Vec<String>>()
}
fn is_llvm_debug(llvm_config_path: &Path) -> bool {
llvm_config(llvm_config_path, ["--build-mode"]).contains("Debug")
}