pyo3_build_config/
lib.rs

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, sync::OnceLock};
20
21pub use impl_::{
22    cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags,
23    CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple,
24};
25use target_lexicon::OperatingSystem;
26
27/// Adds all the [`#[cfg]` flags](index.html) to the current compilation.
28///
29/// This should be called from a build script.
30///
31/// The full list of attributes added are the following:
32///
33/// | Flag | Description |
34/// | ---- | ----------- |
35/// | `#[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**. |
36/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. |
37/// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. |
38/// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. |
39///
40/// For examples of how to use these attributes,
41#[doc = concat!("[see PyO3's guide](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution/multiple_python_versions.html)")]
42/// .
43#[cfg(feature = "resolve-config")]
44pub fn use_pyo3_cfgs() {
45    print_expected_cfgs();
46    for cargo_command in get().build_script_outputs() {
47        println!("{cargo_command}")
48    }
49}
50
51/// Adds linker arguments suitable for PyO3's `extension-module` feature.
52///
53/// This should be called from a build script.
54///
55/// The following link flags are added:
56/// - macOS: `-undefined dynamic_lookup`
57/// - wasm32-unknown-emscripten: `-sSIDE_MODULE=2 -sWASM_BIGINT`
58///
59/// All other platforms currently are no-ops, however this may change as necessary
60/// in future.
61pub fn add_extension_module_link_args() {
62    _add_extension_module_link_args(&impl_::target_triple_from_env(), std::io::stdout())
63}
64
65fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Write) {
66    if matches!(triple.operating_system, OperatingSystem::Darwin(_)) {
67        writeln!(writer, "cargo:rustc-cdylib-link-arg=-undefined").unwrap();
68        writeln!(writer, "cargo:rustc-cdylib-link-arg=dynamic_lookup").unwrap();
69    } else if triple == &Triple::from_str("wasm32-unknown-emscripten").unwrap() {
70        writeln!(writer, "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2").unwrap();
71        writeln!(writer, "cargo:rustc-cdylib-link-arg=-sWASM_BIGINT").unwrap();
72    }
73}
74
75/// Adds linker arguments suitable for linking against the Python framework on macOS.
76///
77/// This should be called from a build script.
78///
79/// The following link flags are added:
80/// - macOS: `-Wl,-rpath,<framework_prefix>`
81///
82/// All other platforms currently are no-ops.
83#[cfg(feature = "resolve-config")]
84pub fn add_python_framework_link_args() {
85    let interpreter_config = pyo3_build_script_impl::resolve_interpreter_config().unwrap();
86    _add_python_framework_link_args(
87        &interpreter_config,
88        &impl_::target_triple_from_env(),
89        impl_::is_linking_libpython(),
90        std::io::stdout(),
91    )
92}
93
94#[cfg(feature = "resolve-config")]
95fn _add_python_framework_link_args(
96    interpreter_config: &InterpreterConfig,
97    triple: &Triple,
98    link_libpython: bool,
99    mut writer: impl std::io::Write,
100) {
101    if matches!(triple.operating_system, OperatingSystem::Darwin(_)) && link_libpython {
102        if let Some(framework_prefix) = interpreter_config.python_framework_prefix.as_ref() {
103            writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{framework_prefix}").unwrap();
104        }
105    }
106}
107
108/// Loads the configuration determined from the build environment.
109///
110/// Because this will never change in a given compilation run, this is cached in a `OnceLock`.
111#[cfg(feature = "resolve-config")]
112pub fn get() -> &'static InterpreterConfig {
113    static CONFIG: OnceLock<InterpreterConfig> = OnceLock::new();
114    CONFIG.get_or_init(|| {
115        // Check if we are in a build script and cross compiling to a different target.
116        let cross_compile_config_path = resolve_cross_compile_config_path();
117        let cross_compiling = cross_compile_config_path
118            .as_ref()
119            .map(|path| path.exists())
120            .unwrap_or(false);
121
122        // CONFIG_FILE is generated in build.rs, so it's content can vary
123        #[allow(unknown_lints, clippy::const_is_empty)]
124        if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() {
125            interpreter_config
126        } else if !CONFIG_FILE.is_empty() {
127            InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
128        } else if cross_compiling {
129            InterpreterConfig::from_path(cross_compile_config_path.as_ref().unwrap())
130        } else {
131            InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))
132        }
133        .expect("failed to parse PyO3 config")
134    })
135}
136
137/// Build configuration provided by `PYO3_CONFIG_FILE`. May be empty if env var not set.
138#[doc(hidden)]
139#[cfg(feature = "resolve-config")]
140const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt"));
141
142/// Build configuration discovered by `pyo3-build-config` build script. Not aware of
143/// cross-compilation settings.
144#[doc(hidden)]
145#[cfg(feature = "resolve-config")]
146const HOST_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config.txt"));
147
148/// Returns the path where PyO3's build.rs writes its cross compile configuration.
149///
150/// The config file will be named `$OUT_DIR/<triple>/pyo3-build-config.txt`.
151///
152/// Must be called from a build script, returns `None` if not.
153#[doc(hidden)]
154#[cfg(feature = "resolve-config")]
155fn resolve_cross_compile_config_path() -> Option<PathBuf> {
156    env::var_os("TARGET").map(|target| {
157        let mut path = PathBuf::from(env!("OUT_DIR"));
158        path.push(Path::new(&target));
159        path.push("pyo3-build-config.txt");
160        path
161    })
162}
163
164/// Helper to print a feature cfg with a minimum rust version required.
165fn print_feature_cfg(minor_version_required: u32, cfg: &str) {
166    let minor_version = rustc_minor_version().unwrap_or(0);
167
168    if minor_version >= minor_version_required {
169        println!("cargo:rustc-cfg={cfg}");
170    }
171
172    // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before
173    if minor_version >= 80 {
174        println!("cargo:rustc-check-cfg=cfg({cfg})");
175    }
176}
177
178/// Use certain features if we detect the compiler being used supports them.
179///
180/// Features may be removed or added as MSRV gets bumped or new features become available,
181/// so this function is unstable.
182#[doc(hidden)]
183pub fn print_feature_cfgs() {
184    print_feature_cfg(79, "c_str_lit");
185    // Actually this is available on 1.78, but we should avoid
186    // https://github.com/rust-lang/rust/issues/124651 just in case
187    print_feature_cfg(79, "diagnostic_namespace");
188    print_feature_cfg(83, "io_error_more");
189    print_feature_cfg(83, "mut_ref_in_const_fn");
190    print_feature_cfg(85, "fn_ptr_eq");
191    print_feature_cfg(86, "from_bytes_with_nul_error");
192}
193
194/// Registers `pyo3`s config names as reachable cfg expressions
195///
196/// - <https://github.com/rust-lang/cargo/pull/13571>
197/// - <https://doc.rust-lang.org/nightly/cargo/reference/build-scripts.html#rustc-check-cfg>
198#[doc(hidden)]
199pub fn print_expected_cfgs() {
200    if rustc_minor_version().is_some_and(|version| version < 80) {
201        // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before
202        return;
203    }
204
205    println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)");
206    println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)");
207    println!("cargo:rustc-check-cfg=cfg(PyPy)");
208    println!("cargo:rustc-check-cfg=cfg(GraalPy)");
209    println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))");
210    println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)");
211    println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)");
212
213    // allow `Py_3_*` cfgs from the minimum supported version up to the
214    // maximum minor version (+1 for development for the next)
215    for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 {
216        println!("cargo:rustc-check-cfg=cfg(Py_3_{i})");
217    }
218}
219
220/// Private exports used in PyO3's build.rs
221///
222/// Please don't use these - they could change at any time.
223#[doc(hidden)]
224pub mod pyo3_build_script_impl {
225    #[cfg(feature = "resolve-config")]
226    use crate::errors::{Context, Result};
227
228    #[cfg(feature = "resolve-config")]
229    use super::*;
230
231    pub mod errors {
232        pub use crate::errors::*;
233    }
234    pub use crate::impl_::{
235        cargo_env_var, env_var, is_linking_libpython, make_cross_compile_config, InterpreterConfig,
236        PythonVersion,
237    };
238
239    /// Gets the configuration for use from PyO3's build script.
240    ///
241    /// Differs from .get() above only in the cross-compile case, where PyO3's build script is
242    /// required to generate a new config (as it's the first build script which has access to the
243    /// correct value for CARGO_CFG_TARGET_OS).
244    #[cfg(feature = "resolve-config")]
245    pub fn resolve_interpreter_config() -> Result<InterpreterConfig> {
246        // CONFIG_FILE is generated in build.rs, so it's content can vary
247        #[allow(unknown_lints, clippy::const_is_empty)]
248        if !CONFIG_FILE.is_empty() {
249            let mut interperter_config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))?;
250            interperter_config.generate_import_libs()?;
251            Ok(interperter_config)
252        } else if let Some(interpreter_config) = make_cross_compile_config()? {
253            // This is a cross compile and need to write the config file.
254            let path = resolve_cross_compile_config_path()
255                .expect("resolve_interpreter_config() must be called from a build script");
256            let parent_dir = path.parent().ok_or_else(|| {
257                format!(
258                    "failed to resolve parent directory of config file {}",
259                    path.display()
260                )
261            })?;
262            std::fs::create_dir_all(parent_dir).with_context(|| {
263                format!(
264                    "failed to create config file directory {}",
265                    parent_dir.display()
266                )
267            })?;
268            interpreter_config.to_writer(&mut std::fs::File::create(&path).with_context(
269                || format!("failed to create config file at {}", path.display()),
270            )?)?;
271            Ok(interpreter_config)
272        } else {
273            InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))
274        }
275    }
276}
277
278fn rustc_minor_version() -> Option<u32> {
279    static RUSTC_MINOR_VERSION: OnceLock<Option<u32>> = OnceLock::new();
280    *RUSTC_MINOR_VERSION.get_or_init(|| {
281        let rustc = env::var_os("RUSTC")?;
282        let output = Command::new(rustc).arg("--version").output().ok()?;
283        let version = core::str::from_utf8(&output.stdout).ok()?;
284        let mut pieces = version.split('.');
285        if pieces.next() != Some("rustc 1") {
286            return None;
287        }
288        pieces.next()?.parse().ok()
289    })
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn extension_module_link_args() {
298        let mut buf = Vec::new();
299
300        // Does nothing on non-mac
301        _add_extension_module_link_args(
302            &Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
303            &mut buf,
304        );
305        assert_eq!(buf, Vec::new());
306
307        _add_extension_module_link_args(
308            &Triple::from_str("x86_64-apple-darwin").unwrap(),
309            &mut buf,
310        );
311        assert_eq!(
312            std::str::from_utf8(&buf).unwrap(),
313            "cargo:rustc-cdylib-link-arg=-undefined\n\
314             cargo:rustc-cdylib-link-arg=dynamic_lookup\n"
315        );
316
317        buf.clear();
318        _add_extension_module_link_args(
319            &Triple::from_str("wasm32-unknown-emscripten").unwrap(),
320            &mut buf,
321        );
322        assert_eq!(
323            std::str::from_utf8(&buf).unwrap(),
324            "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2\n\
325             cargo:rustc-cdylib-link-arg=-sWASM_BIGINT\n"
326        );
327    }
328
329    #[cfg(feature = "resolve-config")]
330    #[test]
331    fn python_framework_link_args() {
332        let mut buf = Vec::new();
333
334        let interpreter_config = InterpreterConfig {
335            implementation: PythonImplementation::CPython,
336            version: PythonVersion {
337                major: 3,
338                minor: 13,
339            },
340            shared: true,
341            abi3: false,
342            lib_name: None,
343            lib_dir: None,
344            executable: None,
345            pointer_width: None,
346            build_flags: BuildFlags::default(),
347            suppress_build_script_link_lines: false,
348            extra_build_script_lines: vec![],
349            python_framework_prefix: Some(
350                "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(),
351            ),
352        };
353        // Does nothing on non-mac
354        _add_python_framework_link_args(
355            &interpreter_config,
356            &Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
357            true,
358            &mut buf,
359        );
360        assert_eq!(buf, Vec::new());
361
362        _add_python_framework_link_args(
363            &interpreter_config,
364            &Triple::from_str("x86_64-apple-darwin").unwrap(),
365            true,
366            &mut buf,
367        );
368        assert_eq!(
369            std::str::from_utf8(&buf).unwrap(),
370            "cargo:rustc-link-arg=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Library/Frameworks\n"
371        );
372    }
373}