1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use std::collections::HashMap;
use std::io;
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str::FromStr;

pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;


/// Extract compilation vars from the specified interpreter.
pub fn get_config_from_interpreter<P: AsRef<Path>>(interpreter: P) -> Result<InterpreterConfig> {
    let script = r#"
import json
import platform
import struct
import sys
import sysconfig

PYPY = platform.python_implementation() == "PyPy"

try:
    base_prefix = sys.base_prefix
except AttributeError:
    base_prefix = sys.exec_prefix

libdir = sysconfig.get_config_var('LIBDIR')

print("version_major", sys.version_info[0])
print("version_minor", sys.version_info[1])
print("implementation", platform.python_implementation())
if libdir is not None:
    print("libdir", libdir)
print("ld_version", sysconfig.get_config_var('LDVERSION') or sysconfig.get_config_var('py_version_short'))
print("base_prefix", base_prefix)
print("shared", PYPY or bool(sysconfig.get_config_var('Py_ENABLE_SHARED')))
print("executable", sys.executable)
print("calcsize_pointer", struct.calcsize("P"))
"#;
    let output = run_python_script(interpreter.as_ref(), script)?;
    let map: HashMap<String, String> = output
        .lines()
        .filter_map(|line| {
            let mut i = line.splitn(2, ' ');
            Some((i.next()?.into(), i.next()?.into()))
        })
        .collect();
    Ok(InterpreterConfig {
        version: PythonVersion {
            major: map["version_major"].parse()?,
            minor: map["version_minor"].parse()?,
            implementation: map["implementation"].parse()?,
        },
        libdir: map.get("libdir").cloned(),
        shared: map["shared"] == "True",
        ld_version: map["ld_version"].clone(),
        base_prefix: map["base_prefix"].clone(),
        executable: map["executable"].clone().into(),
        calcsize_pointer: map["calcsize_pointer"].parse()?,
    })
}

/// Information about a Python interpreter
#[derive(Debug)]
pub struct InterpreterConfig {
    pub version: PythonVersion,
    pub libdir: Option<String>,
    pub shared: bool,
    pub ld_version: String,
    /// Prefix used for determining the directory of libpython
    pub base_prefix: String,
    pub executable: PathBuf,
    pub calcsize_pointer: u32,
}

#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum PythonImplementation {
    CPython,
    PyPy,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PythonVersion {
    pub major: u8,
    // minor == None means any minor version will do
    pub minor: u8,
    pub implementation: PythonImplementation,
}

impl fmt::Display for PythonVersion {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?} {}.{}", self.implementation, self.major, self.minor)
    }
}

impl FromStr for PythonImplementation {
    type Err = Box<dyn std::error::Error>;
    fn from_str(s: &str) -> Result<Self> {
        match s {
            "CPython" => Ok(PythonImplementation::CPython),
            "PyPy" => Ok(PythonImplementation::PyPy),
            _ => Err(format!("Invalid interpreter: {}", s).into()),
        }
    }
}

/// Run a python script using the specified interpreter binary.
fn run_python_script(interpreter: &Path, script: &str) -> Result<String> {
    let out = Command::new(interpreter)
        .args(&["-c", script])
        .stderr(Stdio::inherit())
        .output();

    match out {
        Err(err) => {
            if err.kind() == io::ErrorKind::NotFound {
                return Err(format!(
                    "Could not find any interpreter at {}, \
                     are you sure you have Python installed on your PATH?",
                    interpreter.display()
                )
                .into());
            } else {
                return Err(format!(
                    "Failed to run the Python interpreter at {}: {}",
                    interpreter.display(),
                    err
                )
                .into());
            }
        }
        Ok(ref ok) if !ok.status.success() => {
            return Err(format!("Python script failed: {}", script).into())
        }
        Ok(ok) => Ok(String::from_utf8(ok.stdout)?),
    }
}

/// Search for python interpreters and yield them in order.
///
/// The following locations are checked in the order listed:
///
/// 1. `python`
/// 2. `python3`
pub fn find_interpreters() -> impl Iterator<Item = InterpreterConfig> {
    ["python", "python3"]
        .iter()
        .filter_map(|interpreter| {
            get_config_from_interpreter(Path::new(interpreter)).ok()
        })
}

/// Return the first interpreter matching the given criterion.
pub fn find_interpreter_matching<F>(f: F) -> Option<InterpreterConfig>
where
    F: FnMut(&InterpreterConfig) -> bool
{
    find_interpreters().find(f)
}