diff --git a/CHANGELOG.md b/CHANGELOG.md index ceaf6a98ae0..0902d5ea8a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Cargo.toml b/Cargo.toml index 6b66c39d32d..34131b3a5ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index f18221ae6d4..f57116a3bc5 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -8,6 +8,7 @@ use std::{ io::{BufRead, BufReader, Read, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, + str, str::FromStr, }; @@ -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); } } @@ -339,6 +340,12 @@ print("mingw", get_platform().startswith("mingw")) InterpreterConfig::from_reader(reader) } + #[doc(hidden)] + pub fn from_cargo_dep_env() -> Option> { + 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 { let reader = BufReader::new(reader); @@ -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__`](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 { @@ -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 { + 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(&self, script: &str, envs: I) -> Result + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + 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)] @@ -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 { + 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(interpreter: &Path, script: &str, envs: I) -> Result +where + I: IntoIterator, + K: AsRef, + V: AsRef, +{ let out = Command::new(interpreter) .env("PYTHONIOENCODING", "utf-8") + .envs(envs) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) @@ -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"); + } } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 8a52fe0dfa5..814ff1da811 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -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 = 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()) @@ -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") }) } diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index c8d65d5d1c2..ddc60e62e5e 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -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(()) }