Skip to content

Commit

Permalink
Refactor to serialize the whole InterpreterConfig into DEP_PYTHON_PYO…
Browse files Browse the repository at this point in the history
…3_CONFIG
  • Loading branch information
Ashley Anderson committed Mar 17, 2022
1 parent 6e0a20c commit f424900
Show file tree
Hide file tree
Showing 7 changed files with 35 additions and 62 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Expand Up @@ -98,7 +98,6 @@ full = [
"indexmap",
"eyre",
"anyhow",
"pyo3-build-config/export-config"
]

[[bench]]
Expand Down
2 changes: 0 additions & 2 deletions guide/src/building_and_distribution.md
Expand Up @@ -50,8 +50,6 @@ If you save the above output config from `PYO3_PRINT_CONFIG` to a file, it is po

If your build environment is unusual enough that PyO3's regular configuration detection doesn't work, using a config file like this will give you the flexibility to make PyO3 work for you. To see the full set of options supported, see the documentation for the [`InterpreterConfig` struct](https://docs.rs/pyo3-build-config/{{#PYO3_DOCS_VERSION}}/pyo3_build_config/struct.InterpreterConfig.html).

The `export-config` feature of the `pyo3-build-config` crate also allows these values to be exported for use in other crates, and `pyo3-build-config` offers a funciton for reading them in and constructing an `InterpreterConfig`.

## Building Python extension modules

Python extension modules need to be compiled differently depending on the OS (and architecture) that they are being compiled for. As well as multiple OSes (and architectures), there are also many different Python versions which are actively supported. Packages uploaded to [PyPI](https://pypi.org/) usually want to upload prebuilt "wheels" covering many OS/arch/version combinations so that users on all these different platforms don't have to compile the package themselves. Package vendors can opt-in to the "abi3" limited Python API which allows their wheels to be used on multiple Python versions, reducing the number of wheels they need to compile, but restricts the functionality they can use.
Expand Down
8 changes: 0 additions & 8 deletions guide/src/features.md
Expand Up @@ -81,14 +81,6 @@ The `resolve-config` feature of the `pyo3-build-config` crate controls whether t
build script automatically resolves a Python interpreter / build configuration. This feature is primarily useful when building PyO3
itself. By default this feature is not enabled, meaning you can freely use `pyo3-build-config` as a standalone library to read or write PyO3 build configuration files or resolve metadata about a Python interpreter.

### `export-config`

The `export-config` feature of the `pyo3-build-config` crate controls whether
that crate exports the build configuration for use in other crates. This
feature may be useful if building an additional dependency that requires access
to the Python interpreter used to build PyO3 itself. This feature should be
used with caution as it can impact build portability.

## Optional Dependencies

These features enable conversions between Python types and types from other Rust crates, enabling easy access to the rest of the Rust ecosystem.
Expand Down
3 changes: 0 additions & 3 deletions pyo3-build-config/Cargo.toml
Expand Up @@ -20,9 +20,6 @@ default = []
# script. If this feature isn't enabled, the build script no-ops.
resolve-config = []

# Export the resolved build config into a file for use by e.g. other build scripts.
export-config = ["resolve-config"]

abi3 = []
abi3-py37 = ["abi3-py38"]
abi3-py38 = ["abi3-py39"]
Expand Down
77 changes: 30 additions & 47 deletions pyo3-build-config/src/impl_.rs
Expand Up @@ -4,10 +4,11 @@ use std::{
env,
ffi::{OsStr, OsString},
fmt::Display,
fs::{self, DirEntry, File},
fs::{self, DirEntry},
io::{BufRead, BufReader, Read, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
str,
str::FromStr,
};

Expand All @@ -21,8 +22,6 @@ use crate::{
const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 };
/// Maximum Python version that can be used as minimum required Python version with abi3.
const ABI3_MAX_MINOR: u8 = 9;
/// Name of config file exported by "export-config" feature.
const EXPORT_CONFIG_FILENAME: &str = ".pyo3-export-config";

/// Gets an environment variable owned by cargo.
///
Expand Down Expand Up @@ -155,25 +154,7 @@ impl InterpreterConfig {
}

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

if cfg!(feature = "export-config") {
let output_path =
Path::new(&cargo_env_var("OUT_DIR").unwrap()).join(EXPORT_CONFIG_FILENAME);
if let Ok(config_file) = File::create(&output_path) {
if self.to_writer(config_file).is_err() {
warn!(
"Failed to export build config - this may be a problem for external crates that \
depend on the pyo3 config."
);
} else {
println!(
"cargo:PYO3_EXPORT_CONFIG={}",
output_path.canonicalize().unwrap().to_str().unwrap()
);
}
}
println!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag);
}
}

Expand Down Expand Up @@ -359,13 +340,11 @@ print("mingw", get_platform().startswith("mingw"))
InterpreterConfig::from_reader(reader)
}

/// Create an InterpreterConfig generated from pyo3-build-config using the "export-config"
/// feature
#[cfg(feature = "export-config")]
pub fn from_pyo3_export_config() -> Result<Self> {
InterpreterConfig::from_path(Path::new(
&cargo_env_var("DEP_PYTHON_PYO3_EXPORT_CONFIG").unwrap(),
))
#[doc(hidden)]
pub fn from_cargo_link_env() -> Result<Self> {
// un-escape any newlines in the exported config
let buf = cargo_env_var("DEP_PYTHON_PYO3_CONFIG").unwrap().replace("\\n", "\n");
InterpreterConfig::from_reader(buf.as_bytes())
}

#[doc(hidden)]
Expand Down Expand Up @@ -448,6 +427,19 @@ print("mingw", get_platform().startswith("mingw"))
})
}

#[doc(hidden)]
pub fn to_cargo_link_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 @@ -503,7 +495,7 @@ print("mingw", get_platform().startswith("mingw"))
Path::new(
self.executable
.as_ref()
.expect("No interpreter executable!"),
.expect("no interpreter executable"),
),
script,
envs,
Expand All @@ -512,13 +504,14 @@ print("mingw", get_platform().startswith("mingw"))

/// Run a python script using the executable of this InterpreterConfig.
pub fn run_python_script(&self, script: &str) -> Result<String> {
run_python_script(
run_python_script_with_envs(
Path::new(
self.executable
.as_ref()
.expect("No interpreter executable!"),
.expect("no interpreter executable"),
),
script,
std::iter::empty::<(&str, &str)>(),
)
}
}
Expand Down Expand Up @@ -605,6 +598,11 @@ fn is_abi3() -> bool {
cargo_env_var("CARGO_FEATURE_ABI3").is_some()
}

#[allow(unused)]
pub fn link_env_var_set() -> bool {
cargo_env_var("DEP_PYTHON_PYO3_CONFIG").is_some()
}

#[derive(Debug, PartialEq)]
struct TargetInfo {
/// The `arch` component of the compilation target triple.
Expand Down Expand Up @@ -1945,19 +1943,4 @@ mod tests {
.expect("failed to run Python script");
assert_eq!(out.trim_end(), "42");
}

#[test]
#[cfg(feature = "export-config")]
fn test_emit_pyo3_configs_roundtrip() {
let interpreter = make_interpreter_config()
.expect("could not get InterpreterConfig from installed interpreter");
interpreter.emit_pyo3_cfgs();
// config path env var is not set during testing of this crate alone, as it relies on the
// pyo3 python links manifest key, so we have to hard-code the same path
let output_path =
Path::new(&cargo_env_var("OUT_DIR").unwrap()).join(EXPORT_CONFIG_FILENAME);
let reimported_interpreter = InterpreterConfig::from_path(output_path)
.expect("could not generated InterpreterConfig from exported config");
assert_eq!(interpreter, reimported_interpreter);
}
}
3 changes: 2 additions & 1 deletion pyo3-build-config/src/lib.rs
Expand Up @@ -61,7 +61,6 @@ 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();
Expand All @@ -72,6 +71,8 @@ pub fn get() -> &'static InterpreterConfig {
Ok(abi3_config())
} else if impl_::cross_compile_env_vars().any() {
InterpreterConfig::from_path(DEFAULT_CROSS_COMPILE_CONFIG_PATH)
} else if impl_::link_env_var_set() {
InterpreterConfig::from_cargo_link_env()
} else {
InterpreterConfig::from_reader(Cursor::new(HOST_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_link_env()?;

Ok(())
}

Expand Down

0 comments on commit f424900

Please sign in to comment.