Skip to content

Commit 8f669e7

Browse files
authored
attempt to improve unsupported Python version error (#5519)
* attempt to improve unsupported Python version error * newsfragment * don't check the internet for latest version * allow some dead code for now
1 parent 012e095 commit 8f669e7

File tree

4 files changed

+119
-49
lines changed

4 files changed

+119
-49
lines changed

newsfragments/5519.packaging.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Provide a better error message when building an outdated PyO3 for a too-new Python version.

pyo3-build-config/src/impl_.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -881,17 +881,10 @@ pub fn is_extension_module() -> bool {
881881
|| env_var("PYO3_BUILD_EXTENSION_MODULE").is_some()
882882
}
883883

884-
/// Checks if we need to link to `libpython` for the current build target.
885-
///
886-
/// Must be called from a PyO3 crate build script.
887-
pub fn is_linking_libpython() -> bool {
888-
is_linking_libpython_for_target(&target_triple_from_env())
889-
}
890-
891884
/// Checks if we need to link to `libpython` for the target.
892885
///
893886
/// Must be called from a PyO3 crate build script.
894-
fn is_linking_libpython_for_target(target: &Triple) -> bool {
887+
pub fn is_linking_libpython_for_target(target: &Triple) -> bool {
895888
target.operating_system == OperatingSystem::Windows
896889
// See https://github.com/PyO3/pyo3/issues/4068#issuecomment-2051159852
897890
|| target.operating_system == OperatingSystem::Aix
@@ -992,6 +985,7 @@ impl CrossCompileConfig {
992985
///
993986
/// The conversion can not fail because `PYO3_CROSS_LIB_DIR` variable
994987
/// is ensured contain a valid UTF-8 string.
988+
#[allow(dead_code)]
995989
fn lib_dir_string(&self) -> Option<String> {
996990
self.lib_dir
997991
.as_ref()
@@ -1118,6 +1112,7 @@ pub fn cross_compiling_from_to(
11181112
///
11191113
/// This must be called from PyO3's build script, because it relies on environment
11201114
/// variables such as `CARGO_CFG_TARGET_OS` which aren't available at any other time.
1115+
#[allow(dead_code)]
11211116
pub fn cross_compiling_from_cargo_env() -> Result<Option<CrossCompileConfig>> {
11221117
let env_vars = CrossCompileEnvVars::from_env();
11231118
let host = Triple::host();
@@ -1350,6 +1345,7 @@ fn ends_with(entry: &DirEntry, pat: &str) -> bool {
13501345
///
13511346
/// Returns `None` if the library directory is not available, and a runtime error
13521347
/// when no or multiple sysconfigdata files are found.
1348+
#[allow(dead_code)]
13531349
fn find_sysconfigdata(cross: &CrossCompileConfig) -> Result<Option<PathBuf>> {
13541350
let mut sysconfig_paths = find_all_sysconfigdata(cross)?;
13551351
if sysconfig_paths.is_empty() {
@@ -1541,6 +1537,7 @@ fn search_lib_dir(path: impl AsRef<Path>, cross: &CrossCompileConfig) -> Result<
15411537
/// [1]: https://github.com/python/cpython/blob/3.8/Lib/sysconfig.py#L348
15421538
///
15431539
/// Returns `None` when the target Python library directory is not set.
1540+
#[allow(dead_code)]
15441541
fn cross_compile_from_sysconfigdata(
15451542
cross_compile_config: &CrossCompileConfig,
15461543
) -> Result<Option<InterpreterConfig>> {
@@ -1563,7 +1560,7 @@ fn cross_compile_from_sysconfigdata(
15631560
/// Windows, macOS and Linux.
15641561
///
15651562
/// Must be called from a PyO3 crate build script.
1566-
#[allow(unused_mut)]
1563+
#[allow(unused_mut, dead_code)]
15671564
fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result<InterpreterConfig> {
15681565
let version = cross_compile_config
15691566
.version
@@ -1678,6 +1675,7 @@ fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result<Interpre
16781675
/// when no target Python interpreter is found.
16791676
///
16801677
/// Must be called from a PyO3 crate build script.
1678+
#[allow(dead_code)]
16811679
fn load_cross_compile_config(
16821680
cross_compile_config: CrossCompileConfig,
16831681
) -> Result<InterpreterConfig> {
@@ -1704,6 +1702,7 @@ const WINDOWS_ABI3_LIB_NAME: &str = "python3";
17041702
const WINDOWS_ABI3_DEBUG_LIB_NAME: &str = "python3_d";
17051703

17061704
/// Generates the default library name for the target platform.
1705+
#[allow(dead_code)]
17071706
fn default_lib_name_for_target(
17081707
version: PythonVersion,
17091708
implementation: PythonImplementation,
@@ -1915,6 +1914,7 @@ fn get_host_interpreter(abi3_version: Option<PythonVersion>) -> Result<Interpret
19151914
///
19161915
/// This must be called from PyO3's build script, because it relies on environment variables such as
19171916
/// CARGO_CFG_TARGET_OS which aren't available at any other time.
1917+
#[allow(dead_code)]
19181918
pub fn make_cross_compile_config() -> Result<Option<InterpreterConfig>> {
19191919
let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? {
19201920
let mut interpreter_config = load_cross_compile_config(cross_config)?;

pyo3-build-config/src/lib.rs

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Wr
8383
/// All other platforms currently are no-ops.
8484
#[cfg(feature = "resolve-config")]
8585
pub fn add_python_framework_link_args() {
86+
let target = impl_::target_triple_from_env();
8687
_add_python_framework_link_args(
8788
get(),
88-
&impl_::target_triple_from_env(),
89-
impl_::is_linking_libpython(),
89+
&target,
90+
impl_::is_linking_libpython_for_target(&target),
9091
std::io::stdout(),
9192
)
9293
}
@@ -235,21 +236,19 @@ pub fn print_expected_cfgs() {
235236
///
236237
/// Please don't use these - they could change at any time.
237238
#[doc(hidden)]
239+
#[cfg(feature = "resolve-config")]
238240
pub mod pyo3_build_script_impl {
239-
#[cfg(feature = "resolve-config")]
240241
use crate::errors::{Context, Result};
241242

242-
#[cfg(feature = "resolve-config")]
243243
use super::*;
244244

245245
pub mod errors {
246246
pub use crate::errors::*;
247247
}
248248
pub use crate::impl_::{
249-
cargo_env_var, env_var, is_linking_libpython, make_cross_compile_config,
249+
cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config,
250250
target_triple_from_env, InterpreterConfig, PythonVersion,
251251
};
252-
253252
pub enum BuildConfigSource {
254253
/// Config was provided by `PYO3_CONFIG_FILE`.
255254
ConfigFile,
@@ -273,7 +272,6 @@ pub mod pyo3_build_script_impl {
273272
///
274273
/// Steps 2 and 3 are necessary because `pyo3-ffi`'s build script is the first code run which knows
275274
/// the correct target triple.
276-
#[cfg(feature = "resolve-config")]
277275
pub fn resolve_build_config(target: &Triple) -> Result<BuildConfig> {
278276
// CONFIG_FILE is generated in build.rs, so it's content can vary
279277
#[allow(unknown_lints, clippy::const_is_empty)]
@@ -315,6 +313,43 @@ pub mod pyo3_build_script_impl {
315313
})
316314
}
317315
}
316+
317+
/// Helper to generate an error message when the configured Python version is newer
318+
/// than PyO3's current supported version.
319+
pub struct MaximumVersionExceeded {
320+
message: String,
321+
}
322+
323+
impl MaximumVersionExceeded {
324+
pub fn new(
325+
interpreter_config: &InterpreterConfig,
326+
supported_version: PythonVersion,
327+
) -> Self {
328+
let implementation = match interpreter_config.implementation {
329+
PythonImplementation::CPython => "Python",
330+
PythonImplementation::PyPy => "PyPy",
331+
PythonImplementation::GraalPy => "GraalPy",
332+
};
333+
let version = &interpreter_config.version;
334+
let message = format!(
335+
"the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\
336+
= help: this package is being built with PyO3 version {current_version}\n\
337+
= help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\
338+
= help: updating this package to the latest version of PyO3 may provide compatibility with this {implementation} version",
339+
current_version = env!("CARGO_PKG_VERSION")
340+
);
341+
Self { message }
342+
}
343+
344+
pub fn add_help(&mut self, help: &str) {
345+
self.message.push_str("\n= help: ");
346+
self.message.push_str(help);
347+
}
348+
349+
pub fn finish(self) -> String {
350+
self.message
351+
}
352+
}
318353
}
319354

320355
fn rustc_minor_version() -> Option<u32> {
@@ -412,4 +447,43 @@ mod tests {
412447
"cargo:rustc-link-arg=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Library/Frameworks\n"
413448
);
414449
}
450+
451+
#[test]
452+
#[cfg(feature = "resolve-config")]
453+
fn test_maximum_version_exceeded_formatting() {
454+
let interpreter_config = InterpreterConfig {
455+
implementation: PythonImplementation::CPython,
456+
version: PythonVersion {
457+
major: 3,
458+
minor: 13,
459+
},
460+
shared: true,
461+
abi3: false,
462+
lib_name: None,
463+
lib_dir: None,
464+
executable: None,
465+
pointer_width: None,
466+
build_flags: BuildFlags::default(),
467+
suppress_build_script_link_lines: false,
468+
extra_build_script_lines: vec![],
469+
python_framework_prefix: None,
470+
};
471+
let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new(
472+
&interpreter_config,
473+
PythonVersion {
474+
major: 3,
475+
minor: 12,
476+
},
477+
);
478+
error.add_help("this is a help message");
479+
let error = error.finish();
480+
let expected = concat!("\
481+
the configured Python version (3.13) is newer than PyO3's maximum supported version (3.12)\n\
482+
= help: this package is being built with PyO3 version ", env!("CARGO_PKG_VERSION"), "\n\
483+
= help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\
484+
= help: updating this package to the latest version of PyO3 may provide compatibility with this Python version\n\
485+
= help: this is a help message"
486+
);
487+
assert_eq!(error, expected);
488+
}
415489
}

pyo3-ffi/build.rs

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use pyo3_build_config::{
22
bail, ensure, print_feature_cfgs,
33
pyo3_build_script_impl::{
4-
cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_build_config,
5-
target_triple_from_env, BuildConfig, BuildConfigSource, InterpreterConfig, PythonVersion,
4+
cargo_env_var, env_var, errors::Result, is_linking_libpython_for_target,
5+
resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource,
6+
InterpreterConfig, MaximumVersionExceeded, PythonVersion,
67
},
78
warn, PythonImplementation,
89
};
@@ -54,20 +55,20 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
5455
versions.min,
5556
);
5657
if interpreter_config.version > versions.max {
57-
ensure!(!interpreter_config.is_free_threaded(),
58-
"The configured Python interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\
59-
= help: please check if an updated version of PyO3 is available. Current version: {}\n\
60-
= help: The free-threaded build of CPython does not support the limited API so this check cannot be suppressed.",
61-
interpreter_config.version, versions.max, std::env::var("CARGO_PKG_VERSION").unwrap()
62-
);
63-
ensure!(env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1"),
64-
"the configured Python interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\
65-
= help: please check if an updated version of PyO3 is available. Current version: {}\n\
66-
= help: set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI",
67-
interpreter_config.version,
68-
versions.max,
69-
std::env::var("CARGO_PKG_VERSION").unwrap(),
70-
);
58+
let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max);
59+
if interpreter_config.is_free_threaded() {
60+
error.add_help(
61+
"the free-threaded build of CPython does not support the limited API so this check cannot be suppressed.",
62+
);
63+
return Err(error.finish().into());
64+
}
65+
66+
if !env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY")
67+
.is_some_and(|os_str| os_str == "1")
68+
{
69+
error.add_help("set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI");
70+
return Err(error.finish().into());
71+
}
7172
}
7273
}
7374
PythonImplementation::PyPy => {
@@ -79,14 +80,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
7980
versions.min,
8081
);
8182
// PyO3 does not support abi3, so we cannot offer forward compatibility
82-
ensure!(
83-
interpreter_config.version <= versions.max,
84-
"the configured PyPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\
85-
= help: please check if an updated version of PyO3 is available. Current version: {}",
86-
interpreter_config.version,
87-
versions.max,
88-
std::env::var("CARGO_PKG_VERSION").unwrap()
89-
);
83+
if interpreter_config.version > versions.max {
84+
let error = MaximumVersionExceeded::new(interpreter_config, versions.max);
85+
return Err(error.finish().into());
86+
}
9087
}
9188
PythonImplementation::GraalPy => {
9289
let versions = SUPPORTED_VERSIONS_GRAALPY;
@@ -97,14 +94,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
9794
versions.min,
9895
);
9996
// GraalPy does not support abi3, so we cannot offer forward compatibility
100-
ensure!(
101-
interpreter_config.version <= versions.max,
102-
"the configured GraalPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\
103-
= help: please check if an updated version of PyO3 is available. Current version: {}",
104-
interpreter_config.version,
105-
versions.max,
106-
std::env::var("CARGO_PKG_VERSION").unwrap()
107-
);
97+
if interpreter_config.version > versions.max {
98+
let error = MaximumVersionExceeded::new(interpreter_config, versions.max);
99+
return Err(error.finish().into());
100+
}
108101
}
109102
}
110103

@@ -209,7 +202,9 @@ fn configure_pyo3() -> Result<()> {
209202
// Serialize the whole interpreter config into DEP_PYTHON_PYO3_CONFIG env var.
210203
interpreter_config.to_cargo_dep_env()?;
211204

212-
if is_linking_libpython() && !interpreter_config.suppress_build_script_link_lines {
205+
if is_linking_libpython_for_target(&target)
206+
&& !interpreter_config.suppress_build_script_link_lines
207+
{
213208
emit_link_config(&build_config)?;
214209
}
215210

0 commit comments

Comments
 (0)