use pyo3_build_config::{ bail, ensure, pyo3_build_script_impl::{ cargo_env_var, env_var, errors::Result, is_linking_libpython_for_target, print_feature_cfgs, print_libpython_rpath_link_args, resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, MaximumVersionExceeded, }, warn, InterpreterConfig, PythonAbiKind, PythonImplementation, PythonVersion, StableAbi, }; /// Minimum Python version PyO3 supports. struct SupportedVersions { min: PythonVersion, max: PythonVersion, } const SUPPORTED_VERSIONS_CPYTHON: SupportedVersions = SupportedVersions { min: PythonVersion { major: 3, minor: 8 }, max: PythonVersion { major: 3, minor: 15, }, }; const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions { min: PythonVersion { major: 3, minor: 11, }, max: SUPPORTED_VERSIONS_CPYTHON.max, }; const SUPPORTED_VERSIONS_GRAALPY: SupportedVersions = SupportedVersions { min: PythonVersion { major: 3, minor: 10, }, max: SUPPORTED_VERSIONS_CPYTHON.max, }; const MIN_FREE_THREADED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 14, }; const PY_3_15: PythonVersion = PythonVersion { major: 3, minor: 15, }; fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { // This is an undocumented env var which is only really intended to be used in CI / for testing // and development. if std::env::var("UNSAFE_PYO3_SKIP_VERSION_CHECK").as_deref() == Ok("1") { return Ok(()); } match interpreter_config.target_abi().implementation() { PythonImplementation::CPython => { let versions = SUPPORTED_VERSIONS_CPYTHON; let interp_version = interpreter_config.target_abi().version(); ensure!( interpreter_config.version() >= versions.min, "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", interpreter_config.version(), versions.min, ); let v_plus_1 = PythonVersion { major: versions.max.major, minor: versions.max.minor + 1, }; if interp_version == v_plus_1 { warn!( "Using experimental support for the Python {}.{} ABI. \ Build artifacts may not be compatible with the final release of CPython, \ so do not distribute them.", v_plus_1.major, v_plus_1.minor, ); } else if interp_version > v_plus_1 { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); if interpreter_config.target_abi().kind().is_free_threaded() { if interp_version >= PY_3_15 { if env_var("PYO3_USE_STABLE_ABI_FORWARD_COMPATIBILITY") .is_none_or(|os_str| os_str != "1") { error.add_help( "set PYO3_USE_STABLE_ABI_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the free-threaded stable ABI" ); return Err(error.finish().into()); } } else { error.add_help(format!( "the free-threaded build of CPython {}.{} does not support the limited API so this check cannot be suppressed.", interp_version.major, interp_version.minor ).as_str()); return Err(error.finish().into()); } } if env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_none_or(|os_str| os_str != "1") && env_var("PYO3_USE_STABLE_ABI_FORWARD_COMPATIBILITY") .is_none_or(|os_str| os_str != "1") { error.add_help("set PYO3_USE_STABLE_ABI_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI"); return Err(error.finish().into()); } } if interpreter_config.target_abi().kind().is_free_threaded() { ensure!( interpreter_config.target_abi().version() >= MIN_FREE_THREADED_VERSION, "PyO3 does not support the free-threaded build of CPython versions below {}, the selected Python version is {}", MIN_FREE_THREADED_VERSION, interpreter_config.target_abi().version(), ); } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; ensure!( interpreter_config.target_abi().version() >= versions.min, "the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", interpreter_config.target_abi().version(), versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility if interpreter_config.target_abi().version() > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } } PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; ensure!( interpreter_config.target_abi().version() >= versions.min, "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", interpreter_config.target_abi().version(), versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility if interpreter_config.target_abi().version() > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } } PythonImplementation::RustPython => {} } if let PythonAbiKind::Stable(abi) = interpreter_config.target_abi().kind() { match interpreter_config.target_abi().implementation() { PythonImplementation::CPython => match abi { StableAbi::Abi3t => { ensure!( interpreter_config.target_abi().version() >= PY_3_15, "Abi3t builds are not supported on CPython targets before Python 3.15" ) } StableAbi::Abi3 => {} }, PythonImplementation::PyPy => warn!( "PyPy does not yet support {abi} so the build artifacts will be version-specific. \ See https://github.com/pypy/pypy/issues/3397 for more information." ), PythonImplementation::GraalPy => warn!( "GraalPy does not support {abi} so the build artifacts will be version-specific." ), PythonImplementation::RustPython => {} } } Ok(()) } fn ensure_target_pointer_width(interpreter_config: &InterpreterConfig) -> Result<()> { if let Some(pointer_width) = interpreter_config.pointer_width() { // Try to check whether the target architecture matches the python library let rust_target = match cargo_env_var("CARGO_CFG_TARGET_POINTER_WIDTH") .unwrap() .as_str() { "64" => 64, "32" => 32, x => bail!("unexpected Rust target pointer width: {}", x), }; ensure!( rust_target == pointer_width, "your Rust target architecture ({}-bit) does not match your python interpreter ({}-bit)", rust_target, pointer_width ); } Ok(()) } /// `raw-dylib` currently does not support arbitrary names /// (see https://internals.rust-lang.org/t/support-renames-with-link-name-kind-raw-dylib/24415) /// so if the lib name is not one of the known subset, we must fall back to full linking. fn lib_name_is_known_for_raw_dylib(lib_name: &str) -> bool { // pyo3_dll cfg for raw-dylib linking on Windows if matches!( lib_name, "python3" | "python3_d" | "python3t" | "python3t_d" ) { return true; } // support raw-dylib linking for all CPython versions supported, plus the next prerelease for i in SUPPORTED_VERSIONS_CPYTHON.min.minor..=SUPPORTED_VERSIONS_CPYTHON.max.minor + 1 { if lib_name == format!("python3{i}") || lib_name == format!("python3{i}_d") { return true; } if i >= 13 && (lib_name == format!("python3{i}t") || lib_name == format!("python3{i}t_d")) { return true; } } // PyPy DLL names (libpypy3.X-c.dll) for i in SUPPORTED_VERSIONS_PYPY.min.minor..=SUPPORTED_VERSIONS_PYPY.max.minor { if lib_name == format!("libpypy3.{i}-c") { return true; } } false } /// Whether to use raw-dylib linking. /// /// Currently, this only applies if all of the following are true: /// - The target OS is Windows. /// - The Python library name is one of the [known subset][lib_name_is_known_for_raw_dylib]. /// - The `PYO3_USE_RAW_DYLIB` environment variable is not set, or is set to `1`. /// /// NB in some cases (e.g. mixed C / Rust builds) it might be necessary to link the full Python /// library rather than rely on the symbols which PyO3 defines as raw-dylib, which is why /// we have the opt-out env var. fn should_use_raw_dylib_linking(lib_name: &str) -> bool { let target_os = cargo_env_var("CARGO_CFG_TARGET_OS").unwrap(); if target_os != "windows" { return false; } match ( lib_name_is_known_for_raw_dylib(lib_name), env_var("PYO3_USE_RAW_DYLIB"), ) { (true, None) => true, (true, Some(os_str)) if os_str == "1" => true, (false, Some(os_str)) if os_str == "1" => { warn!( "PYO3_USE_RAW_DYLIB is set to 1 but the Python library name is not recognized. \ Falling back to full linking." ); false } _ => false, } } fn emit_link_config(build_config: &BuildConfig) -> Result<()> { let target_os = cargo_env_var("CARGO_CFG_TARGET_OS").unwrap(); let interpreter_config = &build_config.interpreter_config; let lib_name = interpreter_config .lib_name() .ok_or("attempted to link to Python shared library but config does not contain lib_name")?; if should_use_raw_dylib_linking(lib_name) { // Use raw-dylib linking: emit a cfg so that `extern_libpython!` picks the // right `#[link(name = "...", kind = "raw-dylib")]` attribute at compile time. // This eliminates the need for import libraries (.lib files) entirely. // // Note: raw-dylib is inherently dynamic linking. Static embedding of the // Python interpreter on Windows is not supported by this path (and is not // officially supported by CPython on Windows). println!("cargo:rustc-cfg=pyo3_use_raw_dylib"); println!("cargo:rustc-cfg=pyo3_dll=\"{lib_name}\""); return Ok(()); } println!( "cargo:rustc-link-lib={link_model}{alias}{lib_name}", link_model = if interpreter_config.shared() { "" } else { "static=" }, // on windows we emit `#[link(name = "pythonXY")]` attributes // and need this alias here to get the right name for the final link alias = if target_os == "windows" { "pythonXY:" } else { "" } ); if let Some(lib_dir) = interpreter_config.lib_dir() { println!("cargo:rustc-link-search=native={lib_dir}"); } else if matches!(build_config.source, BuildConfigSource::CrossCompile) { warn!( "The output binary will link to libpython, \ but PYO3_CROSS_LIB_DIR environment variable is not set. \ Ensure that the target Python library directory is \ in the rustc native library search path." ); } Ok(()) } /// Prepares the PyO3 crate for compilation. /// /// This uses pyo3-build-config implementation to detect the target Python interpreter and validate /// it's suitable for building with. /// /// Emits the cargo configuration based on this config as well as a few checks of the Rust compiler /// version to enable features which aren't supported on MSRV. fn configure_pyo3_ffi() -> Result<()> { let target = target_triple_from_env(); let build_config = resolve_build_config(&target)?; let interpreter_config = &build_config.interpreter_config; if env_var("PYO3_PRINT_CONFIG").is_some_and(|os_str| os_str == "1") { print_config_and_exit(interpreter_config); } ensure_python_version(interpreter_config)?; ensure_target_pointer_width(interpreter_config)?; // Serialize the whole interpreter config into DEP_PYTHON_PYO3_CONFIG env var. interpreter_config.to_cargo_dep_env()?; if is_linking_libpython_for_target(&target) && !interpreter_config.suppress_build_script_link_lines() { emit_link_config(&build_config)?; } for cfg in interpreter_config.build_script_outputs() { println!("{cfg}") } // Extra lines come last, to support last write wins. for line in interpreter_config.extra_build_script_lines() { println!("{line}"); } print_feature_cfgs(); // Make `cargo test` etc work with non-system Python installations print_libpython_rpath_link_args(&target, interpreter_config); Ok(()) } fn print_config_and_exit(config: &InterpreterConfig) { println!("\n-- PYO3_PRINT_CONFIG=1 is set, printing configuration and halting compile --"); config .to_writer(std::io::stdout()) .expect("failed to print config to stdout"); println!("\nnote: unset the PYO3_PRINT_CONFIG environment variable and retry to compile with the above config"); std::process::exit(101); } fn main() { pyo3_build_config::print_expected_cfgs(); if let Err(e) = configure_pyo3_ffi() { eprintln!("error: {}", e.report()); std::process::exit(1) } }