// Copyright 2018 Kyle Mayes // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. 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; /// `libclang` directory patterns for FreeBSD and Linux. 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", ]; thread_local! { /// The errors encountered when attempting to execute console commands. static COMMAND_ERRORS: RefCell>> = RefCell::default(); } /// Executes the supplied console command, returning the `stdout` output if the /// command was successfully executed. fn run_command(name: &str, command: &str, arguments: &[&str]) -> Option { macro_rules! error { ($error:expr) => {{ COMMAND_ERRORS.with(|e| e.borrow_mut() .entry(name.into()) .or_insert_with(Vec::new) .push(format!( "couldn't execute `{} {}` ({})", command, arguments.join(" "), $error, ))); }}; } let output = match Command::new(command).args(arguments).output() { Ok(output) => output, Err(error) => { error!(format!("error: {}", error)); return None; } }; if !output.status.success() { error!(format!("exit code: {}", output.status)); return None; } Some(String::from_utf8_lossy(&output.stdout).into_owned()) } /// Executes `llvm-config`, returning the `stdout` output if the command was /// successfully executed. 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) } /// A struct that prints errors encountered when attempting to execute console /// commands on drop if not discarded. #[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 "), ) } } } /// Returns the paths to and the filenames of the files matching the supplied /// filename patterns in the supplied directory. fn search_directory(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> { // Join the directory to the filename patterns to obtain the path patterns. let paths = filenames .iter() .filter_map(|f| directory.join(f).to_str().map(ToOwned::to_owned)); // Prevent wildcards from matching path separators. let mut options = MatchOptions::new(); options.require_literal_separator = true; paths .flat_map(|p| { if let Ok(paths) = glob::glob_with(&p, options) { paths.filter_map(Result::ok).collect() } else { vec![] } }) .filter_map(|p| { let filename = p.file_name().and_then(|f| f.to_str())?; // 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::>() } /// Returns the paths to and the filenames of the files matching the supplied /// filename patterns in the supplied directory, checking any relevant sibling /// directories. 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 } /// Returns the paths to and the filenames of the `libclang` static or dynamic /// libraries matching the supplied filename patterns. pub fn search_libclang_directories(files: &[String], variable: &str) -> Vec<(PathBuf, String)> { // Use the path provided by the relevant environment variable. if let Ok(path) = env::var(variable).map(|d| Path::new(&d).to_path_buf()) { // Check if the path is referring to a matching file already. if let Some(parent) = path.parent() { let filename = path.file_name().unwrap().to_str().unwrap(); let libraries = search_directories(parent, files); if libraries.iter().any(|(_, f)| f == filename) { return vec![(parent.into(), filename.into())]; } } return search_directories(&path, files); } let mut found = vec![]; // Search the `bin` and `lib` directories in directory provided 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"), files)); found.extend(search_directories(&directory.join("lib"), files)); found.extend(search_directories(&directory.join("lib64"), files)); } // Search the toolchain directory in the directory provided by // `xcode-select --print-path`. if cfg!(target_os = "macos") { if let Some(output) = run_command("xcode-select", "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, files)); } } // Search the directories provided by the `LD_LIBRARY_PATH` environment // variable. if let Ok(path) = env::var("LD_LIBRARY_PATH") { for directory in path.split(':').map(Path::new) { found.extend(search_directories(&directory, files)); } } // Determine the `libclang` directory patterns. let directories = if cfg!(any(target_os = "freebsd", target_os = "linux")) { DIRECTORIES_LINUX } else if cfg!(target_os = "macos") { DIRECTORIES_MACOS } else if cfg!(target_os = "windows") { DIRECTORIES_WINDOWS } 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, files)); } } } found }