use std::{
env,
ffi::{OsStr, OsString},
fs, io,
io::{Error, ErrorKind},
path::{Path, PathBuf},
process::{exit, Command},
};
#[cfg(target_family = "unix")]
use std::os::unix::ffi::OsStrExt;
#[cfg(target_family = "windows")]
use std::os::windows::ffi::OsStringExt;
const ENVVAR_R_INCLUDE_DIR: &str = "R_INCLUDE_DIR";
const ENVVAR_R_HOME: &str = "R_HOME";
const ENVVAR_R_VERSION: &str = "LIBRSYS_R_VERSION";
const ENVVAR_BINDINGS_PATH: &str = "LIBRSYS_BINDINGS_PATH";
#[cfg(feature = "use-bindgen")]
const ENVVAR_LIBCLANG_INCLUDE_PATH: &str = "LIBRSYS_LIBCLANG_INCLUDE_PATH";
#[cfg(feature = "use-bindgen")]
const ENVVAR_BINDINGS_OUTPUT_PATH: &str = "LIBRSYS_BINDINGS_OUTPUT_PATH";
#[allow(dead_code)]
struct InstallationPaths {
r_home: PathBuf,
include: PathBuf,
library: PathBuf,
}
impl InstallationPaths {
fn get_r_binary(&self) -> PathBuf {
if cfg!(windows) {
Path::new(&self.library).join("R.exe")
} else {
Path::new(&self.r_home).join("bin").join("R")
}
}
}
#[allow(dead_code)]
#[derive(Debug)]
struct RVersionInfo {
major: String,
minor: String,
patch: String,
devel: bool,
}
impl RVersionInfo {
fn get_r_bindings_filename(&self, target_os: &str, target_arch: &str) -> PathBuf {
let devel_suffix = if self.devel { "-devel" } else { "" };
PathBuf::from(format!(
"bindings-{}-{}-R{}.{}{}.rs",
target_os, target_arch, self.major, self.minor, devel_suffix
))
}
}
#[derive(Debug)]
enum EnvVarError {
EnvVarNotPresent,
InvalidEnvVar(&'static str),
RInvocationError(io::Error),
InvalidROutput(&'static str),
}
#[cfg(target_family = "unix")]
fn byte_array_to_os_string(bytes: &[u8]) -> OsString {
let os_str = OsStr::from_bytes(bytes);
os_str.to_os_string()
}
#[cfg(target_family = "windows")]
fn wide_from_console_string(bytes: &[u8]) -> Vec<u16> {
assert!(bytes.len() < std::i32::MAX as usize);
let mut wide;
let mut len;
unsafe {
let cp = winapi::um::consoleapi::GetConsoleCP();
len = winapi::um::stringapiset::MultiByteToWideChar(
cp,
0,
bytes.as_ptr() as *const i8,
bytes.len() as i32,
std::ptr::null_mut(),
0,
);
wide = Vec::with_capacity(len as usize);
len = winapi::um::stringapiset::MultiByteToWideChar(
cp,
0,
bytes.as_ptr() as *const i8,
bytes.len() as i32,
wide.as_mut_ptr(),
len,
);
wide.set_len(len as usize);
}
wide
}
#[cfg(target_family = "windows")]
fn byte_array_to_os_string(bytes: &[u8]) -> OsString {
let wide = wide_from_console_string(bytes);
OsString::from_wide(&wide)
}
fn r_command<S: AsRef<OsStr>>(r_binary: S, script: &str) -> io::Result<OsString> {
let out = Command::new(r_binary)
.args(&["-s", "-e", script])
.output()?;
if !out.stderr.is_empty() {
println!(
"> {}",
byte_array_to_os_string(&out.stderr)
.as_os_str()
.to_string_lossy()
);
}
Ok(byte_array_to_os_string(&out.stdout))
}
fn get_r_home() -> io::Result<PathBuf> {
if let Some(r_home) = env::var_os(ENVVAR_R_HOME) {
return Ok(PathBuf::from(r_home));
}
let rout = r_command("R", r#"cat(normalizePath(R.home()))"#)?;
if !rout.is_empty() {
Ok(PathBuf::from(rout))
} else {
Err(Error::new(ErrorKind::Other, "Cannot find R home."))
}
}
fn get_r_library(r_home: &Path) -> PathBuf {
let pkg_target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
match (cfg!(windows), pkg_target_arch.as_str()) {
(true, "x86_64") => Path::new(r_home).join("bin").join("x64"),
(true, "x86") => Path::new(r_home).join("bin").join("i386"),
(true, _) => panic!("Unknown architecture"),
(false, _) => Path::new(r_home).join("lib"),
}
}
fn get_r_include(r_home: &Path, library: &Path) -> io::Result<PathBuf> {
if let Some(include) = env::var_os(ENVVAR_R_INCLUDE_DIR) {
return Ok(PathBuf::from(include));
}
let r_binary = InstallationPaths {
r_home: r_home.to_path_buf(),
include: PathBuf::new(), library: library.to_path_buf(),
}
.get_r_binary();
let rout = r_command(&r_binary, r#"cat(normalizePath(R.home('include')))"#)?;
if !rout.is_empty() {
Ok(PathBuf::from(rout))
} else {
Err(Error::new(ErrorKind::Other, "Cannot find R include."))
}
}
fn probe_r_paths() -> io::Result<InstallationPaths> {
let r_home = get_r_home()?;
let library = get_r_library(&r_home);
let include = get_r_include(&r_home, &library)?;
Ok(InstallationPaths {
r_home,
include,
library,
})
}
fn parse_r_version(r_version: String) -> Result<RVersionInfo, EnvVarError> {
let (r_version, devel) = match *r_version.split('-').collect::<Vec<&str>>().as_slice() {
[r_version, devel] => (r_version, Some(devel)),
[r_version] => (r_version, None),
_ => return Err(EnvVarError::InvalidEnvVar("Invalid format")),
};
let r_version_split = r_version
.split('.')
.map(|s| {
if !s.is_empty() && s.chars().all(|c| c.is_digit(10)) {
Some(s)
} else {
None
}
})
.collect::<Vec<Option<&str>>>();
let (major, minor, patch) = match *r_version_split.as_slice() {
[] | [None, ..] => return Err(EnvVarError::InvalidEnvVar("Cannot find R major version")),
[_, None, ..] => return Err(EnvVarError::InvalidEnvVar("Cannot find R minor version")),
[_, _, None, ..] => return Err(EnvVarError::InvalidEnvVar("Cannot find R patch level")),
[Some(major), Some(minor), Some(patch)] => {
(major.to_string(), minor.to_string(), patch.to_string())
}
_ => return Err(EnvVarError::InvalidEnvVar("Invalid format")),
};
let devel = match devel {
Some("devel") => true,
Some(_) => {
return Err(EnvVarError::InvalidEnvVar(
"Cannot find R development status",
))
}
None => false,
};
Ok(RVersionInfo {
major,
minor,
patch,
devel,
})
}
fn get_r_version_from_env(r_version_env_var: &str) -> Result<RVersionInfo, EnvVarError> {
std::env::var(r_version_env_var)
.map_err(|_| EnvVarError::EnvVarNotPresent)
.and_then(parse_r_version)
}
fn get_r_version_from_r(r_paths: &InstallationPaths) -> Result<RVersionInfo, EnvVarError> {
let r_binary = r_paths.get_r_binary();
let out = r_command(
&r_binary,
r#"cat(sprintf('%s.%s%s\n', R.version$major, R.version$minor, if(isTRUE(grepl('devel', R.version$status, fixed = TRUE))) '-devel' else ''))"#,
)
.map_err(EnvVarError::RInvocationError)?;
let out = out.as_os_str().to_string_lossy().into_owned();
let mut lines = out.lines();
match lines.next() {
Some(v) => parse_r_version(v.to_string()),
None => Err(EnvVarError::InvalidROutput("Cannot find R version")),
}
}
fn get_r_version(
r_version_env_var: &str,
r_paths: &InstallationPaths,
) -> Result<RVersionInfo, EnvVarError> {
match get_r_version_from_env(r_version_env_var) {
Ok(v) => Ok(v),
Err(EnvVarError::EnvVarNotPresent) => get_r_version_from_r(r_paths),
e @ Err(_) => e,
}
}
fn set_r_version_vars(ver: &RVersionInfo) {
println!("cargo:r_version_major={}", ver.major); println!("cargo:r_version_minor={}", ver.minor); println!("cargo:r_version_patch={}", ver.patch); println!("cargo:r_version_devel={}", ver.devel); }
#[cfg(feature = "use-bindgen")]
fn generate_bindings(r_paths: &InstallationPaths, version_info: &RVersionInfo) {
let mut bindgen_builder = bindgen::Builder::default()
.blacklist_item("FP_NAN")
.blacklist_item("FP_INFINITE")
.blacklist_item("FP_ZERO")
.blacklist_item("FP_SUBNORMAL")
.blacklist_item("FP_NORMAL")
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks));
let target = env::var("TARGET").expect("Could not get the target triple");
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
println!(
"Generating bindings for target: {}, os: {}, architecture: {}",
target, target_os, target_arch
);
bindgen_builder = bindgen_builder.clang_args(&[
format!("-I{}", r_paths.include.display()),
format!("--target={}", target),
]);
if let Some(alt_include) = env::var_os(ENVVAR_LIBCLANG_INCLUDE_PATH) {
bindgen_builder =
bindgen_builder.clang_arg(format!("-I{}", PathBuf::from(alt_include).display()));
}
if target_os == "windows" && target_arch == "x86" {
bindgen_builder = bindgen_builder
.blacklist_item("max_align_t")
.blacklist_item("__mingw_ldbl_type_t");
}
let bindings = bindgen_builder
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var_os("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings to default output path!");
if let Some(alt_target) = env::var_os(ENVVAR_BINDINGS_OUTPUT_PATH) {
let out_path = PathBuf::from(alt_target);
if !out_path.exists() {
fs::create_dir(&out_path).expect(&format!(
"Couldn't create output directory for bindings: {}",
out_path.display()
));
}
let bindings_file_full = version_info.get_r_bindings_filename(&target_os, &target_arch);
let out_file = out_path.join(bindings_file_full);
bindings
.write_to_file(&out_file)
.expect(&format!("Couldn't write bindings: {}", out_file.display()));
}
}
#[allow(dead_code)]
fn retrieve_prebuild_bindings(version_info: &RVersionInfo) {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let bindings_path = PathBuf::from(
env::var_os(ENVVAR_BINDINGS_PATH).unwrap_or_else(|| OsString::from("bindings")),
);
let bindings_file_full = version_info.get_r_bindings_filename(&target_os, &target_arch);
let bindings_file_novers = PathBuf::from(format!("bindings-{}-{}.rs", target_os, target_arch));
let mut from = bindings_path.join(bindings_file_full);
if !from.exists() {
from = bindings_path.join(bindings_file_novers);
if !from.exists() {
panic!(
"Cannot find libR-sys bindings file for R {}.{}.{}{} on {} in {}. Consider compiling with --features use-bindgen.",
version_info.major, version_info.minor, version_info.patch, version_info.devel, target_os, bindings_path.display()
)
} else {
println!(
"Warning: using generic {}-{} libR-sys bindings. These may not work for R {}.{}.{}{}.",
target_os, target_arch, version_info.major, version_info.minor, version_info.patch, version_info.devel
);
}
}
fs::copy(
&from,
PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("bindings.rs"),
)
.expect("No precomputed bindings available!");
println!("cargo:rerun-if-changed={}", from.display());
}
fn main() {
let r_paths = probe_r_paths();
let r_paths = match r_paths {
Ok(result) => result,
Err(error) => {
println!("Problem locating local R install: {:?}", error);
exit(1);
}
};
println!("cargo:rustc-env=R_HOME={}", r_paths.r_home.display());
println!("cargo:r_home={}", r_paths.r_home.display());
println!("cargo:rustc-link-search={}", r_paths.library.display());
println!("cargo:rustc-link-lib=dylib=R");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=wrapper.h");
let version_info =
get_r_version(ENVVAR_R_VERSION, &r_paths).expect("Could not obtain R version");
set_r_version_vars(&version_info);
#[cfg(feature = "use-bindgen")]
generate_bindings(&r_paths, &version_info);
#[cfg(not(feature = "use-bindgen"))]
retrieve_prebuild_bindings(&version_info);
}