use std::{
collections::HashMap,
ffi::{OsStr, OsString},
io,
path::Path,
process::Command,
};
#[cfg(target_family = "unix")]
use std::os::unix::ffi::OsStrExt;
#[cfg(target_family = "windows")]
use std::os::windows::ffi::OsStringExt;
#[derive(Debug)]
struct ConfigVariables {
map: HashMap<String, String>,
}
impl ConfigVariables {
fn get_r_cmd_config(&self, key: &str) -> String {
match self.map.get(key) {
Some(value) => value.to_string(),
None => String::from(""),
}
}
}
#[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()
}
#[link(name = "kernel32")]
#[cfg(target_family = "windows")]
extern "system" {
#[link_name = "GetConsoleCP"]
fn get_console_code_page() -> u32;
#[link_name = "MultiByteToWideChar"]
fn multi_byte_to_wide_char(
CodePage: u32,
dwFlags: u32,
lpMultiByteStr: *const u8,
cbMultiByte: i32,
lpWideCharStr: *mut u16,
cchWideChar: i32,
) -> i32;
}
#[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 = get_console_code_page();
len = multi_byte_to_wide_char(
cp,
0,
bytes.as_ptr() as *const u8,
bytes.len() as i32,
std::ptr::null_mut(),
0,
);
wide = Vec::with_capacity(len as usize);
len = multi_byte_to_wide_char(
cp,
0,
bytes.as_ptr() as *const u8,
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 get_r_home() -> String {
let output = Command::new("R").arg("RHOME").output();
match output {
Ok(output) if output.status.success() => {
String::from_utf8(output.stdout)
.expect("Invalid UTF-8 in RHOME output")
.trim()
.to_string()
}
Ok(output) => {
eprintln!(
"Error: Command `R RHOME` failed with status: {}\nStderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
panic!("Failed to detect RHOME using `R RHOME`. Is R installed and in your PATH?");
}
Err(err) => {
panic!(
"Failed to execute `R RHOME`: {}. Is R installed and in your PATH?",
err
);
}
}
}
fn r_cmd_config<S: AsRef<OsStr>>(r_binary: S) -> io::Result<OsString> {
let out = Command::new(r_binary)
.args(&["CMD", "config", "--all"])
.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 build_r_cmd_configs() -> ConfigVariables {
let r_binary = format!(r"{}/bin/R", get_r_home());
let r_configs = r_cmd_config(r_binary);
let mut rcmd_config_map = HashMap::new();
match r_configs {
Ok(configs) => {
let input = configs.as_os_str().to_string_lossy();
for line in input.lines() {
if line.starts_with("##") {
break;
}
let parts: Vec<_> = line.split('=').map(str::trim).collect();
if let [name, value] = parts.as_slice() {
rcmd_config_map.insert(name.to_string(), value.to_string());
}
}
}
_ => (),
}
ConfigVariables {
map: rcmd_config_map,
}
}
fn get_libs_and_paths(strings: Vec<String>) -> (Vec<String>, Vec<String>) {
let mut paths: Vec<String> = Vec::new();
let mut libs: Vec<String> = Vec::new();
for s in &strings {
let parts: Vec<&str> = s.split_whitespace().collect();
for part in parts {
if part.starts_with("-L") {
paths.push(part[2..].to_string());
} else if part.starts_with("-l") {
libs.push(part[2..].to_string());
}
}
}
(paths, libs)
}
fn main() {
let r_configs = build_r_cmd_configs();
let (lib_paths, libs) = get_libs_and_paths(
[
r_configs.get_r_cmd_config("BLAS_LIBS"),
r_configs.get_r_cmd_config("LAPACK_LIBS"),
r_configs.get_r_cmd_config("FLIBS"),
]
.to_vec(),
);
for path in lib_paths.iter() {
if Path::new(path).exists() {
println!("cargo:rustc-link-search={}", path);
}
}
for lib in libs.iter() {
println!("cargo:rustc-link-lib=dylib={}", lib);
}
println!("cargo:rerun-if-changed=build.rs");
}