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