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