Skip to content

Commit

Permalink
pyo3-build-config: refactor: Add helper methods
Browse files Browse the repository at this point in the history
Attempt to extract common code patterns into methods
and standalone helper functions.

Make some of the new helper functions public within the PyO3 crate
and reuse them in the build scripts.

Add an optional fourth "triple" member to `TargetInfo` and use it
for the Windows MinGW target detection.

Apply small fixes to comments and namespacing.

No user visible functionality change.
  • Loading branch information
ravenexp committed Mar 23, 2022
1 parent 7f62c96 commit 05e7a12
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 114 deletions.
220 changes: 134 additions & 86 deletions pyo3-build-config/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,29 @@ print("mingw", get_platform().startswith("mingw"))
}
Ok(())
}

/// Lowers the configured version to the abi3 version, if set.
fn fixup_for_abi3_version(&mut self, abi3_version: Option<PythonVersion>) -> Result<()> {
// PyPy doesn't support abi3; don't adjust the version
if self.implementation.is_pypy() {
return Ok(());
}

if let Some(version) = abi3_version {
ensure!(
version <= self.version,
"cannot set a minimum Python version {} higher than the interpreter version {} \
(the minimum Python version is implied by the abi3-py3{} feature)",
version,
self.version,
version.minor,
);

self.version = version;
}

Ok(())
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -542,15 +565,43 @@ impl FromStr for PythonImplementation {
}
}

fn is_abi3() -> bool {
/// Checks if the `abi3` feature is enabled for the PyO3 crate.
///
/// Must be called from a PyO3 crate build script.
pub fn is_abi3() -> bool {
cargo_env_var("CARGO_FEATURE_ABI3").is_some()
}

/// Gets the minimum supported Python version from PyO3 `abi3-py*` features.
///
/// Must be called from a PyO3 crate build script.
pub fn get_abi3_version() -> Option<PythonVersion> {
let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=ABI3_MAX_MINOR)
.find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{}", i)).is_some());
minor_version.map(|minor| PythonVersion { major: 3, minor })
}

/// Checks if the `extension-module` feature is enabled for the PyO3 crate.
///
/// Must be called from a PyO3 crate build script.
pub fn is_extension_module() -> bool {
cargo_env_var("CARGO_FEATURE_EXTENSION_MODULE").is_some()
}

/// Checks if we need to link to `libpython` for the current build target.
///
/// Must be called from a crate PyO3 build script.
pub fn is_linking_libpython() -> bool {
let target = TargetInfo::from_cargo_env().expect("must be called from a build script");

target.os == "windows" || target.os == "android" || !is_extension_module()
}

#[derive(Debug, PartialEq)]
struct TargetInfo {
/// The `arch` component of the compilation target triple.
///
/// e.g. x86_64, i386, arm, thumb, mips, etc.
/// e.g. x86_64, aarch64, x86, arm, thumb, mips, etc.
arch: String,

/// The `vendor` component of the compilation target triple.
Expand All @@ -562,9 +613,26 @@ struct TargetInfo {
///
/// e.g. darwin, freebsd, linux, windows, etc.
os: String,

/// The optional `env` component of the compilation target triple.
///
/// e.g. gnu, musl, msvc, etc.
env: Option<String>,
}

impl TargetInfo {
fn from_triple(arch: &str, vendor: &str, os: &str, env: Option<&str>) -> Self {
TargetInfo {
arch: arch.to_owned(),
vendor: vendor.to_owned(),
os: os.to_owned(),
env: env.map(|s| s.to_owned()),
}
}

/// Gets the compile target from the Cargo environment.
///
/// Must be called from a build script.
fn from_cargo_env() -> Result<Self> {
Ok(Self {
arch: cargo_env_var("CARGO_CFG_TARGET_ARCH")
Expand All @@ -573,6 +641,7 @@ impl TargetInfo {
.ok_or("expected CARGO_CFG_TARGET_VENDOR env var")?,
os: cargo_env_var("CARGO_CFG_TARGET_OS")
.ok_or("expected CARGO_CFG_TARGET_OS env var")?,
env: cargo_env_var("CARGO_CFG_TARGET_ENV"),
})
}

Expand All @@ -592,6 +661,32 @@ impl TargetInfo {
}
)
}

fn is_cross_compiling_from(&self, host: &str) -> bool {
let target_triple = self.to_target_triple();

// Not cross-compiling if arch-vendor-os is all the same
// e.g. x86_64-unknown-linux-musl on x86_64-unknown-linux-gnu host
// x86_64-pc-windows-gnu on x86_64-pc-windows-msvc host
let host_target_compatible = host.starts_with(&target_triple)
// Not cross-compiling to compile for 32-bit Python from windows 64-bit
|| (target_triple == "i686-pc-windows" && host.starts_with("x86_64-pc-windows"))
// Not cross-compiling to compile for x86-64 Python from macOS arm64
|| (target_triple == "x86_64-apple-darwin" && host == "aarch64-apple-darwin")
// Not cross-compiling to compile for arm64 Python from macOS x86_64
|| (target_triple == "aarch64-apple-darwin" && host == "x86_64-apple-darwin");

// Cross compiling if not compiler compatible
!host_target_compatible
}

fn is_windows(&self) -> bool {
self.os == "windows"
}

fn is_windows_mingw(&self) -> bool {
self.is_windows() && self.env.as_ref().map_or(false, |env| env == "gnu")
}
}

/// Configuration needed by PyO3 to cross-compile for a target platform.
Expand All @@ -615,22 +710,13 @@ impl CrossCompileConfig {
Ok(CrossCompileConfig {
lib_dir: env_vars
.pyo3_cross_lib_dir
.as_ref()
.ok_or(
"The PYO3_CROSS_LIB_DIR environment variable must be set when cross-compiling",
)?
.into(),
target_info,
version: env_vars
.pyo3_cross_python_version
.map(|os_string| {
let utf8_str = os_string
.to_str()
.ok_or("PYO3_CROSS_PYTHON_VERSION is not valid utf-8.")?;
utf8_str
.parse()
.context("failed to parse PYO3_CROSS_PYTHON_VERSION")
})
.transpose()?,
version: env_vars.parse_version()?,
})
}
}
Expand All @@ -647,6 +733,23 @@ impl CrossCompileEnvVars {
|| self.pyo3_cross_lib_dir.is_some()
|| self.pyo3_cross_python_version.is_some()
}

fn parse_version(&self) -> Result<Option<PythonVersion>> {
let version = self
.pyo3_cross_python_version
.as_ref()
.map(|os_string| {
let utf8_str = os_string
.to_str()
.ok_or("PYO3_CROSS_PYTHON_VERSION is not valid UTF-8")?;
utf8_str
.parse()
.context("failed to parse PYO3_CROSS_PYTHON_VERSION")
})
.transpose()?;

Ok(version)
}
}

pub(crate) fn cross_compile_env_vars() -> CrossCompileEnvVars {
Expand Down Expand Up @@ -678,34 +781,15 @@ pub fn cross_compiling(
target_os: &str,
) -> Result<Option<CrossCompileConfig>> {
let env_vars = cross_compile_env_vars();
let target_info = TargetInfo::from_triple(target_arch, target_vendor, target_os, None);

let target_info = TargetInfo {
arch: target_arch.to_owned(),
vendor: target_vendor.to_owned(),
os: target_os.to_owned(),
};

if !env_vars.any() && is_not_cross_compiling(host, &target_info) {
if !env_vars.any() && !target_info.is_cross_compiling_from(host) {
return Ok(None);
}

CrossCompileConfig::from_env_vars(env_vars, target_info).map(Some)
}

fn is_not_cross_compiling(host: &str, target_info: &TargetInfo) -> bool {
let target_triple = target_info.to_target_triple();
// Not cross-compiling if arch-vendor-os is all the same
// e.g. x86_64-unknown-linux-musl on x86_64-unknown-linux-gnu host
// x86_64-pc-windows-gnu on x86_64-pc-windows-msvc host
host.starts_with(&target_triple)
// Not cross-compiling to compile for 32-bit Python from windows 64-bit
|| (target_triple == "i686-pc-windows" && host.starts_with("x86_64-pc-windows"))
// Not cross-compiling to compile for x86-64 Python from macOS arm64
|| (target_triple == "x86_64-apple-darwin" && host == "aarch64-apple-darwin")
// Not cross-compiling to compile for arm64 Python from macOS x86_64
|| (target_triple == "aarch64-apple-darwin" && host == "x86_64-apple-darwin")
}

#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum BuildFlag {
Expand Down Expand Up @@ -1103,7 +1187,7 @@ fn windows_hardcoded_cross_compile(
version,
PythonImplementation::CPython,
abi3,
false,
cross_compile_config.target_info.is_windows_mingw(),
)),
lib_dir: cross_compile_config.lib_dir.to_str().map(String::from),
executable: None,
Expand Down Expand Up @@ -1273,37 +1357,6 @@ pub fn find_interpreter() -> Result<PathBuf> {
}
}

pub fn get_abi3_version() -> Option<PythonVersion> {
let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=ABI3_MAX_MINOR)
.find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{}", i)).is_some());
minor_version.map(|minor| PythonVersion { major: 3, minor })
}

/// Lowers the configured version to the abi3 version, if set.
fn fixup_config_for_abi3(
config: &mut InterpreterConfig,
abi3_version: Option<PythonVersion>,
) -> Result<()> {
// PyPy doesn't support abi3; don't adjust the version
if config.implementation.is_pypy() {
return Ok(());
}

if let Some(version) = abi3_version {
ensure!(
version <= config.version,
"cannot set a minimum Python version {} higher than the interpreter version {} \
(the minimum Python version is implied by the abi3-py3{} feature)",
version,
config.version,
version.minor,
);

config.version = version;
}
Ok(())
}

/// Generates an interpreter config suitable for cross-compilation.
///
/// This must be called from PyO3's build script, because it relies on environment variables such as
Expand All @@ -1319,11 +1372,11 @@ pub fn make_cross_compile_config() -> Result<Option<InterpreterConfig>> {
let interpreter_config = if env_vars.any() {
let cross_config = CrossCompileConfig::from_env_vars(env_vars, target_info)?;
let mut interpreter_config = load_cross_compile_config(cross_config)?;
fixup_config_for_abi3(&mut interpreter_config, get_abi3_version())?;
interpreter_config.fixup_for_abi3_version(get_abi3_version())?;
Some(interpreter_config)
} else {
ensure!(
host == target || is_not_cross_compiling(&host, &target_info),
host == target || !target_info.is_cross_compiling_from(&host),
"PyO3 detected compile host {host} and build target {target}, but none of PYO3_CROSS, PYO3_CROSS_LIB_DIR \
or PYO3_CROSS_PYTHON_VERSION environment variables are set.",
host=host,
Expand All @@ -1340,7 +1393,7 @@ pub fn make_cross_compile_config() -> Result<Option<InterpreterConfig>> {
#[allow(dead_code)]
pub fn make_interpreter_config() -> Result<InterpreterConfig> {
let mut interpreter_config = InterpreterConfig::from_interpreter(find_interpreter()?)?;
fixup_config_for_abi3(&mut interpreter_config, get_abi3_version())?;
interpreter_config.fixup_for_abi3_version(get_abi3_version())?;
Ok(interpreter_config)
}

Expand Down Expand Up @@ -1596,11 +1649,7 @@ mod tests {
let cross_config = CrossCompileConfig {
lib_dir: "C:\\some\\path".into(),
version: Some(PythonVersion { major: 3, minor: 7 }),
target_info: TargetInfo {
os: "os".into(),
arch: "arch".into(),
vendor: "vendor".into(),
},
target_info: TargetInfo::from_triple("x86", "pc", "windows", Some("msvc")),
};

assert_eq!(
Expand Down Expand Up @@ -1722,7 +1771,9 @@ mod tests {
extra_build_script_lines: vec![],
};

fixup_config_for_abi3(&mut config, Some(PythonVersion { major: 3, minor: 7 })).unwrap();
config
.fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 7 }))
.unwrap();
assert_eq!(config.version, PythonVersion { major: 3, minor: 7 });
}

Expand All @@ -1742,12 +1793,13 @@ mod tests {
extra_build_script_lines: vec![],
};

assert!(
fixup_config_for_abi3(&mut config, Some(PythonVersion { major: 3, minor: 8 }))
.unwrap_err()
.to_string()
.contains("cannot set a minimum Python version 3.8 higher than the interpreter version 3.7")
);
assert!(config
.fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 8 }))
.unwrap_err()
.to_string()
.contains(
"cannot set a minimum Python version 3.8 higher than the interpreter version 3.7"
));
}

#[test]
Expand All @@ -1772,11 +1824,7 @@ mod tests {
let cross = CrossCompileConfig {
lib_dir: lib_dir.into(),
version: Some(interpreter_config.version),
target_info: TargetInfo {
arch: "x86_64".into(),
vendor: "unknown".into(),
os: "linux".into(),
},
target_info: TargetInfo::from_triple("x86_64", "unknown", "linux", Some("gnu")),
};

let sysconfigdata_path = match find_sysconfigdata(&cross) {
Expand Down
5 changes: 3 additions & 2 deletions pyo3-build-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ pub mod pyo3_build_script_impl {
pub use crate::errors::*;
}
pub use crate::impl_::{
cargo_env_var, env_var, make_cross_compile_config, InterpreterConfig, PythonVersion,
cargo_env_var, env_var, is_linking_libpython, make_cross_compile_config, InterpreterConfig,
PythonVersion,
};

/// Gets the configuration for use from PyO3's build script.
Expand All @@ -177,7 +178,7 @@ pub mod pyo3_build_script_impl {
InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
} else if !ABI3_CONFIG.is_empty() {
Ok(abi3_config())
} else if let Some(interpreter_config) = impl_::make_cross_compile_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 = Path::new(DEFAULT_CROSS_COMPILE_CONFIG_PATH);
let parent_dir = path.parent().ok_or_else(|| {
Expand Down
Loading

0 comments on commit 05e7a12

Please sign in to comment.