python_config/lib.rs
1//! # python-config-rs
2//!
3//! Just like the `python3-config` script that's installed
4//! with your Python distribution, `python-config-rs` helps you
5//! find information about your Python distribution.
6//!
7//! ```no_run
8//! use python_config::PythonConfig;
9//!
10//! let cfg = PythonConfig::new(); // Python 3
11//!
12//! // Print include directories
13//! println!("Includes: {}", cfg.includes().unwrap());
14//! // Print installation prefix
15//! println!("Installation prefix: {}", cfg.prefix().unwrap());
16//! ```
17//!
18//! `python-config` may be most useful in your `build.rs`
19//! script, or in any application where you need to find
20//!
21//! - the location of Python libraries
22//! - the include directory for Python headers
23//! - any of the things available via `python-config`
24//!
25//! Essentially, this is a reimplementation of the
26//! `python3-config` script with a Rust interface. We work
27//! directly with your Python interpreter, just in case
28//! a `python-config` script is not on your system.
29//!
30//! We provide a new binary, `python3-config`, in case (for whatever
31//! reason) you'd like to use this version of `python3-config`
32//! instead of the distribution's script. We have tests that
33//! show our script takes the exact same inputs and returns
34//! the exact same outputs. Note that the tests only work if
35//! you have a Python 3 distribution that includes a
36//! `python3-config` script.
37//!
38//! ## 3 > 2
39//!
40//! We make the choice for you: by default, we favor Python 3
41//! over Python 2. If you need Python 2 support, use the more
42//! explicit interface to create the corresponding `PythonConfig`
43//! handle. Note that, while the Python 2 interface should work,
44//! it's gone through significantly less testing.
45//!
46//! The `python3-config` binary in this crate is Python 3 only.
47
48mod cmdr;
49#[macro_use]
50mod script;
51
52use cmdr::SysCommand;
53
54use semver;
55
56use std::io;
57use std::path::{self, PathBuf};
58
59/// Selectable Python version
60#[derive(PartialEq, Eq, Debug)]
61pub enum Version {
62 /// Python 3
63 Three,
64 /// Python 2
65 Two,
66}
67
68/// Describes a few possible errors from the `PythonConfig` interface
69#[derive(Debug)]
70pub enum Error {
71 /// An I/O error occured while interfacing the interpreter
72 IO(io::Error),
73 /// This function is for Python 3 only
74 ///
75 /// This will be the return error for methods returning
76 /// a [`Py3Only<T>`](type.Py3Only.html) type.
77 Python3Only,
78 /// Other, one-off errors, with reasoning provided as a string
79 Other(&'static str),
80}
81
82impl From<io::Error> for Error {
83 fn from(err: io::Error) -> Self {
84 Error::IO(err)
85 }
86}
87
88impl From<Error> for io::Error {
89 fn from(err: Error) -> Self {
90 match err {
91 Error::IO(err) => err,
92 Error::Python3Only => io::Error::new(
93 io::ErrorKind::Other,
94 "this function is only available for Python 3",
95 ),
96 Error::Other(why) => io::Error::new(io::ErrorKind::Other, why),
97 }
98 }
99}
100
101/// The result type denoting a return `T` or
102/// an [`Error`](enum.Error.html).
103pub type PyResult<T> = Result<T, Error>;
104
105/// The result type denotes that this function
106/// is only available when interfacing a Python 3
107/// interpreter.
108///
109/// It's the same as the normal [`PyResult`](type.PyResult.html)
110/// used throughout this module, but it's just a little
111/// type hint.
112pub type Py3Only<T> = Result<T, Error>;
113
114#[inline]
115fn other_err(what: &'static str) -> Error {
116 Error::Other(what)
117}
118
119/// Defines the script with a common prelude of imports
120/// and helper functions. Returns a single string that
121/// represents the script.
122fn build_script(lines: &[&str]) -> String {
123 let mut script = String::new();
124 script.push_str("from __future__ import print_function\n");
125 script.push_str("import sysconfig\n");
126 script.push_str("pyver = sysconfig.get_config_var('VERSION')\n");
127 script.push_str("getvar = sysconfig.get_config_var\n");
128 script.push_str(&lines.join("\n"));
129 script
130}
131
132/// Exposes Python configuration information
133pub struct PythonConfig {
134 /// The commander that provides responses to our commands
135 cmdr: SysCommand,
136 /// The version of the Python interpreter we're using
137 ver: Version,
138}
139
140impl Default for PythonConfig {
141 fn default() -> PythonConfig {
142 PythonConfig::new()
143 }
144}
145
146impl PythonConfig {
147 /// Create a new `PythonConfig` that uses the system installed Python 3
148 /// interpreter to query configuration information.
149 pub fn new() -> Self {
150 PythonConfig::version(Version::Three)
151 }
152
153 /// Create a new `PythonConfig` that uses the system installed Python
154 /// of version `version`.
155 ///
156 /// # Example
157 ///
158 /// ```
159 /// use python_config::{PythonConfig, Version};
160 ///
161 /// // Use the system-wide Python2 interpreter
162 /// let cfg = PythonConfig::version(Version::Two);
163 /// ```
164 pub fn version(version: Version) -> Self {
165 match version {
166 Version::Three => Self::with_commander(version, SysCommand::new("python3")),
167 Version::Two => Self::with_commander(version, SysCommand::new("python2")),
168 }
169 }
170
171 fn with_commander(ver: Version, cmdr: SysCommand) -> Self {
172 PythonConfig { cmdr, ver }
173 }
174
175 fn is_py3(&self) -> Result<(), Error> {
176 if self.ver != Version::Three {
177 Err(Error::Python3Only)
178 } else {
179 Ok(())
180 }
181 }
182
183 /// Create a `PythonConfig` that uses the interpreter at the path `interpreter`.
184 ///
185 /// This fails if the path cannot be represented as a string, or if a query
186 /// for the Python version fails.
187 ///
188 /// # Example
189 ///
190 /// ```no_run
191 /// use python_config::PythonConfig;
192 ///
193 /// let cfg = PythonConfig::interpreter("/usr/local/bin/python3");
194 /// assert!(cfg.is_ok());
195 /// ```
196 pub fn interpreter<P: AsRef<path::Path>>(interpreter: P) -> PyResult<Self> {
197 let cmdr = SysCommand::new(
198 interpreter
199 .as_ref()
200 .to_str()
201 .ok_or_else(|| other_err("unable to coerce interpreter path to string"))?,
202 );
203 // Assume Python 3 unless the semver tells us otherwise
204 let mut cfg = PythonConfig {
205 cmdr,
206 ver: Version::Three,
207 };
208
209 if cfg.semantic_version()?.major == 2 {
210 cfg.ver = Version::Two;
211 }
212
213 Ok(cfg)
214 }
215
216 /// Returns the Python version string
217 ///
218 /// This is the raw return of `python --version`. Consider using
219 /// [`semantic_version`](struct.PythonConfig.html#method.semantic_version)
220 /// for something more useful.
221 pub fn version_raw(&self) -> PyResult<String> {
222 self.cmdr.commands(&["--version"]).map_err(From::from)
223 }
224
225 /// Returns the Python version as a semver
226 pub fn semantic_version(&self) -> PyResult<semver::Version> {
227 self.version_raw()
228 .and_then(|resp| {
229 let mut witer = resp.split_whitespace();
230 witer.next(); // 'Python'
231 let ver = witer.next().ok_or_else(|| {
232 other_err("expected --version to return a string resembling 'Python X.Y.Z'")
233 })?;
234 semver::Version::parse(ver).map_err(|_| other_err("unable to parse semver"))
235 })
236 .map_err(From::from)
237 }
238
239 fn script(&self, lines: &[&str]) -> PyResult<String> {
240 self.cmdr
241 .commands(&["-c", &build_script(lines)])
242 .map_err(From::from)
243 }
244
245 /// Returns the installation prefix of the Python interpreter as a string
246 pub fn prefix(&self) -> PyResult<String> {
247 self.script(&["print(getvar('prefix'))"])
248 }
249
250 /// Like [`prefix`](#method.prefix), but returns
251 /// the installation prefix as a `PathBuf`.
252 pub fn prefix_path(&self) -> PyResult<PathBuf> {
253 self.prefix().map(PathBuf::from)
254 }
255
256 /// Returns the executable path prefix for the Python interpreter as a string
257 pub fn exec_prefix(&self) -> PyResult<String> {
258 self.script(&["print(getvar('exec_prefix'))"])
259 }
260
261 /// Like [`exec_prefix`](#method.exec_prefix), but
262 /// returns the executable prefix as a `PathBuf`.
263 pub fn exec_prefix_path(&self) -> PyResult<PathBuf> {
264 self.exec_prefix().map(PathBuf::from)
265 }
266
267 /// Returns a list of paths that represent the include paths
268 /// for the distribution's headers. This is a space-delimited
269 /// string of paths prefixed with `-I`.
270 pub fn includes(&self) -> PyResult<String> {
271 self.script(&[
272 "flags = ['-I' + sysconfig.get_path('include'), '-I' + sysconfig.get_path('platinclude')]",
273 "print(' '.join(flags))",
274 ])
275 }
276
277 /// Returns a list of paths that represent the include paths
278 /// for the distribution's headers. Unlike [`includes`](#method.includes),
279 /// This is simply a collection of paths.
280 pub fn include_paths(&self) -> PyResult<Vec<PathBuf>> {
281 self.script(&[
282 "print(sysconfig.get_path('include'))",
283 "print(sysconfig.get_path('platinclude'))",
284 ])
285 .map(|resp| resp.lines().map(PathBuf::from).collect())
286 }
287
288 /// All the flags useful for C compilation. This includes the include
289 /// paths (see [`includes`](#method.includes)) as well as other compiler
290 /// flags for this target. The return is a string with spaces separating
291 /// the flags.
292 pub fn cflags(&self) -> PyResult<String> {
293 self.script(&[
294 "flags = ['-I' + sysconfig.get_path('include'), '-I' + sysconfig.get_path('platinclude')]",
295 linux_line!("flags.extend(getvar('BASECFLAGS').split())"),
296 linux_line!("flags.extend(getvar('CONFIGURE_CFLAGS').split())"),
297 macos_line!("flags.extend(getvar('CFLAGS').split())"),
298 "print(' '.join(flags))",
299 ])
300 }
301
302 /// Returns linker flags required for linking this Python
303 /// distribution. All libraries / frameworks have the appropriate `-l`
304 /// or `-framework` prefixes.
305 pub fn libs(&self) -> PyResult<String> {
306 self.script(&[
307 "import sys",
308 "libs = ['-lpython' + pyver + sys.abiflags]",
309 "libs += getvar('LIBS').split()",
310 "libs += getvar('SYSLIBS').split()",
311 "print(' '.join(libs))",
312 ])
313 }
314
315 /// Returns linker flags required for creating
316 /// a shared library for this Python distribution. All libraries / frameworks
317 /// have the appropriate `-l` or `-framework` prefixes.
318 pub fn ldflags(&self) -> PyResult<String> {
319 self.script(&[
320 "import sys",
321 "libs = ['-lpython' + pyver + sys.abiflags]",
322 linux_line!["libs.insert(0, '-L' + getvar('exec_prefix') + '/lib')"],
323 "libs += getvar('LIBS').split()",
324 "libs += getvar('SYSLIBS').split()",
325 "if not getvar('Py_ENABLED_SHARED'):",
326 tab!("libs.insert(0, '-L' + getvar('LIBPL'))"),
327 "if not getvar('PYTHONFRAMEWORK'):",
328 tab!("libs.extend(getvar('LINKFORSHARED').split())"),
329 "print(' '.join(libs))",
330 ])
331 }
332
333 /// Returns a string that represents the file extension for this distribution's library
334 ///
335 /// This is only available when your interpreter is a Python 3 interpreter! This is for
336 /// feature parity with the `python3-config` script.
337 pub fn extension_suffix(&self) -> Py3Only<String> {
338 self.is_py3()?;
339 let resp = self.script(&["print(getvar('EXT_SUFFIX'))"])?;
340 Ok(resp)
341 }
342
343 /// The ABI flags specified when building this Python distribution
344 ///
345 /// This is only available when your interpreter is a Python 3 interpreter! This is for
346 /// feature parity with the `python3-config` script.
347 pub fn abi_flags(&self) -> Py3Only<String> {
348 self.is_py3()?;
349 let resp = self.script(&["import sys", "print(sys.abiflags)"])?;
350 Ok(resp)
351 }
352
353 /// The location of the distribution's actual `python3-config` script
354 ///
355 /// This is only available when your interpreter is a Python 3 interpreter! This is for
356 /// feature parity with the `python3-config` script.
357 pub fn config_dir(&self) -> Py3Only<String> {
358 self.is_py3()?;
359 let resp = self.script(&["print(getvar('LIBPL'))"])?;
360 Ok(resp)
361 }
362
363 /// Like [`config_dir`](#method.config_dir), but returns the path to
364 /// the distribution's `python-config` script as a `PathBuf`.
365 ///
366 /// This is only available when your interpreter is a Python 3 interpreter! This is for
367 /// feature parity with the `python3-config` script.
368 pub fn config_dir_path(&self) -> Py3Only<PathBuf> {
369 self.config_dir().map(PathBuf::from)
370 }
371}