1
// SPDX-License-Identifier: Apache-2.0
2

            
3
use std::cell::RefCell;
4
use std::collections::HashMap;
5
use std::env;
6
use std::path::{Path, PathBuf};
7
use std::process::Command;
8

            
9
use glob::{MatchOptions, Pattern};
10

            
11
//================================================
12
// Commands
13
//================================================
14

            
15
thread_local! {
16
    /// The errors encountered by the build script while executing commands.
17
    static COMMAND_ERRORS: RefCell<HashMap<String, Vec<String>>> = RefCell::default();
18
}
19

            
20
/// Adds an error encountered by the build script while executing a command.
21
2
fn add_command_error(name: &str, path: &str, arguments: &[&str], message: String) {
22
2
    COMMAND_ERRORS.with(|e| {
23
2
        e.borrow_mut()
24
2
            .entry(name.into())
25
2
            .or_default()
26
2
            .push(format!(
27
2
                "couldn't execute `{} {}` (path={}) ({})",
28
                name,
29
2
                arguments.join(" "),
30
                path,
31
                message,
32
            ))
33
2
    });
34
}
35

            
36
/// A struct that prints the errors encountered by the build script while
37
/// executing commands when dropped (unless explictly discarded).
38
///
39
/// This is handy because we only want to print these errors when the build
40
/// script fails to link to an instance of `libclang`. For example, if
41
/// `llvm-config` couldn't be executed but an instance of `libclang` was found
42
/// anyway we don't want to pollute the build output with irrelevant errors.
43
#[derive(Default)]
44
pub struct CommandErrorPrinter {
45
    discard: bool,
46
}
47

            
48
impl CommandErrorPrinter {
49
    pub fn discard(mut self) {
50
        self.discard = true;
51
    }
52
}
53

            
54
impl Drop for CommandErrorPrinter {
55
    fn drop(&mut self) {
56
        if self.discard {
57
            return;
58
        }
59

            
60
        let errors = COMMAND_ERRORS.with(|e| e.borrow().clone());
61

            
62
        if let Some(errors) = errors.get("llvm-config") {
63
            println!(
64
                "cargo:warning=could not execute `llvm-config` one or more \
65
                times, if the LLVM_CONFIG_PATH environment variable is set to \
66
                a full path to valid `llvm-config` executable it will be used \
67
                to try to find an instance of `libclang` on your system: {}",
68
                errors
69
                    .iter()
70
                    .map(|e| format!("\"{}\"", e))
71
                    .collect::<Vec<_>>()
72
                    .join("\n  "),
73
            )
74
        }
75

            
76
        if let Some(errors) = errors.get("xcode-select") {
77
            println!(
78
                "cargo:warning=could not execute `xcode-select` one or more \
79
                times, if a valid instance of this executable is on your PATH \
80
                it will be used to try to find an instance of `libclang` on \
81
                your system: {}",
82
                errors
83
                    .iter()
84
                    .map(|e| format!("\"{}\"", e))
85
                    .collect::<Vec<_>>()
86
                    .join("\n  "),
87
            )
88
        }
89
    }
90
}
91

            
92
#[cfg(test)]
93
lazy_static::lazy_static! {
94
    pub static ref RUN_COMMAND_MOCK: std::sync::Mutex<
95
        Option<Box<dyn Fn(&str, &str, &[&str]) -> Option<String> + Send + Sync + 'static>>,
96
    > = std::sync::Mutex::new(None);
97
}
98

            
99
/// Executes a command and returns the `stdout` output if the command was
100
/// successfully executed (errors are added to `COMMAND_ERRORS`).
101
2
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
2
    let output = match Command::new(path).args(arguments).output() {
108
        Ok(output) => output,
109
2
        Err(error) => {
110
2
            let message = format!("error: {}", error);
111
2
            add_command_error(name, path, arguments, message);
112
2
            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`).
127
2
pub fn run_llvm_config(arguments: &[&str]) -> Option<String> {
128
2
    let path = env::var("LLVM_CONFIG_PATH").unwrap_or_else(|_| "llvm-config".into());
129
2
    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`).
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\\*\\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.
207
768
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
768
    let directory = Pattern::escape(directory.to_str().unwrap());
211
768
    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
768
    let paths = filenames
216
768
        .iter()
217
3072
        .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
768
    let mut options = MatchOptions::new();
222
768
    options.require_literal_separator = true;
223

            
224
768
    paths
225
3072
        .map(|p| glob::glob_with(&p, options))
226
768
        .filter_map(Result::ok)
227
768
        .flatten()
228
768
        .filter_map(|p| {
229
56
            let path = p.ok()?;
230
50
            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
50
            if filename.contains("-cpp.") {
237
14
                return None;
238
            }
239

            
240
36
            Some((path.parent().unwrap().to_owned(), filename.into()))
241
56
        })
242
768
        .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.
248
768
fn search_directories(directory: &Path, filenames: &[String]) -> Vec<(PathBuf, String)> {
249
768
    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
768
    if target_os!("windows") && directory.ends_with("lib") {
257
        let sibling = directory.parent().unwrap().join("bin");
258
        results.extend(search_directory(&sibling, filenames));
259
    }
260

            
261
768
    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.
266
2
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
2
    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
2
    let mut found = vec![];
284

            
285
    // Search the `bin` and `lib` directories in the directory returned by
286
    // `llvm-config --prefix`.
287
2
    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
2
    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
2
    if let Ok(path) = env::var("LD_LIBRARY_PATH") {
306
10
        for directory in env::split_paths(&path) {
307
10
            found.extend(search_directories(&directory, filenames));
308
        }
309
    }
310

            
311
    // Determine the `libclang` directory patterns.
312
2
    let directories: Vec<&str> = if target_os!("haiku") {
313
        DIRECTORIES_HAIKU.into()
314
2
    } else if target_os!("linux") || target_os!("freebsd") {
315
2
        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
2
    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
2
        directories
340
    };
341

            
342
    // Search the directories provided by the `libclang` directory patterns.
343
2
    let mut options = MatchOptions::new();
344
2
    options.case_sensitive = false;
345
2
    options.require_literal_separator = true;
346
14
    for directory in directories.iter() {
347
14
        if let Ok(directories) = glob::glob_with(directory, options) {
348
5558
            for directory in directories.filter_map(Result::ok).filter(|p| p.is_dir()) {
349
758
                found.extend(search_directories(&directory, filenames));
350
            }
351
        }
352
    }
353

            
354
2
    found
355
}