Allow selection of debug build interpreters

This commit is contained in:
Zanie Blue 2025-02-14 14:24:26 -06:00
parent 9cdfad191c
commit 9991174276
8 changed files with 13586 additions and 31 deletions

View file

@ -618,6 +618,8 @@ def main() -> None:
# The `t` abiflag for freethreading Python.
# https://peps.python.org/pep-0703/#build-configuration-changes
"gil_disabled": bool(sysconfig.get_config_var("Py_GIL_DISABLED")),
# https://docs.python.org/3/using/configure.html#debug-build
"debug_enabled": bool(sysconfig.get_config_var("Py_DEBUG")),
# Determine if the interpreter is 32-bit or 64-bit.
# https://github.com/python/cpython/blob/b228655c227b2ca298a8ffac44d14ce3d22f6faa/Lib/venv/__init__.py#L136
"pointer_size": "64" if sys.maxsize > 2**32 else "32",

View file

@ -145,7 +145,9 @@ pub(crate) struct DiscoveryPreferences {
pub enum PythonVariant {
#[default]
Default,
Debug,
Freethreaded,
FreethreadedDebug,
}
/// A Python discovery version request.
@ -1344,17 +1346,32 @@ impl PythonVariant {
fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
match self {
PythonVariant::Default => !interpreter.gil_disabled(),
PythonVariant::Debug => interpreter.debug_enabled(),
PythonVariant::Freethreaded => interpreter.gil_disabled(),
PythonVariant::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",
}
}
}
@ -1978,12 +1995,14 @@ impl VersionRequest {
}
// Include free-threaded variants
if self.is_freethreaded() {
if let Some(variant) = self.variant() {
if variant != PythonVariant::Default {
for i in 0..names.len() {
let name = names[i].with_variant(PythonVariant::Freethreaded);
let name = names[i].with_variant(variant);
names.push(name);
}
}
}
names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
names.reverse();
@ -2272,7 +2291,9 @@ impl VersionRequest {
| Self::MajorMinor(_, _, variant)
| Self::MajorMinorPatch(_, _, _, variant)
| Self::MajorMinorPrerelease(_, _, _, variant)
| Self::Range(_, variant) => variant == &PythonVariant::Freethreaded,
| Self::Range(_, variant) => {
variant == &PythonVariant::Freethreaded || variant == &PythonVariant::Default
}
}
}
@ -2317,11 +2338,23 @@ impl FromStr for VersionRequest {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Check if the version request is for a free-threaded Python version
// Check if the version request is for a debug Python version
let (s, variant) = s
.strip_suffix('t')
.map_or((s, PythonVariant::Default), |s| {
(s, PythonVariant::Freethreaded)
.strip_suffix('d')
.map_or((s, PythonVariant::Default), |s| (s, PythonVariant::Debug));
// Check if the version request is for a free-threaded Python version
let (s, variant) = s.strip_suffix('t').map_or((s, variant), |s| {
(
s,
if variant == PythonVariant::Default {
PythonVariant::Freethreaded
} else if variant == PythonVariant::Debug {
PythonVariant::FreethreadedDebug
} else {
unreachable!()
},
)
});
if variant == PythonVariant::Freethreaded && s.ends_with('t') {
@ -2329,6 +2362,13 @@ impl FromStr for VersionRequest {
return Err(Error::InvalidVersionRequest(format!("{s}t")));
}
if variant == PythonVariant::Debug && s.ends_with('d') {
// More than one trailing "d" is not allowed
return Err(Error::InvalidVersionRequest(format!("{s}d")));
}
// TODO(zanieb): Special-case error for use of `dt` instead of `td`
let Ok(version) = Version::from_str(s) else {
return parse_version_specifiers_request(s, variant);
};
@ -2422,7 +2462,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"),
}
}
}
@ -2452,23 +2494,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}"),
}

File diff suppressed because it is too large Load diff

View file

@ -388,7 +388,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,

View file

@ -34,6 +34,7 @@ use crate::{
};
/// A Python executable and its associated platform markers.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub struct Interpreter {
platform: Platform,
@ -54,6 +55,7 @@ pub struct Interpreter {
prefix: Option<Prefix>,
pointer_size: PointerSize,
gil_disabled: bool,
debug_enabled: bool,
}
impl Interpreter {
@ -77,6 +79,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,
@ -206,7 +209,13 @@ impl Interpreter {
pub fn variant(&self) -> PythonVariant {
if self.gil_disabled() {
if self.debug_enabled() {
PythonVariant::FreethreadedDebug
} else {
PythonVariant::Freethreaded
}
} else if self.debug_enabled() {
PythonVariant::Debug
} else {
PythonVariant::default()
}
@ -452,6 +461,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()
@ -713,6 +728,7 @@ pub enum InterpreterInfoError {
},
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Deserialize, Serialize, Clone)]
struct InterpreterInfo {
platform: Platform,
@ -730,6 +746,7 @@ struct InterpreterInfo {
standalone: bool,
pointer_size: PointerSize,
gil_disabled: bool,
debug_enabled: bool,
}
impl InterpreterInfo {
@ -1084,6 +1101,7 @@ mod tests {
},
"pointer_size": "64",
"gil_disabled": true
"debug_enabled": false
}
"##};

View file

@ -506,7 +506,7 @@ impl ManagedPythonInstallation {
let stdlib = if matches!(self.key.os, Os(target_lexicon::OperatingSystem::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)

View file

@ -56,6 +56,8 @@ def prepare_variant(variant: str | None) -> str | None:
return "PythonVariant::Default"
case "freethreaded":
return "PythonVariant::Freethreaded"
case "freethreaded+debug":
return "PythonVariant::FreethreadedDebug"
case "debug":
return "PythonVariant::Debug"
case _:
@ -126,8 +128,6 @@ def main() -> None:
data["versions"] = [
{"key": key, "value": prepare_value(value)}
for key, value in json.loads(VERSION_METADATA.read_text()).items()
# Exclude debug variants for now, we don't support them in the Rust side
if value["variant"] != "debug"
]
# Render the template

View file

@ -660,6 +660,79 @@ fn python_install_freethreaded() {
"###);
}
#[test]
fn python_install_debug() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Install the latest version
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13d"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No download found for request: cpython-3.13d-[PLATFORM]
");
let bin_python = context
.bin_dir
.child(format!("python3.13d{}", std::env::consts::EXE_SUFFIX));
// The executable should be installed in the bin directory
bin_python.assert(predicate::path::exists());
// On Unix, it should be a link
#[cfg(unix)]
bin_python.assert(predicate::path::is_symlink());
// The executable should "work"
uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str())
.arg("-c").arg("import subprocess; print('hello world')"), @r###"
success: true
exit_code: 0
----- stdout -----
hello world
----- stderr -----
"###);
// Should be distinct from 3.13
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.2 in [TIME]
+ cpython-3.13.2-[PLATFORM]
"###);
// Should work with older Python versions too
uv_snapshot!(context.filters(), context.python_install().arg("3.12d"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No download found for request: cpython-3.12d-[PLATFORM]
"###);
uv_snapshot!(context.filters(), context.python_uninstall().arg("--all"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Searching for Python installations
Uninstalled 2 versions in [TIME]
- cpython-3.13.2-[PLATFORM]
- cpython-3.13.2+freethreaded-[PLATFORM] (python3.13t)
"###);
}
#[test]
fn python_install_invalid_request() {
let context: TestContext = TestContext::new_with_versions(&[])