Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pyo3-build-config: fix build for windows gnu targets #1759

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
pyo3-build-config: inline config when not cross compiling
  • Loading branch information
David Hewitt committed Aug 4, 2021
commit 9507979d934b7ca3b46a0da30674dcf81126c92f
67 changes: 17 additions & 50 deletions build.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 {}",
Expand All @@ -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);
}
Expand Down Expand Up @@ -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)
}
}
31 changes: 30 additions & 1 deletion pyo3-build-config/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
// 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() {
// Empty build script to force cargo to produce the "OUT_DIR" environment variable.
if let Err(e) = generate_build_config() {
eprintln!("error: {}", e.report());
std::process::exit(1)
}
}
28 changes: 28 additions & 0 deletions pyo3-build-config/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ pub struct Error {
source: Option<Box<dyn std::error::Error>>,
}

/// 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)
Expand All @@ -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<String> for Error {
fn from(value: String) -> Self {
Self {
Expand Down
50 changes: 45 additions & 5 deletions pyo3-build-config/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ impl InterpreterConfig {
self.implementation == PythonImplementation::PyPy
}

#[doc(hidden)]
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
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<Self> {
let reader = BufReader::new(reader);
Expand Down Expand Up @@ -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<Option<CrossCompileConfig>> {
let cross = env_var("PYO3_CROSS");
let cross_lib_dir = env_var("PYO3_CROSS_LIB_DIR");
Expand Down Expand Up @@ -1029,6 +1049,30 @@ fn get_abi3_minor_version() -> Option<u8> {
.find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{}", i)).is_some())
}

pub fn make_cross_compile_config() -> Result<Option<InterpreterConfig>> {
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<InterpreterConfig> {
let abi3_version = get_abi3_minor_version();

Expand Down Expand Up @@ -1059,11 +1103,7 @@ pub fn make_interpreter_config() -> Result<InterpreterConfig> {
}
}

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 {
Expand Down
26 changes: 15 additions & 11 deletions pyo3-build-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -25,20 +25,24 @@ pub use impl_::{
pub fn get() -> &'static InterpreterConfig {
static CONFIG: OnceCell<InterpreterConfig> = 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.
///
Expand Down