diff --git a/build.rs b/build.rs index 9674e217..714ca0c3 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,9 @@ -use std::{env, ffi::OsString, path::Path, process::Command}; +use std::{env, io::Cursor, path::Path, process::Command}; use pyo3_build_config::{ bail, cargo_env_var, ensure, env_var, errors::{Context, Result}, - InterpreterConfig, PythonVersion, + make_cross_compile_config, InterpreterConfig, PythonVersion, }; /// Minimum Python version PyO3 supports. @@ -114,41 +114,16 @@ fn emit_cargo_configuration(interpreter_config: &InterpreterConfig) -> Result<() /// The result is written to pyo3_build_config::PATH, which downstream scripts can read from /// (including `pyo3-macros-backend` during macro expansion). fn configure_pyo3() -> Result<()> { - let write_config_file = env_var("PYO3_WRITE_CONFIG_FILE").map_or(false, |os_str| os_str == "1"); - let custom_config_file_path = env_var("PYO3_CONFIG_FILE"); - if let Some(path) = &custom_config_file_path { - ensure!( - Path::new(path).is_absolute(), - "PYO3_CONFIG_FILE must be absolute" - ); - } - let (interpreter_config, path_to_write) = match (write_config_file, custom_config_file_path) { - (true, Some(path)) => { - // Create new interpreter config and write it to config file - (pyo3_build_config::make_interpreter_config()?, Some(path)) - } - (true, None) => bail!("PYO3_CONFIG_FILE must be set when PYO3_WRITE_CONFIG_FILE is set"), - (false, Some(path)) => { - // Read custom config file - let path = Path::new(&path); - println!("cargo:rerun-if-changed={}", path.display()); - let config_file = std::fs::File::open(path) - .with_context(|| format!("failed to read config file at {}", path.display()))?; - let reader = std::io::BufReader::new(config_file); - ( - pyo3_build_config::InterpreterConfig::from_reader(reader)?, - None, - ) - } - (false, None) => ( - // Create new interpreter config and write it to the default location - pyo3_build_config::make_interpreter_config()?, - Some(OsString::from(pyo3_build_config::DEFAULT_CONFIG_PATH)), - ), - }; - - if let Some(path) = path_to_write { + let interpreter_config = if let Some(path) = env_var("PYO3_CONFIG_FILE") { let path = Path::new(&path); + // This is necessary because the compilations that access PYO3_CONFIG_FILE (build scripts, + // proc macros) have many different working directories, so a relative path is no good. + ensure!(path.is_absolute(), "PYO3_CONFIG_FILE must be an absolute path"); + println!("cargo:rerun-if-changed={}", path.display()); + InterpreterConfig::from_path(path)? + } else if let Some(interpreter_config) = make_cross_compile_config()? { + // This is a cross compile, need to write the config file. + let path = Path::new(&pyo3_build_config::DEFAULT_CROSS_COMPILE_CONFIG_PATH); let parent_dir = path.parent().ok_or_else(|| { format!( "failed to resolve parent directory of config file {}", @@ -165,7 +140,11 @@ fn configure_pyo3() -> Result<()> { .to_writer(&mut std::fs::File::create(&path).with_context(|| { format!("failed to create config file at {}", path.display()) })?)?; - } + interpreter_config + } else { + InterpreterConfig::from_reader(Cursor::new(pyo3_build_config::HOST_CONFIG))? + }; + if env_var("PYO3_PRINT_CONFIG").map_or(false, |os_str| os_str == "1") { print_config_and_exit(&interpreter_config); } @@ -201,20 +180,8 @@ fn print_config_and_exit(config: &InterpreterConfig) { } fn main() { - // Print out error messages using display, to get nicer formatting. if let Err(e) = configure_pyo3() { - use std::error::Error; - eprintln!("error: {}", e); - let mut source = e.source(); - if source.is_some() { - eprintln!("caused by:"); - let mut index = 0; - while let Some(some_source) = source { - eprintln!(" - {}: {}", index, some_source); - source = some_source.source(); - index += 1; - } - } + eprintln!("error: {}", e.report()); std::process::exit(1) } } diff --git a/pyo3-build-config/build.rs b/pyo3-build-config/build.rs index b94555bb..ae23362e 100644 --- a/pyo3-build-config/build.rs +++ b/pyo3-build-config/build.rs @@ -1,3 +1,32 @@ -fn main() { - // Empty build script to force cargo to produce the "OUT_DIR" environment variable. +// Import some modules from this crate inline to generate the build config. +// Allow dead code because not all code in the modules is used in this build script. + +#[path = "src/impl_.rs"] +#[allow(dead_code)] +mod impl_; + +#[path = "src/errors.rs"] +#[allow(dead_code)] +mod errors; + +use std::{env, path::Path}; + +use errors::{Result, Context}; + +fn generate_build_config() -> Result<()> { + // Create new interpreter config and write it to the default location + let interpreter_config = impl_::make_interpreter_config()?; + + let path = Path::new(&env::var_os("OUT_DIR").unwrap()).join("pyo3-build-config.txt"); + interpreter_config + .to_writer(&mut std::fs::File::create(&path).with_context(|| { + format!("failed to create config file at {}", path.display()) + })?) +} + +fn main() { + if let Err(e) = generate_build_config() { + eprintln!("error: {}", e.report()); + std::process::exit(1) + } } diff --git a/pyo3-build-config/src/errors.rs b/pyo3-build-config/src/errors.rs index 8d2903a3..dfa3f837 100644 --- a/pyo3-build-config/src/errors.rs +++ b/pyo3-build-config/src/errors.rs @@ -26,6 +26,16 @@ pub struct Error { source: Option>, } +/// Error report inspired by +/// https://blog.rust-lang.org/inside-rust/2021/07/01/What-the-error-handling-project-group-is-working-towards.html#2-error-reporter +pub struct ErrorReport<'a>(&'a Error); + +impl Error { + pub fn report(&self) -> ErrorReport<'_> { + ErrorReport(self) + } +} + impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.value) @@ -38,6 +48,24 @@ impl std::error::Error for Error { } } +impl std::fmt::Display for ErrorReport<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::error::Error; + self.0.fmt(f)?; + let mut source = self.0.source(); + if source.is_some() { + writeln!(f, "\ncaused by:")?; + let mut index = 0; + while let Some(some_source) = source { + writeln!(f, " - {}: {}", index, some_source)?; + source = some_source.source(); + index += 1; + } + } + Ok(()) + } +} + impl From for Error { fn from(value: String) -> Self { Self { diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 7b6594d8..e1834269 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -85,6 +85,20 @@ impl InterpreterConfig { self.implementation == PythonImplementation::PyPy } + #[doc(hidden)] + pub fn from_path(path: impl AsRef) -> Result { + let path = path.as_ref(); + let config_file = + std::fs::File::open(path).with_context(|| { + format!( + "failed to open PyO3 config file at {}", + path.display() + ) + })?; + let reader = std::io::BufReader::new(config_file); + InterpreterConfig::from_reader(reader) + } + #[doc(hidden)] pub fn from_reader(reader: impl Read) -> Result { let reader = BufReader::new(reader); @@ -303,6 +317,12 @@ struct CrossCompileConfig { arch: String, } +pub fn any_cross_compiling_env_vars_set() -> bool { + env::var_os("PYO3_CROSS").is_some() + || env::var_os("PYO3_CROSS_LIB_DIR").is_some() + || env::var_os("PYO3_CROSS_PYTHON_VERSION").is_some() +} + fn cross_compiling() -> Result> { let cross = env_var("PYO3_CROSS"); let cross_lib_dir = env_var("PYO3_CROSS_LIB_DIR"); @@ -1029,6 +1049,30 @@ fn get_abi3_minor_version() -> Option { .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{}", i)).is_some()) } +pub fn make_cross_compile_config() -> Result> { + let abi3_version = get_abi3_minor_version(); + + let mut interpreter_config = if let Some(paths) = cross_compiling()? { + load_cross_compile_info(paths)? + } else { + return Ok(None); + }; + + // Fixup minor version if abi3-pyXX feature set + if let Some(abi3_minor_version) = abi3_version { + ensure!( + abi3_minor_version <= interpreter_config.version.minor, + "You cannot set a mininimum Python version 3.{} higher than the interpreter version 3.{}", + abi3_minor_version, + interpreter_config.version.minor + ); + + interpreter_config.version.minor = abi3_minor_version; + } + + Ok(Some(interpreter_config)) +} + pub fn make_interpreter_config() -> Result { let abi3_version = get_abi3_minor_version(); @@ -1059,11 +1103,7 @@ pub fn make_interpreter_config() -> Result { } } - let mut interpreter_config = if let Some(paths) = cross_compiling()? { - load_cross_compile_info(paths)? - } else { - get_config_from_interpreter(&find_interpreter()?)? - }; + let mut interpreter_config = get_config_from_interpreter(&find_interpreter()?)?; // Fixup minor version if abi3-pyXX feature set if let Some(abi3_minor_version) = abi3_version { diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 5a974923..7d77f11f 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -7,14 +7,14 @@ pub mod errors; mod impl_; -use std::{ffi::OsString, path::Path}; +use std::io::Cursor; use once_cell::sync::OnceCell; // Used in PyO3's build.rs #[doc(hidden)] pub use impl_::{ - cargo_env_var, env_var, find_interpreter, get_config_from_interpreter, make_interpreter_config, + cargo_env_var, env_var, find_interpreter, get_config_from_interpreter, make_interpreter_config, make_cross_compile_config, InterpreterConfig, PythonImplementation, PythonVersion, }; @@ -25,20 +25,24 @@ pub use impl_::{ pub fn get() -> &'static InterpreterConfig { static CONFIG: OnceCell = OnceCell::new(); CONFIG.get_or_init(|| { - let config_path = std::env::var_os("PYO3_CONFIG_FILE") - .unwrap_or_else(|| OsString::from(DEFAULT_CONFIG_PATH)); - let config_file = std::fs::File::open(DEFAULT_CONFIG_PATH).expect(&format!( - "failed to open PyO3 config file at {}", - Path::new(&config_path).display() - )); - let reader = std::io::BufReader::new(config_file); - InterpreterConfig::from_reader(reader).expect("failed to parse config file") + if let Some(path) = std::env::var_os("PYO3_CONFIG_FILE") { + // Config file set - use that + InterpreterConfig::from_path(path) + } else if impl_::any_cross_compiling_env_vars_set() { + InterpreterConfig::from_path(DEFAULT_CROSS_COMPILE_CONFIG_PATH) + } else { + InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG)) + }.expect("failed to parse PyO3 config file") }) } /// Path where PyO3's build.rs will write configuration by default. #[doc(hidden)] -pub const DEFAULT_CONFIG_PATH: &str = concat!(env!("OUT_DIR"), "/pyo3-build-config.txt"); +pub const DEFAULT_CROSS_COMPILE_CONFIG_PATH: &str = concat!(env!("OUT_DIR"), "/pyo3-cross-compile-config.txt"); + +/// Build configuration discovered by `pyo3-build-config` build script. Not aware of +/// cross-compilation settings. +pub const HOST_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config.txt")); /// Adds all the [`#[cfg]` flags](index.html) to the current compilation. ///