1 // Copyright 2016 Kyle Mayes
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 //! Finds the required `libclang` libraries and links to them.
16 //!
17 //! # Environment Variables
18 //!
19 //! This build script can make use of several environment variables to help it find the required
20 //! static or dynamic libraries.
21 //!
22 //! * `LLVM_CONFIG_PATH` - provides a path to an `llvm-config` executable
23 //! * `LIBCLANG_PATH` - provides a path to a directory containing a `libclang` shared library
24 //! * `LIBCLANG_STATIC_PATH` - provides a path to a directory containing LLVM and Clang static libraries
25
26 #![allow(unused_attributes)]
27
28 extern crate glob;
29
30 use std::env;
31 use std::fs::{self, File};
32 use std::io::{Read, Seek, SeekFrom};
33 use std::path::{Path, PathBuf};
34 use std::process::{Command};
35
36 use glob::{MatchOptions};
37
38 /// Returns the version in the supplied file if one can be found.
find_version(file: &str) -> Option<&str>39 fn find_version(file: &str) -> Option<&str> {
40 if file.starts_with("libclang.so.") {
41 Some(&file[12..])
42 } else if file.starts_with("libclang-") {
43 Some(&file[9..])
44 } else {
45 None
46 }
47 }
48
49 /// Returns the components of the version appended to the supplied file.
parse_version(file: &Path) -> Vec<u32>50 fn parse_version(file: &Path) -> Vec<u32> {
51 let file = file.file_name().and_then(|f| f.to_str()).unwrap_or("");
52 let version = find_version(file).unwrap_or("");
53 version.split('.').map(|s| s.parse::<u32>().unwrap_or(0)).collect()
54 }
55
56 /// Returns a path to one of the supplied files if such a file can be found in the supplied directory.
contains(directory: &Path, files: &[String]) -> Option<PathBuf>57 fn contains(directory: &Path, files: &[String]) -> Option<PathBuf> {
58 // Join the directory to the files to obtain our glob patterns.
59 let patterns = files.iter().filter_map(|f| directory.join(f).to_str().map(ToOwned::to_owned));
60
61 // Prevent wildcards from matching path separators.
62 let mut options = MatchOptions::new();
63 options.require_literal_separator = true;
64
65 // Collect any files that match the glob patterns.
66 let mut matches = patterns.flat_map(|p| {
67 if let Ok(paths) = glob::glob_with(&p, &options) {
68 paths.filter_map(Result::ok).collect()
69 } else {
70 vec![]
71 }
72 }).collect::<Vec<_>>();
73
74 // Sort the matches by their version, preferring shorter and higher versions.
75 matches.sort_by_key(|m| parse_version(m));
76 matches.pop()
77 }
78
79 /// Runs a console command, returning the output if the command was successfully executed.
run(command: &str, arguments: &[&str]) -> Option<String>80 fn run(command: &str, arguments: &[&str]) -> Option<String> {
81 Command::new(command).args(arguments).output().map(|o| {
82 String::from_utf8_lossy(&o.stdout).into_owned()
83 }).ok()
84 }
85
86 /// Runs `llvm-config`, returning the output if the command was successfully executed.
run_llvm_config(arguments: &[&str]) -> Result<String, String>87 fn run_llvm_config(arguments: &[&str]) -> Result<String, String> {
88 match run(&env::var("LLVM_CONFIG_PATH").unwrap_or_else(|_| "llvm-config".into()), arguments) {
89 Some(output) => Ok(output),
90 None => {
91 let message = format!(
92 "couldn't execute `llvm-config {}`, set the LLVM_CONFIG_PATH environment variable \
93 to a path to a valid `llvm-config` executable",
94 arguments.join(" "),
95 );
96 Err(message)
97 },
98 }
99 }
100
101 /// Backup search directory globs for FreeBSD and Linux.
102 const SEARCH_LINUX: &[&str] = &[
103 "/usr/lib*",
104 "/usr/lib*/*",
105 "/usr/lib*/*/*",
106 "/usr/local/lib*",
107 "/usr/local/lib*/*",
108 "/usr/local/lib*/*/*",
109 "/usr/local/llvm*/lib",
110 ];
111
112 /// Backup search directory globs for OS X.
113 const SEARCH_OSX: &[&str] = &[
114 "/usr/local/opt/llvm*/lib",
115 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib",
116 "/Library/Developer/CommandLineTools/usr/lib",
117 "/usr/local/opt/llvm*/lib/llvm*/lib",
118 ];
119
120 /// Backup search directory globs for Windows.
121 const SEARCH_WINDOWS: &[&str] = &[
122 "C:\\LLVM\\lib",
123 "C:\\Program Files*\\LLVM\\lib",
124 "C:\\MSYS*\\MinGW*\\lib",
125 ];
126
127 /// Returns the ELF class from the ELF header in the supplied file.
parse_elf_header(file: &PathBuf) -> Result<u8, String>128 fn parse_elf_header(file: &PathBuf) -> Result<u8, String> {
129 let mut file = try!(File::open(file).map_err(|e| e.to_string()));
130 let mut elf = [0; 5];
131 try!(file.read_exact(&mut elf).map_err(|e| e.to_string()));
132 if elf[..4] == [127, 69, 76, 70] {
133 Ok(elf[4])
134 } else {
135 Err("invalid ELF header".into())
136 }
137 }
138
139 /// Returns the magic number from the PE header in the supplied file.
parse_pe_header(file: &PathBuf) -> Result<u16, String>140 fn parse_pe_header(file: &PathBuf) -> Result<u16, String> {
141 let mut file = try!(File::open(file).map_err(|e| e.to_string()));
142 let mut pe = [0; 4];
143
144 // Determine the header offset.
145 try!(file.seek(SeekFrom::Start(0x3C)).map_err(|e| e.to_string()));
146 try!(file.read_exact(&mut pe).map_err(|e| e.to_string()));
147 let offset = i32::from(pe[0]) + (i32::from(pe[1]) << 8) + (i32::from(pe[2]) << 16) + (i32::from(pe[3]) << 24);
148
149 // Determine the validity of the header.
150 try!(file.seek(SeekFrom::Start(offset as u64)).map_err(|e| e.to_string()));
151 try!(file.read_exact(&mut pe).map_err(|e| e.to_string()));
152 if pe != [80, 69, 0, 0] {
153 return Err("invalid PE header".into());
154 }
155
156 // Find the magic number.
157 try!(file.seek(SeekFrom::Current(20)).map_err(|e| e.to_string()));
158 try!(file.read_exact(&mut pe).map_err(|e| e.to_string()));
159 Ok(u16::from(pe[0]) + (u16::from(pe[1]) << 8))
160 }
161
162 /// Indicates the type of library being searched for.
163 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
164 enum Library {
165 Dynamic,
166 Static,
167 }
168
169 impl Library {
170 /// Checks whether the supplied file is a valid library for the architecture.
check(&self, file: &PathBuf) -> Result<(), String>171 fn check(&self, file: &PathBuf) -> Result<(), String> {
172 if cfg!(any(target_os="freebsd", target_os="linux")) {
173 if *self == Library::Static {
174 return Ok(());
175 }
176 let class = try!(parse_elf_header(file));
177 if cfg!(target_pointer_width="32") && class != 1 {
178 return Err("invalid ELF class (64-bit)".into());
179 }
180 if cfg!(target_pointer_width="64") && class != 2 {
181 return Err("invalid ELF class (32-bit)".into());
182 }
183 Ok(())
184 } else if cfg!(target_os="windows") {
185 if *self == Library::Static {
186 return Ok(());
187 }
188 let magic = try!(parse_pe_header(file));
189 if cfg!(target_pointer_width="32") && magic != 267 {
190 return Err("invalid DLL (64-bit)".into());
191 }
192 if cfg!(target_pointer_width="64") && magic != 523 {
193 return Err("invalid DLL (32-bit)".into());
194 }
195 Ok(())
196 } else {
197 Ok(())
198 }
199 }
200 }
201
202 /// Searches for a library, returning the directory it can be found in if the search was successful.
find(library: Library, files: &[String], env: &str) -> Result<PathBuf, String>203 fn find(library: Library, files: &[String], env: &str) -> Result<PathBuf, String> {
204 let mut skipped = vec![];
205
206 /// Attempts to return the supplied file.
207 macro_rules! try_file {
208 ($file:expr) => ({
209 match library.check(&$file) {
210 Ok(_) => return Ok($file),
211 Err(message) => skipped.push(format!("({}: {})", $file.display(), message)),
212 }
213 });
214 }
215
216 /// Searches the supplied directory and, on Windows, any relevant sibling directories.
217 macro_rules! search_directory {
218 ($directory:ident) => {
219 if let Some(file) = contains(&$directory, files) {
220 try_file!(file);
221 }
222
223 // On Windows, `libclang.dll` is usually found in the LLVM `bin` directory while
224 // `libclang.lib` is usually found in the LLVM `lib` directory. To keep things
225 // consistent with other platforms, only LLVM `lib` directories are included in the
226 // backup search directory globs so we need to search the LLVM `bin` directory here.
227 if cfg!(target_os="windows") && $directory.ends_with("lib") {
228 let sibling = $directory.parent().unwrap().join("bin");
229 if let Some(file) = contains(&sibling, files) {
230 try_file!(file);
231 }
232 }
233 }
234 }
235
236 // Search the directory provided by the relevant environment variable if it is set.
237 if let Ok(directory) = env::var(env).map(|d| Path::new(&d).to_path_buf()) {
238 search_directory!(directory);
239 }
240
241 // Search the `bin` and `lib` subdirectories in the directory returned by
242 // `llvm-config --prefix` if `llvm-config` is available.
243 if let Ok(output) = run_llvm_config(&["--prefix"]) {
244 let directory = Path::new(output.lines().next().unwrap()).to_path_buf();
245 let bin = directory.join("bin");
246 if let Some(file) = contains(&bin, files) {
247 try_file!(file);
248 }
249 let lib = directory.join("lib");
250 if let Some(file) = contains(&lib, files) {
251 try_file!(file);
252 }
253 }
254
255 // Search the `LD_LIBRARY_PATH` directories.
256 if let Ok(path) = env::var("LD_LIBRARY_PATH") {
257 for directory in path.split(':').map(Path::new) {
258 search_directory!(directory);
259 }
260 }
261
262 // Search the backup directories.
263 let search = if cfg!(any(target_os="freebsd", target_os="linux")) {
264 SEARCH_LINUX
265 } else if cfg!(target_os="macos") {
266 SEARCH_OSX
267 } else if cfg!(target_os="windows") {
268 SEARCH_WINDOWS
269 } else {
270 &[]
271 };
272 for pattern in search {
273 let mut options = MatchOptions::new();
274 options.case_sensitive = false;
275 options.require_literal_separator = true;
276 if let Ok(paths) = glob::glob_with(pattern, &options) {
277 for path in paths.filter_map(Result::ok).filter(|p| p.is_dir()) {
278 search_directory!(path);
279 }
280 }
281 }
282
283 let message = format!(
284 "couldn't find any of [{}], set the {} environment variable to a path where one of these \
285 files can be found (skipped: [{}])",
286 files.iter().map(|f| format!("'{}'", f)).collect::<Vec<_>>().join(", "),
287 env,
288 skipped.join(", "),
289 );
290 Err(message)
291 }
292
293 /// Searches for a `libclang` shared library, returning the path to such a shared library if the
294 /// search was successful.
find_shared_library() -> Result<PathBuf, String>295 pub fn find_shared_library() -> Result<PathBuf, String> {
296 let mut files = vec![format!("{}clang{}", env::consts::DLL_PREFIX, env::consts::DLL_SUFFIX)];
297 if cfg!(any(target_os="freebsd", target_os="linux", target_os="openbsd")) {
298 // Some BSDs and Linux distributions don't create a `libclang.so` symlink, so we need to
299 // look for any versioned files (e.g., `libclang.so.3.9` or `libclang-3.9.so`).
300 files.push("libclang.so.*".into());
301 files.push("libclang-*.so".into());
302 }
303 if cfg!(target_os="windows") {
304 // The official LLVM build uses `libclang.dll` on Windows instead of `clang.dll`. However,
305 // unofficial builds such as MinGW use `clang.dll`.
306 files.push("libclang.dll".into());
307 }
308 find(Library::Dynamic, &files, "LIBCLANG_PATH")
309 }
310
311 /// Returns the name of an LLVM or Clang library from a path to such a library.
get_library_name(path: &Path) -> Option<String>312 fn get_library_name(path: &Path) -> Option<String> {
313 path.file_stem().map(|p| {
314 let string = p.to_string_lossy();
315 if string.starts_with("lib") {
316 string[3..].to_owned()
317 } else {
318 string.to_string()
319 }
320 })
321 }
322
323 /// Returns the LLVM libraries required to link to `libclang` statically.
get_llvm_libraries() -> Vec<String>324 fn get_llvm_libraries() -> Vec<String> {
325 run_llvm_config(&["--libs"]).unwrap().split_whitespace().filter_map(|p| {
326 // Depending on the version of `llvm-config` in use, listed libraries may be in one of two
327 // forms, a full path to the library or simply prefixed with `-l`.
328 if p.starts_with("-l") {
329 Some(p[2..].into())
330 } else {
331 get_library_name(Path::new(p))
332 }
333 }).collect()
334 }
335
336 /// Clang libraries required to link to `libclang` 3.5 and later statically.
337 const CLANG_LIBRARIES: &[&str] = &[
338 "clang",
339 "clangAST",
340 "clangAnalysis",
341 "clangBasic",
342 "clangDriver",
343 "clangEdit",
344 "clangFrontend",
345 "clangIndex",
346 "clangLex",
347 "clangParse",
348 "clangRewrite",
349 "clangSema",
350 "clangSerialization",
351 ];
352
353 /// Returns the Clang libraries required to link to `libclang` statically.
get_clang_libraries<P: AsRef<Path>>(directory: P) -> Vec<String>354 fn get_clang_libraries<P: AsRef<Path>>(directory: P) -> Vec<String> {
355 let pattern = directory.as_ref().join("libclang*.a").to_string_lossy().to_string();
356 if let Ok(libraries) = glob::glob(&pattern) {
357 libraries.filter_map(|l| l.ok().and_then(|l| get_library_name(&l))).collect()
358 } else {
359 CLANG_LIBRARIES.iter().map(|l| l.to_string()).collect()
360 }
361 }
362
363 /// Find and link to `libclang` statically.
364 #[cfg_attr(feature="runtime", allow(dead_code))]
link_static()365 fn link_static() {
366 let name = if cfg!(target_os="windows") { "libclang.lib" } else { "libclang.a" };
367 let file = find(Library::Static, &[name.into()], "LIBCLANG_STATIC_PATH").unwrap();
368 let directory = file.parent().unwrap();
369
370 // Specify required Clang static libraries.
371 println!("cargo:rustc-link-search=native={}", directory.display());
372 for library in get_clang_libraries(directory) {
373 println!("cargo:rustc-link-lib=static={}", library);
374 }
375
376 // Determine the shared mode used by LLVM.
377 let mode = run_llvm_config(&["--shared-mode"]).map(|m| m.trim().to_owned());
378 let prefix = if mode.ok().map_or(false, |m| m == "static") { "static=" } else { "" };
379
380 // Specify required LLVM static libraries.
381 println!("cargo:rustc-link-search=native={}", run_llvm_config(&["--libdir"]).unwrap().trim_right());
382 for library in get_llvm_libraries() {
383 println!("cargo:rustc-link-lib={}{}", prefix, library);
384 }
385
386 // Specify required system libraries.
387 // MSVC doesn't need this, as it tracks dependencies inside `.lib` files.
388 if cfg!(target_os="freebsd") {
389 println!("cargo:rustc-flags=-l ffi -l ncursesw -l c++ -l z");
390 } else if cfg!(target_os="linux") {
391 println!("cargo:rustc-flags=-l ffi -l ncursesw -l stdc++ -l z");
392 } else if cfg!(target_os="macos") {
393 println!("cargo:rustc-flags=-l ffi -l ncurses -l c++ -l z");
394 }
395 }
396
397 /// Find and link to `libclang` dynamically.
398 #[cfg_attr(feature="runtime", allow(dead_code))]
link_dynamic()399 fn link_dynamic() {
400 let file = find_shared_library().unwrap();
401 let directory = file.parent().unwrap();
402 println!("cargo:rustc-link-search={}", directory.display());
403
404 if cfg!(all(target_os="windows", target_env="msvc")) {
405 // Find the `libclang` stub static library required for the MSVC toolchain.
406 let libdir = if !directory.ends_with("bin") {
407 directory.to_owned()
408 } else {
409 directory.parent().unwrap().join("lib")
410 };
411 if libdir.join("libclang.lib").exists() {
412 println!("cargo:rustc-link-search={}", libdir.display());
413 } else if libdir.join("libclang.dll.a").exists() {
414 // MSYS and MinGW use `libclang.dll.a` instead of `libclang.lib`. It is linkable with
415 // the MSVC linker, but Rust doesn't recognize the `.a` suffix, so we need to copy it
416 // with a different name.
417 //
418 // FIXME: Maybe we can just hardlink or symlink it?
419 let out = env::var("OUT_DIR").unwrap();
420 fs::copy(libdir.join("libclang.dll.a"), Path::new(&out).join("libclang.lib")).unwrap();
421 println!("cargo:rustc-link-search=native={}", out);
422 } else {
423 panic!(
424 "using '{}', so 'libclang.lib' or 'libclang.dll.a' must be available in {}",
425 file.display(),
426 libdir.display(),
427 );
428 }
429 println!("cargo:rustc-link-lib=dylib=libclang");
430 } else {
431 println!("cargo:rustc-link-lib=dylib=clang");
432 }
433 }
434
435 #[cfg_attr(feature="runtime", allow(dead_code))]
main()436 fn main() {
437 if cfg!(feature="runtime") {
438 if cfg!(feature="static") {
439 panic!("`runtime` and `static` features can't be combined");
440 }
441 } else if cfg!(feature="static") {
442 link_static();
443 } else {
444 link_dynamic();
445 }
446
447 if let Ok(output) = run_llvm_config(&["--includedir"]) {
448 let directory = Path::new(output.trim_right());
449 println!("cargo:include={}", directory.display());
450 }
451 }
452