1// SPDX-License-Identifier: Apache-2.0 2 3extern crate glob; 4 5use std::cell::RefCell; 6use std::collections::HashMap; 7use std::env; 8use std::path::{Path, PathBuf}; 9use std::process::Command; 10 11use glob::{MatchOptions, Pattern}; 12 13//================================================ 14// Commands 15//================================================ 16 17thread_local! { 18 /// The errors encountered by the build script while executing commands. 19 static COMMAND_ERRORS: RefCell<HashMap<String, Vec<String>>> = RefCell::default(); 20} 21 22/// Adds an error encountered by the build script while executing a command. 23fn add_command_error(name: &str, path: &str, arguments: &[&str], message: String) { 24 COMMAND_ERRORS.with(|e| { 25 e.borrow_mut() 26 .entry(name.into()) 27 .or_insert_with(Vec::new) 28 .push(format!( 29 "couldn't execute `{} {}` (path={}) ({})", 30 name, 31 arguments.join(" "), 32 path, 33 message, 34 )) 35 }); 36} 37 38/// A struct that prints the errors encountered by the build script while 39/// executing commands when dropped (unless explictly discarded). 40/// 41/// This is handy because we only want to print these errors when the build 42/// script fails to link to an instance of `libclang`. For example, if 43/// `llvm-config` couldn't be executed but an instance of `libclang` was found 44/// anyway we don't want to pollute the build output with irrelevant errors. 45#[derive(Default)] 46pub struct CommandErrorPrinter { 47 discard: bool, 48} 49 50impl CommandErrorPrinter { 51 pub fn discard(mut self) { 52 self.discard = true; 53 } 54} 55 56impl Drop for CommandErrorPrinter { 57 fn drop(&mut self) { 58 if self.discard { 59 return; 60 } 61 62 let errors = COMMAND_ERRORS.with(|e| e.borrow().clone()); 63 64 if let Some(errors) = errors.get("llvm-config") { 65 println!( 66 "cargo:warning=could not execute `llvm-config` one or more \ 67 times, if the LLVM_CONFIG_PATH environment variable is set to \ 68 a full path to valid `llvm-config` executable it will be used \ 69 to try to find an instance of `libclang` on your system: {}", 70 errors 71 .iter() 72 .map(|e| format!("\"{}\"", e)) 73 .collect::<Vec<_>>() 74 .join("\n "), 75 ) 76 } 77 78 if let Some(errors) = errors.get("xcode-select") { 79 println!( 80 "cargo:warning=could not execute `xcode-select` one or more \ 81 times, if a valid instance of this executable is on your PATH \ 82 it will be used to try to find an instance of `libclang` on \ 83 your system: {}", 84 errors 85 .iter() 86 .map(|e| format!("\"{}\"", e)) 87 .collect::<Vec<_>>() 88 .join("\n "), 89 ) 90 } 91 } 92} 93 94/// Executes a command and returns the `stdout` output if the command was 95/// successfully executed (errors are added to `COMMAND_ERRORS`). 96fn run_command(name: &str, path: &str, arguments: &[&str]) -> Option<String> { 97 let output = match Command::new(path).args(arguments).output() { 98 Ok(output) => output, 99 Err(error) => { 100 let message = format!("error: {}", error); 101 add_command_error(name, path, arguments, message); 102 return None; 103 } 104 }; 105 106 if output.status.success() { 107 Some(String::from_utf8_lossy(&output.stdout).into_owned()) 108 } else { 109 let message = format!("exit code: {}", output.status); 110 add_command_error(name, path, arguments, message); 111 None 112 } 113} 114 115/// Executes the `llvm-config` command and returns the `stdout` output if the 116/// command was successfully executed (errors are added to `COMMAND_ERRORS`). 117pub fn run_llvm_config(arguments: &[&str]) -> Option<String> { 118 let path = env::var("LLVM_CONFIG_PATH").unwrap_or_else(|_| "llvm-config".into()); 119 run_command("llvm-config", &path, arguments) 120} 121 122/// Executes the `xcode-select` command and returns the `stdout` output if the 123/// command was successfully executed (errors are added to `COMMAND_ERRORS`). 124pub fn run_xcode_select(arguments: &[&str]) -> Option<String> { 125 run_command("xcode-select", "xcode-select", arguments) 126} 127 128//================================================ 129// Search Directories 130//================================================ 131 132/// `libclang` directory patterns for Haiku. 133const DIRECTORIES_HAIKU: &[&str] = &[ 134 "/boot/system/lib", 135 "/boot/system/develop/lib", 136 "/boot/system/non-packaged/lib", 137 "/boot/system/non-packaged/develop/lib", 138 "/boot/home/config/non-packaged/lib", 139 "/boot/home/config/non-packaged/develop/lib", 140]; 141 142/// `libclang` directory patterns for Linux (and FreeBSD). 143const DIRECTORIES_LINUX: &[&str] = &[ 144 "/usr/lib*", 145 "/usr/lib*/*", 146 "/usr/lib*/*/*", 147 "/usr/local/lib*", 148 "/usr/local/lib*/*", 149 "/usr/local/lib*/*/*", 150 "/usr/local/llvm*/lib*", 151]; 152 153/// `libclang` directory patterns for macOS. 154const DIRECTORIES_MACOS: &[&str] = &[ 155 "/usr/local/opt/llvm*/lib", 156 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib", 157 "/Library/Developer/CommandLineTools/usr/lib", 158 "/usr/local/opt/llvm*/lib/llvm*/lib", 159]; 160 161/// `libclang` directory patterns for Windows. 162const DIRECTORIES_WINDOWS: &[&str] = &[ 163 "C:\\LLVM\\lib", 164 "C:\\Program Files*\\LLVM\\lib", 165 "C:\\MSYS*\\MinGW*\\lib", 166 // LLVM + Clang can be installed as a component of Visual Studio. 167 // https://github.com/KyleMayes/clang-sys/issues/121 168 "C:\\Program Files*\\Microsoft Visual Studio\\*\\BuildTools\\VC\\Tools\\Llvm\\**\\bin", 169 // LLVM + Clang can be installed using Scoop (https://scoop.sh). 170 // Other Windows package managers install LLVM + Clang to previously listed 171 // system-wide directories. 172 "C:\\Users\\*\\scoop\\apps\\llvm\\current\\bin", 173]; 174 175/// `libclang` directory patterns for illumos 176const DIRECTORIES_ILLUMOS: &[&str] = &[ 177 "/opt/ooce/clang-*/lib", 178 "/opt/ooce/llvm-*/lib", 179]; 180 181//================================================ 182// Searching 183//================================================ 184 185/// Finds the files in a directory that match one or more filename glob patterns 186/// and returns the paths to and filenames of those files. 187fn search_directory(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> { 188 // Escape the specified directory in case it contains characters that have 189 // special meaning in glob patterns (e.g., `[` or `]`). 190 let directory = Pattern::escape(directory.to_str().unwrap()); 191 let directory = Path::new(&directory); 192 193 // Join the escaped directory to the filename glob patterns to obtain 194 // complete glob patterns for the files being searched for. 195 let paths = filenames 196 .iter() 197 .map(|f| directory.join(f).to_str().unwrap().to_owned()); 198 199 // Prevent wildcards from matching path separators to ensure that the search 200 // is limited to the specified directory. 201 let mut options = MatchOptions::new(); 202 options.require_literal_separator = true; 203 204 paths 205 .map(|p| glob::glob_with(&p, options)) 206 .filter_map(Result::ok) 207 .flatten() 208 .filter_map(|p| { 209 let path = p.ok()?; 210 let filename = path.file_name()?.to_str().unwrap(); 211 212 // The `libclang_shared` library has been renamed to `libclang-cpp` 213 // in Clang 10. This can cause instances of this library (e.g., 214 // `libclang-cpp.so.10`) to be matched by patterns looking for 215 // instances of `libclang`. 216 if filename.contains("-cpp.") { 217 return None; 218 } 219 220 Some((directory.to_owned(), filename.into())) 221 }) 222 .collect::<Vec<_>>() 223} 224 225/// Finds the files in a directory (and any relevant sibling directories) that 226/// match one or more filename glob patterns and returns the paths to and 227/// filenames of those files. 228fn search_directories(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> { 229 let mut results = search_directory(directory, filenames); 230 231 // On Windows, `libclang.dll` is usually found in the LLVM `bin` directory 232 // while `libclang.lib` is usually found in the LLVM `lib` directory. To 233 // keep things consistent with other platforms, only LLVM `lib` directories 234 // are included in the backup search directory globs so we need to search 235 // the LLVM `bin` directory here. 236 if cfg!(target_os = "windows") && directory.ends_with("lib") { 237 let sibling = directory.parent().unwrap().join("bin"); 238 results.extend(search_directory(&sibling, filenames).into_iter()); 239 } 240 241 results 242} 243 244/// Finds the `libclang` static or dynamic libraries matching one or more 245/// filename glob patterns and returns the paths to and filenames of those files. 246pub fn search_libclang_directories(filenames: &[String], variable: &str) -> Vec<(PathBuf, String)> { 247 // Search only the path indicated by the relevant environment variable 248 // (e.g., `LIBCLANG_PATH`) if it is set. 249 if let Ok(path) = env::var(variable).map(|d| Path::new(&d).to_path_buf()) { 250 // Check if the path is a matching file. 251 if let Some(parent) = path.parent() { 252 let filename = path.file_name().unwrap().to_str().unwrap(); 253 let libraries = search_directories(parent, filenames); 254 if libraries.iter().any(|(_, f)| f == filename) { 255 return vec![(parent.into(), filename.into())]; 256 } 257 } 258 259 // Check if the path is directory containing a matching file. 260 return search_directories(&path, filenames); 261 } 262 263 let mut found = vec![]; 264 265 // Search the `bin` and `lib` directories in the directory returned by 266 // `llvm-config --prefix`. 267 if let Some(output) = run_llvm_config(&["--prefix"]) { 268 let directory = Path::new(output.lines().next().unwrap()).to_path_buf(); 269 found.extend(search_directories(&directory.join("bin"), filenames)); 270 found.extend(search_directories(&directory.join("lib"), filenames)); 271 found.extend(search_directories(&directory.join("lib64"), filenames)); 272 } 273 274 // Search the toolchain directory in the directory returned by 275 // `xcode-select --print-path`. 276 if cfg!(target_os = "macos") { 277 if let Some(output) = run_xcode_select(&["--print-path"]) { 278 let directory = Path::new(output.lines().next().unwrap()).to_path_buf(); 279 let directory = directory.join("Toolchains/XcodeDefault.xctoolchain/usr/lib"); 280 found.extend(search_directories(&directory, filenames)); 281 } 282 } 283 284 // Search the directories in the `LD_LIBRARY_PATH` environment variable. 285 if let Ok(path) = env::var("LD_LIBRARY_PATH") { 286 for directory in env::split_paths(&path) { 287 found.extend(search_directories(&directory, filenames)); 288 } 289 } 290 291 // Determine the `libclang` directory patterns. 292 let directories = if cfg!(target_os = "haiku") { 293 DIRECTORIES_HAIKU 294 } else if cfg!(any(target_os = "linux", target_os = "freebsd")) { 295 DIRECTORIES_LINUX 296 } else if cfg!(target_os = "macos") { 297 DIRECTORIES_MACOS 298 } else if cfg!(target_os = "windows") { 299 DIRECTORIES_WINDOWS 300 } else if cfg!(target_os = "illumos") { 301 DIRECTORIES_ILLUMOS 302 } else { 303 &[] 304 }; 305 306 // Search the directories provided by the `libclang` directory patterns. 307 let mut options = MatchOptions::new(); 308 options.case_sensitive = false; 309 options.require_literal_separator = true; 310 for directory in directories.iter().rev() { 311 if let Ok(directories) = glob::glob_with(directory, options) { 312 for directory in directories.filter_map(Result::ok).filter(|p| p.is_dir()) { 313 found.extend(search_directories(&directory, filenames)); 314 } 315 } 316 } 317 318 found 319} 320