Skip to main content

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
19pub struct LibraryPath {
20    /// Path to the library for linking (where .lib/.so/.dylib is)
21    pub path: PathBuf,
22    /// Library name for linking
23    pub library: String,
24    /// Path for runtime search (where .dll/.so/.dylib is for PATH/LD_LIBRARY_PATH)
25    pub search: PathBuf,
26}
27
28/// Find files recursively in a directory matching filename
29fn find_files_recursively<P: AsRef<Path>>(
30    root_dir: P,
31    filename: &str,
32    max_depth: Option<usize>,
33) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
34    let mut matches = Vec::new();
35    let mut stack = vec![(root_dir.as_ref().to_path_buf(), 0)];
36
37    while let Some((current_dir, depth)) = stack.pop() {
38        if let Some(max) = max_depth {
39            if depth > max {
40                continue;
41            }
42        }
43
44        if let Ok(entries) = fs::read_dir(&current_dir) {
45            for entry in entries.flatten() {
46                let path = entry.path();
47
48                if path.is_file() {
49                    // Simple filename comparison
50                    if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
51                        if file_name == filename {
52                            matches.push(path);
53                        }
54                    }
55                } else if path.is_dir() {
56                    stack.push((path, depth + 1));
57                }
58            }
59        }
60    }
61
62    Ok(matches)
63}
64
65fn platform_install_paths() -> Result<InstallPath, Box<dyn std::error::Error>> {
66    if cfg!(target_os = "windows") {
67        // Defaults to path: C:\Users\Default\AppData\Local
68        let local_app_data = env::var("LOCALAPPDATA")
69            .unwrap_or_else(|_| String::from("C:\\Users\\Default\\AppData\\Local"));
70
71        Ok(InstallPath {
72            paths: vec![PathBuf::from(local_app_data)
73                .join("MetaCall")
74                .join("metacall")],
75            names: vec!["metacall.lib", "metacalld.lib"],
76        })
77    } else if cfg!(target_os = "macos") {
78        Ok(InstallPath {
79            paths: vec![
80                PathBuf::from("/opt/homebrew/lib/"),
81                PathBuf::from("/usr/local/lib/"),
82            ],
83            names: vec!["libmetacall.dylib", "libmetacalld.dylib"],
84        })
85    } else if cfg!(target_os = "linux") {
86        Ok(InstallPath {
87            paths: vec![PathBuf::from("/usr/local/lib/"), PathBuf::from("/gnu/lib/")],
88            names: vec!["libmetacall.so", "libmetacalld.so"],
89        })
90    } else {
91        Err(format!("Platform {} not supported", env::consts::OS).into())
92    }
93}
94
95/// Get search paths, checking for custom installation path first
96fn get_search_config() -> Result<InstallPath, Box<dyn std::error::Error>> {
97    // First, check if user specified a custom path
98    if let Ok(custom_path) = env::var("METACALL_INSTALL_PATH") {
99        // For custom paths, we need to search for any metacall library variant
100        return Ok(InstallPath {
101            paths: vec![PathBuf::from(custom_path)],
102            names: vec![
103                "libmetacall.so",
104                "libmetacalld.so",
105                "libmetacall.dylib",
106                "libmetacalld.dylib",
107                "metacall.lib",
108                "metacalld.lib",
109            ],
110        });
111    }
112
113    // Fall back to platform-specific paths
114    platform_install_paths()
115}
116
117/// Get the parent path and library name
118fn get_parent_and_library(path: &Path) -> Option<(PathBuf, String)> {
119    let parent = path.parent()?.to_path_buf();
120
121    // Get the file stem (filename without extension)
122    let stem = path.file_stem()?.to_str()?;
123
124    // Remove "lib" prefix if present
125    let cleaned_stem = stem.strip_prefix("lib").unwrap_or(stem).to_string();
126
127    Some((parent, cleaned_stem))
128}
129
130/// Strip the Windows extended-length path prefix (\\?\) if present
131/// fs::canonicalize() on Windows returns paths with this prefix which can cause issues
132#[cfg(target_os = "windows")]
133fn strip_extended_length_prefix(path: PathBuf) -> PathBuf {
134    let path_str = path.to_string_lossy();
135    if let Some(stripped) = path_str.strip_prefix(r"\\?\") {
136        PathBuf::from(stripped)
137    } else {
138        path
139    }
140}
141
142/// Find the runtime DLL on Windows
143/// This searches for metacall.dll or metacalld.dll recursively
144#[cfg(target_os = "windows")]
145fn find_metacall_dll(
146    search_paths: &[PathBuf],
147    library_name: &str,
148) -> Result<PathBuf, Box<dyn std::error::Error>> {
149    // Determine the DLL name based on the library name (metacall or metacalld)
150    let dll_name = format!("{}.dll", library_name);
151
152    for search_path in search_paths {
153        match find_files_recursively(search_path, &dll_name, None) {
154            Ok(files) if !files.is_empty() => {
155                let found_dll = fs::canonicalize(&files[0])?;
156                if let Some(parent) = found_dll.parent() {
157                    return Ok(strip_extended_length_prefix(parent.to_path_buf()));
158                }
159            }
160            _ => continue,
161        }
162    }
163
164    Err(format!(
165        "MetaCall DLL ({}) not found. Searched in: {}",
166        dll_name,
167        search_paths
168            .iter()
169            .map(|p| p.display().to_string())
170            .collect::<Vec<_>>()
171            .join(", ")
172    )
173    .into())
174}
175
176/// Find the MetaCall library
177/// This orchestrates the search process
178pub fn find_metacall_library() -> Result<LibraryPath, Box<dyn std::error::Error>> {
179    let search_config = get_search_config()?;
180
181    // Search in each configured path
182    for search_path in &search_config.paths {
183        for name in &search_config.names {
184            // Search with no limit in depth
185            match find_files_recursively(search_path, name, None) {
186                Ok(files) if !files.is_empty() => {
187                    let found_lib = fs::canonicalize(&files[0])?;
188
189                    match get_parent_and_library(&found_lib) {
190                        Some((parent, library_name)) => {
191                            // On Windows, strip the extended-length path prefix and find DLL separately
192                            #[cfg(target_os = "windows")]
193                            let (lib_path, search_path) = {
194                                let cleaned_parent = strip_extended_length_prefix(parent);
195                                let dll_search = match find_metacall_dll(
196                                    &search_config.paths,
197                                    &library_name,
198                                ) {
199                                    Ok(dll_path) => dll_path,
200                                    Err(e) => {
201                                        println!(
202                                            "cargo:warning=Could not find DLL, using library path: {}",
203                                            e
204                                        );
205                                        cleaned_parent.clone()
206                                    }
207                                };
208                                (cleaned_parent, dll_search)
209                            };
210
211                            // On non-Windows platforms, the shared library is used for both
212                            // linking and runtime, so search path is the same as lib path
213                            #[cfg(not(target_os = "windows"))]
214                            let (lib_path, search_path) = (parent.clone(), parent);
215
216                            return Ok(LibraryPath {
217                                path: lib_path,
218                                library: library_name,
219                                search: search_path,
220                            });
221                        }
222                        None => continue,
223                    };
224                }
225                Ok(_) => {
226                    // No files found in this path, continue searching
227                    continue;
228                }
229                Err(e) => {
230                    println!(
231                        "cargo:warning=Error searching in {}: {}",
232                        search_path.display(),
233                        e
234                    );
235                    continue;
236                }
237            }
238        }
239    }
240
241    // If we get here, library wasn't found
242    let search_paths: Vec<String> = search_config
243        .paths
244        .iter()
245        .map(|p| p.display().to_string())
246        .collect();
247
248    Err(format!(
249        "MetaCall library not found. Searched in: {}. \
250        If you have it installed elsewhere, set METACALL_INSTALL_PATH environment variable.",
251        search_paths.join(", ")
252    )
253    .into())
254}
255
256fn define_library_search_path(env_var: &str, separator: &str, path: &Path) -> String {
257    // Get the current value of the env var, if any
258    let existing = env::var(env_var).unwrap_or_default();
259    let path_str: String = String::from(path.to_str().unwrap());
260
261    // Append to it
262    let combined = if existing.is_empty() {
263        path_str
264    } else {
265        format!("{}{}{}", existing, separator, path_str)
266    };
267
268    format!("{}={}", env_var, combined)
269}
270
271/// Set RPATH for runtime library discovery
272/// This binaries work outside cargo
273fn set_rpath(lib_path: &Path) {
274    let path_str = lib_path.to_str().unwrap();
275
276    #[cfg(target_os = "linux")]
277    {
278        // On Linux, use RPATH with $ORIGIN for relocatable binaries
279        println!("cargo:rustc-link-arg=-Wl,-rpath,{}", path_str);
280        // Also set a backup rpath relative to the executable location
281        println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
282        println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN/../lib");
283    }
284
285    #[cfg(target_os = "macos")]
286    {
287        // On macOS, use @rpath and @loader_path
288        println!("cargo:rustc-link-arg=-Wl,-rpath,{}", path_str);
289        // Also set loader-relative paths for relocatable binaries
290        println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path");
291        println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path/../lib");
292    }
293
294    #[cfg(target_os = "aix")]
295    {
296        // Add default system library paths to avoid breaking standard lookup
297        println!(
298            "cargo:rustc-link-arg=-Wl,-blibpath:{}:/usr/lib:/lib",
299            path_str
300        );
301    }
302
303    #[cfg(target_os = "windows")]
304    {
305        // Windows doesn't use RPATH, but we can inform the user
306        println!(
307            "cargo:warning=On Windows, make sure {} is in your PATH or next to your executable",
308            path_str
309        );
310    }
311}
312
313pub fn build() {
314    // When running tests from CMake
315    if let Ok(val) = env::var("PROJECT_OUTPUT_DIR") {
316        // Link search path to build folder
317        println!("cargo:rustc-link-search=native={val}");
318
319        // Link against correct version of metacall
320        match env::var("CMAKE_BUILD_TYPE") {
321            Ok(val) => {
322                if val == "Debug" {
323                    // Try to link the debug version when running tests
324                    println!("cargo:rustc-link-lib=dylib=metacalld");
325                } else {
326                    println!("cargo:rustc-link-lib=dylib=metacall");
327                }
328            }
329            Err(_) => {
330                println!("cargo:rustc-link-lib=dylib=metacall");
331            }
332        }
333    } else {
334        // When building from Cargo, try to find MetaCall
335        match find_metacall_library() {
336            Ok(lib_path) => {
337                // Define linker flags
338                println!("cargo:rustc-link-search=native={}", lib_path.path.display());
339                println!("cargo:rustc-link-lib=dylib={}", lib_path.library);
340
341                // Set RPATH so the binary can find libraries at runtime
342                set_rpath(&lib_path.path);
343
344                // Set the runtime environment variable for finding the library during tests
345                #[cfg(target_os = "linux")]
346                const ENV_VAR: &str = "LD_LIBRARY_PATH";
347
348                #[cfg(target_os = "macos")]
349                const ENV_VAR: &str = "DYLD_LIBRARY_PATH";
350
351                #[cfg(target_os = "windows")]
352                const ENV_VAR: &str = "PATH";
353
354                #[cfg(target_os = "aix")]
355                const ENV_VAR: &str = "LIBPATH";
356
357                #[cfg(any(target_os = "linux", target_os = "macos", target_os = "aix"))]
358                const SEPARATOR: &str = ":";
359
360                #[cfg(target_os = "windows")]
361                const SEPARATOR: &str = ";";
362
363                println!(
364                    "cargo:rustc-env={}",
365                    define_library_search_path(ENV_VAR, SEPARATOR, &lib_path.search)
366                );
367
368                println!(
369                    "Library {} found in: {} with runtime search path: {}",
370                    lib_path.library,
371                    lib_path.path.display(),
372                    lib_path.search.display()
373                );
374            }
375            Err(e) => {
376                // Print the error
377                println!("cargo:warning={e}");
378                std::process::exit(1);
379            }
380        }
381    }
382}