| // SPDX-License-Identifier: Apache-2.0 |
| |
| extern crate glob; |
| |
| use std::cell::RefCell; |
| use std::collections::HashMap; |
| use std::env; |
| use std::path::{Path, PathBuf}; |
| use std::process::Command; |
| |
| use glob::{MatchOptions, Pattern}; |
| |
| //================================================ |
| // Commands |
| //================================================ |
| |
| thread_local! { |
| /// The errors encountered by the build script while executing commands. |
| static COMMAND_ERRORS: RefCell<HashMap<String, Vec<String>>> = RefCell::default(); |
| } |
| |
| /// Adds an error encountered by the build script while executing a command. |
| fn add_command_error(name: &str, path: &str, arguments: &[&str], message: String) { |
| COMMAND_ERRORS.with(|e| { |
| e.borrow_mut() |
| .entry(name.into()) |
| .or_insert_with(Vec::new) |
| .push(format!( |
| "couldn't execute `{} {}` (path={}) ({})", |
| name, |
| arguments.join(" "), |
| path, |
| message, |
| )) |
| }); |
| } |
| |
| /// A struct that prints the errors encountered by the build script while |
| /// executing commands when dropped (unless explictly discarded). |
| /// |
| /// This is handy because we only want to print these errors when the build |
| /// script fails to link to an instance of `libclang`. For example, if |
| /// `llvm-config` couldn't be executed but an instance of `libclang` was found |
| /// anyway we don't want to pollute the build output with irrelevant errors. |
| #[derive(Default)] |
| pub struct CommandErrorPrinter { |
| discard: bool, |
| } |
| |
| impl CommandErrorPrinter { |
| pub fn discard(mut self) { |
| self.discard = true; |
| } |
| } |
| |
| impl Drop for CommandErrorPrinter { |
| fn drop(&mut self) { |
| if self.discard { |
| return; |
| } |
| |
| let errors = COMMAND_ERRORS.with(|e| e.borrow().clone()); |
| |
| if let Some(errors) = errors.get("llvm-config") { |
| println!( |
| "cargo:warning=could not execute `llvm-config` one or more \ |
| times, if the LLVM_CONFIG_PATH environment variable is set to \ |
| a full path to valid `llvm-config` executable it will be used \ |
| to try to find an instance of `libclang` on your system: {}", |
| errors |
| .iter() |
| .map(|e| format!("\"{}\"", e)) |
| .collect::<Vec<_>>() |
| .join("\n "), |
| ) |
| } |
| |
| if let Some(errors) = errors.get("xcode-select") { |
| println!( |
| "cargo:warning=could not execute `xcode-select` one or more \ |
| times, if a valid instance of this executable is on your PATH \ |
| it will be used to try to find an instance of `libclang` on \ |
| your system: {}", |
| errors |
| .iter() |
| .map(|e| format!("\"{}\"", e)) |
| .collect::<Vec<_>>() |
| .join("\n "), |
| ) |
| } |
| } |
| } |
| |
| /// Executes a command and returns the `stdout` output if the command was |
| /// successfully executed (errors are added to `COMMAND_ERRORS`). |
| fn run_command(name: &str, path: &str, arguments: &[&str]) -> Option<String> { |
| let output = match Command::new(path).args(arguments).output() { |
| Ok(output) => output, |
| Err(error) => { |
| let message = format!("error: {}", error); |
| add_command_error(name, path, arguments, message); |
| return None; |
| } |
| }; |
| |
| if output.status.success() { |
| Some(String::from_utf8_lossy(&output.stdout).into_owned()) |
| } else { |
| let message = format!("exit code: {}", output.status); |
| add_command_error(name, path, arguments, message); |
| None |
| } |
| } |
| |
| /// Executes the `llvm-config` command and returns the `stdout` output if the |
| /// command was successfully executed (errors are added to `COMMAND_ERRORS`). |
| pub fn run_llvm_config(arguments: &[&str]) -> Option<String> { |
| let path = env::var("LLVM_CONFIG_PATH").unwrap_or_else(|_| "llvm-config".into()); |
| run_command("llvm-config", &path, arguments) |
| } |
| |
| /// Executes the `xcode-select` command and returns the `stdout` output if the |
| /// command was successfully executed (errors are added to `COMMAND_ERRORS`). |
| pub fn run_xcode_select(arguments: &[&str]) -> Option<String> { |
| run_command("xcode-select", "xcode-select", arguments) |
| } |
| |
| //================================================ |
| // Search Directories |
| //================================================ |
| |
| /// `libclang` directory patterns for Haiku. |
| const DIRECTORIES_HAIKU: &[&str] = &[ |
| "/boot/system/lib", |
| "/boot/system/develop/lib", |
| "/boot/system/non-packaged/lib", |
| "/boot/system/non-packaged/develop/lib", |
| "/boot/home/config/non-packaged/lib", |
| "/boot/home/config/non-packaged/develop/lib", |
| ]; |
| |
| /// `libclang` directory patterns for Linux (and FreeBSD). |
| const DIRECTORIES_LINUX: &[&str] = &[ |
| "/usr/lib*", |
| "/usr/lib*/*", |
| "/usr/lib*/*/*", |
| "/usr/local/lib*", |
| "/usr/local/lib*/*", |
| "/usr/local/lib*/*/*", |
| "/usr/local/llvm*/lib*", |
| ]; |
| |
| /// `libclang` directory patterns for macOS. |
| const DIRECTORIES_MACOS: &[&str] = &[ |
| "/usr/local/opt/llvm*/lib", |
| "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib", |
| "/Library/Developer/CommandLineTools/usr/lib", |
| "/usr/local/opt/llvm*/lib/llvm*/lib", |
| ]; |
| |
| /// `libclang` directory patterns for Windows. |
| const DIRECTORIES_WINDOWS: &[&str] = &[ |
| "C:\\LLVM\\lib", |
| "C:\\Program Files*\\LLVM\\lib", |
| "C:\\MSYS*\\MinGW*\\lib", |
| // LLVM + Clang can be installed as a component of Visual Studio. |
| // https://github.com/KyleMayes/clang-sys/issues/121 |
| "C:\\Program Files*\\Microsoft Visual Studio\\*\\BuildTools\\VC\\Tools\\Llvm\\**\\bin", |
| // LLVM + Clang can be installed using Scoop (https://scoop.sh). |
| // Other Windows package managers install LLVM + Clang to previously listed |
| // system-wide directories. |
| "C:\\Users\\*\\scoop\\apps\\llvm\\current\\bin", |
| ]; |
| |
| /// `libclang` directory patterns for illumos |
| const DIRECTORIES_ILLUMOS: &[&str] = &[ |
| "/opt/ooce/clang-*/lib", |
| "/opt/ooce/llvm-*/lib", |
| ]; |
| |
| //================================================ |
| // Searching |
| //================================================ |
| |
| /// Finds the files in a directory that match one or more filename glob patterns |
| /// and returns the paths to and filenames of those files. |
| fn search_directory(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> { |
| // Escape the specified directory in case it contains characters that have |
| // special meaning in glob patterns (e.g., `[` or `]`). |
| let directory = Pattern::escape(directory.to_str().unwrap()); |
| let directory = Path::new(&directory); |
| |
| // Join the escaped directory to the filename glob patterns to obtain |
| // complete glob patterns for the files being searched for. |
| let paths = filenames |
| .iter() |
| .map(|f| directory.join(f).to_str().unwrap().to_owned()); |
| |
| // Prevent wildcards from matching path separators to ensure that the search |
| // is limited to the specified directory. |
| let mut options = MatchOptions::new(); |
| options.require_literal_separator = true; |
| |
| paths |
| .map(|p| glob::glob_with(&p, options)) |
| .filter_map(Result::ok) |
| .flatten() |
| .filter_map(|p| { |
| let path = p.ok()?; |
| let filename = path.file_name()?.to_str().unwrap(); |
| |
| // The `libclang_shared` library has been renamed to `libclang-cpp` |
| // in Clang 10. This can cause instances of this library (e.g., |
| // `libclang-cpp.so.10`) to be matched by patterns looking for |
| // instances of `libclang`. |
| if filename.contains("-cpp.") { |
| return None; |
| } |
| |
| Some((directory.to_owned(), filename.into())) |
| }) |
| .collect::<Vec<_>>() |
| } |
| |
| /// Finds the files in a directory (and any relevant sibling directories) that |
| /// match one or more filename glob patterns and returns the paths to and |
| /// filenames of those files. |
| fn search_directories(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> { |
| let mut results = search_directory(directory, filenames); |
| |
| // On Windows, `libclang.dll` is usually found in the LLVM `bin` directory |
| // while `libclang.lib` is usually found in the LLVM `lib` directory. To |
| // keep things consistent with other platforms, only LLVM `lib` directories |
| // are included in the backup search directory globs so we need to search |
| // the LLVM `bin` directory here. |
| if cfg!(target_os = "windows") && directory.ends_with("lib") { |
| let sibling = directory.parent().unwrap().join("bin"); |
| results.extend(search_directory(&sibling, filenames).into_iter()); |
| } |
| |
| results |
| } |
| |
| /// Finds the `libclang` static or dynamic libraries matching one or more |
| /// filename glob patterns and returns the paths to and filenames of those files. |
| pub fn search_libclang_directories(filenames: &[String], variable: &str) -> Vec<(PathBuf, String)> { |
| // Search only the path indicated by the relevant environment variable |
| // (e.g., `LIBCLANG_PATH`) if it is set. |
| if let Ok(path) = env::var(variable).map(|d| Path::new(&d).to_path_buf()) { |
| // Check if the path is a matching file. |
| if let Some(parent) = path.parent() { |
| let filename = path.file_name().unwrap().to_str().unwrap(); |
| let libraries = search_directories(parent, filenames); |
| if libraries.iter().any(|(_, f)| f == filename) { |
| return vec![(parent.into(), filename.into())]; |
| } |
| } |
| |
| // Check if the path is directory containing a matching file. |
| return search_directories(&path, filenames); |
| } |
| |
| let mut found = vec![]; |
| |
| // Search the `bin` and `lib` directories in the directory returned by |
| // `llvm-config --prefix`. |
| if let Some(output) = run_llvm_config(&["--prefix"]) { |
| let directory = Path::new(output.lines().next().unwrap()).to_path_buf(); |
| found.extend(search_directories(&directory.join("bin"), filenames)); |
| found.extend(search_directories(&directory.join("lib"), filenames)); |
| found.extend(search_directories(&directory.join("lib64"), filenames)); |
| } |
| |
| // Search the toolchain directory in the directory returned by |
| // `xcode-select --print-path`. |
| if cfg!(target_os = "macos") { |
| if let Some(output) = run_xcode_select(&["--print-path"]) { |
| let directory = Path::new(output.lines().next().unwrap()).to_path_buf(); |
| let directory = directory.join("Toolchains/XcodeDefault.xctoolchain/usr/lib"); |
| found.extend(search_directories(&directory, filenames)); |
| } |
| } |
| |
| // Search the directories in the `LD_LIBRARY_PATH` environment variable. |
| if let Ok(path) = env::var("LD_LIBRARY_PATH") { |
| for directory in env::split_paths(&path) { |
| found.extend(search_directories(&directory, filenames)); |
| } |
| } |
| |
| // Determine the `libclang` directory patterns. |
| let directories = if cfg!(target_os = "haiku") { |
| DIRECTORIES_HAIKU |
| } else if cfg!(any(target_os = "linux", target_os = "freebsd")) { |
| DIRECTORIES_LINUX |
| } else if cfg!(target_os = "macos") { |
| DIRECTORIES_MACOS |
| } else if cfg!(target_os = "windows") { |
| DIRECTORIES_WINDOWS |
| } else if cfg!(target_os = "illumos") { |
| DIRECTORIES_ILLUMOS |
| } else { |
| &[] |
| }; |
| |
| // Search the directories provided by the `libclang` directory patterns. |
| let mut options = MatchOptions::new(); |
| options.case_sensitive = false; |
| options.require_literal_separator = true; |
| for directory in directories.iter().rev() { |
| if let Ok(directories) = glob::glob_with(directory, options) { |
| for directory in directories.filter_map(Result::ok).filter(|p| p.is_dir()) { |
| found.extend(search_directories(&directory, filenames)); |
| } |
| } |
| } |
| |
| found |
| } |