diff --git a/newsfragments/5519.packaging.md b/newsfragments/5519.packaging.md new file mode 100644 index 00000000000..9299b9dc326 --- /dev/null +++ b/newsfragments/5519.packaging.md @@ -0,0 +1 @@ +Provide a better error message when building an outdated PyO3 for a too-new Python version. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index dd0fbb91a3e..02ea760a97d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -881,17 +881,10 @@ pub fn is_extension_module() -> bool { || env_var("PYO3_BUILD_EXTENSION_MODULE").is_some() } -/// Checks if we need to link to `libpython` for the current build target. -/// -/// Must be called from a PyO3 crate build script. -pub fn is_linking_libpython() -> bool { - is_linking_libpython_for_target(&target_triple_from_env()) -} - /// Checks if we need to link to `libpython` for the target. /// /// Must be called from a PyO3 crate build script. -fn is_linking_libpython_for_target(target: &Triple) -> bool { +pub fn is_linking_libpython_for_target(target: &Triple) -> bool { target.operating_system == OperatingSystem::Windows // See https://github.com/PyO3/pyo3/issues/4068#issuecomment-2051159852 || target.operating_system == OperatingSystem::Aix @@ -992,6 +985,7 @@ impl CrossCompileConfig { /// /// The conversion can not fail because `PYO3_CROSS_LIB_DIR` variable /// is ensured contain a valid UTF-8 string. + #[allow(dead_code)] fn lib_dir_string(&self) -> Option { self.lib_dir .as_ref() @@ -1118,6 +1112,7 @@ pub fn cross_compiling_from_to( /// /// This must be called from PyO3's build script, because it relies on environment /// variables such as `CARGO_CFG_TARGET_OS` which aren't available at any other time. +#[allow(dead_code)] pub fn cross_compiling_from_cargo_env() -> Result> { let env_vars = CrossCompileEnvVars::from_env(); let host = Triple::host(); @@ -1350,6 +1345,7 @@ fn ends_with(entry: &DirEntry, pat: &str) -> bool { /// /// Returns `None` if the library directory is not available, and a runtime error /// when no or multiple sysconfigdata files are found. +#[allow(dead_code)] fn find_sysconfigdata(cross: &CrossCompileConfig) -> Result> { let mut sysconfig_paths = find_all_sysconfigdata(cross)?; if sysconfig_paths.is_empty() { @@ -1541,6 +1537,7 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Result< /// [1]: https://github.com/python/cpython/blob/3.8/Lib/sysconfig.py#L348 /// /// Returns `None` when the target Python library directory is not set. +#[allow(dead_code)] fn cross_compile_from_sysconfigdata( cross_compile_config: &CrossCompileConfig, ) -> Result> { @@ -1563,7 +1560,7 @@ fn cross_compile_from_sysconfigdata( /// Windows, macOS and Linux. /// /// Must be called from a PyO3 crate build script. -#[allow(unused_mut)] +#[allow(unused_mut, dead_code)] fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result { let version = cross_compile_config .version @@ -1678,6 +1675,7 @@ fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result Result { @@ -1704,6 +1702,7 @@ const WINDOWS_ABI3_LIB_NAME: &str = "python3"; const WINDOWS_ABI3_DEBUG_LIB_NAME: &str = "python3_d"; /// Generates the default library name for the target platform. +#[allow(dead_code)] fn default_lib_name_for_target( version: PythonVersion, implementation: PythonImplementation, @@ -1915,6 +1914,7 @@ fn get_host_interpreter(abi3_version: Option) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { let mut interpreter_config = load_cross_compile_config(cross_config)?; diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 3ce21ee1916..fc5ba27e3ef 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -83,10 +83,11 @@ fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Wr /// All other platforms currently are no-ops. #[cfg(feature = "resolve-config")] pub fn add_python_framework_link_args() { + let target = impl_::target_triple_from_env(); _add_python_framework_link_args( get(), - &impl_::target_triple_from_env(), - impl_::is_linking_libpython(), + &target, + impl_::is_linking_libpython_for_target(&target), std::io::stdout(), ) } @@ -235,21 +236,19 @@ pub fn print_expected_cfgs() { /// /// Please don't use these - they could change at any time. #[doc(hidden)] +#[cfg(feature = "resolve-config")] pub mod pyo3_build_script_impl { - #[cfg(feature = "resolve-config")] use crate::errors::{Context, Result}; - #[cfg(feature = "resolve-config")] use super::*; pub mod errors { pub use crate::errors::*; } pub use crate::impl_::{ - cargo_env_var, env_var, is_linking_libpython, make_cross_compile_config, + cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config, target_triple_from_env, InterpreterConfig, PythonVersion, }; - pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. ConfigFile, @@ -273,7 +272,6 @@ pub mod pyo3_build_script_impl { /// /// Steps 2 and 3 are necessary because `pyo3-ffi`'s build script is the first code run which knows /// the correct target triple. - #[cfg(feature = "resolve-config")] pub fn resolve_build_config(target: &Triple) -> Result { // CONFIG_FILE is generated in build.rs, so it's content can vary #[allow(unknown_lints, clippy::const_is_empty)] @@ -315,6 +313,43 @@ pub mod pyo3_build_script_impl { }) } } + + /// Helper to generate an error message when the configured Python version is newer + /// than PyO3's current supported version. + pub struct MaximumVersionExceeded { + message: String, + } + + impl MaximumVersionExceeded { + pub fn new( + interpreter_config: &InterpreterConfig, + supported_version: PythonVersion, + ) -> Self { + let implementation = match interpreter_config.implementation { + PythonImplementation::CPython => "Python", + PythonImplementation::PyPy => "PyPy", + PythonImplementation::GraalPy => "GraalPy", + }; + let version = &interpreter_config.version; + let message = format!( + "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ + = help: this package is being built with PyO3 version {current_version}\n\ + = help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\ + = help: updating this package to the latest version of PyO3 may provide compatibility with this {implementation} version", + current_version = env!("CARGO_PKG_VERSION") + ); + Self { message } + } + + pub fn add_help(&mut self, help: &str) { + self.message.push_str("\n= help: "); + self.message.push_str(help); + } + + pub fn finish(self) -> String { + self.message + } + } } fn rustc_minor_version() -> Option { @@ -412,4 +447,43 @@ mod tests { "cargo:rustc-link-arg=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Library/Frameworks\n" ); } + + #[test] + #[cfg(feature = "resolve-config")] + fn test_maximum_version_exceeded_formatting() { + let interpreter_config = InterpreterConfig { + implementation: PythonImplementation::CPython, + version: PythonVersion { + major: 3, + minor: 13, + }, + shared: true, + abi3: false, + lib_name: None, + lib_dir: None, + executable: None, + pointer_width: None, + build_flags: BuildFlags::default(), + suppress_build_script_link_lines: false, + extra_build_script_lines: vec![], + python_framework_prefix: None, + }; + let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new( + &interpreter_config, + PythonVersion { + major: 3, + minor: 12, + }, + ); + error.add_help("this is a help message"); + let error = error.finish(); + let expected = concat!("\ + the configured Python version (3.13) is newer than PyO3's maximum supported version (3.12)\n\ + = help: this package is being built with PyO3 version ", env!("CARGO_PKG_VERSION"), "\n\ + = help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\ + = help: updating this package to the latest version of PyO3 may provide compatibility with this Python version\n\ + = help: this is a help message" + ); + assert_eq!(error, expected); + } } diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 6118e81a46f..48f8441e008 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -1,8 +1,9 @@ use pyo3_build_config::{ bail, ensure, print_feature_cfgs, pyo3_build_script_impl::{ - cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_build_config, - target_triple_from_env, BuildConfig, BuildConfigSource, InterpreterConfig, PythonVersion, + cargo_env_var, env_var, errors::Result, is_linking_libpython_for_target, + resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, + InterpreterConfig, MaximumVersionExceeded, PythonVersion, }, warn, PythonImplementation, }; @@ -51,20 +52,20 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { versions.min, ); if interpreter_config.version > versions.max { - ensure!(!interpreter_config.is_free_threaded(), - "The configured Python interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ - = help: please check if an updated version of PyO3 is available. Current version: {}\n\ - = help: The free-threaded build of CPython does not support the limited API so this check cannot be suppressed.", - interpreter_config.version, versions.max, std::env::var("CARGO_PKG_VERSION").unwrap() - ); - ensure!(env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1"), - "the configured Python interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ - = help: please check if an updated version of PyO3 is available. Current version: {}\n\ - = help: set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI", - interpreter_config.version, - versions.max, - std::env::var("CARGO_PKG_VERSION").unwrap(), - ); + let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); + if interpreter_config.is_free_threaded() { + error.add_help( + "the free-threaded build of CPython does not support the limited API so this check cannot be suppressed.", + ); + return Err(error.finish().into()); + } + + if !env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY") + .is_some_and(|os_str| os_str == "1") + { + error.add_help("set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI"); + return Err(error.finish().into()); + } } } PythonImplementation::PyPy => { @@ -76,14 +77,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - ensure!( - interpreter_config.version <= versions.max, - "the configured PyPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ - = help: please check if an updated version of PyO3 is available. Current version: {}", - interpreter_config.version, - versions.max, - std::env::var("CARGO_PKG_VERSION").unwrap() - ); + if interpreter_config.version > versions.max { + let error = MaximumVersionExceeded::new(interpreter_config, versions.max); + return Err(error.finish().into()); + } } PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; @@ -94,14 +91,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - ensure!( - interpreter_config.version <= versions.max, - "the configured GraalPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ - = help: please check if an updated version of PyO3 is available. Current version: {}", - interpreter_config.version, - versions.max, - std::env::var("CARGO_PKG_VERSION").unwrap() - ); + if interpreter_config.version > versions.max { + let error = MaximumVersionExceeded::new(interpreter_config, versions.max); + return Err(error.finish().into()); + } } } @@ -206,7 +199,9 @@ fn configure_pyo3() -> Result<()> { // Serialize the whole interpreter config into DEP_PYTHON_PYO3_CONFIG env var. interpreter_config.to_cargo_dep_env()?; - if is_linking_libpython() && !interpreter_config.suppress_build_script_link_lines { + if is_linking_libpython_for_target(&target) + && !interpreter_config.suppress_build_script_link_lines + { emit_link_config(&build_config)?; }