extern crate cc;
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate semver;
use regex::Regex;
use semver::Version;
use std::env;
use std::ffi::OsStr;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
use std::process::Command;
lazy_static! {
static ref ENV_LLVM_PREFIX: String =
format!("LLVM_SYS_{}_PREFIX", env!("CARGO_PKG_VERSION_MAJOR"));
static ref ENV_IGNORE_BLOCKLIST: String =
format!("LLVM_SYS_{}_IGNORE_BLOCKLIST", env!("CARGO_PKG_VERSION_MAJOR"));
static ref ENV_STRICT_VERSIONING: String =
format!("LLVM_SYS_{}_STRICT_VERSIONING", env!("CARGO_PKG_VERSION_MAJOR"));
static ref ENV_NO_CLEAN_CFLAGS: String =
format!("LLVM_SYS_{}_NO_CLEAN_CFLAGS", env!("CARGO_PKG_VERSION_MAJOR"));
static ref ENV_USE_DEBUG_MSVCRT: String =
format!("LLVM_SYS_{}_USE_DEBUG_MSVCRT", env!("CARGO_PKG_VERSION_MAJOR"));
static ref ENV_FORCE_FFI: String =
format!("LLVM_SYS_{}_FFI_WORKAROUND", env!("CARGO_PKG_VERSION_MAJOR"));
}
lazy_static! {
static ref CRATE_VERSION: Version = {
let crate_version = Version::parse(env!("CARGO_PKG_VERSION"))
.expect("Crate version is somehow not valid semver");
Version {
major: crate_version.major / 10,
minor: crate_version.major % 10,
.. crate_version
}
};
static ref LLVM_CONFIG_PATH: Option<PathBuf> = locate_llvm_config();
}
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 locate_llvm_config() -> Option<PathBuf> {
let prefix = env::var_os(&*ENV_LLVM_PREFIX)
.map(|p| PathBuf::from(p).join("bin"))
.unwrap_or_else(PathBuf::new);
if let Some(x) = llvm_compatible_binary_name(&prefix) {
return Some(x);
}
if target_os_is("macos") {
if let Some(p) = homebrew_prefix(Some(&format!("llvm@{}", CRATE_VERSION.major)))
.or_else(|| homebrew_prefix(Some("llvm")))
{
return llvm_compatible_binary_name(&PathBuf::from(p).join("bin"));
}
}
None
}
fn llvm_compatible_binary_name(prefix: &Path) -> Option<PathBuf> {
for binary_name in llvm_config_binary_names() {
let binary_name = prefix.join(binary_name);
match llvm_version(&binary_name) {
Ok(ref version) if is_compatible_llvm(version) => {
return Some(binary_name);
}
Ok(version) => {
println!(
"Found LLVM version {} on PATH, but need {}.",
version, *CRATE_VERSION
);
}
Err(ref e) if e.kind() == ErrorKind::NotFound => {
}
Err(e) => panic!("Failed to search PATH for llvm-config: {}", e),
}
}
None
}
fn llvm_config_binary_names() -> std::vec::IntoIter<String> {
let mut base_names = vec![
"llvm-config".into(),
format!("llvm-config-{}", CRATE_VERSION.major),
format!("llvm-config{}", CRATE_VERSION.major),
format!("llvm{}-config", CRATE_VERSION.major),
format!(
"llvm-config-{}.{}",
CRATE_VERSION.major, CRATE_VERSION.minor
),
format!("llvm-config{}{}", CRATE_VERSION.major, CRATE_VERSION.minor),
];
if target_os_is("windows") {
let mut exe_names = base_names.clone();
for name in exe_names.iter_mut() {
name.push_str(".exe");
}
base_names.extend(exe_names);
}
base_names.into_iter()
}
fn is_blocklisted_llvm(llvm_version: &Version) -> Option<&'static str> {
static BLOCKLIST: &'static [(u64, u64, u64, &'static str)] = &[];
if let Some(x) = env::var_os(&*ENV_IGNORE_BLOCKLIST) {
if &x == "YES" {
println!(
"cargo:warning=Ignoring blocklist entry for LLVM {}",
llvm_version
);
return None;
} else {
println!(
"cargo:warning={} is set but not exactly \"YES\"; blocklist is still honored.",
*ENV_IGNORE_BLOCKLIST
);
}
}
for &(major, minor, patch, reason) in BLOCKLIST.iter() {
let bad_version = Version {
major: major,
minor: minor,
patch: patch,
pre: semver::Prerelease::EMPTY,
build: semver::BuildMetadata::EMPTY,
};
if &bad_version == llvm_version {
return Some(reason);
}
}
None
}
fn is_compatible_llvm(llvm_version: &Version) -> bool {
if let Some(reason) = is_blocklisted_llvm(llvm_version) {
println!(
"Found LLVM {}, which is blocklisted: {}",
llvm_version, reason
);
return false;
}
let strict =
env::var_os(&*ENV_STRICT_VERSIONING).is_some() || cfg!(feature = "strict-versioning");
if strict {
llvm_version.major == CRATE_VERSION.major && llvm_version.minor == CRATE_VERSION.minor
} else {
llvm_version.major >= CRATE_VERSION.major
|| (llvm_version.major == CRATE_VERSION.major
&& llvm_version.minor >= CRATE_VERSION.minor)
}
}
fn llvm_config(arg: &str) -> String {
try_llvm_config(Some(arg).into_iter()).expect("Surprising failure from llvm-config")
}
fn try_llvm_config<'a>(arg: impl Iterator<Item = &'a str>) -> io::Result<String> {
llvm_config_ex(&*LLVM_CONFIG_PATH.clone().unwrap(), arg)
}
fn llvm_config_ex<'a, S: AsRef<OsStr>>(
binary: S,
args: impl Iterator<Item = &'a str>,
) -> io::Result<String> {
Command::new(binary).args(args).output().and_then(|output| {
if output.status.code() != Some(0) {
Err(io::Error::new(
io::ErrorKind::Other,
format!(
"llvm-config failed with error code {:?}",
output.status.code()
),
))
} else if output.stdout.is_empty() {
Err(io::Error::new(
io::ErrorKind::NotFound,
"llvm-config returned empty output",
))
} else {
Ok(String::from_utf8(output.stdout)
.expect("Output from llvm-config was not valid UTF-8"))
}
})
}
fn llvm_version<S: AsRef<OsStr>>(binary: &S) -> io::Result<Version> {
let version_str = llvm_config_ex(binary.as_ref(), ["--version"].iter().copied())?;
let re = Regex::new(r"^(?P<major>\d+)\.(?P<minor>\d+)(?:\.(?P<patch>\d+))??").unwrap();
let c = match re.captures(&version_str) {
Some(c) => c,
None => {
panic!(
"Could not determine LLVM version from llvm-config. Version string: {}",
version_str
);
}
};
let s = match c.name("patch") {
None => format!("{}.0", &c[0]),
Some(_) => c[0].to_string(),
};
Ok(Version::parse(&s).unwrap())
}
fn get_system_libraries(kind: LibraryKind) -> Vec<String> {
let link_arg = match kind {
LibraryKind::Static => "--link-static",
LibraryKind::Dynamic => "--link-shared",
};
try_llvm_config(["--system-libs", link_arg].iter().copied())
.expect("Surprising failure from llvm-config")
.split(&[' ', '\n'] as &[char])
.filter(|s| !s.is_empty())
.map(|flag| {
if target_env_is("msvc") {
assert!(
flag.ends_with(".lib"),
"system library {:?} does not appear to be a MSVC library file",
flag
);
&flag[..flag.len() - 4]
} else {
if flag.starts_with("-l") {
if target_os_is("macos") && flag.starts_with("-llib") && flag.ends_with(".tbd")
{
return flag[5..flag.len() - 4].to_owned();
}
if let Some(i) = flag.find(".so.") {
return flag[2..i].to_owned();
}
return flag[2..].to_owned();
}
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 = soname
.rsplit_once(target_dylib_extension())
.expect("Shared library should be a .so file")
.0;
stem.trim_start_matches("lib")
} else {
panic!(
"Unable to parse result of llvm-config --system-libs: was {:?}",
flag
)
}
}
.to_owned()
})
.chain(get_system_libcpp().map(str::to_owned))
.collect::<Vec<String>>()
}
fn get_system_library_dirs() -> impl IntoIterator<Item = String> {
if target_os_is("openbsd") || target_os_is("freebsd") {
Some("/usr/local/lib".to_string())
} else if target_os_is("macos") {
if let Some(p) = homebrew_prefix(None) {
Some(format!("{}/lib", p))
} else {
None
}
} else {
None
}
}
fn homebrew_prefix(name: Option<&str>) -> Option<String> {
let mut cmd = Command::new("brew");
cmd.arg("--prefix");
if let Some(name) = name {
cmd.arg(name);
}
cmd.output()
.ok()
.filter(|o| !o.stdout.is_empty())
.and_then(|out| String::from_utf8(out.stdout).ok())
.map(|val| val.trim().to_string())
}
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 from_is_static(is_static: bool) -> Self {
if is_static {
LibraryKind::Static
} else {
LibraryKind::Dynamic
}
}
pub fn string(&self) -> &'static str {
match self {
LibraryKind::Static => "static",
LibraryKind::Dynamic => "dylib",
}
}
}
fn get_link_libraries(preferences: &LinkingPreferences) -> (LibraryKind, Vec<String>) {
fn get_link_libraries_impl(is_static: bool) -> std::io::Result<String> {
if target_env_is("msvc") && !is_static {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Dynamic linking to LLVM is not supported on Windows.",
));
}
let link_arg = if is_static {
"--link-static"
} else {
"--link-shared"
};
try_llvm_config(["--libnames", link_arg].iter().copied())
}
fn lib_kind(is_static: bool) -> &'static str {
if is_static {
"static"
} else {
"shared"
}
}
let is_static = preferences.prefer_static;
let first_err;
match get_link_libraries_impl(is_static) {
Ok(s) => {
return (
LibraryKind::from_is_static(is_static),
extract_library(&s, is_static),
)
}
Err(e) => first_err = e,
}
let mut second_err = None;
if !preferences.force {
println!(
"cargo:warning=failed to get {} libraries from llvm-config, falling back to {}.",
lib_kind(is_static),
lib_kind(!is_static),
);
println!("cargo:warning=error: {}", first_err);
match get_link_libraries_impl(!is_static) {
Ok(s) => {
return (
LibraryKind::from_is_static(!is_static),
extract_library(&s, !is_static),
)
}
Err(e) => second_err = Some(e),
}
}
let first_error = format!(
"linking {} library error: {}",
lib_kind(is_static),
first_err
);
let second_error = if let Some(err) = second_err {
format!("\nlinking {} library error: {}", lib_kind(!is_static), err)
} else {
String::new()
};
panic!(
"failed to get linking libraries from llvm-config.\n{}{}",
first_error, second_error
);
}
fn extract_library(s: &str, is_static: bool) -> Vec<String> {
s.split(&[' ', '\n'] as &[char])
.filter(|s| !s.is_empty())
.map(|name| {
if is_static {
if name.ends_with(".a") {
&name[3..name.len() - 2]
} else if name.ends_with(".lib") {
&name[..name.len() - 4]
} else {
panic!("{:?} does not look like a static library name", name)
}
} else {
if name.ends_with(".dylib") {
&name[3..name.len() - 6]
} else if name.ends_with(".so") {
&name[3..name.len() - 3]
} else if name.ends_with(".dll") || name.ends_with(".lib") {
&name[..name.len() - 4]
} else {
panic!("{:?} does not look like a shared library name", name)
}
}
.to_string()
})
.collect::<Vec<String>>()
}
#[derive(Debug, Clone, Copy)]
struct LinkingPreferences {
prefer_static: bool,
force: bool,
}
impl LinkingPreferences {
fn init() -> LinkingPreferences {
let prefer_static = cfg!(feature = "prefer-static");
let prefer_dynamic = cfg!(feature = "prefer-dynamic");
let force_static = cfg!(feature = "force-static");
let force_dynamic = cfg!(feature = "force-dynamic");
if [prefer_static, prefer_dynamic, force_static, force_dynamic]
.iter()
.filter(|&&x| x)
.count()
> 1
{
panic!(
"Only one of the features `prefer-static`, `prefer-dynamic`, `force-static`, \
`force-dynamic` can be enabled at once."
);
}
let force_static = force_static || !(prefer_static || prefer_dynamic || force_dynamic);
LinkingPreferences {
prefer_static: force_static || prefer_static,
force: force_static || force_dynamic,
}
}
}
fn get_llvm_cflags() -> String {
let output = llvm_config("--cflags");
let no_clean = env::var_os(&*ENV_NO_CLEAN_CFLAGS).is_some();
if no_clean || target_env_is("msvc") {
return output;
}
llvm_config("--cflags")
.split(&[' ', '\n'][..])
.filter(|word| !word.starts_with("-W"))
.collect::<Vec<_>>()
.join(" ")
}
fn is_llvm_debug() -> bool {
llvm_config("--build-mode").contains("Debug")
}
fn main() {
println!("cargo:rerun-if-env-changed={}", &*ENV_LLVM_PREFIX);
if let Ok(path) = env::var(&*ENV_LLVM_PREFIX) {
println!("cargo:rerun-if-changed={}", path);
}
println!("cargo:rerun-if-env-changed={}", &*ENV_IGNORE_BLOCKLIST);
println!("cargo:rerun-if-env-changed={}", &*ENV_STRICT_VERSIONING);
println!("cargo:rerun-if-env-changed={}", &*ENV_NO_CLEAN_CFLAGS);
println!("cargo:rerun-if-env-changed={}", &*ENV_USE_DEBUG_MSVCRT);
println!("cargo:rerun-if-env-changed={}", &*ENV_FORCE_FFI);
if cfg!(feature = "no-llvm-linking") && cfg!(feature = "disable-alltargets-init") {
return;
}
if LLVM_CONFIG_PATH.is_none() {
println!("cargo:rustc-cfg=LLVM_SYS_NOT_FOUND");
return;
}
if !cfg!(feature = "disable-alltargets-init") {
std::env::set_var("CFLAGS", get_llvm_cflags());
cc::Build::new()
.file("wrappers/target.c")
.compile("targetwrappers");
}
if cfg!(feature = "no-llvm-linking") {
return;
}
let libdir = llvm_config("--libdir");
println!(
"cargo:config_path={}",
LLVM_CONFIG_PATH.clone().unwrap().display()
); println!("cargo:libdir={}", libdir);
let preferences = LinkingPreferences::init();
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(&preferences);
for name in libs {
println!("cargo:rustc-link-lib={}={}", kind.string(), name);
}
let sys_lib_kind = if target_env_is("musl") {
LibraryKind::Static
} else {
LibraryKind::Dynamic
};
for name in get_system_libraries(kind) {
println!("cargo:rustc-link-lib={}={}", sys_lib_kind.string(), name);
}
let use_debug_msvcrt = env::var_os(&*ENV_USE_DEBUG_MSVCRT).is_some();
if target_env_is("msvc") && (use_debug_msvcrt || is_llvm_debug()) {
println!("cargo:rustc-link-lib={}", "msvcrtd");
}
let force_ffi = env::var_os(&*ENV_FORCE_FFI).is_some();
if force_ffi {
println!("cargo:rustc-link-lib=dylib={}", "ffi");
}
}