• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // SPDX-License-Identifier: Apache-2.0
2 
3 //! Provides helper functionality.
4 
5 use std::path::{Path, PathBuf};
6 use std::process::Command;
7 use std::{env, io};
8 
9 use glob::{self, Pattern};
10 
11 use libc::c_int;
12 
13 use super::CXVersion;
14 
15 //================================================
16 // Structs
17 //================================================
18 
19 /// A `clang` executable.
20 #[derive(Clone, Debug)]
21 pub struct Clang {
22     /// The path to this `clang` executable.
23     pub path: PathBuf,
24     /// The version of this `clang` executable if it could be parsed.
25     pub version: Option<CXVersion>,
26     /// The directories searched by this `clang` executable for C headers if
27     /// they could be parsed.
28     pub c_search_paths: Option<Vec<PathBuf>>,
29     /// The directories searched by this `clang` executable for C++ headers if
30     /// they could be parsed.
31     pub cpp_search_paths: Option<Vec<PathBuf>>,
32 }
33 
34 impl Clang {
new(path: impl AsRef<Path>, args: &[String]) -> Self35     fn new(path: impl AsRef<Path>, args: &[String]) -> Self {
36         Self {
37             path: path.as_ref().into(),
38             version: parse_version(path.as_ref()),
39             c_search_paths: parse_search_paths(path.as_ref(), "c", args),
40             cpp_search_paths: parse_search_paths(path.as_ref(), "c++", args),
41         }
42     }
43 
44     /// Returns a `clang` executable if one can be found.
45     ///
46     /// If the `CLANG_PATH` environment variable is set, that is the instance of
47     /// `clang` used. Otherwise, a series of directories are searched. First, if
48     /// a path is supplied, that is the first directory searched. Then, the
49     /// directory returned by `llvm-config --bindir` is searched. On macOS
50     /// systems, `xcodebuild -find clang` will next be queried. Last, the
51     /// directories in the system's `PATH` are searched.
52     ///
53     /// ## Cross-compilation
54     ///
55     /// If target arguments are provided (e.g., `-target` followed by a target
56     /// like `x86_64-unknown-linux-gnu`) then this method will prefer a
57     /// target-prefixed instance of `clang` (e.g.,
58     /// `x86_64-unknown-linux-gnu-clang` for the above example).
find(path: Option<&Path>, args: &[String]) -> Option<Clang>59     pub fn find(path: Option<&Path>, args: &[String]) -> Option<Clang> {
60         if let Ok(path) = env::var("CLANG_PATH") {
61             return Some(Clang::new(path, args));
62         }
63 
64         // Determine the cross-compilation target, if any.
65 
66         let mut target = None;
67         for i in 0..args.len() {
68             if args[i] == "-target" && i + 1 < args.len() {
69                 target = Some(&args[i + 1]);
70             }
71         }
72 
73         // Collect the paths to search for a `clang` executable in.
74 
75         let mut paths = vec![];
76 
77         if let Some(path) = path {
78             paths.push(path.into());
79         }
80 
81         if let Ok(path) = run_llvm_config(&["--bindir"]) {
82             if let Some(line) = path.lines().next() {
83                 paths.push(line.into());
84             }
85         }
86 
87         if cfg!(target_os = "macos") {
88             if let Ok((path, _)) = run("xcodebuild", &["-find", "clang"]) {
89                 if let Some(line) = path.lines().next() {
90                     paths.push(line.into());
91                 }
92             }
93         }
94 
95         if let Ok(path) = env::var("PATH") {
96             paths.extend(env::split_paths(&path));
97         }
98 
99         // First, look for a target-prefixed `clang` executable.
100 
101         if let Some(target) = target {
102             let default = format!("{}-clang{}", target, env::consts::EXE_SUFFIX);
103             let versioned = format!("{}-clang-[0-9]*{}", target, env::consts::EXE_SUFFIX);
104             let patterns = &[&default[..], &versioned[..]];
105             for path in &paths {
106                 if let Some(path) = find(path, patterns) {
107                     return Some(Clang::new(path, args));
108                 }
109             }
110         }
111 
112         // Otherwise, look for any other `clang` executable.
113 
114         let default = format!("clang{}", env::consts::EXE_SUFFIX);
115         let versioned = format!("clang-[0-9]*{}", env::consts::EXE_SUFFIX);
116         let patterns = &[&default[..], &versioned[..]];
117         for path in paths {
118             if let Some(path) = find(&path, patterns) {
119                 return Some(Clang::new(path, args));
120             }
121         }
122 
123         None
124     }
125 }
126 
127 //================================================
128 // Functions
129 //================================================
130 
131 /// Returns the first match to the supplied glob patterns in the supplied
132 /// directory if there are any matches.
find(directory: &Path, patterns: &[&str]) -> Option<PathBuf>133 fn find(directory: &Path, patterns: &[&str]) -> Option<PathBuf> {
134     // Escape the directory in case it contains characters that have special
135     // meaning in glob patterns (e.g., `[` or `]`).
136     let directory = if let Some(directory) = directory.to_str() {
137         Path::new(&Pattern::escape(directory)).to_owned()
138     } else {
139         return None;
140     };
141 
142     for pattern in patterns {
143         let pattern = directory.join(pattern).to_string_lossy().into_owned();
144         if let Some(path) = glob::glob(&pattern).ok()?.filter_map(|p| p.ok()).next() {
145             if path.is_file() && is_executable(&path).unwrap_or(false) {
146                 return Some(path);
147             }
148         }
149     }
150 
151     None
152 }
153 
154 #[cfg(unix)]
is_executable(path: &Path) -> io::Result<bool>155 fn is_executable(path: &Path) -> io::Result<bool> {
156     use std::ffi::CString;
157     use std::os::unix::ffi::OsStrExt;
158 
159     let path = CString::new(path.as_os_str().as_bytes())?;
160     unsafe { Ok(libc::access(path.as_ptr(), libc::X_OK) == 0) }
161 }
162 
163 #[cfg(not(unix))]
is_executable(_: &Path) -> io::Result<bool>164 fn is_executable(_: &Path) -> io::Result<bool> {
165     Ok(true)
166 }
167 
168 /// Attempts to run an executable, returning the `stdout` and `stderr` output if
169 /// successful.
run(executable: &str, arguments: &[&str]) -> Result<(String, String), String>170 fn run(executable: &str, arguments: &[&str]) -> Result<(String, String), String> {
171     Command::new(executable)
172         .args(arguments)
173         .output()
174         .map(|o| {
175             let stdout = String::from_utf8_lossy(&o.stdout).into_owned();
176             let stderr = String::from_utf8_lossy(&o.stderr).into_owned();
177             (stdout, stderr)
178         })
179         .map_err(|e| format!("could not run executable `{}`: {}", executable, e))
180 }
181 
182 /// Runs `clang`, returning the `stdout` and `stderr` output.
run_clang(path: &Path, arguments: &[&str]) -> (String, String)183 fn run_clang(path: &Path, arguments: &[&str]) -> (String, String) {
184     run(&path.to_string_lossy().into_owned(), arguments).unwrap()
185 }
186 
187 /// Runs `llvm-config`, returning the `stdout` output if successful.
run_llvm_config(arguments: &[&str]) -> Result<String, String>188 fn run_llvm_config(arguments: &[&str]) -> Result<String, String> {
189     let config = env::var("LLVM_CONFIG_PATH").unwrap_or_else(|_| "llvm-config".to_string());
190     run(&config, arguments).map(|(o, _)| o)
191 }
192 
193 /// Parses a version number if possible, ignoring trailing non-digit characters.
parse_version_number(number: &str) -> Option<c_int>194 fn parse_version_number(number: &str) -> Option<c_int> {
195     number
196         .chars()
197         .take_while(|c| c.is_digit(10))
198         .collect::<String>()
199         .parse()
200         .ok()
201 }
202 
203 /// Parses the version from the output of a `clang` executable if possible.
parse_version(path: &Path) -> Option<CXVersion>204 fn parse_version(path: &Path) -> Option<CXVersion> {
205     let output = run_clang(path, &["--version"]).0;
206     let start = output.find("version ")? + 8;
207     let mut numbers = output[start..].split_whitespace().next()?.split('.');
208     let major = numbers.next().and_then(parse_version_number)?;
209     let minor = numbers.next().and_then(parse_version_number)?;
210     let subminor = numbers.next().and_then(parse_version_number).unwrap_or(0);
211     Some(CXVersion {
212         Major: major,
213         Minor: minor,
214         Subminor: subminor,
215     })
216 }
217 
218 /// Parses the search paths from the output of a `clang` executable if possible.
parse_search_paths(path: &Path, language: &str, args: &[String]) -> Option<Vec<PathBuf>>219 fn parse_search_paths(path: &Path, language: &str, args: &[String]) -> Option<Vec<PathBuf>> {
220     let mut clang_args = vec!["-E", "-x", language, "-", "-v"];
221     clang_args.extend(args.iter().map(|s| &**s));
222     let output = run_clang(path, &clang_args).1;
223     let start = output.find("#include <...> search starts here:")? + 34;
224     let end = output.find("End of search list.")?;
225     let paths = output[start..end].replace("(framework directory)", "");
226     Some(
227         paths
228             .lines()
229             .filter(|l| !l.is_empty())
230             .map(|l| Path::new(l.trim()).into())
231             .collect(),
232     )
233 }
234