Skip to content

Commit

Permalink
pyo3-build-config: Inline the PYO3_NO_PYTHON switch
Browse files Browse the repository at this point in the history
This patch folds the `PYO3_NO_PYTHON` + `abi3` special case into
the existing native and cross compilation code paths.

The cross compilation route is now guaranteed to behave the same
whether `PYO3_NO_PYTHON` is set or not (except for sysconfigdata
discovery for the Unix targets).

The native compilation route now stores the hardcoded abi3 interpreter
configuration in place of the discovered configuration blob.
  • Loading branch information
ravenexp committed Apr 5, 2022
1 parent 0f868e7 commit 29476b0
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 80 deletions.
36 changes: 4 additions & 32 deletions pyo3-build-config/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ mod errors;
use std::{env, path::Path};

use errors::{Context, Result};
use impl_::{
env_var, get_abi3_version, make_interpreter_config, BuildFlags, InterpreterConfig,
PythonImplementation,
};
use impl_::{env_var, make_interpreter_config, InterpreterConfig};

fn configure(interpreter_config: Option<InterpreterConfig>, name: &str) -> Result<bool> {
let target = Path::new(&env::var_os("OUT_DIR").unwrap()).join(name);
Expand Down Expand Up @@ -52,37 +49,12 @@ fn config_file() -> Result<Option<InterpreterConfig>> {
}
}

/// If PYO3_NO_PYTHON is set with abi3, use standard abi3 settings.
pub fn abi3_config() -> Option<InterpreterConfig> {
if let Some(version) = get_abi3_version() {
if env_var("PYO3_NO_PYTHON").is_some() {
return Some(InterpreterConfig {
version,
// NB PyPy doesn't support abi3 yet
implementation: PythonImplementation::CPython,
abi3: true,
lib_name: None,
lib_dir: None,
build_flags: BuildFlags::default(),
pointer_width: None,
executable: None,
shared: true,
suppress_build_script_link_lines: false,
extra_build_script_lines: vec![],
});
}
}
None
}

fn generate_build_configs() -> Result<()> {
let mut configured = false;
configured |= configure(config_file()?, "pyo3-build-config-file.txt")?;
configured |= configure(abi3_config(), "pyo3-build-config-abi3.txt")?;
let configured = configure(config_file()?, "pyo3-build-config-file.txt")?;

if configured {
// Don't bother trying to find an interpreter on the host system if at least one of the
// config file or abi3 settings are present
// Don't bother trying to find an interpreter on the host system
// if the user-provided config file is present.
configure(None, "pyo3-build-config.txt")?;
} else {
configure(Some(make_interpreter_config()?), "pyo3-build-config.txt")?;
Expand Down
155 changes: 131 additions & 24 deletions pyo3-build-config/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,17 @@ impl FromStr for PythonImplementation {
}
}

/// Checks if we should look for a Python interpreter installation
/// to get the target interpreter configuration.
///
/// Returns `false` if `PYO3_NO_PYTHON` environment variable is set.
fn have_python_interpreter() -> bool {
env_var("PYO3_NO_PYTHON").is_none()
}

/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate.
///
/// Must be called from a PyO3 crate build script.
fn is_abi3() -> bool {
cargo_env_var("CARGO_FEATURE_ABI3").is_some()
}
Expand Down Expand Up @@ -1387,6 +1398,46 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result<In
})
}

/// Generates "default" interpreter configuration when compiling "abi3" extensions
/// without a working Python interpreter.
///
/// `version` specifies the minimum supported Stable ABI CPython version.
///
/// This should work for most CPython extension modules when compiling on
/// Windows, macOS and Linux.
///
/// Must be called from a PyO3 crate build script.
fn default_abi3_config(host: &Triple, version: PythonVersion) -> InterpreterConfig {
// FIXME: PyPy does not support the Stable ABI yet.
let implementation = PythonImplementation::CPython;
let abi3 = true;

let lib_name = if host.operating_system == OperatingSystem::Windows {
Some(default_lib_name_windows(
version,
implementation,
abi3,
false,
))
} else {
None
};

InterpreterConfig {
implementation,
version,
shared: true,
abi3,
lib_name,
lib_dir: None,
executable: None,
pointer_width: None,
build_flags: BuildFlags::default(),
suppress_build_script_link_lines: false,
extra_build_script_lines: vec![],
}
}

/// Detects the cross compilation target interpreter configuration from all
/// available sources (PyO3 environment variables, Python sysconfigdata, etc.).
///
Expand All @@ -1397,30 +1448,31 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result<In
fn load_cross_compile_config(
cross_compile_config: CrossCompileConfig,
) -> Result<InterpreterConfig> {
// Load the defaults for Windows even when `PYO3_CROSS_LIB_DIR` is set
// since it has no sysconfigdata files in it.
if cross_compile_config.target.operating_system == OperatingSystem::Windows {
return default_cross_compile(&cross_compile_config);
}

// Try to find and parse sysconfigdata files on other targets
// and fall back to the defaults when none are found.
if let Some(config) = cross_compile_from_sysconfigdata(&cross_compile_config)? {
Ok(config)
let windows = cross_compile_config.target.operating_system == OperatingSystem::Windows;

let config = if windows || !have_python_interpreter() {
// Load the defaults for Windows even when `PYO3_CROSS_LIB_DIR` is set
// since it has no sysconfigdata files in it.
// Also, do not try to look for sysconfigdata when `PYO3_NO_PYTHON` variable is set.
default_cross_compile(&cross_compile_config)?
} else if let Some(config) = cross_compile_from_sysconfigdata(&cross_compile_config)? {
// Try to find and parse sysconfigdata files on other targets.
config
} else {
let config = default_cross_compile(&cross_compile_config)?;

if config.lib_name.is_some() && config.lib_dir.is_none() {
warn!(
"The output binary will link to libpython, \
but PYO3_CROSS_LIB_DIR environment variable is not set. \
Ensure that the target Python library directory is \
in the rustc native library search path."
);
}
// Fall back to the defaults when nothing else can be done.
default_cross_compile(&cross_compile_config)?
};

Ok(config)
if config.lib_name.is_some() && config.lib_dir.is_none() {
warn!(
"The output binary will link to libpython, \
but PYO3_CROSS_LIB_DIR environment variable is not set. \
Ensure that the target Python library directory is \
in the rustc native library search path."
);
}

Ok(config)
}

// Link against python3.lib for the stable ABI on Windows.
Expand Down Expand Up @@ -1591,9 +1643,18 @@ pub fn make_cross_compile_config() -> Result<Option<InterpreterConfig>> {
/// Only used by `pyo3-build-config` build script.
#[allow(dead_code)]
pub fn make_interpreter_config() -> Result<InterpreterConfig> {
let mut interpreter_config = InterpreterConfig::from_interpreter(find_interpreter()?)?;
interpreter_config.fixup_for_abi3_version(get_abi3_version())?;
Ok(interpreter_config)
let abi3_version = get_abi3_version();

if have_python_interpreter() {
let mut interpreter_config = InterpreterConfig::from_interpreter(find_interpreter()?)?;
interpreter_config.fixup_for_abi3_version(abi3_version)?;
Ok(interpreter_config)
} else if let Some(version) = abi3_version {
let host = Triple::host();
Ok(default_abi3_config(&host, version))
} else {
bail!("An abi3-py3* feature must be specified when compiling without a Python interpreter.")
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1844,6 +1905,52 @@ mod tests {
);
}

#[test]
fn windows_hardcoded_abi3_compile() {
let host = triple!("x86_64-pc-windows-msvc");
let min_version = "3.7".parse().unwrap();

assert_eq!(
default_abi3_config(&host, min_version),
InterpreterConfig {
implementation: PythonImplementation::CPython,
version: PythonVersion { major: 3, minor: 7 },
shared: true,
abi3: true,
lib_name: Some("python3".into()),
lib_dir: None,
executable: None,
pointer_width: None,
build_flags: BuildFlags::default(),
suppress_build_script_link_lines: false,
extra_build_script_lines: vec![],
}
);
}

#[test]
fn unix_hardcoded_abi3_compile() {
let host = triple!("x86_64-unknown-linux-gnu");
let min_version = "3.9".parse().unwrap();

assert_eq!(
default_abi3_config(&host, min_version),
InterpreterConfig {
implementation: PythonImplementation::CPython,
version: PythonVersion { major: 3, minor: 9 },
shared: true,
abi3: true,
lib_name: None,
lib_dir: None,
executable: None,
pointer_width: None,
build_flags: BuildFlags::default(),
suppress_build_script_link_lines: false,
extra_build_script_lines: vec![],
}
);
}

#[test]
fn windows_hardcoded_cross_compile() {
let env_vars = CrossCompileEnvVars {
Expand Down
24 changes: 0 additions & 24 deletions pyo3-build-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ pub fn get() -> &'static InterpreterConfig {
interpreter_config
} else if !CONFIG_FILE.is_empty() {
InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
} else if !ABI3_CONFIG.is_empty() {
Ok(abi3_config())
} else if cross_compiling {
InterpreterConfig::from_path(cross_compile_config_path.as_ref().unwrap())
} else {
Expand All @@ -100,12 +98,6 @@ pub fn get() -> &'static InterpreterConfig {
#[cfg(feature = "resolve-config")]
const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt"));

/// Build configuration set if abi3 features enabled and `PYO3_NO_PYTHON` env var present. Empty if
/// not both present.
#[doc(hidden)]
#[cfg(feature = "resolve-config")]
const ABI3_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-abi3.txt"));

/// Build configuration discovered by `pyo3-build-config` build script. Not aware of
/// cross-compilation settings.
#[doc(hidden)]
Expand All @@ -128,20 +120,6 @@ fn resolve_cross_compile_config_path() -> Option<PathBuf> {
})
}

#[cfg(feature = "resolve-config")]
fn abi3_config() -> InterpreterConfig {
let mut interpreter_config = InterpreterConfig::from_reader(Cursor::new(ABI3_CONFIG))
.expect("failed to parse hardcoded PyO3 abi3 config");
// If running from a build script on Windows, tweak the hardcoded abi3 config to contain
// the standard lib name (this is necessary so that abi3 extension modules using
// PYO3_NO_PYTHON on Windows can link)
if std::env::var("CARGO_CFG_TARGET_OS").map_or(false, |target_os| target_os == "windows") {
assert_eq!(interpreter_config.lib_name, None);
interpreter_config.lib_name = Some("python3".to_owned())
}
interpreter_config
}

/// Use certain features if we detect the compiler being used supports them.
///
/// Features may be removed or added as MSRV gets bumped or new features become available,
Expand Down Expand Up @@ -200,8 +178,6 @@ pub mod pyo3_build_script_impl {
pub fn resolve_interpreter_config() -> Result<InterpreterConfig> {
if !CONFIG_FILE.is_empty() {
InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
} else if !ABI3_CONFIG.is_empty() {
Ok(abi3_config())
} else if let Some(interpreter_config) = make_cross_compile_config()? {
// This is a cross compile and need to write the config file.
let path = resolve_cross_compile_config_path()
Expand Down

0 comments on commit 29476b0

Please sign in to comment.