1 // SPDX-License-Identifier: Apache-2.0
2 
3 use std::env;
4 use std::fs::File;
5 use std::io::{self, Error, ErrorKind, Read, Seek, SeekFrom};
6 use std::path::{Path, PathBuf};
7 
8 use super::common;
9 
10 //================================================
11 // Validation
12 //================================================
13 
14 /// Extracts the ELF class from the ELF header in a shared library.
parse_elf_header(path: &Path) -> io::Result<u8>15 fn parse_elf_header(path: &Path) -> io::Result<u8> {
16     let mut file = File::open(path)?;
17     let mut buffer = [0; 5];
18     file.read_exact(&mut buffer)?;
19     if buffer[..4] == [127, 69, 76, 70] {
20         Ok(buffer[4])
21     } else {
22         Err(Error::new(ErrorKind::InvalidData, "invalid ELF header"))
23     }
24 }
25 
26 /// Extracts the magic number from the PE header in a shared library.
parse_pe_header(path: &Path) -> io::Result<u16>27 fn parse_pe_header(path: &Path) -> io::Result<u16> {
28     let mut file = File::open(path)?;
29 
30     // Extract the header offset.
31     let mut buffer = [0; 4];
32     let start = SeekFrom::Start(0x3C);
33     file.seek(start)?;
34     file.read_exact(&mut buffer)?;
35     let offset = i32::from_le_bytes(buffer);
36 
37     // Check the validity of the header.
38     file.seek(SeekFrom::Start(offset as u64))?;
39     file.read_exact(&mut buffer)?;
40     if buffer != [80, 69, 0, 0] {
41         return Err(Error::new(ErrorKind::InvalidData, "invalid PE header"));
42     }
43 
44     // Extract the magic number.
45     let mut buffer = [0; 2];
46     file.seek(SeekFrom::Current(20))?;
47     file.read_exact(&mut buffer)?;
48     Ok(u16::from_le_bytes(buffer))
49 }
50 
51 /// Checks that a `libclang` shared library matches the target platform.
validate_library(path: &Path) -> Result<(), String>52 fn validate_library(path: &Path) -> Result<(), String> {
53     if target_os!("linux") || target_os!("freebsd") {
54         let class = parse_elf_header(path).map_err(|e| e.to_string())?;
55 
56         if target_pointer_width!("32") && class != 1 {
57             return Err("invalid ELF class (64-bit)".into());
58         }
59 
60         if target_pointer_width!("64") && class != 2 {
61             return Err("invalid ELF class (32-bit)".into());
62         }
63 
64         Ok(())
65     } else if target_os!("windows") {
66         let magic = parse_pe_header(path).map_err(|e| e.to_string())?;
67 
68         if target_pointer_width!("32") && magic != 267 {
69             return Err("invalid DLL (64-bit)".into());
70         }
71 
72         if target_pointer_width!("64") && magic != 523 {
73             return Err("invalid DLL (32-bit)".into());
74         }
75 
76         Ok(())
77     } else {
78         Ok(())
79     }
80 }
81 
82 //================================================
83 // Searching
84 //================================================
85 
86 /// Extracts the version components in a `libclang` shared library filename.
parse_version(filename: &str) -> Vec<u32>87 fn parse_version(filename: &str) -> Vec<u32> {
88     let version = if let Some(version) = filename.strip_prefix("libclang.so.") {
89         version
90     } else if filename.starts_with("libclang-") {
91         &filename[9..filename.len() - 3]
92     } else {
93         return vec![];
94     };
95 
96     version.split('.').map(|s| s.parse().unwrap_or(0)).collect()
97 }
98 
99 /// Finds `libclang` shared libraries and returns the paths to, filenames of,
100 /// and versions of those shared libraries.
search_libclang_directories(runtime: bool) -> Result<Vec<(PathBuf, String, Vec<u32>)>, String>101 fn search_libclang_directories(runtime: bool) -> Result<Vec<(PathBuf, String, Vec<u32>)>, String> {
102     let mut files = vec![format!(
103         "{}clang{}",
104         env::consts::DLL_PREFIX,
105         env::consts::DLL_SUFFIX
106     )];
107 
108     if target_os!("linux") {
109         // Some Linux distributions don't create a `libclang.so` symlink, so we
110         // need to look for versioned files (e.g., `libclang-3.9.so`).
111         files.push("libclang-*.so".into());
112 
113         // Some Linux distributions don't create a `libclang.so` symlink and
114         // don't have versioned files as described above, so we need to look for
115         // suffix versioned files (e.g., `libclang.so.1`). However, `ld` cannot
116         // link to these files, so this will only be included when linking at
117         // runtime.
118         if runtime {
119             files.push("libclang.so.*".into());
120             files.push("libclang-*.so.*".into());
121         }
122     }
123 
124     if target_os!("freebsd") || target_os!("haiku") || target_os!("netbsd") || target_os!("openbsd") {
125         // Some BSD distributions don't create a `libclang.so` symlink either,
126         // but use a different naming scheme for versioned files (e.g.,
127         // `libclang.so.7.0`).
128         files.push("libclang.so.*".into());
129     }
130 
131     if target_os!("windows") {
132         // The official LLVM build uses `libclang.dll` on Windows instead of
133         // `clang.dll`. However, unofficial builds such as MinGW use `clang.dll`.
134         files.push("libclang.dll".into());
135     }
136 
137     // Find and validate `libclang` shared libraries and collect the versions.
138     let mut valid = vec![];
139     let mut invalid = vec![];
140     for (directory, filename) in common::search_libclang_directories(&files, "LIBCLANG_PATH") {
141         let path = directory.join(&filename);
142         match validate_library(&path) {
143             Ok(()) => {
144                 let version = parse_version(&filename);
145                 valid.push((directory, filename, version))
146             }
147             Err(message) => invalid.push(format!("({}: {})", path.display(), message)),
148         }
149     }
150 
151     if !valid.is_empty() {
152         return Ok(valid);
153     }
154 
155     let message = format!(
156         "couldn't find any valid shared libraries matching: [{}], set the \
157          `LIBCLANG_PATH` environment variable to a path where one of these files \
158          can be found (invalid: [{}])",
159         files
160             .iter()
161             .map(|f| format!("'{}'", f))
162             .collect::<Vec<_>>()
163             .join(", "),
164         invalid.join(", "),
165     );
166 
167     Err(message)
168 }
169 
170 /// Finds the "best" `libclang` shared library and returns the directory and
171 /// filename of that library.
find(runtime: bool) -> Result<(PathBuf, String), String>172 pub fn find(runtime: bool) -> Result<(PathBuf, String), String> {
173     search_libclang_directories(runtime)?
174         .iter()
175         // We want to find the `libclang` shared library with the highest
176         // version number, hence `max_by_key` below.
177         //
178         // However, in the case where there are multiple such `libclang` shared
179         // libraries, we want to use the order in which they appeared in the
180         // list returned by `search_libclang_directories` as a tiebreaker since
181         // that function returns `libclang` shared libraries in descending order
182         // of preference by how they were found.
183         //
184         // `max_by_key`, perhaps surprisingly, returns the *last* item with the
185         // maximum key rather than the first which results in the opposite of
186         // the tiebreaking behavior we want. This is easily fixed by reversing
187         // the list first.
188         .rev()
189         .max_by_key(|f| &f.2)
190         .cloned()
191         .map(|(path, filename, _)| (path, filename))
192         .ok_or_else(|| "unreachable".into())
193 }
194 
195 //================================================
196 // Linking
197 //================================================
198 
199 /// Finds and links to a `libclang` shared library.
200 #[cfg(not(feature = "runtime"))]
link()201 pub fn link() {
202     let cep = common::CommandErrorPrinter::default();
203 
204     use std::fs;
205 
206     let (directory, filename) = find(false).unwrap();
207     println!("cargo:rustc-link-search={}", directory.display());
208 
209     if cfg!(all(target_os = "windows", target_env = "msvc")) {
210         // Find the `libclang` stub static library required for the MSVC
211         // toolchain.
212         let lib = if !directory.ends_with("bin") {
213             directory
214         } else {
215             directory.parent().unwrap().join("lib")
216         };
217 
218         if lib.join("libclang.lib").exists() {
219             println!("cargo:rustc-link-search={}", lib.display());
220         } else if lib.join("libclang.dll.a").exists() {
221             // MSYS and MinGW use `libclang.dll.a` instead of `libclang.lib`.
222             // It is linkable with the MSVC linker, but Rust doesn't recognize
223             // the `.a` suffix, so we need to copy it with a different name.
224             //
225             // FIXME: Maybe we can just hardlink or symlink it?
226             let out = env::var("OUT_DIR").unwrap();
227             fs::copy(
228                 lib.join("libclang.dll.a"),
229                 Path::new(&out).join("libclang.lib"),
230             )
231             .unwrap();
232             println!("cargo:rustc-link-search=native={}", out);
233         } else {
234             panic!(
235                 "using '{}', so 'libclang.lib' or 'libclang.dll.a' must be \
236                  available in {}",
237                 filename,
238                 lib.display(),
239             );
240         }
241 
242         println!("cargo:rustc-link-lib=dylib=libclang");
243     } else {
244         let name = filename.trim_start_matches("lib");
245 
246         // Strip extensions and trailing version numbers (e.g., the `.so.7.0` in
247         // `libclang.so.7.0`).
248         let name = match name.find(".dylib").or_else(|| name.find(".so")) {
249             Some(index) => &name[0..index],
250             None => name,
251         };
252 
253         println!("cargo:rustc-link-lib=dylib={}", name);
254     }
255 
256     cep.discard();
257 }
258