metacall_sys/
lib.rs

1use std::{
2    env, fs,
3    path::{Path, PathBuf},
4    vec,
5};
6
7// Search for MetaCall libraries in platform-specific locations
8// Handle custom installation paths via environment variables
9// Find configuration files recursively
10// Provide helpful error messages when things aren't found
11
12/// Represents the install paths for a platform
13struct InstallPath {
14    paths: Vec<PathBuf>,
15    names: Vec<&'static str>,
16}
17
18/// Represents the match of a library when it's found
19struct LibraryPath {
20    path: PathBuf,
21    library: String,
22}
23
24/// Find files recursively in a directory matching a pattern
25fn find_files_recursively<P: AsRef<Path>>(
26    root_dir: P,
27    filename: &str,
28    max_depth: Option<usize>,
29) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
30    let mut matches = Vec::new();
31    let mut stack = vec![(root_dir.as_ref().to_path_buf(), 0)];
32
33    while let Some((current_dir, depth)) = stack.pop() {
34        if let Some(max) = max_depth {
35            if depth > max {
36                continue;
37            }
38        }
39
40        if let Ok(entries) = fs::read_dir(&current_dir) {
41            for entry in entries.flatten() {
42                let path = entry.path();
43
44                if path.is_file() {
45                    // Simple filename comparison instead of regex
46                    if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
47                        if file_name == filename {
48                            matches.push(path);
49                        }
50                    }
51                } else if path.is_dir() {
52                    stack.push((path, depth + 1));
53                }
54            }
55        }
56    }
57
58    Ok(matches)
59}
60
61fn platform_install_paths() -> Result<InstallPath, Box<dyn std::error::Error>> {
62    if cfg!(target_os = "windows") {
63        // Defaults to path: C:\Users\Default\AppData\Local
64        let local_app_data = env::var("LOCALAPPDATA")
65            .unwrap_or_else(|_| String::from("C:\\Users\\Default\\AppData\\Local"));
66
67        Ok(InstallPath {
68            paths: vec![PathBuf::from(local_app_data)
69                .join("MetaCall")
70                .join("metacall")],
71            names: vec!["metacall.lib"],
72        })
73    } else if cfg!(target_os = "macos") {
74        Ok(InstallPath {
75            paths: vec![
76                PathBuf::from("/opt/homebrew/lib/"),
77                PathBuf::from("/usr/local/lib/"),
78            ],
79            names: vec!["libmetacall.dylib"],
80        })
81    } else if cfg!(target_os = "linux") {
82        Ok(InstallPath {
83            paths: vec![PathBuf::from("/usr/local/lib/"), PathBuf::from("/gnu/lib/")],
84            names: vec!["libmetacall.so"],
85        })
86    } else {
87        Err(format!("Platform {} not supported", env::consts::OS).into())
88    }
89}
90
91/// Get search paths, checking for custom installation path first
92fn get_search_config() -> Result<InstallPath, Box<dyn std::error::Error>> {
93    // First, check if user specified a custom path
94    if let Ok(custom_path) = env::var("METACALL_INSTALL_PATH") {
95        // For custom paths, we need to search for any metacall library variant
96        return Ok(InstallPath {
97            paths: vec![PathBuf::from(custom_path)],
98            names: vec![
99                "libmetacall.so",
100                "libmetacalld.so",
101                "libmetacall.dylib",
102                "libmetacalld.dylib",
103                "metacall.lib",
104                "metacalld.lib",
105            ],
106        });
107    }
108
109    // Fall back to platform-specific paths
110    platform_install_paths()
111}
112
113/// Get the parent path and library name
114fn get_parent_and_library(path: &Path) -> Option<(PathBuf, String)> {
115    let parent = path.parent()?.to_path_buf();
116
117    // Get the file stem (filename without extension)
118    let stem = path.file_stem()?.to_str()?;
119
120    // Remove "lib" prefix if present
121    let cleaned_stem = stem.strip_prefix("lib").unwrap_or(stem).to_string();
122
123    Some((parent, cleaned_stem))
124}
125
126/// Find the MetaCall library
127/// This orchestrates the search process
128fn find_metacall_library() -> Result<LibraryPath, Box<dyn std::error::Error>> {
129    let search_config = get_search_config()?;
130
131    // Search in each configured path
132    for search_path in &search_config.paths {
133        for name in &search_config.names {
134            // Search with no limit in depth
135            match find_files_recursively(search_path, name, None) {
136                Ok(files) if !files.is_empty() => {
137                    let found_lib = fs::canonicalize(&files[0])?;
138
139                    match get_parent_and_library(&found_lib) {
140                        Some((parent, name)) => {
141                            return Ok(LibraryPath {
142                                path: parent,
143                                library: name,
144                            })
145                        }
146                        None => continue,
147                    };
148                }
149                Ok(_) => {
150                    // No files found in this path, continue searching
151                    continue;
152                }
153                Err(e) => {
154                    eprintln!("Error searching in {}: {}", search_path.display(), e);
155                    continue;
156                }
157            }
158        }
159    }
160
161    // If we get here, library wasn't found
162    let search_paths: Vec<String> = search_config
163        .paths
164        .iter()
165        .map(|p| p.display().to_string())
166        .collect();
167
168    Err(format!(
169        "MetaCall library not found. Searched in: {}. \
170        If you have it installed elsewhere, set METACALL_INSTALL_PATH environment variable.",
171        search_paths.join(", ")
172    )
173    .into())
174}
175
176fn define_library_search_path(env_var: &str, separator: &str, path: &Path) -> String {
177    // Get the current value of the env var, if any
178    let existing = env::var(env_var).unwrap_or_default();
179    let path_str: String = String::from(path.to_str().unwrap());
180
181    // Append to it
182    let combined = if existing.is_empty() {
183        path_str
184    } else {
185        format!("{}{}{}", existing, separator, path_str)
186    };
187
188    format!("{}={}", env_var, combined)
189}
190
191pub fn build() {
192    // When running tests from CMake
193    if let Ok(val) = env::var("PROJECT_OUTPUT_DIR") {
194        // Link search path to build folder
195        println!("cargo:rustc-link-search=native={val}");
196
197        // Link against correct version of metacall
198        match env::var("CMAKE_BUILD_TYPE") {
199            Ok(val) => {
200                if val == "Debug" {
201                    // Try to link the debug version when running tests
202                    println!("cargo:rustc-link-lib=dylib=metacalld");
203                } else {
204                    println!("cargo:rustc-link-lib=dylib=metacall");
205                }
206            }
207            Err(_) => {
208                println!("cargo:rustc-link-lib=dylib=metacall");
209            }
210        }
211    } else {
212        // When building from Cargo, try to find MetaCall
213        match find_metacall_library() {
214            Ok(lib_path) => {
215                // Define linker flags
216                println!("cargo:rustc-link-search=native={}", lib_path.path.display());
217                println!("cargo:rustc-link-lib=dylib={}", lib_path.library);
218
219                // Set the runtime environment variable for finding the library during tests
220                #[cfg(target_os = "linux")]
221                const ENV_VAR: &str = "LD_LIBRARY_PATH";
222
223                #[cfg(target_os = "macos")]
224                const ENV_VAR: &str = "DYLD_LIBRARY_PATH";
225
226                #[cfg(target_os = "windows")]
227                const ENV_VAR: &str = "PATH";
228
229                #[cfg(target_os = "aix")]
230                const ENV_VAR: &str = "LIBPATH";
231
232                #[cfg(any(target_os = "linux", target_os = "macos", target_os = "aix"))]
233                const SEPARATOR: &str = ":";
234
235                #[cfg(target_os = "windows")]
236                const SEPARATOR: &str = ";";
237
238                println!(
239                    "cargo:rustc-env={}",
240                    define_library_search_path(ENV_VAR, SEPARATOR, &lib_path.path)
241                );
242            }
243            Err(e) => {
244                // Print the error
245                eprintln!(
246                    "Failed to find MetaCall library with: {e} \
247                    Still trying to link in case the library is in system paths"
248                );
249
250                // Still try to link in case the library is in system paths
251                println!("cargo:rustc-link-lib=dylib=metacall")
252            }
253        }
254    }
255}