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

Add export-config feature to pyo3-build-config #2092

Merged
merged 12 commits into from Mar 23, 2022
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Allow dependent crates to access config values from `pyo3-build-config` via cargo link dep env vars. [#2092](https://github.com/PyO3/pyo3/pull/2092)
- Added methods on `InterpreterConfig` to run Python scripts using the configured executable. [#2092](https://github.com/PyO3/pyo3/pull/2092)

### Fixed

- Considered `PYTHONFRAMEWORK` when cross compiling in order that on macos cross compiling against a [Framework bundle](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html) is considered shared. [#2233](https://github.com/PyO3/pyo3/pull/2233)
Expand Down
13 changes: 12 additions & 1 deletion Cargo.toml
Expand Up @@ -87,7 +87,18 @@ nightly = []

# Activates all additional features
# This is mostly intended for testing purposes - activating *all* of these isn't particularly useful.
full = ["macros", "pyproto", "multiple-pymethods", "num-bigint", "num-complex", "hashbrown", "serde", "indexmap", "eyre", "anyhow"]
full = [
"macros",
"pyproto",
"multiple-pymethods",
"num-bigint",
"num-complex",
"hashbrown",
"serde",
"indexmap",
"eyre",
"anyhow",
]

[[bench]]
name = "bench_call"
Expand Down
101 changes: 100 additions & 1 deletion pyo3-build-config/src/impl_.rs
Expand Up @@ -8,6 +8,7 @@ use std::{
io::{BufRead, BufReader, Read, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
str,
str::FromStr,
};

Expand Down Expand Up @@ -153,7 +154,7 @@ impl InterpreterConfig {
}

for flag in &self.build_flags.0 {
println!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag)
println!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag);
}
}

Expand Down Expand Up @@ -339,6 +340,12 @@ print("mingw", get_platform().startswith("mingw"))
InterpreterConfig::from_reader(reader)
}

#[doc(hidden)]
pub fn from_cargo_dep_env() -> Option<Result<Self>> {
cargo_env_var("DEP_PYTHON_PYO3_CONFIG")
.map(|buf| InterpreterConfig::from_reader(buf.replace("\\n", "\n").as_bytes()))
}

#[doc(hidden)]
pub fn from_reader(reader: impl Read) -> Result<Self> {
let reader = BufReader::new(reader);
Expand Down Expand Up @@ -419,6 +426,29 @@ print("mingw", get_platform().startswith("mingw"))
})
}

#[doc(hidden)]
/// Serialize the `InterpreterConfig` and print it to the environment for Cargo to pass along
/// to dependent packages during build time.
///
/// NB: writing to the cargo environment requires the
/// [`links`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key)
/// manifest key to be set. In this case that means this is called by the `pyo3-ffi` crate and
/// available for dependent package build scripts in `DEP_PYTHON_PYO3_CONFIG`. See
/// documentation for the
/// [`DEP_<name>_<key>`](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts)
/// environment variable.
pub fn to_cargo_dep_env(&self) -> Result<()> {
let mut buf = Vec::new();
self.to_writer(&mut buf)?;
// escape newlines in env var
if let Ok(config) = str::from_utf8(&buf) {
println!("cargo:PYO3_CONFIG={}", config.replace('\n', "\\n"));
} else {
bail!("unable to emit interpreter config to link env for downstream use");
}
Ok(())
}

#[doc(hidden)]
pub fn to_writer(&self, mut writer: impl Write) -> Result<()> {
macro_rules! write_line {
Expand Down Expand Up @@ -461,6 +491,38 @@ print("mingw", get_platform().startswith("mingw"))
}
Ok(())
}

/// Run a python script using the [`InterpreterConfig::executable`].
///
/// # Panics
///
/// This function will panic if the [`executable`](InterpreterConfig::executable) is `None`.
pub fn run_python_script(&self, script: &str) -> Result<String> {
run_python_script_with_envs(
Path::new(self.executable.as_ref().expect("no interpreter executable")),
script,
std::iter::empty::<(&str, &str)>(),
)
}

/// Run a python script using the [`InterpreterConfig::executable`] with additional
/// environment variables (e.g. PYTHONPATH) set.
///
/// # Panics
///
/// This function will panic if the [`executable`](InterpreterConfig::executable) is `None`.
pub fn run_python_script_with_envs<I, K, V>(&self, script: &str, envs: I) -> Result<String>
aganders3 marked this conversation as resolved.
Show resolved Hide resolved
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
run_python_script_with_envs(
Path::new(self.executable.as_ref().expect("no interpreter executable")),
script,
envs,
)
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -1183,8 +1245,20 @@ fn default_lib_name_unix(

/// Run a python script using the specified interpreter binary.
fn run_python_script(interpreter: &Path, script: &str) -> Result<String> {
run_python_script_with_envs(interpreter, script, std::iter::empty::<(&str, &str)>())
}

/// Run a python script using the specified interpreter binary with additional environment
/// variables (e.g. PYTHONPATH) set.
fn run_python_script_with_envs<I, K, V>(interpreter: &Path, script: &str, envs: I) -> Result<String>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let out = Command::new(interpreter)
.env("PYTHONIOENCODING", "utf-8")
.envs(envs)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
Expand Down Expand Up @@ -1848,4 +1922,29 @@ mod tests {
.is_none()
);
}

#[test]
fn test_run_python_script() {
// as above, this should be okay in CI where Python is presumed installed
let interpreter = make_interpreter_config()
.expect("could not get InterpreterConfig from installed interpreter");
let out = interpreter
.run_python_script("print(2 + 2)")
.expect("failed to run Python script");
assert_eq!(out.trim_end(), "4");
}

#[test]
fn test_run_python_script_with_envs() {
// as above, this should be okay in CI where Python is presumed installed
let interpreter = make_interpreter_config()
.expect("could not get InterpreterConfig from installed interpreter");
let out = interpreter
.run_python_script_with_envs(
"import os; print(os.getenv('PYO3_TEST'))",
vec![("PYO3_TEST", "42")],
)
.expect("failed to run Python script");
assert_eq!(out.trim_end(), "42");
}
}
7 changes: 4 additions & 3 deletions pyo3-build-config/src/lib.rs
Expand Up @@ -61,12 +61,13 @@ fn _add_extension_module_link_args(target_os: &str, mut writer: impl std::io::Wr
/// Loads the configuration determined from the build environment.
///
/// Because this will never change in a given compilation run, this is cached in a `once_cell`.
#[doc(hidden)]
#[cfg(feature = "resolve-config")]
pub fn get() -> &'static InterpreterConfig {
static CONFIG: OnceCell<InterpreterConfig> = OnceCell::new();
CONFIG.get_or_init(|| {
if !CONFIG_FILE.is_empty() {
if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() {
interpreter_config
} else if !CONFIG_FILE.is_empty() {
InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
} else if !ABI3_CONFIG.is_empty() {
Ok(abi3_config())
Expand All @@ -75,7 +76,7 @@ pub fn get() -> &'static InterpreterConfig {
} else {
InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))
}
.expect("failed to parse PyO3 config file")
.expect("failed to parse PyO3 config")
})
}

Expand Down
3 changes: 3 additions & 0 deletions pyo3-ffi/build.rs
Expand Up @@ -69,6 +69,9 @@ fn emit_link_config(interpreter_config: &InterpreterConfig) -> Result<()> {
}
}

// serialize the whole interpreter config in DEP_PYTHON_PYO3_CONFIG
interpreter_config.to_cargo_dep_env()?;

Ok(())
}

Expand Down