python3_config/
lib.rs

1use std::error;
2use std::fmt;
3use std::str::FromStr;
4
5use rustpython_parser::ast::{Expression, ExpressionType, Number, StatementType, StringGroup};
6use rustpython_parser::error::ParseError;
7use rustpython_parser::parser;
8
9/// Represents an error during parsing
10#[derive(Debug)]
11pub enum Error {
12    /// Python source code syntax error
13    SyntaxError(ParseError),
14    /// missing build_time_vars variable
15    MissingBuildTimeVars,
16    /// missing required key in configuration
17    KeyError(&'static str),
18}
19
20impl fmt::Display for Error {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Error::SyntaxError(err) => err.fmt(f),
24            Error::MissingBuildTimeVars => write!(f, "missing build_time_vars variable"),
25            Error::KeyError(key) => write!(f, "missing required key {}", key),
26        }
27    }
28}
29
30impl error::Error for Error {
31    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
32        match self {
33            Error::SyntaxError(err) => Some(err),
34            Error::MissingBuildTimeVars => None,
35            Error::KeyError(_) => None,
36        }
37    }
38}
39
40impl From<ParseError> for Error {
41    fn from(err: ParseError) -> Self {
42        Self::SyntaxError(err)
43    }
44}
45
46/// Python configuration information
47#[derive(Debug, Clone)]
48pub struct PythonConfig {
49    sys_config_data: SysConfigData,
50}
51
52impl PythonConfig {
53    /// Parse from `_sysconfigdata.py` content
54    pub fn parse(src: &str) -> Result<Self, Error> {
55        let sys_config_data = SysConfigData::parse(src)?;
56        Ok(Self { sys_config_data })
57    }
58
59    /// Returns Python version
60    pub fn version(&self) -> &str {
61        &self.sys_config_data.build_time_vars.version
62    }
63
64    /// Returns Python major version
65    pub fn version_major(&self) -> u32 {
66        let version = self.version();
67        version
68            .split('.')
69            .next()
70            .and_then(|x| x.parse::<u32>().ok())
71            .unwrap()
72    }
73
74    /// Returns Python minor version
75    pub fn version_minor(&self) -> u32 {
76        let version = self.version();
77        version
78            .split('.')
79            .nth(1)
80            .and_then(|x| x.parse::<u32>().ok())
81            .unwrap()
82    }
83
84    /// Returns the installation prefix of the Python interpreter
85    pub fn prefix(&self) -> &str {
86        &self.sys_config_data.build_time_vars.prefix
87    }
88
89    /// Returns the executable path prefix for the Python interpreter
90    pub fn exec_prefix(&self) -> &str {
91        &self.sys_config_data.build_time_vars.exec_prefix
92    }
93
94    /// C compilation flags
95    pub fn cflags(&self) -> &str {
96        &self.sys_config_data.build_time_vars.cflags
97    }
98
99    /// Returns linker flags required for linking this Python
100    /// distribution. All libraries / frameworks have the appropriate `-l`
101    /// or `-framework` prefixes.
102    pub fn libs(&self) -> &str {
103        &self.sys_config_data.build_time_vars.libs
104    }
105
106    /// Returns linker flags required for creating
107    /// a shared library for this Python distribution. All libraries / frameworks
108    /// have the appropriate `-L`, `-l`, or `-framework` prefixes.
109    pub fn ldflags(&self) -> &str {
110        &self.sys_config_data.build_time_vars.ldflags
111    }
112
113    /// Returns the file extension for this distribution's library
114    pub fn ext_suffix(&self) -> &str {
115        &self.sys_config_data.build_time_vars.ext_suffix
116    }
117
118    /// The ABI flags specified when building this Python distribution
119    pub fn abiflags(&self) -> &str {
120        &self.sys_config_data.build_time_vars.abiflags
121    }
122
123    /// The location of the distribution's `python3-config` script
124    pub fn config_dir(&self) -> &str {
125        &self.sys_config_data.build_time_vars.config_dir
126    }
127
128    /// Returns the C headers include directory
129    pub fn include_dir(&self) -> &str {
130        &self.sys_config_data.build_time_vars.include_dir
131    }
132
133    /// Returns library directory
134    pub fn lib_dir(&self) -> &str {
135        &self.sys_config_data.build_time_vars.lib_dir
136    }
137
138    /// Returns ld version
139    pub fn ld_version(&self) -> &str {
140        &self.sys_config_data.build_time_vars.ld_version
141    }
142
143    /// Returns SOABI
144    pub fn soabi(&self) -> &str {
145        &self.sys_config_data.build_time_vars.soabi
146    }
147
148    /// Returns shared library suffix
149    pub fn shlib_suffix(&self) -> &str {
150        &self.sys_config_data.build_time_vars.shlib_suffix
151    }
152
153    /// Returns whether this distribution is built with `--enable-shared`
154    pub fn enable_shared(&self) -> bool {
155        self.sys_config_data.build_time_vars.py_enable_shared
156    }
157
158    /// Returns whether this distribution is built with `Py_DEBUG`
159    pub fn debug(&self) -> bool {
160        self.sys_config_data.build_time_vars.py_debug
161    }
162
163    /// Returns whether this distribution is built with `Py_REF_DEBUG`
164    pub fn ref_debug(&self) -> bool {
165        self.sys_config_data.build_time_vars.py_ref_debug
166    }
167
168    /// Returns whether this distribution is built with thread
169    pub fn with_thread(&self) -> bool {
170        self.sys_config_data.build_time_vars.with_thread
171    }
172
173    /// Returns pointer size (size of C `void*`) of this distribution
174    pub fn pointer_size(&self) -> u32 {
175        self.sys_config_data.build_time_vars.size_of_void_p
176    }
177}
178
179#[derive(Debug, Clone)]
180struct SysConfigData {
181    pub build_time_vars: BuildTimeVars,
182}
183
184#[derive(Debug, Clone, Default)]
185struct BuildTimeVars {
186    pub abiflags: String,
187    pub count_allocs: bool,
188    pub cflags: String,
189    pub config_dir: String,
190    pub ext_suffix: String,
191    pub exec_prefix: String,
192    pub include_dir: String,
193    pub lib_dir: String,
194    pub libs: String,
195    pub ldflags: String,
196    pub ld_version: String,
197    pub prefix: String,
198    pub py_debug: bool,
199    pub py_ref_debug: bool,
200    pub py_trace_refs: bool,
201    pub py_enable_shared: bool,
202    pub soabi: String,
203    pub shlib_suffix: String,
204    pub size_of_void_p: u32,
205    pub with_thread: bool,
206    pub version: String,
207}
208
209impl SysConfigData {
210    pub fn parse(src: &str) -> Result<Self, Error> {
211        let program = parser::parse_program(src)?;
212        let mut vars = BuildTimeVars::default();
213        for stmt in program.statements {
214            if let StatementType::Assign { targets, value } = stmt.node {
215                let var_name = targets.get(0).ok_or(Error::MissingBuildTimeVars)?;
216                match &var_name.node {
217                    ExpressionType::Identifier { name } if name == "build_time_vars" => {}
218                    _ => continue,
219                }
220                if let ExpressionType::Dict { elements } = value.node {
221                    for (key, value) in elements {
222                        if let Some(key) = key.and_then(|key| get_string(&key)) {
223                            match key.as_str() {
224                                "ABIFLAGS" => {
225                                    vars.abiflags = get_string(&value).unwrap_or_default()
226                                }
227                                "COUNT_ALLOCS" => vars.count_allocs = get_bool(&value),
228                                "CFLAGS" => vars.cflags = get_string(&value).unwrap_or_default(),
229                                "LIBPL" => vars.config_dir = get_string(&value).unwrap_or_default(),
230                                "EXT_SUFFIX" => {
231                                    vars.ext_suffix = get_string(&value).unwrap_or_default()
232                                }
233                                "exec_prefix" => {
234                                    vars.exec_prefix = get_string(&value).unwrap_or_default()
235                                }
236                                "INCLUDEDIR" => {
237                                    vars.include_dir = get_string(&value).unwrap_or_default()
238                                }
239                                "LIBDIR" => vars.lib_dir = get_string(&value).unwrap_or_default(),
240                                "LIBS" => vars.libs = get_string(&value).unwrap_or_default(),
241                                "LDFLAGS" => vars.ldflags = get_string(&value).unwrap_or_default(),
242                                "LDVERSION" => {
243                                    vars.ld_version = get_string(&value).unwrap_or_default()
244                                }
245                                "prefix" => vars.prefix = get_string(&value).unwrap_or_default(),
246                                "Py_DEBUG" => vars.py_debug = get_bool(&value),
247                                "Py_ENABLE_SHARED" => vars.py_enable_shared = get_bool(&value),
248                                "Py_REF_DEBUG" => vars.py_ref_debug = get_bool(&value),
249                                "Py_TRACE_REFS" => vars.py_trace_refs = get_bool(&value),
250                                "SOABI" => vars.soabi = get_string(&value).unwrap_or_default(),
251                                "SHLIB_SUFFIX" => {
252                                    vars.shlib_suffix = get_string(&value).unwrap_or_default()
253                                }
254                                "SIZEOF_VOID_P" => {
255                                    vars.size_of_void_p = get_number(&value)
256                                        .ok_or(Error::KeyError("SIZEOF_VOID_P"))?
257                                        as u32
258                                }
259                                "VERSION" => {
260                                    vars.version =
261                                        get_string(&value).ok_or(Error::KeyError("VERSION"))?
262                                }
263                                _ => continue,
264                            }
265                        } else {
266                            continue;
267                        }
268                    }
269                }
270            }
271        }
272        if vars.version.is_empty() {
273            // no build_time_vars found
274            return Err(Error::MissingBuildTimeVars);
275        }
276        Ok(SysConfigData {
277            build_time_vars: vars,
278        })
279    }
280}
281
282fn get_string(expr: &Expression) -> Option<String> {
283    match &expr.node {
284        ExpressionType::String { value: sg } => match sg {
285            StringGroup::Constant { value } => Some(value.to_string()),
286            StringGroup::Joined { values } => {
287                let mut s = String::new();
288                for value in values {
289                    if let StringGroup::Constant { value: cs } = value {
290                        s.push_str(&cs)
291                    }
292                }
293                Some(s)
294            }
295            _ => None,
296        },
297        _ => None,
298    }
299}
300
301fn get_number(expr: &Expression) -> Option<i32> {
302    use num_traits::cast::ToPrimitive;
303
304    match &expr.node {
305        ExpressionType::Number { value } => {
306            if let Number::Integer { value } = value {
307                value.to_i32()
308            } else {
309                None
310            }
311        }
312        _ => None,
313    }
314}
315
316fn get_bool(expr: &Expression) -> bool {
317    get_number(expr).map(|x| x == 1).unwrap_or(false)
318}
319
320impl FromStr for PythonConfig {
321    type Err = Error;
322
323    fn from_str(s: &str) -> Result<Self, Self::Err> {
324        Self::parse(s)
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::{Error, PythonConfig};
331    use std::fs;
332
333    #[test]
334    fn read_python_sysconfig_data() {
335        let src =
336            fs::read_to_string("tests/fixtures/cpython38_sysconfigdata__darwin_darwin.py").unwrap();
337        let config = PythonConfig::parse(&src).unwrap();
338        assert_eq!(config.abiflags(), "");
339        assert_eq!(config.soabi(), "cpython-38-darwin");
340        assert_eq!(config.version(), "3.8");
341        assert_eq!(config.version_major(), 3);
342        assert_eq!(config.version_minor(), 8);
343
344        // Test FromStr impl
345        let config: PythonConfig = src.parse().unwrap();
346        assert_eq!(config.abiflags(), "");
347    }
348
349    #[test]
350    fn read_invalid_python_sysconfig_data() {
351        let config = PythonConfig::parse("i++").unwrap_err();
352        assert!(matches!(config, Error::SyntaxError(_)));
353
354        let config = PythonConfig::parse("").unwrap_err();
355        assert!(matches!(config, Error::MissingBuildTimeVars));
356
357        let config = PythonConfig::parse("i = 0").unwrap_err();
358        assert!(matches!(config, Error::MissingBuildTimeVars));
359
360        let config =
361            PythonConfig::parse("build_time_vars = {'VERSION': '3.8', 'SIZEOF_VOID_P': ''}")
362                .unwrap_err();
363        assert!(matches!(config, Error::KeyError("SIZEOF_VOID_P")));
364    }
365}