Skip to content

Commit 0726e20

Browse files
committed
add BuildScriptContext
1 parent 7f9cc54 commit 0726e20

4 files changed

Lines changed: 183 additions & 70 deletions

File tree

build.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use std::env;
22

3-
use pyo3_build_config::pyo3_build_script_impl::{cargo_env_var, errors::Result};
3+
use pyo3_build_config::pyo3_build_script_impl::errors::Result;
44
use pyo3_build_config::{
5-
add_python_framework_link_args, bail, print_feature_cfgs, InterpreterConfig,
5+
add_python_framework_link_args, bail, print_feature_cfgs, InterpreterConfig, BUILD_CTX,
66
};
77

88
fn ensure_auto_initialize_ok(interpreter_config: &InterpreterConfig) -> Result<()> {
9-
if cargo_env_var("CARGO_FEATURE_AUTO_INITIALIZE").is_some() && !interpreter_config.shared {
9+
if *BUILD_CTX.cargo.cargo_feature_auto_initialize && !interpreter_config.shared {
1010
bail!(
1111
"The `auto-initialize` feature is enabled, but your python installation only supports \
1212
embedding the Python interpreter statically. If you are attempting to run tests, or a \

pyo3-build-config/src/impl_.rs

Lines changed: 166 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::{
1313
path::{Path, PathBuf},
1414
process::{Command, Stdio},
1515
str::{self, FromStr},
16+
sync::LazyLock,
1617
};
1718

1819
pub use target_lexicon::Triple;
@@ -51,26 +52,146 @@ thread_local! {
5152
static READ_ENV_VARS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
5253
}
5354

54-
/// Gets an environment variable owned by cargo.
55-
///
56-
/// Environment variables set by cargo are expected to be valid UTF8.
57-
pub fn cargo_env_var(var: &str) -> Option<String> {
58-
env::var_os(var).map(|os_string| os_string.to_str().unwrap().into())
55+
pub static BUILD_CTX: LazyLock<BuildScriptContext> = LazyLock::new(BuildScriptContext::new);
56+
57+
pub struct BuildScriptContext {
58+
pub ext: ExtEnv,
59+
pub cargo: CargoEnv,
5960
}
6061

61-
/// Gets an external environment variable, and registers the build script to rerun if
62-
/// the variable changes.
63-
pub fn env_var(var: &str) -> Option<OsString> {
64-
if cfg!(feature = "resolve-config") {
65-
println!("cargo:rerun-if-env-changed={var}");
62+
pub struct ExtEnv {
63+
pub is_print_config: LazyLock<bool>,
64+
pub use_abi13_forward_compatibility: LazyLock<bool>,
65+
pub pyo3_config_file: LazyLock<Option<PathBuf>>,
66+
pub pyo3_python: LazyLock<Option<OsString>>,
67+
pub pyo3_no_python: LazyLock<bool>,
68+
pub pyo3_build_extension_module: LazyLock<bool>,
69+
pub pyo3_cross: LazyLock<Option<OsString>>,
70+
pub pyo3_cross_lib_dir: LazyLock<Option<OsString>>,
71+
pub pyo3_cross_python_version: LazyLock<Option<OsString>>,
72+
pub pyo3_cross_python_implementation: LazyLock<Option<OsString>>,
73+
pub python_sysconfigdata_name: LazyLock<Option<OsString>>,
74+
pub virtual_env: LazyLock<Option<OsString>>,
75+
pub conda_prefix: LazyLock<Option<OsString>>,
76+
}
77+
78+
pub struct CargoEnv {
79+
pub dep_python_pyo3_config: LazyLock<Option<String>>,
80+
pub cargo_feature_abi3: LazyLock<bool>,
81+
pub cargo_feature_extension_module: LazyLock<bool>,
82+
83+
/// The minimum supported Python version from PyO3 `abi3-py*` features.
84+
/// Must be called from a PyO3 crate build script.
85+
pub abi3_version: LazyLock<Option<PythonVersion>>,
86+
pub cargo_cfg_target_pointer_width:
87+
LazyLock<Result<u32, Box<dyn std::error::Error + Send + Sync>>>,
88+
pub cargo_cfg_target_os: LazyLock<String>,
89+
pub cargo_feature_auto_initialize: LazyLock<bool>,
90+
}
91+
92+
impl BuildScriptContext {
93+
pub fn new() -> Self {
94+
Self {
95+
ext: ExtEnv::new(),
96+
cargo: CargoEnv::new(),
97+
}
6698
}
67-
#[cfg(test)]
68-
{
69-
READ_ENV_VARS.with(|env_vars| {
70-
env_vars.borrow_mut().push(var.to_owned());
71-
});
99+
}
100+
101+
impl Default for BuildScriptContext {
102+
fn default() -> Self {
103+
Self::new()
104+
}
105+
}
106+
107+
impl ExtEnv {
108+
pub fn new() -> Self {
109+
Self {
110+
is_print_config: LazyLock::new(|| {
111+
Self::env_var("PYO3_PRINT_CONFIG").is_some_and(|os_str| os_str == "1")
112+
}),
113+
use_abi13_forward_compatibility: LazyLock::new(|| {
114+
Self::env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY")
115+
.is_some_and(&|os_str| os_str == "1")
116+
}),
117+
pyo3_config_file: LazyLock::new(|| {
118+
Self::env_var("PYO3_CONFIG_FILE").map(PathBuf::from)
119+
}),
120+
pyo3_python: LazyLock::new(|| Self::env_var("PYO3_PYTHON")),
121+
pyo3_no_python: LazyLock::new(|| Self::env_var("PYO3_NO_PYTHON").is_none()),
122+
pyo3_build_extension_module: LazyLock::new(|| {
123+
Self::env_var("PYO3_BUILD_EXTENSION_MODULE").is_some()
124+
}),
125+
pyo3_cross: LazyLock::new(|| Self::env_var("PYO3_CROSS")),
126+
pyo3_cross_lib_dir: LazyLock::new(|| Self::env_var("PYO3_CROSS_LIB_DIR")),
127+
pyo3_cross_python_version: LazyLock::new(|| Self::env_var("PYO3_CROSS_PYTHON_VERSION")),
128+
pyo3_cross_python_implementation: LazyLock::new(|| {
129+
Self::env_var("PYO3_CROSS_PYTHON_IMPLEMENTATION")
130+
}),
131+
python_sysconfigdata_name: LazyLock::new(|| {
132+
Self::env_var("_PYTHON_SYSCONFIGDATA_NAME")
133+
}),
134+
virtual_env: LazyLock::new(|| Self::env_var("VIRTUAL_ENV")),
135+
conda_prefix: LazyLock::new(|| Self::env_var("CONDA_PREFIX")),
136+
}
137+
}
138+
139+
/// Gets an external environment variable, and registers the build script to rerun if
140+
/// the variable changes.
141+
pub fn env_var(var: &str) -> Option<OsString> {
142+
if cfg!(feature = "resolve-config") {
143+
println!("cargo:rerun-if-env-changed={var}");
144+
}
145+
#[cfg(test)]
146+
{
147+
READ_ENV_VARS.with(|env_vars| {
148+
env_vars.borrow_mut().push(var.to_owned());
149+
});
150+
}
151+
env::var_os(var)
152+
}
153+
}
154+
155+
impl CargoEnv {
156+
fn new() -> Self {
157+
Self {
158+
dep_python_pyo3_config: LazyLock::new(|| Self::cargo_env_var("DEP_PYTHON_PYO3_CONFIG")),
159+
cargo_feature_abi3: LazyLock::new(|| {
160+
Self::cargo_env_var("CARGO_FEATURE_ABI3").is_some()
161+
}),
162+
cargo_feature_extension_module: LazyLock::new(|| {
163+
Self::cargo_env_var("CARGO_FEATURE_EXTENSION_MODULE").is_some()
164+
}),
165+
abi3_version: LazyLock::new(|| {
166+
let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=ABI3_MAX_MINOR)
167+
.find(|i| Self::cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{i}")).is_some());
168+
minor_version.map(|minor| PythonVersion { major: 3, minor })
169+
}),
170+
cargo_cfg_target_pointer_width: LazyLock::new(|| {
171+
Ok(
172+
match Self::cargo_env_var("CARGO_CFG_TARGET_POINTER_WIDTH").as_deref() {
173+
Some("64") => 64,
174+
Some("32") => 32,
175+
Some(x) => bail!("unexpected Rust target pointer width: {}", x),
176+
None => bail!("CARGO_CFG_TARGET_POINTER_WIDTH is unset"),
177+
},
178+
)
179+
}),
180+
cargo_cfg_target_os: LazyLock::new(|| {
181+
Self::cargo_env_var("CARGO_CFG_TARGET_OS").unwrap()
182+
}),
183+
cargo_feature_auto_initialize: LazyLock::new(|| {
184+
Self::cargo_env_var("CARGO_FEATURE_AUTO_INITIALIZE").is_some()
185+
}),
186+
}
187+
}
188+
189+
/// Gets an environment variable owned by cargo.
190+
///
191+
/// Environment variables set by cargo are expected to be valid UTF8.
192+
pub fn cargo_env_var(var: &str) -> Option<String> {
193+
env::var_os(var).map(|os_string| os_string.to_str().unwrap().into())
72194
}
73-
env::var_os(var)
74195
}
75196

76197
/// Gets the compilation target triple from environment variables set by Cargo.
@@ -455,8 +576,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED"))
455576
/// The `abi3` features, if set, may apply an `abi3` constraint to the Python version.
456577
#[allow(dead_code)] // only used in build.rs
457578
pub(super) fn from_pyo3_config_file_env() -> Option<Result<Self>> {
458-
env_var("PYO3_CONFIG_FILE").map(|path| {
459-
let path = Path::new(&path);
579+
BUILD_CTX.ext.pyo3_config_file.as_ref().map(|path| {
460580
println!("cargo:rerun-if-changed={}", path.display());
461581
// Absolute path is necessary because this build script is run with a cwd different to the
462582
// original `cargo build` instruction.
@@ -473,7 +593,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED"))
473593
// TODO: abi3 is a property of the build mode, not the interpreter. Should this be
474594
// removed from `InterpreterConfig`?
475595
config.abi3 |= is_abi3();
476-
config.fixup_for_abi3_version(get_abi3_version())?;
596+
config.fixup_for_abi3_version(*BUILD_CTX.cargo.abi3_version)?;
477597

478598
Ok(config)
479599
})
@@ -490,8 +610,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED"))
490610

491611
#[doc(hidden)]
492612
pub fn from_cargo_dep_env() -> Option<Result<Self>> {
493-
cargo_env_var("DEP_PYTHON_PYO3_CONFIG")
494-
.map(|buf| InterpreterConfig::from_reader(&*unescape(&buf)))
613+
BUILD_CTX
614+
.cargo
615+
.dep_python_pyo3_config
616+
.as_ref()
617+
.map(|buf| InterpreterConfig::from_reader(&*unescape(buf)))
495618
}
496619

497620
#[doc(hidden)]
@@ -834,24 +957,14 @@ impl FromStr for PythonImplementation {
834957
///
835958
/// Returns `false` if `PYO3_NO_PYTHON` environment variable is set.
836959
fn have_python_interpreter() -> bool {
837-
env_var("PYO3_NO_PYTHON").is_none()
960+
*BUILD_CTX.ext.pyo3_no_python
838961
}
839962

840963
/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate.
841964
///
842965
/// Must be called from a PyO3 crate build script.
843966
fn is_abi3() -> bool {
844-
cargo_env_var("CARGO_FEATURE_ABI3").is_some()
845-
|| env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1")
846-
}
847-
848-
/// Gets the minimum supported Python version from PyO3 `abi3-py*` features.
849-
///
850-
/// Must be called from a PyO3 crate build script.
851-
pub fn get_abi3_version() -> Option<PythonVersion> {
852-
let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=ABI3_MAX_MINOR)
853-
.find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{i}")).is_some());
854-
minor_version.map(|minor| PythonVersion { major: 3, minor })
967+
*BUILD_CTX.cargo.cargo_feature_abi3 || *BUILD_CTX.ext.use_abi13_forward_compatibility
855968
}
856969

857970
/// Checks if the `extension-module` feature is enabled for the PyO3 crate.
@@ -862,8 +975,7 @@ pub fn get_abi3_version() -> Option<PythonVersion> {
862975
///
863976
/// Must be called from a PyO3 crate build script.
864977
pub fn is_extension_module() -> bool {
865-
cargo_env_var("CARGO_FEATURE_EXTENSION_MODULE").is_some()
866-
|| env_var("PYO3_BUILD_EXTENSION_MODULE").is_some()
978+
*BUILD_CTX.cargo.cargo_feature_extension_module || *BUILD_CTX.ext.pyo3_build_extension_module
867979
}
868980

869981
/// Checks if we need to link to `libpython` for the target.
@@ -1002,10 +1114,13 @@ impl CrossCompileEnvVars {
10021114
/// Registers the build script to rerun if any of the variables changes.
10031115
fn from_env() -> Self {
10041116
CrossCompileEnvVars {
1005-
pyo3_cross: env_var("PYO3_CROSS"),
1006-
pyo3_cross_lib_dir: env_var("PYO3_CROSS_LIB_DIR"),
1007-
pyo3_cross_python_version: env_var("PYO3_CROSS_PYTHON_VERSION"),
1008-
pyo3_cross_python_implementation: env_var("PYO3_CROSS_PYTHON_IMPLEMENTATION"),
1117+
pyo3_cross: BUILD_CTX.ext.pyo3_cross.clone(),
1118+
pyo3_cross_lib_dir: BUILD_CTX.ext.pyo3_cross_lib_dir.clone(),
1119+
pyo3_cross_python_version: BUILD_CTX.ext.pyo3_cross_python_version.clone(),
1120+
pyo3_cross_python_implementation: BUILD_CTX
1121+
.ext
1122+
.pyo3_cross_python_implementation
1123+
.clone(),
10091124
}
10101125
}
10111126

@@ -1412,13 +1527,13 @@ pub fn find_all_sysconfigdata(cross: &CrossCompileConfig) -> Result<Vec<PathBuf>
14121527
return Ok(Vec::new());
14131528
};
14141529

1415-
let sysconfig_name = env_var("_PYTHON_SYSCONFIGDATA_NAME");
1530+
let sysconfig_name = BUILD_CTX.ext.python_sysconfigdata_name.as_deref();
14161531
let mut sysconfig_paths = sysconfig_paths
14171532
.iter()
14181533
.filter_map(|p| {
14191534
let canonical = fs::canonicalize(p).ok();
14201535
match &sysconfig_name {
1421-
Some(_) => canonical.filter(|p| p.file_stem() == sysconfig_name.as_deref()),
1536+
Some(_) => canonical.filter(|p| p.file_stem() == sysconfig_name),
14221537
None => canonical,
14231538
}
14241539
})
@@ -1554,7 +1669,7 @@ fn cross_compile_from_sysconfigdata(
15541669
fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result<InterpreterConfig> {
15551670
let version = cross_compile_config
15561671
.version
1557-
.or_else(get_abi3_version)
1672+
.or_else(|| *BUILD_CTX.cargo.abi3_version)
15581673
.ok_or_else(||
15591674
format!(
15601675
"PYO3_CROSS_PYTHON_VERSION or an abi3-py3* feature must be specified \
@@ -1829,11 +1944,14 @@ fn conda_env_interpreter(conda_prefix: &OsStr, windows: bool) -> PathBuf {
18291944
}
18301945

18311946
fn get_env_interpreter() -> Option<PathBuf> {
1832-
match (env_var("VIRTUAL_ENV"), env_var("CONDA_PREFIX")) {
1947+
match (
1948+
BUILD_CTX.ext.virtual_env.as_ref(),
1949+
BUILD_CTX.ext.conda_prefix.as_ref(),
1950+
) {
18331951
// Use cfg rather than CARGO_CFG_TARGET_OS because this affects where files are located on the
18341952
// build host
1835-
(Some(dir), None) => Some(venv_interpreter(&dir, cfg!(windows))),
1836-
(None, Some(dir)) => Some(conda_env_interpreter(&dir, cfg!(windows))),
1953+
(Some(dir), None) => Some(venv_interpreter(dir, cfg!(windows))),
1954+
(None, Some(dir)) => Some(conda_env_interpreter(dir, cfg!(windows))),
18371955
(Some(_), Some(_)) => {
18381956
warn!(
18391957
"Both VIRTUAL_ENV and CONDA_PREFIX are set. PyO3 will ignore both of these for \
@@ -1857,7 +1975,7 @@ pub fn find_interpreter() -> Result<PathBuf> {
18571975
// See https://github.com/PyO3/pyo3/issues/2724
18581976
println!("cargo:rerun-if-env-changed=PYO3_ENVIRONMENT_SIGNATURE");
18591977

1860-
if let Some(exe) = env_var("PYO3_PYTHON") {
1978+
if let Some(exe) = BUILD_CTX.ext.pyo3_python.as_ref() {
18611979
Ok(exe.into())
18621980
} else if let Some(env_interpreter) = get_env_interpreter() {
18631981
Ok(env_interpreter)
@@ -1900,7 +2018,7 @@ fn get_host_interpreter(abi3_version: Option<PythonVersion>) -> Result<Interpret
19002018
pub fn make_cross_compile_config() -> Result<Option<InterpreterConfig>> {
19012019
let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? {
19022020
let mut interpreter_config = load_cross_compile_config(cross_config)?;
1903-
interpreter_config.fixup_for_abi3_version(get_abi3_version())?;
2021+
interpreter_config.fixup_for_abi3_version(*BUILD_CTX.cargo.abi3_version)?;
19042022
Some(interpreter_config)
19052023
} else {
19062024
None
@@ -1914,7 +2032,7 @@ pub fn make_cross_compile_config() -> Result<Option<InterpreterConfig>> {
19142032
#[allow(dead_code, unused_mut)]
19152033
pub fn make_interpreter_config() -> Result<InterpreterConfig> {
19162034
let host = Triple::host();
1917-
let abi3_version = get_abi3_version();
2035+
let abi3_version = *BUILD_CTX.cargo.abi3_version;
19182036

19192037
// See if we can safely skip the Python interpreter configuration detection.
19202038
// Unix "abi3" extension modules can usually be built without any interpreter.

pyo3-build-config/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ use std::{env, process::Command, str::FromStr, sync::OnceLock};
2020

2121
pub use impl_::{
2222
cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags,
23-
CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple,
23+
BuildScriptContext, CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion,
24+
Triple, BUILD_CTX,
2425
};
2526

2627
use target_lexicon::OperatingSystem;
@@ -314,8 +315,8 @@ pub mod pyo3_build_script_impl {
314315
pub use crate::errors::*;
315316
}
316317
pub use crate::impl_::{
317-
cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config,
318-
target_triple_from_env, InterpreterConfig, PythonVersion,
318+
is_linking_libpython_for_target, make_cross_compile_config, target_triple_from_env,
319+
InterpreterConfig, PythonVersion,
319320
};
320321
pub enum BuildConfigSource {
321322
/// Config was provided by `PYO3_CONFIG_FILE`.

0 commit comments

Comments
 (0)