#![warn(elided_lifetimes_in_paths, unused_lifetimes)]
mod errors;
mod impl_;
#[cfg(feature = "resolve-config")]
use std::{
io::Cursor,
path::{Path, PathBuf},
};
use std::{env, process::Command, str::FromStr, sync::OnceLock};
pub use impl_::{
cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags,
CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple,
};
use target_lexicon::OperatingSystem;
#[doc = concat!("[see PyForge's guide](https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution/multiple_python_versions.html)")]
#[cfg(feature = "resolve-config")]
pub fn use_pyo3_cfgs() {
print_expected_cfgs();
for cargo_command in get().build_script_outputs() {
println!("{cargo_command}")
}
}
pub fn add_extension_module_link_args() {
_add_extension_module_link_args(
&impl_::target_triple_from_env(),
std::io::stdout(),
rustc_minor_version(),
)
}
fn _add_extension_module_link_args(
triple: &Triple,
mut writer: impl std::io::Write,
rustc_minor_version: Option<u32>,
) {
if matches!(triple.operating_system, OperatingSystem::Darwin(_)) {
writeln!(writer, "cargo:rustc-cdylib-link-arg=-undefined").unwrap();
writeln!(writer, "cargo:rustc-cdylib-link-arg=dynamic_lookup").unwrap();
} else if triple == &Triple::from_str("wasm32-unknown-emscripten").unwrap()
&& rustc_minor_version.is_some_and(|version| version < 95)
{
writeln!(writer, "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2").unwrap();
writeln!(writer, "cargo:rustc-cdylib-link-arg=-sWASM_BIGINT").unwrap();
}
}
#[doc = concat!("[See PyForge's guide](https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution#dynamically-embedding-the-python-interpreter)")]
#[cfg(feature = "resolve-config")]
pub fn add_libpython_rpath_link_args() {
let target = impl_::target_triple_from_env();
_add_libpython_rpath_link_args(
get(),
impl_::is_linking_libpython_for_target(&target),
std::io::stdout(),
)
}
#[cfg(feature = "resolve-config")]
fn _add_libpython_rpath_link_args(
interpreter_config: &InterpreterConfig,
is_linking_libpython: bool,
mut writer: impl std::io::Write,
) {
if is_linking_libpython {
if let Some(lib_dir) = interpreter_config.lib_dir.as_ref() {
writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}").unwrap();
}
}
}
#[cfg(feature = "resolve-config")]
pub fn add_python_framework_link_args() {
let target = impl_::target_triple_from_env();
_add_python_framework_link_args(
get(),
&target,
impl_::is_linking_libpython_for_target(&target),
std::io::stdout(),
)
}
#[cfg(feature = "resolve-config")]
fn _add_python_framework_link_args(
interpreter_config: &InterpreterConfig,
triple: &Triple,
link_libpython: bool,
mut writer: impl std::io::Write,
) {
if matches!(triple.operating_system, OperatingSystem::Darwin(_)) && link_libpython {
if let Some(framework_prefix) = interpreter_config.python_framework_prefix.as_ref() {
writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{framework_prefix}").unwrap();
}
}
}
#[cfg(feature = "resolve-config")]
pub fn get() -> &'static InterpreterConfig {
static CONFIG: OnceLock<InterpreterConfig> = OnceLock::new();
CONFIG.get_or_init(|| {
let cross_compile_config_path = resolve_cross_compile_config_path();
let cross_compiling = cross_compile_config_path
.as_ref()
.map(|path| path.exists())
.unwrap_or(false);
#[allow(
clippy::const_is_empty,
reason = "CONFIG_FILE is generated in build.rs, content can vary"
)]
if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() {
interpreter_config
} else if let Some(interpreter_config) = config_from_pyo3_config_file_env() {
Ok(interpreter_config)
} else if cross_compiling {
InterpreterConfig::from_path(cross_compile_config_path.as_ref().unwrap())
} else {
InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))
}
.expect("failed to parse PyForge config")
})
}
#[cfg(feature = "resolve-config")]
fn config_from_pyo3_config_file_env() -> Option<InterpreterConfig> {
#[doc(hidden)]
const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt"));
#[allow(
clippy::const_is_empty,
reason = "CONFIG_FILE is generated in build.rs, content can vary"
)]
if !CONFIG_FILE.is_empty() {
let config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
.expect("contents of CONFIG_FILE should always be valid (generated by pyo3-build-config's build.rs)");
Some(config)
} else {
None
}
}
#[doc(hidden)]
#[cfg(feature = "resolve-config")]
const HOST_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config.txt"));
#[doc(hidden)]
#[cfg(feature = "resolve-config")]
fn resolve_cross_compile_config_path() -> Option<PathBuf> {
env::var_os("TARGET").map(|target| {
let mut path = PathBuf::from(env!("OUT_DIR"));
path.push(Path::new(&target));
path.push("pyo3-build-config.txt");
path
})
}
fn print_feature_cfg(minor_version_required: u32, cfg: &str) {
let minor_version = rustc_minor_version().unwrap_or(0);
if minor_version >= minor_version_required {
println!("cargo:rustc-cfg={cfg}");
}
if minor_version >= 80 {
println!("cargo:rustc-check-cfg=cfg({cfg})");
}
}
#[doc(hidden)]
pub fn print_feature_cfgs() {
print_feature_cfg(85, "fn_ptr_eq");
print_feature_cfg(86, "from_bytes_with_nul_error");
}
#[doc(hidden)]
pub fn print_expected_cfgs() {
if rustc_minor_version().is_some_and(|version| version < 80) {
return;
}
println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)");
println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)");
println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))");
println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)");
println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)");
for i in 8..=impl_::ABI3_MAX_MINOR + 1 {
println!("cargo:rustc-check-cfg=cfg(Py_3_{i})");
}
let mut dll_names = vec!["python3".to_string(), "python3_d".to_string()];
for i in 8..=impl_::ABI3_MAX_MINOR + 1 {
dll_names.push(format!("python3{i}"));
dll_names.push(format!("python3{i}_d"));
if i >= 13 {
dll_names.push(format!("python3{i}t"));
dll_names.push(format!("python3{i}t_d"));
}
}
let values = dll_names
.iter()
.map(|n| format!("\"{n}\""))
.collect::<Vec<_>>()
.join(", ");
println!("cargo:rustc-check-cfg=cfg(pyo3_dll, values({values}))");
}
#[doc(hidden)]
#[cfg(feature = "resolve-config")]
pub mod pyo3_build_script_impl {
use crate::errors::{Context, Result};
use super::*;
pub mod errors {
pub use crate::errors::*;
}
pub use crate::impl_::{
cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config,
target_triple_from_env, InterpreterConfig, PythonVersion,
};
pub enum BuildConfigSource {
ConfigFile,
Host,
CrossCompile,
}
pub struct BuildConfig {
pub interpreter_config: InterpreterConfig,
pub source: BuildConfigSource,
}
pub fn resolve_build_config(target: &Triple) -> Result<BuildConfig> {
#[allow(
clippy::const_is_empty,
reason = "CONFIG_FILE is generated in build.rs, content can vary"
)]
if let Some(mut interpreter_config) = config_from_pyo3_config_file_env() {
interpreter_config.apply_default_lib_name_to_config_file(target);
Ok(BuildConfig {
interpreter_config,
source: BuildConfigSource::ConfigFile,
})
} else if let Some(interpreter_config) = make_cross_compile_config()? {
let path = resolve_cross_compile_config_path()
.expect("resolve_build_config() must be called from a build script");
let parent_dir = path.parent().ok_or_else(|| {
format!(
"failed to resolve parent directory of config file {}",
path.display()
)
})?;
std::fs::create_dir_all(parent_dir).with_context(|| {
format!(
"failed to create config file directory {}",
parent_dir.display()
)
})?;
interpreter_config.to_writer(&mut std::fs::File::create(&path).with_context(
|| format!("failed to create config file at {}", path.display()),
)?)?;
Ok(BuildConfig {
interpreter_config,
source: BuildConfigSource::CrossCompile,
})
} else {
let interpreter_config = InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))?;
Ok(BuildConfig {
interpreter_config,
source: BuildConfigSource::Host,
})
}
}
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 PyForge's maximum supported version ({supported_version})\n\
= help: this package is being built with PyForge version {current_version}\n\
= help: check https://crates.io/crates/pyo3 for the latest PyForge version available\n\
= help: updating this package to the latest version of PyForge 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> {
static RUSTC_MINOR_VERSION: OnceLock<Option<u32>> = OnceLock::new();
*RUSTC_MINOR_VERSION.get_or_init(|| {
let rustc = env::var_os("RUSTC")?;
let output = Command::new(rustc).arg("--version").output().ok()?;
let version = core::str::from_utf8(&output.stdout).ok()?;
let mut pieces = version.split('.');
if pieces.next() != Some("rustc 1") {
return None;
}
pieces.next()?.parse().ok()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extension_module_link_args() {
let mut buf = Vec::new();
_add_extension_module_link_args(
&Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
&mut buf,
None,
);
assert_eq!(buf, Vec::new());
_add_extension_module_link_args(
&Triple::from_str("x86_64-apple-darwin").unwrap(),
&mut buf,
None,
);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"cargo:rustc-cdylib-link-arg=-undefined\n\
cargo:rustc-cdylib-link-arg=dynamic_lookup\n"
);
buf.clear();
_add_extension_module_link_args(
&Triple::from_str("wasm32-unknown-emscripten").unwrap(),
&mut buf,
Some(94),
);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2\n\
cargo:rustc-cdylib-link-arg=-sWASM_BIGINT\n"
);
buf.clear();
_add_extension_module_link_args(
&Triple::from_str("wasm32-unknown-emscripten").unwrap(),
&mut buf,
Some(95),
);
assert_eq!(std::str::from_utf8(&buf).unwrap(), "");
}
#[cfg(feature = "resolve-config")]
#[test]
fn python_framework_link_args() {
let mut buf = Vec::new();
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: Some(
"/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(),
),
};
_add_python_framework_link_args(
&interpreter_config,
&Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
true,
&mut buf,
);
assert_eq!(buf, Vec::new());
_add_python_framework_link_args(
&interpreter_config,
&Triple::from_str("x86_64-apple-darwin").unwrap(),
true,
&mut buf,
);
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"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 PyForge's maximum supported version (3.12)\n\
= help: this package is being built with PyForge version ", env!("CARGO_PKG_VERSION"), "\n\
= help: check https://crates.io/crates/pyo3 for the latest PyForge version available\n\
= help: updating this package to the latest version of PyForge may provide compatibility with this Python version\n\
= help: this is a help message"
);
assert_eq!(error, expected);
}
}