j7s_diagnostics_py/vendor/pyo3-ffi/build.rs

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)
}
}