use std::{
env, fs,
path::{Path, PathBuf},
vec,
};
struct InstallPath {
paths: Vec<PathBuf>,
names: Vec<&'static str>,
}
struct LibraryPath {
path: PathBuf,
library: String,
}
fn find_files_recursively<P: AsRef<Path>>(
root_dir: P,
filename: &str,
max_depth: Option<usize>,
) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
let mut matches = Vec::new();
let mut stack = vec![(root_dir.as_ref().to_path_buf(), 0)];
while let Some((current_dir, depth)) = stack.pop() {
if let Some(max) = max_depth {
if depth > max {
continue;
}
}
if let Ok(entries) = fs::read_dir(¤t_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name == filename {
matches.push(path);
}
}
} else if path.is_dir() {
stack.push((path, depth + 1));
}
}
}
}
Ok(matches)
}
fn platform_install_paths() -> Result<InstallPath, Box<dyn std::error::Error>> {
if cfg!(target_os = "windows") {
let local_app_data = env::var("LOCALAPPDATA")
.unwrap_or_else(|_| String::from("C:\\Users\\Default\\AppData\\Local"));
Ok(InstallPath {
paths: vec![PathBuf::from(local_app_data)
.join("MetaCall")
.join("metacall")],
names: vec!["metacall.lib", "metacalld.lib"],
})
} else if cfg!(target_os = "macos") {
Ok(InstallPath {
paths: vec![
PathBuf::from("/opt/homebrew/lib/"),
PathBuf::from("/usr/local/lib/"),
],
names: vec!["libmetacall.dylib, libmetacalld.dylib"],
})
} else if cfg!(target_os = "linux") {
Ok(InstallPath {
paths: vec![PathBuf::from("/usr/local/lib/"), PathBuf::from("/gnu/lib/")],
names: vec!["libmetacall.so", "libmetacalld.so"],
})
} else {
Err(format!("Platform {} not supported", env::consts::OS).into())
}
}
fn get_search_config() -> Result<InstallPath, Box<dyn std::error::Error>> {
if let Ok(custom_path) = env::var("METACALL_INSTALL_PATH") {
return Ok(InstallPath {
paths: vec![PathBuf::from(custom_path)],
names: vec![
"libmetacall.so",
"libmetacalld.so",
"libmetacall.dylib",
"libmetacalld.dylib",
"metacall.lib",
"metacalld.lib",
],
});
}
platform_install_paths()
}
fn get_parent_and_library(path: &Path) -> Option<(PathBuf, String)> {
let parent = path.parent()?.to_path_buf();
let stem = path.file_stem()?.to_str()?;
let cleaned_stem = stem.strip_prefix("lib").unwrap_or(stem).to_string();
Some((parent, cleaned_stem))
}
fn find_metacall_library() -> Result<LibraryPath, Box<dyn std::error::Error>> {
let search_config = get_search_config()?;
for search_path in &search_config.paths {
for name in &search_config.names {
match find_files_recursively(search_path, name, None) {
Ok(files) if !files.is_empty() => {
let found_lib = fs::canonicalize(&files[0])?;
match get_parent_and_library(&found_lib) {
Some((parent, name)) => {
return Ok(LibraryPath {
path: parent,
library: name,
})
}
None => continue,
};
}
Ok(_) => {
continue;
}
Err(e) => {
eprintln!("Error searching in {}: {}", search_path.display(), e);
continue;
}
}
}
}
let search_paths: Vec<String> = search_config
.paths
.iter()
.map(|p| p.display().to_string())
.collect();
Err(format!(
"MetaCall library not found. Searched in: {}. \
If you have it installed elsewhere, set METACALL_INSTALL_PATH environment variable.",
search_paths.join(", ")
)
.into())
}
fn define_library_search_path(env_var: &str, separator: &str, path: &Path) -> String {
let existing = env::var(env_var).unwrap_or_default();
let path_str: String = String::from(path.to_str().unwrap());
let combined = if existing.is_empty() {
path_str
} else {
format!("{}{}{}", existing, separator, path_str)
};
format!("{}={}", env_var, combined)
}
fn set_rpath(lib_path: &Path) {
let path_str = lib_path.to_str().unwrap();
#[cfg(target_os = "linux")]
{
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", path_str);
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN/../lib");
}
#[cfg(target_os = "macos")]
{
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", path_str);
println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path");
println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path/../lib");
}
#[cfg(target_os = "aix")]
{
println!("cargo:rustc-link-arg=-Wl,-blibpath:{}:/usr/lib:/lib", path_str);
}
#[cfg(target_os = "windows")]
{
println!(
"cargo:warning=On Windows, make sure {} is in your PATH or next to your executable",
path_str
);
}
}
pub fn build() {
if let Ok(val) = env::var("PROJECT_OUTPUT_DIR") {
println!("cargo:rustc-link-search=native={val}");
match env::var("CMAKE_BUILD_TYPE") {
Ok(val) => {
if val == "Debug" {
println!("cargo:rustc-link-lib=dylib=metacalld");
} else {
println!("cargo:rustc-link-lib=dylib=metacall");
}
}
Err(_) => {
println!("cargo:rustc-link-lib=dylib=metacall");
}
}
} else {
match find_metacall_library() {
Ok(lib_path) => {
println!("cargo:rustc-link-search=native={}", lib_path.path.display());
println!("cargo:rustc-link-lib=dylib={}", lib_path.library);
set_rpath(&lib_path.path);
#[cfg(target_os = "linux")]
const ENV_VAR: &str = "LD_LIBRARY_PATH";
#[cfg(target_os = "macos")]
const ENV_VAR: &str = "DYLD_LIBRARY_PATH";
#[cfg(target_os = "windows")]
const ENV_VAR: &str = "PATH";
#[cfg(target_os = "aix")]
const ENV_VAR: &str = "LIBPATH";
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "aix"))]
const SEPARATOR: &str = ":";
#[cfg(target_os = "windows")]
const SEPARATOR: &str = ";";
println!(
"cargo:rustc-env={}",
define_library_search_path(ENV_VAR, SEPARATOR, &lib_path.path)
);
}
Err(e) => {
eprintln!(
"Failed to find MetaCall library with: {e} \
Still trying to link in case the library is in system paths"
);
println!("cargo:rustc-link-lib=dylib=metacall")
}
}
}
}