370 lines
14 KiB
Rust
370 lines
14 KiB
Rust
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)
|
|
}
|
|
}
|