pyo3-build-config: improve error messaging

This commit is contained in:
David Hewitt 2021-07-14 22:13:58 +01:00
parent 584fc6fe42
commit 2fbe57d629
4 changed files with 225 additions and 88 deletions

View File

@ -1,23 +1,14 @@
use std::{env, process::Command};
use pyo3_build_config::{InterpreterConfig, PythonImplementation, PythonVersion};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
use pyo3_build_config::{
bail, ensure,
errors::{Context, Result},
InterpreterConfig, PythonImplementation, PythonVersion,
};
/// Minimum Python version PyO3 supports.
const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 6 };
// A simple macro for returning an error. Resembles anyhow::bail.
macro_rules! bail {
($msg: expr) => { return Err($msg.into()); };
($fmt: literal $($args: tt)+) => { return Err(format!($fmt $($args)+).into()); };
}
// A simple macro for checking a condition. Resembles anyhow::ensure.
macro_rules! ensure {
($condition:expr, $($args: tt)+) => { if !($condition) { bail!($($args)+) } };
}
fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
ensure!(
interpreter_config.version >= MINIMUM_SUPPORTED_VERSION,
@ -87,9 +78,7 @@ fn get_rustc_link_lib(config: &InterpreterConfig) -> Result<String> {
match config.implementation {
PythonImplementation::CPython => match &config.ld_version {
Some(ld_version) => format!("python{}", ld_version),
None => {
return Err("failed to configure `ld_version` when compiling for unix".into())
}
None => bail!("failed to configure `ld_version` when compiling for unix"),
},
PythonImplementation::PyPy => format!("pypy{}-c", config.version.major),
}
@ -157,7 +146,7 @@ fn emit_cargo_configuration(interpreter_config: &InterpreterConfig) -> Result<()
if env::var_os("CARGO_FEATURE_AUTO_INITIALIZE").is_some() {
if !interpreter_config.shared {
return Err(format!(
bail!(
"The `auto-initialize` feature is enabled, but your python installation only supports \
embedding the Python interpreter statically. If you are attempting to run tests, or a \
binary which is okay to link dynamically, install a Python distribution which ships \
@ -170,15 +159,14 @@ fn emit_cargo_configuration(interpreter_config: &InterpreterConfig) -> Result<()
https://pyo3.rs/v{pyo3_version}/\
building_and_distribution.html#embedding-python-in-rust",
pyo3_version = env::var("CARGO_PKG_VERSION").unwrap()
)
.into());
);
}
// TODO: PYO3_CI env is a hack to workaround CI with PyPy, where the `dev-dependencies`
// currently cause `auto-initialize` to be enabled in CI.
// Once cargo's `resolver = "2"` is stable (~ MSRV Rust 1.52), remove this.
if interpreter_config.is_pypy() && env::var_os("PYO3_CI").is_none() {
return Err("The `auto-initialize` feature is not supported with PyPy.".into());
bail!("The `auto-initialize` feature is not supported with PyPy.");
}
}
@ -194,7 +182,14 @@ fn configure_pyo3() -> Result<()> {
ensure_python_version(&interpreter_config)?;
ensure_target_architecture(&interpreter_config)?;
emit_cargo_configuration(&interpreter_config)?;
interpreter_config.to_writer(&mut std::fs::File::create(pyo3_build_config::PATH)?)?;
interpreter_config.to_writer(
&mut std::fs::File::create(pyo3_build_config::PATH).with_context(|| {
format!(
"failed to create config file at {}",
pyo3_build_config::PATH
)
})?,
)?;
interpreter_config.emit_pyo3_cfgs();
// Enable use of const generics on Rust 1.51 and greater
@ -208,7 +203,18 @@ fn configure_pyo3() -> Result<()> {
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;
}
}
std::process::exit(1)
}
}

View File

@ -0,0 +1,86 @@
/// A simple macro for returning an error. Resembles anyhow::bail.
#[macro_export]
macro_rules! bail {
($msg: expr) => { return Err($msg.into()); };
($fmt: literal $($args: tt)+) => { return Err(format!($fmt $($args)+).into()); };
}
/// A simple macro for checking a condition. Resembles anyhow::ensure.
#[macro_export]
macro_rules! ensure {
($condition:expr, $($args: tt)+) => { if !($condition) { bail!($($args)+) } };
}
/// Show warning. If needed, please extend this macro to support arguments.
#[macro_export]
macro_rules! warn {
($msg: literal) => {
println!(concat!("cargo:warning=", $msg));
};
}
/// A simple error implementation which allows chaining of errors, inspired somewhat by anyhow.
#[derive(Debug)]
pub struct Error {
value: String,
source: Option<Box<dyn std::error::Error>>,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_deref()
}
}
impl From<String> for Error {
fn from(value: String) -> Self {
Self {
value,
source: None,
}
}
}
impl From<&'_ str> for Error {
fn from(value: &str) -> Self {
value.to_string().into()
}
}
impl From<std::convert::Infallible> for Error {
fn from(_: std::convert::Infallible) -> Self {
unreachable!()
}
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub trait Context<T> {
fn context(self, message: impl Into<String>) -> Result<T>;
fn with_context(self, message: impl FnOnce() -> String) -> Result<T>;
}
impl<T, E> Context<T> for Result<T, E>
where
E: std::error::Error + 'static,
{
fn context(self, message: impl Into<String>) -> Result<T> {
self.map_err(|error| Error {
value: message.into(),
source: Some(Box::new(error)),
})
}
fn with_context(self, message: impl FnOnce() -> String) -> Result<T> {
self.map_err(|error| Error {
value: message(),
source: Some(Box::new(error)),
})
}
}

View File

@ -11,31 +11,17 @@ use std::{
str::FromStr,
};
use crate::{
bail, ensure,
errors::{Context, Error, Result},
warn,
};
/// Minimum Python version PyO3 supports.
const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 6 };
/// Maximum Python version that can be used as minimum required Python version with abi3.
const ABI3_MAX_MINOR: u8 = 9;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
// A simple macro for returning an error. Resembles anyhow::bail.
macro_rules! bail {
($msg: expr) => { return Err($msg.into()); };
($fmt: literal $($args: tt)+) => { return Err(format!($fmt $($args)+).into()); };
}
// A simple macro for checking a condition. Resembles anyhow::ensure.
macro_rules! ensure {
($condition:expr, $($args: tt)+) => { if !($condition) { bail!($($args)+) } };
}
// Show warning. If needed, please extend this macro to support arguments.
macro_rules! warn {
($msg: literal) => {
println!(concat!("cargo:warning=", $msg));
};
}
/// Gets an environment variable owned by cargo.
///
/// Environment variables set by cargo are expected to be valid UTF8.
@ -104,20 +90,45 @@ impl InterpreterConfig {
pub fn from_reader(reader: impl Read) -> Result<Self> {
let reader = BufReader::new(reader);
let mut lines = reader.lines();
let major = lines.next().ok_or("expected major version")??.parse()?;
let minor = lines.next().ok_or("expected minor version")??.parse()?;
let libdir = parse_option_string(lines.next().ok_or("expected libdir")??)?;
let shared = lines.next().ok_or("expected shared")??.parse()?;
let abi3 = lines.next().ok_or("expected abi3")??.parse()?;
let ld_version = parse_option_string(lines.next().ok_or("expected ld_version")??)?;
let base_prefix = parse_option_string(lines.next().ok_or("expected base_prefix")??)?;
let executable = parse_option_string(lines.next().ok_or("expected executable")??)?;
let calcsize_pointer =
parse_option_string(lines.next().ok_or("expected calcsize_pointer")??)?;
let implementation = lines.next().ok_or("expected implementation")??.parse()?;
macro_rules! parse_line {
($value:literal) => {
lines
.next()
.ok_or(concat!("reached end of config when reading ", $value))?
.context(concat!("failed to read ", $value, " from config"))?
.parse()
.context(concat!("failed to parse ", $value, " from config"))
};
}
macro_rules! parse_option_line {
($value:literal) => {
parse_option_string(
lines
.next()
.ok_or(concat!("reached end of config when reading ", $value))?
.context(concat!("failed to read ", $value, " from config"))?,
)
.context(concat!("failed to parse ", $value, "from config"))
};
}
let major = parse_line!("major version")?;
let minor = parse_line!("minor version")?;
let libdir = parse_option_line!("libdir")?;
let shared = parse_line!("shared")?;
let abi3 = parse_line!("abi3")?;
let ld_version = parse_option_line!("ld_version")?;
let base_prefix = parse_option_line!("base_prefix")?;
let executable = parse_option_line!("executable")?;
let calcsize_pointer = parse_option_line!("calcsize_pointer")?;
let implementation = parse_line!("implementation")?;
let mut build_flags = BuildFlags(HashSet::new());
for line in lines {
build_flags.0.insert(line?.parse()?);
build_flags
.0
.insert(line.context("failed to read flag from config")?.parse()?);
}
Ok(InterpreterConfig {
version: PythonVersion { major, minor },
@ -135,39 +146,52 @@ impl InterpreterConfig {
#[doc(hidden)]
pub fn to_writer(&self, mut writer: impl Write) -> Result<()> {
macro_rules! writeln_option {
($writer:expr, $opt:expr) => {
match &$opt {
Some(value) => writeln!($writer, "{}", value),
None => writeln!($writer, "null"),
}
macro_rules! write_line {
($value:expr) => {
writeln!(writer, "{}", $value).context(concat!(
"failed to write ",
stringify!($value),
" to config"
))
};
}
writeln!(writer, "{}", self.version.major)?;
writeln!(writer, "{}", self.version.minor)?;
writeln_option!(writer, self.libdir)?;
writeln!(writer, "{}", self.shared)?;
writeln!(writer, "{}", self.abi3)?;
writeln_option!(writer, self.ld_version)?;
writeln_option!(writer, self.base_prefix)?;
writeln_option!(writer, self.executable)?;
writeln_option!(writer, self.calcsize_pointer)?;
writeln!(writer, "{}", self.implementation)?;
macro_rules! write_option_line {
($opt:expr) => {
match &$opt {
Some(value) => writeln!(writer, "{}", value),
None => writeln!(writer, "null"),
}
.context(concat!(
"failed to write ",
stringify!($value),
" to config"
))
};
}
write_line!(self.version.major)?;
write_line!(self.version.minor)?;
write_option_line!(self.libdir)?;
write_line!(self.shared)?;
write_line!(self.abi3)?;
write_option_line!(self.ld_version)?;
write_option_line!(self.base_prefix)?;
write_option_line!(self.executable)?;
write_option_line!(self.calcsize_pointer)?;
write_line!(self.implementation)?;
for flag in &self.build_flags.0 {
writeln!(writer, "{}", flag)?;
write_line!(flag)?;
}
Ok(())
}
}
fn parse_option_string<T: FromStr>(string: String) -> Result<Option<T>>
where
<T as FromStr>::Err: std::error::Error + 'static,
{
fn parse_option_string<T: FromStr>(string: String) -> Result<Option<T>, <T as FromStr>::Err> {
if string == "null" {
Ok(None)
} else {
Ok(string.parse().map(Some)?)
string.parse().map(Some)
}
}
@ -203,12 +227,12 @@ impl Display for PythonImplementation {
}
impl FromStr for PythonImplementation {
type Err = Box<dyn std::error::Error>;
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"CPython" => Ok(PythonImplementation::CPython),
"PyPy" => Ok(PythonImplementation::PyPy),
_ => bail!("Invalid interpreter: {}", s),
_ => bail!("unknown interpreter: {}", s),
}
}
}
@ -219,7 +243,9 @@ fn is_abi3() -> bool {
trait GetPrimitive {
fn get_bool(&self, key: &str) -> Result<bool>;
fn get_numeric<T: FromStr>(&self, key: &str) -> Result<T>;
fn get_numeric<T: FromStr>(&self, key: &str) -> Result<T>
where
T::Err: std::error::Error + 'static;
}
impl GetPrimitive for HashMap<String, String> {
@ -235,11 +261,14 @@ impl GetPrimitive for HashMap<String, String> {
}
}
fn get_numeric<T: FromStr>(&self, key: &str) -> Result<T> {
fn get_numeric<T: FromStr>(&self, key: &str) -> Result<T>
where
T::Err: std::error::Error + 'static,
{
self.get(key)
.ok_or(format!("{} is not defined", key))?
.parse::<T>()
.map_err(|_| format!("Could not parse value of {}", key).into())
.with_context(|| format!("Could not parse value of {}", key))
}
}
@ -334,8 +363,8 @@ impl Display for BuildFlag {
}
impl FromStr for BuildFlag {
type Err = Box<dyn std::error::Error>;
fn from_str(s: &str) -> Result<Self> {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"WITH_THREAD" => Ok(BuildFlag::WITH_THREAD),
"Py_DEBUG" => Ok(BuildFlag::Py_DEBUG),
@ -474,7 +503,12 @@ fn parse_script_output(output: &str) -> HashMap<String, String> {
/// python executable and library. Here it is read and added to a script to extract only what is
/// necessary. This necessitates a python interpreter for the host machine to work.
fn parse_sysconfigdata(config_path: impl AsRef<Path>) -> Result<HashMap<String, String>> {
let mut script = fs::read_to_string(config_path)?;
let mut script = fs::read_to_string(config_path.as_ref()).with_context(|| {
format!(
"failed to read config from {}",
config_path.as_ref().display()
)
})?;
script += r#"
print("version_major", build_time_vars["VERSION"][0]) # 3
print("version_minor", build_time_vars["VERSION"][2]) # E.g., 8
@ -751,7 +785,8 @@ fn run_python_script(interpreter: &Path, script: &str) -> Result<String> {
err
),
Ok(ok) if !ok.status.success() => bail!("Python script failed"),
Ok(ok) => Ok(String::from_utf8(ok.stdout)?),
Ok(ok) => Ok(String::from_utf8(ok.stdout)
.context("failed to parse Python script output as utf-8")?),
}
}
@ -859,8 +894,12 @@ print("calcsize_pointer", struct.calcsize("P"))
};
let version = PythonVersion {
major: map["version_major"].parse()?,
minor: map["version_minor"].parse()?,
major: map["version_major"]
.parse()
.context("failed to parse major version")?,
minor: map["version_minor"]
.parse()
.context("failed to parse minor version")?,
};
let implementation = map["implementation"].parse()?;
@ -874,7 +913,11 @@ print("calcsize_pointer", struct.calcsize("P"))
ld_version: map.get("ld_version").cloned(),
base_prefix: map.get("base_prefix").cloned(),
executable: map.get("executable").cloned(),
calcsize_pointer: Some(map["calcsize_pointer"].parse()?),
calcsize_pointer: Some(
map["calcsize_pointer"]
.parse()
.context("failed to parse calcsize_pointer")?,
),
build_flags: BuildFlags::from_interpreter(interpreter)?.fixup(version, implementation),
})
}

View File

@ -14,6 +14,8 @@
//!
//! For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/main/building_and_distribution/multiple_python_versions.html).
#[doc(hidden)]
pub mod errors;
mod impl_;
use once_cell::sync::OnceCell;