use crate::Manylinux;
use crate::Target;
use failure::{bail, format_err, Error, Fail, ResultExt};
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json;
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 < 5 {
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>, Error> {
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\\:-]+)$").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 Interpreter {
CPython,
PyPy,
}
impl fmt::Display for Interpreter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Interpreter::CPython => write!(f, "CPython"),
Interpreter::PyPy => write!(f, "PyPy"),
}
}
}
#[derive(Serialize, Deserialize)]
struct IntepreterMetadataMessage {
major: usize,
minor: usize,
abiflags: Option<String>,
interpreter: String,
ext_suffix: Option<String>,
m: bool,
u: bool,
d: bool,
platform: String,
abi_tag: Option<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: Interpreter,
pub abi_tag: Option<String>,
}
fn fun_with_abiflags(
message: &IntepreterMetadataMessage,
target: &Target,
) -> Result<String, Error> {
let sane_platform = match message.platform.as_ref() {
"windows" => target.is_windows(),
"linux" => target.is_linux(),
"darwin" => target.is_macos(),
"freebsd" => target.is_freebsd(),
_ => false,
};
if !sane_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 python >= 3.5 is supported, while your using python {}.{}",
message.major,
message.minor
);
}
if message.interpreter == "pypy" {
Ok("".to_string())
} else if target.is_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" {
bail!("A python 3 interpreter on linux or mac os must have 'm' 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) -> String {
match self.interpreter {
Interpreter::CPython => {
let platform = self.target.get_platform_tag(manylinux);
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
)
}
}
Interpreter::PyPy => {
if manylinux != &Manylinux::Off {
eprintln!(
"🛈 Note: PyPy doesn't support the manylinux yet, \
so native wheels are built instead of manylinux wheels"
);
}
let platform = self.target.get_platform_tag(&Manylinux::Off);
format!(
"pp3{abi_tag}-pypy3_{abi_tag}-{platform}",
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 {
Interpreter::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
)
}
}
Interpreter::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,
) -> Result<Option<PythonInterpreter>, Error> {
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 {
bail!(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 = Interpreter::CPython,
"pypy" => interpreter = Interpreter::PyPy,
_ => {
bail!("Invalid interpreter");
}
};
let abiflags = fun_with_abiflags(&message, &target).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,
abi_tag: message.abi_tag,
}))
}
pub fn find_all(target: &Target) -> Result<Vec<PythonInterpreter>, Error> {
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)? {
available_versions.push(version);
}
}
Ok(available_versions)
}
pub fn check_executables(
executables: &[PathBuf],
target: &Target,
) -> Result<Vec<PythonInterpreter>, Error> {
let mut available_versions = Vec::new();
for executable in executables {
if let Some(version) = PythonInterpreter::check_executable(executable, &target)
.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,
self.major,
self.minor,
self.abiflags,
self.executable.display()
)
}
}