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}