1 // SPDX-License-Identifier: Apache-2.0
2
3 extern crate glob;
4
5 use std::cell::RefCell;
6 use std::collections::HashMap;
7 use std::env;
8 use std::path::{Path, PathBuf};
9 use std::process::Command;
10
11 use glob::{MatchOptions, Pattern};
12
13 //================================================
14 // Commands
15 //================================================
16
17 thread_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.
add_command_error(name: &str, path: &str, arguments: &[&str], message: String)23 fn 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)]
46 pub struct CommandErrorPrinter {
47 discard: bool,
48 }
49
50 impl CommandErrorPrinter {
discard(mut self)51 pub fn discard(mut self) {
52 self.discard = true;
53 }
54 }
55
56 impl Drop for CommandErrorPrinter {
drop(&mut self)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 #[cfg(test)]
95 pub static RUN_COMMAND_MOCK: std::sync::Mutex<
96 Option<Box<dyn Fn(&str, &str, &[&str]) -> Option<String> + Send + Sync + 'static>>,
97 > = std::sync::Mutex::new(None);
98
99 /// Executes a command and returns the `stdout` output if the command was
100 /// successfully executed (errors are added to `COMMAND_ERRORS`).
run_command(name: &str, path: &str, arguments: &[&str]) -> Option<String>101 fn run_command(name: &str, path: &str, arguments: &[&str]) -> Option<String> {
102 #[cfg(test)]
103 if let Some(command) = &*RUN_COMMAND_MOCK.lock().unwrap() {
104 return command(name, path, arguments);
105 }
106
107 let output = match Command::new(path).args(arguments).output() {
108 Ok(output) => output,
109 Err(error) => {
110 let message = format!("error: {}", error);
111 add_command_error(name, path, arguments, message);
112 return None;
113 }
114 };
115
116 if output.status.success() {
117 Some(String::from_utf8_lossy(&output.stdout).into_owned())
118 } else {
119 let message = format!("exit code: {}", output.status);
120 add_command_error(name, path, arguments, message);
121 None
122 }
123 }
124
125 /// Executes the `llvm-config` command and returns the `stdout` output if the
126 /// command was successfully executed (errors are added to `COMMAND_ERRORS`).
run_llvm_config(arguments: &[&str]) -> Option<String>127 pub fn run_llvm_config(arguments: &[&str]) -> Option<String> {
128 let path = env::var("LLVM_CONFIG_PATH").unwrap_or_else(|_| "llvm-config".into());
129 run_command("llvm-config", &path, arguments)
130 }
131
132 /// Executes the `xcode-select` command and returns the `stdout` output if the
133 /// command was successfully executed (errors are added to `COMMAND_ERRORS`).
run_xcode_select(arguments: &[&str]) -> Option<String>134 pub fn run_xcode_select(arguments: &[&str]) -> Option<String> {
135 run_command("xcode-select", "xcode-select", arguments)
136 }
137
138 //================================================
139 // Search Directories
140 //================================================
141 // These search directories are listed in order of
142 // preference, so if multiple `libclang` instances
143 // are found when searching matching directories,
144 // the `libclang` instances from earlier
145 // directories will be preferred (though version
146 // takes precedence over location).
147 //================================================
148
149 /// `libclang` directory patterns for Haiku.
150 const DIRECTORIES_HAIKU: &[&str] = &[
151 "/boot/home/config/non-packaged/develop/lib",
152 "/boot/home/config/non-packaged/lib",
153 "/boot/system/non-packaged/develop/lib",
154 "/boot/system/non-packaged/lib",
155 "/boot/system/develop/lib",
156 "/boot/system/lib",
157 ];
158
159 /// `libclang` directory patterns for Linux (and FreeBSD).
160 const DIRECTORIES_LINUX: &[&str] = &[
161 "/usr/local/llvm*/lib*",
162 "/usr/local/lib*/*/*",
163 "/usr/local/lib*/*",
164 "/usr/local/lib*",
165 "/usr/lib*/*/*",
166 "/usr/lib*/*",
167 "/usr/lib*",
168 ];
169
170 /// `libclang` directory patterns for macOS.
171 const DIRECTORIES_MACOS: &[&str] = &[
172 "/usr/local/opt/llvm*/lib/llvm*/lib",
173 "/Library/Developer/CommandLineTools/usr/lib",
174 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib",
175 "/usr/local/opt/llvm*/lib",
176 ];
177
178 /// `libclang` directory patterns for Windows.
179 ///
180 /// The boolean indicates whether the directory pattern should be used when
181 /// compiling for an MSVC target environment.
182 const DIRECTORIES_WINDOWS: &[(&str, bool)] = &[
183 // LLVM + Clang can be installed using Scoop (https://scoop.sh).
184 // Other Windows package managers install LLVM + Clang to other listed
185 // system-wide directories.
186 ("C:\\Users\\*\\scoop\\apps\\llvm\\current\\lib", true),
187 ("C:\\MSYS*\\MinGW*\\lib", false),
188 ("C:\\Program Files*\\LLVM\\lib", true),
189 ("C:\\LLVM\\lib", true),
190 // LLVM + Clang can be installed as a component of Visual Studio.
191 // https://github.com/KyleMayes/clang-sys/issues/121
192 ("C:\\Program Files*\\Microsoft Visual Studio\\*\\BuildTools\\VC\\Tools\\Llvm\\**\\lib", true),
193 ];
194
195 /// `libclang` directory patterns for illumos
196 const DIRECTORIES_ILLUMOS: &[&str] = &[
197 "/opt/ooce/llvm-*/lib",
198 "/opt/ooce/clang-*/lib",
199 ];
200
201 //================================================
202 // Searching
203 //================================================
204
205 /// Finds the files in a directory that match one or more filename glob patterns
206 /// and returns the paths to and filenames of those files.
search_directory(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)>207 fn search_directory(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> {
208 // Escape the specified directory in case it contains characters that have
209 // special meaning in glob patterns (e.g., `[` or `]`).
210 let directory = Pattern::escape(directory.to_str().unwrap());
211 let directory = Path::new(&directory);
212
213 // Join the escaped directory to the filename glob patterns to obtain
214 // complete glob patterns for the files being searched for.
215 let paths = filenames
216 .iter()
217 .map(|f| directory.join(f).to_str().unwrap().to_owned());
218
219 // Prevent wildcards from matching path separators to ensure that the search
220 // is limited to the specified directory.
221 let mut options = MatchOptions::new();
222 options.require_literal_separator = true;
223
224 paths
225 .map(|p| glob::glob_with(&p, options))
226 .filter_map(Result::ok)
227 .flatten()
228 .filter_map(|p| {
229 let path = p.ok()?;
230 let filename = path.file_name()?.to_str().unwrap();
231
232 // The `libclang_shared` library has been renamed to `libclang-cpp`
233 // in Clang 10. This can cause instances of this library (e.g.,
234 // `libclang-cpp.so.10`) to be matched by patterns looking for
235 // instances of `libclang`.
236 if filename.contains("-cpp.") {
237 return None;
238 }
239
240 Some((directory.to_owned(), filename.into()))
241 })
242 .collect::<Vec<_>>()
243 }
244
245 /// Finds the files in a directory (and any relevant sibling directories) that
246 /// match one or more filename glob patterns and returns the paths to and
247 /// filenames of those files.
search_directories(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)>248 fn search_directories(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> {
249 let mut results = search_directory(directory, filenames);
250
251 // On Windows, `libclang.dll` is usually found in the LLVM `bin` directory
252 // while `libclang.lib` is usually found in the LLVM `lib` directory. To
253 // keep things consistent with other platforms, only LLVM `lib` directories
254 // are included in the backup search directory globs so we need to search
255 // the LLVM `bin` directory here.
256 if target_os!("windows") && directory.ends_with("lib") {
257 let sibling = directory.parent().unwrap().join("bin");
258 results.extend(search_directory(&sibling, filenames).into_iter());
259 }
260
261 results
262 }
263
264 /// Finds the `libclang` static or dynamic libraries matching one or more
265 /// filename glob patterns and returns the paths to and filenames of those files.
search_libclang_directories(filenames: &[String], variable: &str) -> Vec<(PathBuf, String)>266 pub fn search_libclang_directories(filenames: &[String], variable: &str) -> Vec<(PathBuf, String)> {
267 // Search only the path indicated by the relevant environment variable
268 // (e.g., `LIBCLANG_PATH`) if it is set.
269 if let Ok(path) = env::var(variable).map(|d| Path::new(&d).to_path_buf()) {
270 // Check if the path is a matching file.
271 if let Some(parent) = path.parent() {
272 let filename = path.file_name().unwrap().to_str().unwrap();
273 let libraries = search_directories(parent, filenames);
274 if libraries.iter().any(|(_, f)| f == filename) {
275 return vec![(parent.into(), filename.into())];
276 }
277 }
278
279 // Check if the path is directory containing a matching file.
280 return search_directories(&path, filenames);
281 }
282
283 let mut found = vec![];
284
285 // Search the `bin` and `lib` directories in the directory returned by
286 // `llvm-config --prefix`.
287 if let Some(output) = run_llvm_config(&["--prefix"]) {
288 let directory = Path::new(output.lines().next().unwrap()).to_path_buf();
289 found.extend(search_directories(&directory.join("bin"), filenames));
290 found.extend(search_directories(&directory.join("lib"), filenames));
291 found.extend(search_directories(&directory.join("lib64"), filenames));
292 }
293
294 // Search the toolchain directory in the directory returned by
295 // `xcode-select --print-path`.
296 if target_os!("macos") {
297 if let Some(output) = run_xcode_select(&["--print-path"]) {
298 let directory = Path::new(output.lines().next().unwrap()).to_path_buf();
299 let directory = directory.join("Toolchains/XcodeDefault.xctoolchain/usr/lib");
300 found.extend(search_directories(&directory, filenames));
301 }
302 }
303
304 // Search the directories in the `LD_LIBRARY_PATH` environment variable.
305 if let Ok(path) = env::var("LD_LIBRARY_PATH") {
306 for directory in env::split_paths(&path) {
307 found.extend(search_directories(&directory, filenames));
308 }
309 }
310
311 // Determine the `libclang` directory patterns.
312 let directories: Vec<&str> = if target_os!("haiku") {
313 DIRECTORIES_HAIKU.into()
314 } else if target_os!("linux") || target_os!("freebsd") {
315 DIRECTORIES_LINUX.into()
316 } else if target_os!("macos") {
317 DIRECTORIES_MACOS.into()
318 } else if target_os!("windows") {
319 let msvc = target_env!("msvc");
320 DIRECTORIES_WINDOWS
321 .iter()
322 .filter(|d| d.1 || !msvc)
323 .map(|d| d.0)
324 .collect()
325 } else if target_os!("illumos") {
326 DIRECTORIES_ILLUMOS.into()
327 } else {
328 vec![]
329 };
330
331 // We use temporary directories when testing the build script so we'll
332 // remove the prefixes that make the directories absolute.
333 let directories = if test!() {
334 directories
335 .iter()
336 .map(|d| d.strip_prefix('/').or_else(|| d.strip_prefix("C:\\")).unwrap_or(d))
337 .collect::<Vec<_>>()
338 } else {
339 directories
340 };
341
342 // Search the directories provided by the `libclang` directory patterns.
343 let mut options = MatchOptions::new();
344 options.case_sensitive = false;
345 options.require_literal_separator = true;
346 for directory in directories.iter() {
347 if let Ok(directories) = glob::glob_with(directory, options) {
348 for directory in directories.filter_map(Result::ok).filter(|p| p.is_dir()) {
349 found.extend(search_directories(&directory, filenames));
350 }
351 }
352 }
353
354 found
355 }
356