// 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>> = 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::>() .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::>() .join("\n "), ) } } } #[cfg(test)] pub static RUN_COMMAND_MOCK: std::sync::Mutex< Option Option + Send + Sync + 'static>>, > = std::sync::Mutex::new(None); /// 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 { #[cfg(test)] if let Some(command) = &*RUN_COMMAND_MOCK.lock().unwrap() { return command(name, path, arguments); } 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 { 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 { run_command("xcode-select", "xcode-select", arguments) } //================================================ // Search Directories //================================================ // These search directories are listed in order of // preference, so if multiple `libclang` instances // are found when searching matching directories, // the `libclang` instances from earlier // directories will be preferred (though version // takes precedence over location). //================================================ /// `libclang` directory patterns for Haiku. const DIRECTORIES_HAIKU: &[&str] = &[ "/boot/home/config/non-packaged/develop/lib", "/boot/home/config/non-packaged/lib", "/boot/system/non-packaged/develop/lib", "/boot/system/non-packaged/lib", "/boot/system/develop/lib", "/boot/system/lib", ]; /// `libclang` directory patterns for Linux (and FreeBSD). const DIRECTORIES_LINUX: &[&str] = &[ "/usr/local/llvm*/lib*", "/usr/local/lib*/*/*", "/usr/local/lib*/*", "/usr/local/lib*", "/usr/lib*/*/*", "/usr/lib*/*", "/usr/lib*", ]; /// `libclang` directory patterns for macOS. const DIRECTORIES_MACOS: &[&str] = &[ "/usr/local/opt/llvm*/lib/llvm*/lib", "/Library/Developer/CommandLineTools/usr/lib", "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib", "/usr/local/opt/llvm*/lib", ]; /// `libclang` directory patterns for Windows. /// /// The boolean indicates whether the directory pattern should be used when /// compiling for an MSVC target environment. const DIRECTORIES_WINDOWS: &[(&str, bool)] = &[ // LLVM + Clang can be installed using Scoop (https://scoop.sh). // Other Windows package managers install LLVM + Clang to other listed // system-wide directories. ("C:\\Users\\*\\scoop\\apps\\llvm\\current\\lib", true), ("C:\\MSYS*\\MinGW*\\lib", false), ("C:\\Program Files*\\LLVM\\lib", true), ("C:\\LLVM\\lib", true), // 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\\**\\lib", true), ]; /// `libclang` directory patterns for illumos const DIRECTORIES_ILLUMOS: &[&str] = &[ "/opt/ooce/llvm-*/lib", "/opt/ooce/clang-*/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::>() } /// 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 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 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: Vec<&str> = if target_os!("haiku") { DIRECTORIES_HAIKU.into() } else if target_os!("linux") || target_os!("freebsd") { DIRECTORIES_LINUX.into() } else if target_os!("macos") { DIRECTORIES_MACOS.into() } else if target_os!("windows") { let msvc = target_env!("msvc"); DIRECTORIES_WINDOWS .iter() .filter(|d| d.1 || !msvc) .map(|d| d.0) .collect() } else if target_os!("illumos") { DIRECTORIES_ILLUMOS.into() } else { vec![] }; // We use temporary directories when testing the build script so we'll // remove the prefixes that make the directories absolute. let directories = if test!() { directories .iter() .map(|d| d.strip_prefix('/').or_else(|| d.strip_prefix("C:\\")).unwrap_or(d)) .collect::>() } else { directories.into() }; // 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() { 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 }