1//! Configuration used by PyO3 for conditional support of varying Python versions.
2//!
3//! This crate exposes functionality to be called from build scripts to simplify building crates
4//! which depend on PyO3.
5//!
6//! It used internally by the PyO3 crate's build script to apply the same configuration.
7
8#![warn(elided_lifetimes_in_paths, unused_lifetimes)]
9
10mod errors;
11mod impl_;
12
13#[cfg(feature = "resolve-config")]
14use std::{
15 io::Cursor,
16 path::{Path, PathBuf},
17};
18
19use std::{env, process::Command, str::FromStr};
20
21#[cfg(feature = "resolve-config")]
22use once_cell::sync::OnceCell;
23
24pub use impl_::{
25 cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags,
26 CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple,
27};
28use target_lexicon::OperatingSystem;
29
30/// Adds all the [`#[cfg]` flags](index.html) to the current compilation.
31///
32/// This should be called from a build script.
33///
34/// The full list of attributes added are the following:
35///
36/// | Flag | Description |
37/// | ---- | ----------- |
38/// | `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_7)]` marks code which can run on Python 3.7 **and newer**. |
39/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. |
40/// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. |
41///
42/// For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/latest/building_and_distribution/multiple_python_versions.html).
43#[cfg(feature = "resolve-config")]
44pub fn use_pyo3_cfgs() {
45 for cargo_command: String in get().build_script_outputs() {
46 println!("{}", cargo_command)
47 }
48}
49
50/// Adds linker arguments suitable for PyO3's `extension-module` feature.
51///
52/// This should be called from a build script.
53///
54/// The following link flags are added:
55/// - macOS: `-undefined dynamic_lookup`
56/// - wasm32-unknown-emscripten: `-sSIDE_MODULE=2 -sWASM_BIGINT`
57///
58/// All other platforms currently are no-ops, however this may change as necessary
59/// in future.
60pub fn add_extension_module_link_args() {
61 _add_extension_module_link_args(&impl_::target_triple_from_env(), writer:std::io::stdout())
62}
63
64fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Write) {
65 if triple.operating_system == OperatingSystem::Darwin {
66 writeln!(writer, "cargo:rustc-cdylib-link-arg=-undefined").unwrap();
67 writeln!(writer, "cargo:rustc-cdylib-link-arg=dynamic_lookup").unwrap();
68 } else if triple == &Triple::from_str("wasm32-unknown-emscripten").unwrap() {
69 writeln!(writer, "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2").unwrap();
70 writeln!(writer, "cargo:rustc-cdylib-link-arg=-sWASM_BIGINT").unwrap();
71 }
72}
73
74/// Loads the configuration determined from the build environment.
75///
76/// Because this will never change in a given compilation run, this is cached in a `once_cell`.
77#[cfg(feature = "resolve-config")]
78pub fn get() -> &'static InterpreterConfig {
79 static CONFIG: OnceCell<InterpreterConfig> = OnceCell::new();
80 CONFIG.get_or_init(|| {
81 // Check if we are in a build script and cross compiling to a different target.
82 let cross_compile_config_path: Option = resolve_cross_compile_config_path();
83 let cross_compiling: bool = cross_compile_config_path
84 .as_ref()
85 .map(|path| path.exists())
86 .unwrap_or(default:false);
87
88 if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() {
89 interpreter_config
90 } else if !CONFIG_FILE.is_empty() {
91 InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
92 } else if cross_compiling {
93 InterpreterConfig::from_path(cross_compile_config_path.as_ref().unwrap())
94 } else {
95 InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))
96 }
97 .expect(msg:"failed to parse PyO3 config")
98 })
99}
100
101/// Build configuration provided by `PYO3_CONFIG_FILE`. May be empty if env var not set.
102#[doc(hidden)]
103#[cfg(feature = "resolve-config")]
104const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt"));
105
106/// Build configuration discovered by `pyo3-build-config` build script. Not aware of
107/// cross-compilation settings.
108#[doc(hidden)]
109#[cfg(feature = "resolve-config")]
110const HOST_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config.txt"));
111
112/// Returns the path where PyO3's build.rs writes its cross compile configuration.
113///
114/// The config file will be named `$OUT_DIR/<triple>/pyo3-build-config.txt`.
115///
116/// Must be called from a build script, returns `None` if not.
117#[doc(hidden)]
118#[cfg(feature = "resolve-config")]
119fn resolve_cross_compile_config_path() -> Option<PathBuf> {
120 env::var_os(key:"TARGET").map(|target: OsString| {
121 let mut path: PathBuf = PathBuf::from(env!("OUT_DIR"));
122 path.push(Path::new(&target));
123 path.push(path:"pyo3-build-config.txt");
124 path
125 })
126}
127
128/// Use certain features if we detect the compiler being used supports them.
129///
130/// Features may be removed or added as MSRV gets bumped or new features become available,
131/// so this function is unstable.
132#[doc(hidden)]
133pub fn print_feature_cfgs() {
134 fn rustc_minor_version() -> Option<u32> {
135 let rustc: OsString = env::var_os(key:"RUSTC")?;
136 let output: Output = Command::new(program:rustc).arg("--version").output().ok()?;
137 let version: &str = core::str::from_utf8(&output.stdout).ok()?;
138 let mut pieces: Split<'_, char> = version.split('.');
139 if pieces.next() != Some("rustc 1") {
140 return None;
141 }
142 pieces.next()?.parse().ok()
143 }
144
145 let rustc_minor_version: u32 = rustc_minor_version().unwrap_or(default:0);
146
147 // Enable use of const initializer for thread_local! on Rust 1.59 and greater
148 if rustc_minor_version >= 59 {
149 println!("cargo:rustc-cfg=thread_local_const_init");
150 }
151
152 // invalid_from_utf8 lint was added in Rust 1.74
153 if rustc_minor_version >= 74 {
154 println!("cargo:rustc-cfg=invalid_from_utf8_lint");
155 }
156}
157
158/// Private exports used in PyO3's build.rs
159///
160/// Please don't use these - they could change at any time.
161#[doc(hidden)]
162pub mod pyo3_build_script_impl {
163 #[cfg(feature = "resolve-config")]
164 use crate::errors::{Context, Result};
165
166 #[cfg(feature = "resolve-config")]
167 use super::*;
168
169 pub mod errors {
170 pub use crate::errors::*;
171 }
172 pub use crate::impl_::{
173 cargo_env_var, env_var, is_linking_libpython, make_cross_compile_config, InterpreterConfig,
174 PythonVersion,
175 };
176
177 /// Gets the configuration for use from PyO3's build script.
178 ///
179 /// Differs from .get() above only in the cross-compile case, where PyO3's build script is
180 /// required to generate a new config (as it's the first build script which has access to the
181 /// correct value for CARGO_CFG_TARGET_OS).
182 #[cfg(feature = "resolve-config")]
183 pub fn resolve_interpreter_config() -> Result<InterpreterConfig> {
184 if !CONFIG_FILE.is_empty() {
185 let mut interperter_config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))?;
186 interperter_config.generate_import_libs()?;
187 Ok(interperter_config)
188 } else if let Some(interpreter_config) = make_cross_compile_config()? {
189 // This is a cross compile and need to write the config file.
190 let path = resolve_cross_compile_config_path()
191 .expect("resolve_interpreter_config() must be called from a build script");
192 let parent_dir = path.parent().ok_or_else(|| {
193 format!(
194 "failed to resolve parent directory of config file {}",
195 path.display()
196 )
197 })?;
198 std::fs::create_dir_all(parent_dir).with_context(|| {
199 format!(
200 "failed to create config file directory {}",
201 parent_dir.display()
202 )
203 })?;
204 interpreter_config.to_writer(&mut std::fs::File::create(&path).with_context(
205 || format!("failed to create config file at {}", path.display()),
206 )?)?;
207 Ok(interpreter_config)
208 } else {
209 InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn extension_module_link_args() {
220 let mut buf = Vec::new();
221
222 // Does nothing on non-mac
223 _add_extension_module_link_args(
224 &Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
225 &mut buf,
226 );
227 assert_eq!(buf, Vec::new());
228
229 _add_extension_module_link_args(
230 &Triple::from_str("x86_64-apple-darwin").unwrap(),
231 &mut buf,
232 );
233 assert_eq!(
234 std::str::from_utf8(&buf).unwrap(),
235 "cargo:rustc-cdylib-link-arg=-undefined\n\
236 cargo:rustc-cdylib-link-arg=dynamic_lookup\n"
237 );
238
239 buf.clear();
240 _add_extension_module_link_args(
241 &Triple::from_str("wasm32-unknown-emscripten").unwrap(),
242 &mut buf,
243 );
244 assert_eq!(
245 std::str::from_utf8(&buf).unwrap(),
246 "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2\n\
247 cargo:rustc-cdylib-link-arg=-sWASM_BIGINT\n"
248 );
249 }
250}
251