Skip to content

Commit

Permalink
Merge pull request #2092 from aganders3/export-conf
Browse files Browse the repository at this point in the history
Add export-config feature to pyo3-build-config
  • Loading branch information
davidhewitt committed Mar 23, 2022
2 parents 7f62c96 + 272d2bc commit 2813c87
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 5 deletions.
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)

### Changed

- Allow `#[pyo3(crate = "...", text_signature = "...")]` options to be used directly in `#[pyclass(crate = "...", text_signature = "...")]`. [#2234](https://github.com/PyO3/pyo3/pull/2234)
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 @@ -154,7 +155,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 @@ -340,6 +341,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 @@ -420,6 +427,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 @@ -462,6 +492,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>
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 @@ -1184,8 +1246,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 @@ -1849,4 +1923,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

0 comments on commit 2813c87

Please sign in to comment.