use crate::auditwheel::Manylinux;
use crate::{BridgeModel, Target};
use anyhow::{bail, format_err, Context, Result};
use regex::Regex;
use serde::Deserialize;
use std::collections::HashSet;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str;
const GET_INTERPRETER_METADATA: &str = include_str!("get_interpreter_metadata.py");
fn windows_interpreter_no_build(
major: usize,
minor: usize,
target_width: usize,
pointer_width: usize,
) -> bool {
if major == 2 {
return true;
}
if major == 3 && minor < 6 {
return true;
}
if pointer_width != target_width {
println!(
"👽 {}.{} is installed as {}-bit, while the target is {}-bit. Skipping.",
major, minor, pointer_width, target_width
);
return true;
}
false
}
fn find_all_windows(target: &Target) -> Result<Vec<String>> {
let code = "import sys; print(sys.executable or '')";
let mut interpreter = vec![];
let mut versions_found = HashSet::new();
let execution = Command::new("py").arg("-0").output();
if let Ok(output) = execution {
let expr = Regex::new(r" -(\d).(\d)-(\d+)(?: .*)?").unwrap();
let lines = str::from_utf8(&output.stdout).unwrap().lines();
for line in lines {
if let Some(capture) = expr.captures(line) {
let context = "Expected a digit";
let major = capture
.get(1)
.unwrap()
.as_str()
.parse::<usize>()
.context(context)?;
let minor = capture
.get(2)
.unwrap()
.as_str()
.parse::<usize>()
.context(context)?;
if !versions_found.contains(&(major, minor)) {
let pointer_width = capture
.get(3)
.unwrap()
.as_str()
.parse::<usize>()
.context(context)?;
if windows_interpreter_no_build(
major,
minor,
target.pointer_width(),
pointer_width,
) {
continue;
}
let version = format!("-{}.{}-{}", major, minor, pointer_width);
let output = Command::new("py")
.args(&[&version, "-c", code])
.output()
.unwrap();
let path = str::from_utf8(&output.stdout).unwrap().trim();
if !output.status.success() || path.trim().is_empty() {
bail!("Couldn't determine the path to python for `py {}`", version);
}
interpreter.push(path.to_string());
versions_found.insert((major, minor));
}
}
}
}
let conda_info = Command::new("conda").arg("info").arg("-e").output();
if let Ok(output) = conda_info {
let lines = str::from_utf8(&output.stdout).unwrap().lines();
let re = Regex::new(r"^([^#].*?)[\s*]+([\w\\:.-]+)\s*$").unwrap();
let mut paths = vec![];
for i in lines {
if let Some(capture) = re.captures(&i) {
if &capture[1] == "base" {
continue;
}
paths.push(String::from(&capture[2]));
}
}
for path in paths {
let executable = Path::new(&path).join("python");
let python_info = Command::new(&executable)
.arg("-c")
.arg("import sys; print(sys.version)")
.output();
let python_info = match python_info {
Ok(python_info) => python_info,
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
continue;
} else {
bail!(
"Error getting Python version info from conda env at {}",
path
);
}
}
};
let version_info = str::from_utf8(&python_info.stdout).unwrap();
let expr = Regex::new(r"(\d).(\d).(\d+)").unwrap();
if let Some(capture) = expr.captures(version_info) {
let major = capture.get(1).unwrap().as_str().parse::<usize>().unwrap();
let minor = capture.get(2).unwrap().as_str().parse::<usize>().unwrap();
if !versions_found.contains(&(major, minor)) {
let pointer_width = if version_info.contains("64 bit (AMD64)") {
64_usize
} else {
32_usize
};
if windows_interpreter_no_build(
major,
minor,
target.pointer_width(),
pointer_width,
) {
continue;
}
interpreter.push(String::from(executable.to_str().unwrap()));
versions_found.insert((major, minor));
}
}
}
}
if interpreter.is_empty() {
bail!(
"Could not find any interpreters, are you sure you have python installed on your PATH?"
);
};
Ok(interpreter)
}
fn find_all_unix() -> Vec<String> {
let interpreter = &[
"python3.5",
"python3.6",
"python3.7",
"python3.8",
"python3.9",
];
interpreter.iter().map(ToString::to_string).collect()
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum InterpreterKind {
CPython,
PyPy,
}
impl fmt::Display for InterpreterKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
InterpreterKind::CPython => write!(f, "CPython"),
InterpreterKind::PyPy => write!(f, "PyPy"),
}
}
}
#[derive(Deserialize)]
struct IntepreterMetadataMessage {
major: usize,
minor: usize,
abiflags: Option<String>,
interpreter: String,
ext_suffix: Option<String>,
platform: String,
abi_tag: Option<String>,
base_prefix: String,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PythonInterpreter {
pub major: usize,
pub minor: usize,
pub abiflags: String,
pub target: Target,
pub executable: PathBuf,
pub ext_suffix: Option<String>,
pub interpreter_kind: InterpreterKind,
pub abi_tag: Option<String>,
pub libs_dir: PathBuf,
}
fn fun_with_abiflags(
message: &IntepreterMetadataMessage,
target: &Target,
bridge: &BridgeModel,
) -> Result<String> {
if bridge != &BridgeModel::Cffi && target.get_python_os() != message.platform {
bail!(
"sys.platform in python, {}, and the rust target, {:?}, don't match ಠ_ಠ",
message.platform,
target,
)
}
if message.major != 3 || message.minor < 5 {
bail!(
"Only python >= 3.5 is supported, while you're using python {}.{}",
message.major,
message.minor
);
}
if message.interpreter == "pypy" {
Ok("".to_string())
} else if message.platform == "windows" {
if message.abiflags.is_some() {
bail!("A python 3 interpreter on windows does not define abiflags in its sysconfig ಠ_ಠ")
} else {
Ok("".to_string())
}
} else if let Some(ref abiflags) = message.abiflags {
if message.minor >= 8 {
Ok(abiflags.to_string())
} else if (abiflags != "m") && (abiflags != "dm") {
bail!("A python 3 interpreter on linux or mac os must have 'm' or 'dm' as abiflags ಠ_ಠ")
} else {
Ok(abiflags.to_string())
}
} else {
bail!("A python 3 interpreter on linux or mac os must define abiflags in its sysconfig ಠ_ಠ")
}
}
impl PythonInterpreter {
pub fn get_tag(&self, manylinux: &Manylinux, universal2: bool) -> String {
match self.interpreter_kind {
InterpreterKind::CPython => {
let platform = self.target.get_platform_tag(manylinux, universal2);
if self.target.is_unix() {
format!(
"cp{major}{minor}-cp{major}{minor}{abiflags}-{platform}",
major = self.major,
minor = self.minor,
abiflags = self.abiflags,
platform = platform
)
} else {
format!(
"cp{major}{minor}-none-{platform}",
major = self.major,
minor = self.minor,
platform = platform
)
}
}
InterpreterKind::PyPy => {
let platform = self.target.get_platform_tag(manylinux, universal2);
format!(
"pp{major}{minor}-pypy{major}{minor}_{abi_tag}-{platform}",
major = self.major,
minor = self.minor,
abi_tag = self
.abi_tag
.clone()
.expect("PyPy's syconfig didn't define an `SOABI` ಠ_ಠ"),
platform = platform,
)
}
}
}
pub fn get_library_name(&self, base: &str) -> String {
match self.interpreter_kind {
InterpreterKind::CPython => {
let platform = self.target.get_shared_platform_tag();
if self.target.is_freebsd() {
format!(
"{base}.cpython-{major}{minor}{abiflags}.so",
base = base,
major = self.major,
minor = self.minor,
abiflags = self.abiflags,
)
} else if self.target.is_unix() {
format!(
"{base}.cpython-{major}{minor}{abiflags}-{platform}.so",
base = base,
major = self.major,
minor = self.minor,
abiflags = self.abiflags,
platform = platform,
)
} else {
format!(
"{base}.cp{major}{minor}-{platform}.pyd",
base = base,
major = self.major,
minor = self.minor,
platform = platform
)
}
}
InterpreterKind::PyPy => format!(
"{base}{ext_suffix}",
base = base,
ext_suffix = self
.ext_suffix
.clone()
.expect("PyPy's syconfig didn't define an `EXT_SUFFIX` ಠ_ಠ")
),
}
}
pub fn check_executable(
executable: impl AsRef<Path>,
target: &Target,
bridge: &BridgeModel,
) -> Result<Option<PythonInterpreter>> {
let output = Command::new(&executable.as_ref())
.args(&["-c", GET_INTERPRETER_METADATA])
.stderr(Stdio::inherit())
.output();
let err_msg = format!(
"Trying to get metadata from the python interpreter '{}' failed",
executable.as_ref().display()
);
let output = match output {
Ok(output) => {
if output.status.success() {
output
} else {
bail!(err_msg);
}
}
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
return Ok(None);
} else {
return Err(err).context(err_msg);
}
}
};
let message: IntepreterMetadataMessage = serde_json::from_slice(&output.stdout)
.context(err_msg)
.context(String::from_utf8_lossy(&output.stdout).trim().to_string())?;
if (message.major == 2 && message.minor != 7) || (message.major == 3 && message.minor < 5) {
return Ok(None);
}
let interpreter;
match message.interpreter.as_str() {
"cpython" => interpreter = InterpreterKind::CPython,
"pypy" => interpreter = InterpreterKind::PyPy,
_ => {
bail!("Invalid interpreter");
}
};
let abiflags = fun_with_abiflags(&message, &target, &bridge).context(format_err!(
"Failed to get information from the python interpreter at {}",
executable.as_ref().display()
))?;
Ok(Some(PythonInterpreter {
major: message.major,
minor: message.minor,
abiflags,
target: target.clone(),
executable: executable.as_ref().to_path_buf(),
ext_suffix: message.ext_suffix,
interpreter_kind: interpreter,
abi_tag: message.abi_tag,
libs_dir: PathBuf::from(message.base_prefix).join("libs"),
}))
}
pub fn find_all(target: &Target, bridge: &BridgeModel) -> Result<Vec<PythonInterpreter>> {
let executables = if target.is_windows() {
find_all_windows(&target)?
} else {
find_all_unix()
};
let mut available_versions = Vec::new();
for executable in executables {
if let Some(version) =
PythonInterpreter::check_executable(&executable, &target, &bridge)?
{
available_versions.push(version);
}
}
Ok(available_versions)
}
pub fn check_executables(
executables: &[PathBuf],
target: &Target,
bridge: &BridgeModel,
) -> Result<Vec<PythonInterpreter>> {
let mut available_versions = Vec::new();
for executable in executables {
if let Some(version) =
PythonInterpreter::check_executable(executable, &target, &bridge).context(
format!("{} is not a valid python interpreter", executable.display()),
)?
{
available_versions.push(version);
} else {
bail!(
"Python interpreter `{}` doesn't exist",
executable.display()
);
}
}
Ok(available_versions)
}
}
impl fmt::Display for PythonInterpreter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {}.{}{} at {}",
self.interpreter_kind,
self.major,
self.minor,
self.abiflags,
self.executable.display()
)
}
}