Allow selection of debug build interpreters (#11520)
Extends the `PythonVariant` logic to support interpreters with the debug flag enabled.
This commit is contained in:
@@ -166,7 +166,9 @@ pub(crate) struct DiscoveryPreferences {
|
||||
pub enum PythonVariant {
|
||||
#[default]
|
||||
Default,
|
||||
Debug,
|
||||
Freethreaded,
|
||||
FreethreadedDebug,
|
||||
}
|
||||
|
||||
/// A Python discovery version request.
|
||||
@@ -1296,6 +1298,7 @@ pub(crate) fn find_python_installation(
|
||||
let installations =
|
||||
find_python_installations(request, environments, preference, cache, preview);
|
||||
let mut first_prerelease = None;
|
||||
let mut first_debug = None;
|
||||
let mut first_managed = None;
|
||||
let mut first_error = None;
|
||||
for result in installations {
|
||||
@@ -1340,6 +1343,20 @@ pub(crate) fn find_python_installation(
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's a debug build and debug builds aren't allowed, skip it — but store it for later
|
||||
// since we'll use a debug build if no other versions are available.
|
||||
if installation.key().variant().is_debug()
|
||||
&& !request.allows_debug()
|
||||
&& !installation.source.allows_debug()
|
||||
&& !has_default_executable_name
|
||||
{
|
||||
debug!("Skipping debug installation {}", installation.key());
|
||||
if first_debug.is_none() {
|
||||
first_debug = Some(installation.clone());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's an alternative implementation and alternative implementations aren't allowed,
|
||||
// skip it. Note we avoid querying these interpreters at all if they're on the search path
|
||||
// and are not requested, but other sources such as the managed installations can include
|
||||
@@ -1382,6 +1399,16 @@ pub(crate) fn find_python_installation(
|
||||
return Ok(Ok(installation));
|
||||
}
|
||||
|
||||
// If we only found debug installations, they're implicitly allowed and we should return the
|
||||
// first one.
|
||||
if let Some(installation) = first_debug {
|
||||
debug!(
|
||||
"Allowing debug installation {}: no non-debug installations",
|
||||
installation.key()
|
||||
);
|
||||
return Ok(Ok(installation));
|
||||
}
|
||||
|
||||
// If we only found pre-releases, they're implicitly allowed and we should return the first one.
|
||||
if let Some(installation) = first_prerelease {
|
||||
debug!(
|
||||
@@ -1641,18 +1668,47 @@ fn is_windows_store_shim(_path: &Path) -> bool {
|
||||
impl PythonVariant {
|
||||
fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
|
||||
match self {
|
||||
// TODO(zanieb): Right now, we allow debug interpreters to be selected by default for
|
||||
// backwards compatibility, but we may want to change this in the future.
|
||||
Self::Default => !interpreter.gil_disabled(),
|
||||
Self::Debug => interpreter.debug_enabled(),
|
||||
Self::Freethreaded => interpreter.gil_disabled(),
|
||||
Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the lib or executable suffix for the variant, e.g., `t` for `python3.13t`.
|
||||
/// Return the executable suffix for the variant, e.g., `t` for `python3.13t`.
|
||||
///
|
||||
/// Returns an empty string for the default Python variant.
|
||||
pub fn suffix(self) -> &'static str {
|
||||
match self {
|
||||
Self::Default => "",
|
||||
Self::Debug => "d",
|
||||
Self::Freethreaded => "t",
|
||||
Self::FreethreadedDebug => "td",
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the lib suffix for the variant, e.g., `t` for `python3.13t` but an empty string for
|
||||
/// `python3.13d` or `python3.13`.
|
||||
pub fn lib_suffix(self) -> &'static str {
|
||||
match self {
|
||||
Self::Default | Self::Debug => "",
|
||||
Self::Freethreaded | Self::FreethreadedDebug => "t",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_freethreaded(self) -> bool {
|
||||
match self {
|
||||
Self::Default | Self::Debug => false,
|
||||
Self::Freethreaded | Self::FreethreadedDebug => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_debug(self) -> bool {
|
||||
match self {
|
||||
Self::Default | Self::Freethreaded => false,
|
||||
Self::Debug | Self::FreethreadedDebug => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1984,6 +2040,19 @@ impl PythonRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this request opts-in to a debug Python version.
|
||||
pub(crate) fn allows_debug(&self) -> bool {
|
||||
match self {
|
||||
Self::Default => false,
|
||||
Self::Any => true,
|
||||
Self::Version(version) => version.is_debug(),
|
||||
Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
|
||||
Self::Implementation(_) => false,
|
||||
Self::ImplementationVersion(_, _) => true,
|
||||
Self::Key(request) => request.allows_debug(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this request opts-in to an alternative Python implementation, e.g., PyPy.
|
||||
pub(crate) fn allows_alternative_implementations(&self) -> bool {
|
||||
match self {
|
||||
@@ -2043,6 +2112,21 @@ impl PythonSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a debug Python installation from this source can be used without opt-in.
|
||||
pub(crate) fn allows_debug(self) -> bool {
|
||||
match self {
|
||||
Self::Managed | Self::Registry | Self::MicrosoftStore => false,
|
||||
Self::SearchPath
|
||||
| Self::SearchPathFirst
|
||||
| Self::CondaPrefix
|
||||
| Self::BaseCondaPrefix
|
||||
| Self::ProvidedPath
|
||||
| Self::ParentInterpreter
|
||||
| Self::ActiveEnvironment
|
||||
| Self::DiscoveredEnvironment => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an alternative Python implementation from this source can be used without opt-in.
|
||||
pub(crate) fn allows_alternative_implementations(self) -> bool {
|
||||
match self {
|
||||
@@ -2423,10 +2507,12 @@ impl VersionRequest {
|
||||
}
|
||||
|
||||
// Include free-threaded variants
|
||||
if self.is_freethreaded() {
|
||||
for i in 0..names.len() {
|
||||
let name = names[i].with_variant(PythonVariant::Freethreaded);
|
||||
names.push(name);
|
||||
if let Some(variant) = self.variant() {
|
||||
if variant != PythonVariant::Default {
|
||||
for i in 0..names.len() {
|
||||
let name = names[i].with_variant(variant);
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2725,6 +2811,18 @@ impl VersionRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this request is for a debug Python variant.
|
||||
pub(crate) fn is_debug(&self) -> bool {
|
||||
match self {
|
||||
Self::Any | Self::Default => false,
|
||||
Self::Major(_, variant)
|
||||
| Self::MajorMinor(_, _, variant)
|
||||
| Self::MajorMinorPatch(_, _, _, variant)
|
||||
| Self::MajorMinorPrerelease(_, _, _, variant)
|
||||
| Self::Range(_, variant) => variant.is_debug(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this request is for a free-threaded Python variant.
|
||||
pub(crate) fn is_freethreaded(&self) -> bool {
|
||||
match self {
|
||||
@@ -2733,7 +2831,7 @@ impl VersionRequest {
|
||||
| Self::MajorMinor(_, _, variant)
|
||||
| Self::MajorMinorPatch(_, _, _, variant)
|
||||
| Self::MajorMinorPrerelease(_, _, _, variant)
|
||||
| Self::Range(_, variant) => variant == &PythonVariant::Freethreaded,
|
||||
| Self::Range(_, variant) => variant.is_freethreaded(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2778,24 +2876,43 @@ impl FromStr for VersionRequest {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// Stripping the 't' suffix produces awkward error messages if the user tries a version
|
||||
// like "latest". HACK: If the version is all letters, don't even try to parse it further.
|
||||
if s.chars().all(char::is_alphabetic) {
|
||||
return Err(Error::InvalidVersionRequest(s.to_string()));
|
||||
}
|
||||
|
||||
// Check if the version request is for a free-threaded Python version
|
||||
let (s, variant) = s
|
||||
.strip_suffix('t')
|
||||
.map_or((s, PythonVariant::Default), |s| {
|
||||
(s, PythonVariant::Freethreaded)
|
||||
});
|
||||
|
||||
if variant == PythonVariant::Freethreaded && s.ends_with('t') {
|
||||
// More than one trailing "t" is not allowed
|
||||
return Err(Error::InvalidVersionRequest(format!("{s}t")));
|
||||
/// Extract the variant from the end of a version request string, returning the prefix and
|
||||
/// the variant type.
|
||||
fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
|
||||
// This cannot be a valid version, just error immediately
|
||||
if s.chars().all(char::is_alphabetic) {
|
||||
return Err(Error::InvalidVersionRequest(s.to_string()));
|
||||
}
|
||||
|
||||
let Some(mut start) = s.rfind(|c: char| c.is_numeric()) else {
|
||||
return Ok((s, PythonVariant::Default));
|
||||
};
|
||||
|
||||
// Advance past the first digit
|
||||
start += 1;
|
||||
|
||||
// Ensure we're not out of bounds
|
||||
if start + 1 > s.len() {
|
||||
return Ok((s, PythonVariant::Default));
|
||||
}
|
||||
|
||||
let variant = &s[start..];
|
||||
let prefix = &s[..start];
|
||||
|
||||
// Strip a leading `+` if present
|
||||
let variant = variant.strip_prefix('+').unwrap_or(variant);
|
||||
|
||||
// TODO(zanieb): Special-case error for use of `dt` instead of `td`
|
||||
|
||||
// If there's not a valid variant, fallback to failure in [`Version::from_str`]
|
||||
let Ok(variant) = PythonVariant::from_str(variant) else {
|
||||
return Ok((s, PythonVariant::Default));
|
||||
};
|
||||
|
||||
Ok((prefix, variant))
|
||||
}
|
||||
|
||||
let (s, variant) = parse_variant(s)?;
|
||||
let Ok(version) = Version::from_str(s) else {
|
||||
return parse_version_specifiers_request(s, variant);
|
||||
};
|
||||
@@ -2808,26 +2925,11 @@ impl FromStr for VersionRequest {
|
||||
return Err(Error::InvalidVersionRequest(s.to_string()));
|
||||
}
|
||||
|
||||
// Check if the local version includes a variant
|
||||
let variant = if version.local().is_empty() {
|
||||
variant
|
||||
} else {
|
||||
// If we already have a variant, do not allow another to be requested
|
||||
if variant != PythonVariant::Default {
|
||||
return Err(Error::InvalidVersionRequest(s.to_string()));
|
||||
}
|
||||
|
||||
let uv_pep440::LocalVersionSlice::Segments([uv_pep440::LocalSegment::String(local)]) =
|
||||
version.local()
|
||||
else {
|
||||
return Err(Error::InvalidVersionRequest(s.to_string()));
|
||||
};
|
||||
|
||||
match local.as_str() {
|
||||
"freethreaded" => PythonVariant::Freethreaded,
|
||||
_ => return Err(Error::InvalidVersionRequest(s.to_string())),
|
||||
}
|
||||
};
|
||||
// We don't allow local version suffixes unless they're variants, in which case they'd
|
||||
// already be stripped.
|
||||
if !version.local().is_empty() {
|
||||
return Err(Error::InvalidVersionRequest(s.to_string()));
|
||||
}
|
||||
|
||||
// Cast the release components into u8s since that's what we use in `VersionRequest`
|
||||
let Ok(release) = try_into_u8_slice(&version.release()) else {
|
||||
@@ -2879,6 +2981,8 @@ impl FromStr for PythonVariant {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"t" | "freethreaded" => Ok(Self::Freethreaded),
|
||||
"d" | "debug" => Ok(Self::Debug),
|
||||
"td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
|
||||
"" => Ok(Self::Default),
|
||||
_ => Err(()),
|
||||
}
|
||||
@@ -2889,7 +2993,9 @@ impl fmt::Display for PythonVariant {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Default => f.write_str("default"),
|
||||
Self::Debug => f.write_str("debug"),
|
||||
Self::Freethreaded => f.write_str("freethreaded"),
|
||||
Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2919,23 +3025,15 @@ impl fmt::Display for VersionRequest {
|
||||
match self {
|
||||
Self::Any => f.write_str("any"),
|
||||
Self::Default => f.write_str("default"),
|
||||
Self::Major(major, PythonVariant::Default) => write!(f, "{major}"),
|
||||
Self::Major(major, PythonVariant::Freethreaded) => write!(f, "{major}t"),
|
||||
Self::MajorMinor(major, minor, PythonVariant::Default) => write!(f, "{major}.{minor}"),
|
||||
Self::MajorMinor(major, minor, PythonVariant::Freethreaded) => {
|
||||
write!(f, "{major}.{minor}t")
|
||||
Self::Major(major, variant) => write!(f, "{major}{}", variant.suffix()),
|
||||
Self::MajorMinor(major, minor, variant) => {
|
||||
write!(f, "{major}.{minor}{}", variant.suffix())
|
||||
}
|
||||
Self::MajorMinorPatch(major, minor, patch, PythonVariant::Default) => {
|
||||
write!(f, "{major}.{minor}.{patch}")
|
||||
Self::MajorMinorPatch(major, minor, patch, variant) => {
|
||||
write!(f, "{major}.{minor}.{patch}{}", variant.suffix())
|
||||
}
|
||||
Self::MajorMinorPatch(major, minor, patch, PythonVariant::Freethreaded) => {
|
||||
write!(f, "{major}.{minor}.{patch}t")
|
||||
}
|
||||
Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Default) => {
|
||||
write!(f, "{major}.{minor}{prerelease}")
|
||||
}
|
||||
Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Freethreaded) => {
|
||||
write!(f, "{major}.{minor}{prerelease}t")
|
||||
Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
|
||||
write!(f, "{major}.{minor}{prerelease}{}", variant.suffix())
|
||||
}
|
||||
Self::Range(specifiers, _) => write!(f, "{specifiers}"),
|
||||
}
|
||||
|
||||
@@ -497,6 +497,11 @@ impl PythonDownloadRequest {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether this download request opts-in to a debug Python version.
|
||||
pub fn allows_debug(&self) -> bool {
|
||||
self.version.as_ref().is_some_and(VersionRequest::is_debug)
|
||||
}
|
||||
|
||||
/// Whether this download request opts-in to alternative Python implementations.
|
||||
pub fn allows_alternative_implementations(&self) -> bool {
|
||||
self.implementation
|
||||
|
||||
@@ -491,7 +491,7 @@ impl fmt::Display for PythonInstallationKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let variant = match self.variant {
|
||||
PythonVariant::Default => String::new(),
|
||||
PythonVariant::Freethreaded => format!("+{}", self.variant),
|
||||
_ => format!("+{}", self.variant),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
@@ -632,7 +632,7 @@ impl fmt::Display for PythonInstallationMinorVersionKey {
|
||||
// and prerelease (with special formatting for the variant).
|
||||
let variant = match self.0.variant {
|
||||
PythonVariant::Default => String::new(),
|
||||
PythonVariant::Freethreaded => format!("+{}", self.0.variant),
|
||||
_ => format!("+{}", self.0.variant),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
|
||||
@@ -37,6 +37,7 @@ use crate::{
|
||||
use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR};
|
||||
|
||||
/// A Python executable and its associated platform markers.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Interpreter {
|
||||
platform: Platform,
|
||||
@@ -59,6 +60,7 @@ pub struct Interpreter {
|
||||
pointer_size: PointerSize,
|
||||
gil_disabled: bool,
|
||||
real_executable: PathBuf,
|
||||
debug_enabled: bool,
|
||||
}
|
||||
|
||||
impl Interpreter {
|
||||
@@ -82,6 +84,7 @@ impl Interpreter {
|
||||
sys_base_exec_prefix: info.sys_base_exec_prefix,
|
||||
pointer_size: info.pointer_size,
|
||||
gil_disabled: info.gil_disabled,
|
||||
debug_enabled: info.debug_enabled,
|
||||
sys_base_prefix: info.sys_base_prefix,
|
||||
sys_base_executable: info.sys_base_executable,
|
||||
sys_executable: info.sys_executable,
|
||||
@@ -212,7 +215,13 @@ impl Interpreter {
|
||||
|
||||
pub fn variant(&self) -> PythonVariant {
|
||||
if self.gil_disabled() {
|
||||
PythonVariant::Freethreaded
|
||||
if self.debug_enabled() {
|
||||
PythonVariant::FreethreadedDebug
|
||||
} else {
|
||||
PythonVariant::Freethreaded
|
||||
}
|
||||
} else if self.debug_enabled() {
|
||||
PythonVariant::Debug
|
||||
} else {
|
||||
PythonVariant::default()
|
||||
}
|
||||
@@ -508,6 +517,12 @@ impl Interpreter {
|
||||
self.gil_disabled
|
||||
}
|
||||
|
||||
/// Return whether this is a debug build of Python, as specified by the sysconfig var
|
||||
/// `Py_DEBUG`.
|
||||
pub fn debug_enabled(&self) -> bool {
|
||||
self.debug_enabled
|
||||
}
|
||||
|
||||
/// Return the `--target` directory for this interpreter, if any.
|
||||
pub fn target(&self) -> Option<&Target> {
|
||||
self.target.as_ref()
|
||||
@@ -877,6 +892,7 @@ pub enum InterpreterInfoError {
|
||||
EmscriptenNotPyodide,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
struct InterpreterInfo {
|
||||
platform: Platform,
|
||||
@@ -895,6 +911,7 @@ struct InterpreterInfo {
|
||||
standalone: bool,
|
||||
pointer_size: PointerSize,
|
||||
gil_disabled: bool,
|
||||
debug_enabled: bool,
|
||||
}
|
||||
|
||||
impl InterpreterInfo {
|
||||
@@ -1299,7 +1316,8 @@ mod tests {
|
||||
"scripts": "bin"
|
||||
},
|
||||
"pointer_size": "64",
|
||||
"gil_disabled": true
|
||||
"gil_disabled": true,
|
||||
"debug_enabled": false
|
||||
}
|
||||
"##};
|
||||
|
||||
|
||||
@@ -294,7 +294,8 @@ mod tests {
|
||||
"scripts": "bin"
|
||||
},
|
||||
"pointer_size": "64",
|
||||
"gil_disabled": {FREE_THREADED}
|
||||
"gil_disabled": {FREE_THREADED},
|
||||
"debug_enabled": false
|
||||
}
|
||||
"##};
|
||||
|
||||
@@ -385,7 +386,8 @@ mod tests {
|
||||
"data": ""
|
||||
},
|
||||
"pointer_size": "32",
|
||||
"gil_disabled": false
|
||||
"gil_disabled": false,
|
||||
"debug_enabled": false
|
||||
}
|
||||
"##};
|
||||
|
||||
|
||||
@@ -574,7 +574,7 @@ impl ManagedPythonInstallation {
|
||||
let stdlib = if self.key.os().is_windows() {
|
||||
self.python_dir().join("Lib")
|
||||
} else {
|
||||
let lib_suffix = self.key.variant.suffix();
|
||||
let lib_suffix = self.key.variant.lib_suffix();
|
||||
let python = if matches!(
|
||||
self.key.implementation,
|
||||
LenientImplementationName::Known(ImplementationName::PyPy)
|
||||
@@ -605,7 +605,7 @@ impl ManagedPythonInstallation {
|
||||
self.path(),
|
||||
self.key.major,
|
||||
self.key.minor,
|
||||
self.key.variant.suffix(),
|
||||
self.key.variant.lib_suffix(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ fn find_sysconfigdata(
|
||||
.join("lib")
|
||||
.join(format!("python{major}.{minor}{suffix}"));
|
||||
if !lib.exists() {
|
||||
return Err(Error::MissingLib);
|
||||
return Err(Error::MissingLib(lib));
|
||||
}
|
||||
|
||||
// Probe the `lib` directory for `_sysconfigdata_`.
|
||||
@@ -270,8 +270,8 @@ fn patch_pkgconfig(contents: &str) -> Option<String> {
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Python installation is missing a `lib` directory")]
|
||||
MissingLib,
|
||||
#[error("Python installation is missing a `lib` directory at: {0}")]
|
||||
MissingLib(PathBuf),
|
||||
#[error("Python installation is missing a `_sysconfigdata_` file")]
|
||||
MissingSysconfigdata,
|
||||
#[error(transparent)]
|
||||
|
||||
Reference in New Issue
Block a user