pub use self::config::InterpreterConfig;
use crate::auditwheel::PlatformTag;
use crate::target::Arch;
use crate::{BridgeModel, BuildContext, Target};
use anyhow::{Context, Result, bail, ensure, format_err};
use pep440_rs::{Version, VersionSpecifiers};
use regex::Regex;
use serde::Deserialize;
use std::collections::HashSet;
use std::fmt;
use std::io::{self, Write};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str::{self, FromStr};
use tracing::{debug, instrument};
mod config;
const GET_INTERPRETER_METADATA: &str = include_str!("get_interpreter_metadata.py");
pub const MINIMUM_PYTHON_MINOR: usize = 7;
pub const MINIMUM_PYPY_MINOR: usize = 8;
pub const MAXIMUM_PYTHON_MINOR: usize = 14;
pub const MAXIMUM_PYPY_MINOR: usize = 11;
fn find_all_windows(
target: &Target,
bridge: &BridgeModel,
requires_python: Option<&VersionSpecifiers>,
) -> Result<Vec<PythonInterpreter>> {
let min_python_minor = bridge.minimal_python_minor_version();
let mut interpreter = vec![];
let mut versions_found = HashSet::new();
macro_rules! maybe_add_interp {
($executable:expr) => {
PythonInterpreter::check_executable($executable, target, bridge).map(|interp| {
if let Some(interp) = interp {
let major = interp.major;
let minor = interp.minor;
if major == 3
&& minor >= min_python_minor
&& !versions_found.contains(&(major, minor))
&& requires_python.map_or(true, |requires_python| {
requires_python.contains(&Version::new([major as u64, minor as u64]))
})
{
interpreter.push(interp);
versions_found.insert((major, minor));
}
}
})
};
}
let execution = Command::new("cmd")
.arg("/c")
.arg("py")
.arg("--list-paths")
.output();
if let Ok(output) = execution {
let expr = Regex::new(r" -(V:)?(\d).(\d+)-?(arm)?(\d*)\s*\*?\s*(.*)?").unwrap();
let stdout = str::from_utf8(&output.stdout).unwrap();
for line in stdout.lines() {
if let Some(capture) = expr.captures(line) {
let major = capture
.get(2)
.unwrap()
.as_str()
.parse::<usize>()
.context("Expected a digit for major version")?;
let minor = capture
.get(3)
.unwrap()
.as_str()
.parse::<usize>()
.context("Expected a digit for minor version")?;
if !versions_found.contains(&(major, minor)) {
let executable = capture.get(6).unwrap().as_str();
let executable_path = Path::new(&executable);
if !executable_path.exists() {
continue;
}
maybe_add_interp!(executable_path)?;
}
}
}
}
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_win = Path::new(&path).join("python.exe");
let executable = if executable_win.exists() {
executable_win
} else {
Path::new(&path).join("python")
};
maybe_add_interp!(executable.as_path())?;
}
}
for minor in min_python_minor..=bridge.maximum_python_minor_version() {
if !versions_found.contains(&(3, minor)) {
let executable = format!("python3.{minor}.exe");
maybe_add_interp!(Path::new(&executable))?;
}
}
if interpreter.is_empty() {
bail!(
"Could not find any interpreters, are you sure you have python installed on your PATH?"
);
};
Ok(interpreter)
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
#[clap(rename_all = "lower")]
pub enum InterpreterKind {
CPython,
PyPy,
GraalPy,
}
impl InterpreterKind {
pub fn is_cpython(&self) -> bool {
matches!(self, InterpreterKind::CPython)
}
pub fn is_pypy(&self) -> bool {
matches!(self, InterpreterKind::PyPy)
}
pub fn is_graalpy(&self) -> bool {
matches!(self, InterpreterKind::GraalPy)
}
}
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"),
InterpreterKind::GraalPy => write!(f, "GraalVM"),
}
}
}
impl FromStr for InterpreterKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"cpython" => Ok(InterpreterKind::CPython),
"pypy" => Ok(InterpreterKind::PyPy),
"graalvm" | "graalpy" => Ok(InterpreterKind::GraalPy),
unknown => Err(format!("Unknown interpreter kind '{unknown}'")),
}
}
}
#[derive(Deserialize)]
struct InterpreterMetadataMessage {
implementation_name: String,
executable: Option<String>,
major: usize,
minor: usize,
abiflags: Option<String>,
interpreter: String,
ext_suffix: Option<String>,
platform: String,
system: String,
soabi: Option<String>,
gil_disabled: bool,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PythonInterpreter {
pub config: InterpreterConfig,
pub executable: PathBuf,
pub platform: Option<String>,
pub runnable: bool,
pub implementation_name: String,
pub soabi: Option<String>,
}
impl Deref for PythonInterpreter {
type Target = InterpreterConfig;
fn deref(&self) -> &Self::Target {
&self.config
}
}
fn fun_with_abiflags(
message: &InterpreterMetadataMessage,
target: &Target,
bridge: &BridgeModel,
) -> Result<String> {
if bridge != &BridgeModel::Cffi
&& target.get_python_os() != message.system
&& !target.cross_compiling()
&& !(target.get_python_os() == "cygwin"
&& message.system.to_lowercase().starts_with("cygwin"))
{
let is_android_compat = target.get_python_os() == "android"
&& message.system == "linux"
&& message.major == 3
&& message.minor <= 12;
if !is_android_compat {
bail!(
"platform.system() in python, {}, and the rust target, {:?}, don't match ಠ_ಠ",
message.system,
target,
)
}
}
if message.major != 3 || message.minor < 7 {
bail!(
"Only python >= 3.7 is supported, while you're using python {}.{}",
message.major,
message.minor
);
}
if message.interpreter == "pypy" || message.interpreter == "graalvm" {
Ok("".to_string())
} else if message.system == "windows" && message.minor < 14 {
if matches!(message.abiflags.as_deref(), Some("") | None) {
if message.minor <= 7 {
Ok("m".to_string())
} else if message.gil_disabled {
ensure!(
message.minor >= 13,
"gil_disabled is only available in python 3.13+ ಠ_ಠ"
);
Ok("t".to_string())
} else {
Ok("".to_string())
}
} else {
bail!(
"A python 3 interpreter on Windows does not define abiflags in its sysconfig before Python 3.14 ಠ_ಠ"
)
}
} 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 macOS must have 'm' or 'dm' as abiflags ಠ_ಠ")
} else {
Ok(abiflags.to_string())
}
} else {
bail!("A python 3 interpreter on Linux or macOS must define abiflags in its sysconfig ಠ_ಠ")
}
}
impl PythonInterpreter {
pub fn has_stable_api(&self) -> bool {
if self.implementation_name.parse::<InterpreterKind>().is_err() {
false
} else {
match self.interpreter_kind {
InterpreterKind::CPython => !self.config.gil_disabled,
InterpreterKind::PyPy | InterpreterKind::GraalPy => false,
}
}
}
pub fn get_tag(&self, context: &BuildContext, platform_tags: &[PlatformTag]) -> Result<String> {
let target = &context.target;
let use_sysconfig_platform = target.is_windows()
|| (target.is_linux() && platform_tags.iter().any(|tag| !tag.is_portable()))
|| target.is_illumos();
let platform = if use_sysconfig_platform {
if let Some(platform) = self.platform.clone() {
platform
} else {
context.get_platform_tag(platform_tags)?
}
} else {
context.get_platform_tag(platform_tags)?
};
let tag = if self.implementation_name.parse::<InterpreterKind>().is_err() {
format!(
"{interpreter}{major}{minor}-{soabi}-{platform}",
interpreter = self.implementation_name,
major = self.major,
minor = self.minor,
soabi = self
.soabi
.as_deref()
.unwrap_or("none")
.replace(['-', '.'], "_"),
platform = platform
)
} else {
match self.interpreter_kind {
InterpreterKind::CPython => {
format!(
"cp{major}{minor}-cp{major}{minor}{abiflags}-{platform}",
major = self.major,
minor = self.minor,
abiflags = self.abiflags,
platform = platform
)
}
InterpreterKind::PyPy => {
format!(
"pp{major}{minor}-{abi_tag}-{platform}",
major = self.major,
minor = self.minor,
abi_tag = calculate_abi_tag(&self.ext_suffix)
.expect("PyPy's syconfig didn't define a valid `EXT_SUFFIX` ಠ_ಠ"),
platform = platform,
)
}
InterpreterKind::GraalPy => {
format!(
"graalpy{major}{minor}-{abi_tag}-{platform}",
major = self.major,
minor = self.minor,
abi_tag = calculate_abi_tag(&self.ext_suffix)
.expect("GraalPy's syconfig didn't define a valid `EXT_SUFFIX` ಠ_ಠ"),
platform = platform,
)
}
}
};
Ok(tag)
}
pub fn get_library_name(&self, base: &str) -> String {
format!(
"{base}{ext_suffix}",
base = base,
ext_suffix = self.ext_suffix
)
}
pub fn is_windows_debug(&self) -> bool {
self.ext_suffix.starts_with("_d.") && self.ext_suffix.ends_with(".pyd")
}
#[instrument(skip_all, fields(executable = %executable.as_ref().display()))]
pub fn check_executable(
executable: impl AsRef<Path>,
target: &Target,
bridge: &BridgeModel,
) -> Result<Option<PythonInterpreter>> {
let output = Command::new(executable.as_ref())
.env("PYTHONNOUSERSITE", "1")
.args(["-c", GET_INTERPRETER_METADATA])
.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 {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.starts_with(&format!(
"pyenv: {}: command not found",
executable.as_ref().display()
)) {
eprintln!(
"⚠️ Warning: skipped unavailable python interpreter '{}' from pyenv",
executable.as_ref().display()
);
return Ok(None);
} else {
eprintln!("{stderr}");
bail!(err_msg);
}
}
}
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
if cfg!(windows) {
if let Some(python) = executable.as_ref().to_str() {
let ver = if python.starts_with("python") {
python.strip_prefix("python").unwrap_or(python)
} else {
python
};
let mut metadata_py = tempfile::NamedTempFile::new()?;
write!(metadata_py, "{GET_INTERPRETER_METADATA}")?;
let mut cmd = Command::new("cmd");
let suffix = match target.target_arch() {
Arch::X86 => "-32",
Arch::X86_64 => "-64",
Arch::Aarch64 => "-arm64",
_ => "",
};
cmd.arg("/c")
.arg("py")
.arg(format!("-{ver}{suffix}"))
.arg(metadata_py.path())
.env("PYTHONNOUSERSITE", "1");
let output = cmd.output();
match output {
Ok(output) if output.status.success() => output,
_ => return Ok(None),
}
} else {
return Ok(None);
}
} else {
return Ok(None);
}
} else {
return Err(err).context(err_msg);
}
}
};
let message: InterpreterMetadataMessage = serde_json::from_slice(&output.stdout)
.context(err_msg)
.context(String::from_utf8_lossy(&output.stdout).trim().to_string())?;
Self::from_metadata_message(executable, target, bridge, message)
}
fn from_metadata_message(
executable: impl AsRef<Path>,
target: &Target,
bridge: &BridgeModel,
message: InterpreterMetadataMessage,
) -> Result<Option<PythonInterpreter>> {
if (message.major == 2 && message.minor != 7) || (message.major == 3 && message.minor < 5) {
debug!(
"Skipping outdated python interpreter '{}'",
executable.as_ref().display()
);
return Ok(None);
}
let interpreter = match message.interpreter.as_str() {
"cpython" => InterpreterKind::CPython,
"pypy" => InterpreterKind::PyPy,
"graalvm" | "graalpy" => InterpreterKind::GraalPy,
other => {
bail!("Unsupported interpreter {}", other);
}
};
let abiflags = fun_with_abiflags(&message, target, bridge).context(format_err!(
"Failed to get information from the python interpreter at {}",
executable.as_ref().display()
))?;
let executable = message
.executable
.map(PathBuf::from)
.unwrap_or_else(|| executable.as_ref().to_path_buf());
if target.is_windows() {
'windows_arch_check: {
let python_arch = match message.platform.as_str().trim() {
"win32" => Arch::X86,
"win-amd64" => Arch::X86_64,
"win-arm64" => Arch::Aarch64,
_ => {
eprintln!(
"⚠️ Warning: '{}' reports unknown platform. This may fail to build.",
executable.display()
);
break 'windows_arch_check;
}
};
if python_arch != target.target_arch() {
eprintln!(
"👽 '{}' reports a platform '{platform}' (architecture '{python_arch}'), while the Rust target is '{target_arch}'. Skipping.",
executable.display(),
platform = message.platform,
python_arch = python_arch,
target_arch = target.target_arch(),
);
return Ok(None);
}
}
}
let platform = if message.platform.starts_with("macosx") {
None
} else {
Some(message.platform.to_lowercase().replace(['-', '.'], "_"))
};
debug!(
"Found {} interpreter at {}",
interpreter,
executable.display()
);
Ok(Some(PythonInterpreter {
config: InterpreterConfig {
major: message.major,
minor: message.minor,
interpreter_kind: interpreter,
abiflags,
ext_suffix: message
.ext_suffix
.context("syconfig didn't define an `EXT_SUFFIX` ಠ_ಠ")?,
pointer_width: None,
gil_disabled: message.gil_disabled,
},
executable,
platform,
runnable: true,
implementation_name: message.implementation_name,
soabi: message.soabi,
}))
}
pub fn from_config(config: InterpreterConfig) -> Self {
let implementation_name = config.interpreter_kind.to_string().to_ascii_lowercase();
PythonInterpreter {
config,
executable: PathBuf::new(),
platform: None,
runnable: false,
implementation_name,
soabi: None,
}
}
pub fn find_by_target(
target: &Target,
requires_python: Option<&VersionSpecifiers>,
bridge: Option<&BridgeModel>,
) -> Vec<PythonInterpreter> {
let min_python_minor = bridge
.map(|bridge| bridge.minimal_python_minor_version())
.unwrap_or(MINIMUM_PYTHON_MINOR);
let min_pypy_minor = bridge
.map(|bridge| bridge.minimal_pypy_minor_version())
.unwrap_or(MINIMUM_PYPY_MINOR);
let supports_free_threaded = bridge
.map(|bridge| bridge.supports_free_threaded())
.unwrap_or(false);
InterpreterConfig::lookup_target(target)
.into_iter()
.filter_map(|config| match requires_python {
Some(requires_python) => {
if requires_python
.contains(&Version::new([config.major as u64, config.minor as u64]))
{
Some(Self::from_config(config))
} else {
None
}
}
None => Some(Self::from_config(config)),
})
.filter_map(|config| match config.interpreter_kind {
InterpreterKind::CPython => {
if config.minor >= min_python_minor {
Some(config)
} else {
None
}
}
InterpreterKind::PyPy => {
if config.minor >= min_pypy_minor {
Some(config)
} else {
None
}
}
InterpreterKind::GraalPy => Some(config),
})
.filter_map(|config| {
if config.gil_disabled && !supports_free_threaded {
None
} else {
Some(config)
}
})
.collect()
}
pub fn find_all(
target: &Target,
bridge: &BridgeModel,
requires_python: Option<&VersionSpecifiers>,
) -> Result<Vec<PythonInterpreter>> {
if target.is_windows() {
return find_all_windows(target, bridge, requires_python);
};
let mut executables: Vec<String> = (bridge.minimal_python_minor_version()
..=bridge.maximum_python_minor_version())
.filter(|minor| {
requires_python
.map(|requires_python| {
requires_python.contains(&Version::new([3, *minor as u64]))
})
.unwrap_or(true)
})
.map(|minor| format!("python3.{minor}"))
.collect();
if *bridge == BridgeModel::Cffi || bridge.is_pyo3() {
executables.extend(
(bridge.minimal_pypy_minor_version()..=bridge.maximum_pypy_minor_version())
.filter(|minor| {
requires_python
.map(|requires_python| {
requires_python.contains(&Version::new([3, *minor as u64]))
})
.unwrap_or(true)
})
.map(|minor| format!("pypy3.{minor}")),
);
}
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();
let mut missing = 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 {
missing.push(executable);
}
}
if !missing.is_empty() {
let missing_str = missing
.iter()
.map(|p| format!("`{}`", p.display()))
.collect::<Vec<_>>()
.join(", ");
bail!(
"The following Python interpreters could not be found: {}",
missing_str
);
}
Ok(available_versions)
}
pub fn run_script(&self, script: &str) -> Result<String> {
if !self.runnable {
bail!("This {} isn't runnable", self);
}
let out = Command::new(&self.executable)
.env("PYTHONIOENCODING", "utf-8")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.and_then(|mut child| {
child
.stdin
.as_mut()
.expect("piped stdin")
.write_all(script.as_bytes())?;
child.wait_with_output()
});
match out {
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
bail!(
"Could not find any interpreter at {}, \
are you sure you have Python installed on your PATH?",
self.executable.display()
);
} else {
bail!(
"Failed to run the Python interpreter at {}: {}",
self.executable.display(),
err
);
}
}
Ok(ok) if !ok.status.success() => bail!("Python script failed"),
Ok(ok) => Ok(String::from_utf8(ok.stdout)?),
}
}
pub fn support_portable_wheels(&self) -> bool {
if !self.runnable {
return true;
}
let out = Command::new(&self.executable)
.args([
"-m",
"pip",
"debug",
"--verbose",
"--disable-pip-version-check",
])
.output();
match out {
Err(_) => true,
Ok(ok) if !ok.status.success() => true,
Ok(ok) => {
if let Ok(stdout) = String::from_utf8(ok.stdout) {
stdout.contains("manylinux") || stdout.contains("musllinux")
} else {
true
}
}
}
}
pub fn environment_signature(&self) -> String {
let pointer_width = self.pointer_width.unwrap_or(64);
format!(
"{}-{}.{}-{}bit",
self.implementation_name, self.major, self.minor, pointer_width
)
}
pub fn get_venv_site_package(&self, venv_base: impl AsRef<Path>, target: &Target) -> PathBuf {
if target.is_unix() {
match self.interpreter_kind {
InterpreterKind::CPython | InterpreterKind::GraalPy => {
let python_dir = format!("python{}.{}", self.major, self.minor);
venv_base
.as_ref()
.join("lib")
.join(python_dir)
.join("site-packages")
}
InterpreterKind::PyPy => venv_base.as_ref().join("site-packages"),
}
} else {
venv_base.as_ref().join("Lib").join("site-packages")
}
}
}
impl fmt::Display for PythonInterpreter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.runnable {
write!(
f,
"{} {}.{}{} at {}",
self.config.interpreter_kind,
self.config.major,
self.config.minor,
self.config.abiflags,
self.executable.display()
)
} else {
write!(
f,
"{} {}.{}{}",
self.config.interpreter_kind,
self.config.major,
self.config.minor,
self.config.abiflags,
)
}
}
}
fn calculate_abi_tag(ext_suffix: &str) -> Option<String> {
let parts = ext_suffix.split('.').collect::<Vec<_>>();
if parts.len() < 3 {
return None;
}
let soabi = parts[1];
let mut soabi_split = soabi.split('-');
let abi = if soabi.starts_with("cpython") {
format!("cp{}", soabi_split.nth(1).unwrap())
} else if soabi.starts_with("cp") {
soabi_split.next().unwrap().to_string()
} else if soabi.starts_with("pypy") {
soabi_split.take(2).collect::<Vec<_>>().join("-")
} else if soabi.starts_with("graalpy") {
soabi_split.take(3).collect::<Vec<_>>().join("-")
} else if !soabi.is_empty() {
soabi_split.nth(1).unwrap().to_string()
} else {
return None;
};
let abi_tag = abi.replace(['.', '-', ' '], "_");
Some(abi_tag)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::{PyO3, PyO3Crate};
use expect_test::expect;
use insta::assert_snapshot;
#[test]
fn test_find_interpreter_by_target() {
let target = Target::from_resolved_target_triple("x86_64-unknown-linux-gnu").unwrap();
let pythons = PythonInterpreter::find_by_target(&target, None, None)
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
let expected = expect![[r#"
[
"CPython 3.7m",
"CPython 3.8",
"CPython 3.9",
"CPython 3.10",
"CPython 3.11",
"CPython 3.12",
"CPython 3.13",
"CPython 3.14",
"PyPy 3.8",
"PyPy 3.9",
"PyPy 3.10",
"PyPy 3.11",
]
"#]];
expected.assert_debug_eq(&pythons);
let pythons = PythonInterpreter::find_by_target(
&target,
None,
Some(&BridgeModel::PyO3(PyO3 {
crate_name: PyO3Crate::PyO3,
version: semver::Version::new(0, 23, 0),
abi3: None,
metadata: None,
})),
)
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
let expected = expect![[r#"
[
"CPython 3.7m",
"CPython 3.8",
"CPython 3.9",
"CPython 3.10",
"CPython 3.11",
"CPython 3.12",
"CPython 3.13",
"CPython 3.14",
"CPython 3.13t",
"CPython 3.14t",
"PyPy 3.9",
"PyPy 3.10",
"PyPy 3.11",
]
"#]];
expected.assert_debug_eq(&pythons);
let pythons = PythonInterpreter::find_by_target(
&target,
Some(&VersionSpecifiers::from_str(">=3.8").unwrap()),
None,
)
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
let expected = expect![[r#"
[
"CPython 3.8",
"CPython 3.9",
"CPython 3.10",
"CPython 3.11",
"CPython 3.12",
"CPython 3.13",
"CPython 3.14",
"PyPy 3.8",
"PyPy 3.9",
"PyPy 3.10",
"PyPy 3.11",
]
"#]];
expected.assert_debug_eq(&pythons);
let pythons = PythonInterpreter::find_by_target(
&target,
Some(&VersionSpecifiers::from_str(">=3.10").unwrap()),
None,
)
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
let expected = expect![[r#"
[
"CPython 3.10",
"CPython 3.11",
"CPython 3.12",
"CPython 3.13",
"CPython 3.14",
"PyPy 3.10",
"PyPy 3.11",
]
"#]];
expected.assert_debug_eq(&pythons);
let pythons = PythonInterpreter::find_by_target(
&target,
Some(&VersionSpecifiers::from_str(">=3.8").unwrap()),
Some(&BridgeModel::PyO3(PyO3 {
crate_name: PyO3Crate::PyO3,
version: semver::Version::new(0, 23, 0),
abi3: None,
metadata: None,
})),
)
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
let expected = expect![[r#"
[
"CPython 3.8",
"CPython 3.9",
"CPython 3.10",
"CPython 3.11",
"CPython 3.12",
"CPython 3.13",
"CPython 3.14",
"CPython 3.13t",
"CPython 3.14t",
"PyPy 3.9",
"PyPy 3.10",
"PyPy 3.11",
]
"#]];
expected.assert_debug_eq(&pythons);
}
#[test]
fn test_calculate_abi_tag() {
let cases = vec![
(".cpython-37m-x86_64-linux-gnu.so", Some("cp37m")),
(".cpython-310-x86_64-linux-gnu.so", Some("cp310")),
(".cpython-310-darwin.so", Some("cp310")),
(".cpython-313t-darwin.so", Some("cp313t")),
(".cp310-win_amd64.pyd", Some("cp310")),
(".cp39-mingw_x86_64.pyd", Some("cp39")),
(".cpython-312-wasm32-wasi.so", Some("cp312")),
(".cpython-38.so", Some("cp38")),
(".pyd", None),
(".so", None),
(".pypy38-pp73-x86_64-linux-gnu.so", Some("pypy38_pp73")),
(
".graalpy-38-native-x86_64-darwin.dylib",
Some("graalpy_38_native"),
),
(".pyston-23-x86_64-linux-gnu.so", Some("23")),
];
for (ext_suffix, expected) in cases {
assert_eq!(calculate_abi_tag(ext_suffix).as_deref(), expected);
}
}
#[test]
fn test_interpreter_from_metadata_windows() {
let target_x64 = Target::from_resolved_target_triple("x86_64-pc-windows-msvc").unwrap();
let target_x86 = Target::from_resolved_target_triple("i686-pc-windows-msvc").unwrap();
let target_arm64 = Target::from_resolved_target_triple("aarch64-pc-windows-msvc").unwrap();
let bridge = BridgeModel::PyO3(PyO3 {
crate_name: PyO3Crate::PyO3,
version: semver::Version::new(0, 26, 0),
abi3: None,
metadata: None,
});
let message = |major, minor, platform: &str| InterpreterMetadataMessage {
major,
minor,
interpreter: "cpython".to_string(),
implementation_name: "CPython".to_string(),
abiflags: None,
ext_suffix: Some(".pyd".to_string()),
platform: platform.to_string(),
executable: None,
soabi: None,
gil_disabled: false,
system: "windows".to_string(),
};
assert_eq!(
PythonInterpreter::from_metadata_message(
"python2.7",
&target_x64,
&bridge,
message(2, 7, "win-amd64"),
)
.unwrap_err()
.to_string(),
"Failed to get information from the python interpreter at python2.7"
);
assert_eq!(
PythonInterpreter::from_metadata_message(
"python3.6",
&target_x64,
&bridge,
message(3, 6, "win-amd64"),
)
.unwrap_err()
.to_string(),
"Failed to get information from the python interpreter at python3.6"
);
for (target, platform) in &[
(&target_x86, "win32"),
(&target_x64, "win-amd64"),
(&target_arm64, "win-arm64"),
] {
assert_eq!(
PythonInterpreter::from_metadata_message(
"python3.10",
target,
&bridge,
message(3, 10, platform),
)
.unwrap()
.unwrap(),
PythonInterpreter {
config: InterpreterConfig {
major: 3,
minor: 10,
interpreter_kind: InterpreterKind::CPython,
abiflags: "".to_string(),
ext_suffix: ".pyd".to_string(),
pointer_width: None,
gil_disabled: false,
},
executable: PathBuf::from("python3.10"),
platform: Some(platform.replace("-", "_")),
runnable: true,
implementation_name: "CPython".to_string(),
soabi: None,
}
);
}
for (target, platform) in &[
(&target_x86, "win-amd64"),
(&target_x86, "win-arm64"),
(&target_x64, "win32"),
(&target_x64, "win-arm64"),
(&target_arm64, "win32"),
(&target_arm64, "win-amd64"),
] {
assert_eq!(
PythonInterpreter::from_metadata_message(
"python3.10",
target,
&bridge,
message(3, 10, platform),
)
.unwrap(),
None
);
}
assert_eq!(
PythonInterpreter::from_metadata_message(
"python3.10",
&target_x64,
&bridge,
message(3, 10, "unknown-platform"),
)
.unwrap()
.unwrap(),
PythonInterpreter {
config: InterpreterConfig {
major: 3,
minor: 10,
interpreter_kind: InterpreterKind::CPython,
abiflags: "".to_string(),
ext_suffix: ".pyd".to_string(),
pointer_width: None,
gil_disabled: false,
},
executable: PathBuf::from("python3.10"),
platform: Some("unknown_platform".to_string()),
runnable: true,
implementation_name: "CPython".to_string(),
soabi: None,
}
);
}
#[test]
fn test_check_executables_single_missing() {
let target = Target::from_resolved_target_triple("x86_64-unknown-linux-gnu").unwrap();
let bridge = BridgeModel::Bin(None);
let executables = vec![PathBuf::from("nonexistent-python-1")];
let result = PythonInterpreter::check_executables(&executables, &target, &bridge);
let err_msg = result.unwrap_err().to_string();
assert_snapshot!(err_msg, @"The following Python interpreters could not be found: `nonexistent-python-1`");
}
#[test]
fn test_check_executables_multiple_missing() {
let target = Target::from_resolved_target_triple("x86_64-unknown-linux-gnu").unwrap();
let bridge = BridgeModel::Bin(None);
let executables = vec![
PathBuf::from("nonexistent-python-1"),
PathBuf::from("nonexistent-python-2"),
];
let result = PythonInterpreter::check_executables(&executables, &target, &bridge);
let err_msg = result.unwrap_err().to_string();
assert_snapshot!(err_msg, @"The following Python interpreters could not be found: `nonexistent-python-1`, `nonexistent-python-2`");
}
}