Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/5519.packaging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Provide a better error message when building an outdated PyO3 for a too-new Python version.
18 changes: 9 additions & 9 deletions pyo3-build-config/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String> {
self.lib_dir
.as_ref()
Expand Down Expand Up @@ -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)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not actionable now, but after #5486 we can make use of #[expect(...)] for these.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it would be nice to apply across the codebase

pub fn cross_compiling_from_cargo_env() -> Result<Option<CrossCompileConfig>> {
let env_vars = CrossCompileEnvVars::from_env();
let host = Triple::host();
Expand Down Expand Up @@ -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<Option<PathBuf>> {
let mut sysconfig_paths = find_all_sysconfigdata(cross)?;
if sysconfig_paths.is_empty() {
Expand Down Expand Up @@ -1541,6 +1537,7 @@ fn search_lib_dir(path: impl AsRef<Path>, 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<Option<InterpreterConfig>> {
Expand All @@ -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<InterpreterConfig> {
let version = cross_compile_config
.version
Expand Down Expand Up @@ -1678,6 +1675,7 @@ fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result<Interpre
/// when no target Python interpreter is found.
///
/// Must be called from a PyO3 crate build script.
#[allow(dead_code)]
fn load_cross_compile_config(
cross_compile_config: CrossCompileConfig,
) -> Result<InterpreterConfig> {
Expand All @@ -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,
Expand Down Expand Up @@ -1915,6 +1914,7 @@ fn get_host_interpreter(abi3_version: Option<PythonVersion>) -> Result<Interpret
///
/// 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 make_cross_compile_config() -> Result<Option<InterpreterConfig>> {
let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? {
let mut interpreter_config = load_cross_compile_config(cross_config)?;
Expand Down
88 changes: 81 additions & 7 deletions pyo3-build-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
}
Expand Down Expand Up @@ -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,
Expand All @@ -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<BuildConfig> {
// CONFIG_FILE is generated in build.rs, so it's content can vary
#[allow(unknown_lints, clippy::const_is_empty)]
Expand Down Expand Up @@ -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<u32> {
Expand Down Expand Up @@ -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\
Comment on lines +481 to +484
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anyone has got a moment to review / comment on this message, this is the last small tweak I'd like to merge into 0.27.

(I think the existing error message has been a frequent source of confusion for users adopting 3.14, maybe this new text is slightly better?)

Copy link
Contributor

@MatthijsKok MatthijsKok Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user who admittedly gets confused by pyo3 from time to time, I feel the new error message is indeed a minor improvement :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads fine to me and is a bit more explicit than before. I guess we can only wait and see if it helps.

= help: this is a help message"
);
assert_eq!(error, expected);
}
}
61 changes: 28 additions & 33 deletions pyo3-ffi/build.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -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 => {
Expand All @@ -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;
Expand All @@ -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());
}
}
}

Expand Down Expand Up @@ -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)?;
}

Expand Down
Loading