320 lines
12 KiB
Rust
320 lines
12 KiB
Rust
// 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
|
|
}
|