Source code

Revision control

Copy as Markdown

Other Tools

// 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 "),↩
)↩
}↩
}↩
}↩
#[cfg(test)]↩
pub static RUN_COMMAND_MOCK: std::sync::Mutex<↩
Option<Box<dyn Fn(&str, &str, &[&str]) -> Option<String> + 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<String> {↩
#[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<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↩
//================================================↩
// 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.↩
("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::<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 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::<Vec<_>>()↩
} 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↩
}↩