• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 /// Executes a command and returns the `stdout` output if the command was
95 /// successfully executed (errors are added to `COMMAND_ERRORS`).
run_command(name: &str, path: &str, arguments: &[&str]) -> Option<String>96 fn 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`).
run_llvm_config(arguments: &[&str]) -> Option<String>117 pub 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`).
run_xcode_select(arguments: &[&str]) -> Option<String>124 pub 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.
133 const 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).
143 const 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.
154 const 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.
162 const 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
176 const 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.
search_directory(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)>187 fn 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.
search_directories(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)>228 fn 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.
search_libclang_directories(filenames: &[String], variable: &str) -> Vec<(PathBuf, String)>246 pub 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