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};
25
26use target_lexicon::OperatingSystem;
27
28/// Adds all the [`#[cfg]` flags](index.html) to the current compilation.
29///
30/// This should be called from a build script.
31///
32/// The full list of attributes added are the following:
33///
34/// | Flag | Description |
35/// | ---- | ----------- |
36/// | `#[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**. |
37/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. |
38/// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. |
39/// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. |
40///
41/// For examples of how to use these attributes,
42#[doc = concat!("[see PyO3's guide](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution/multiple_python_versions.html)")]
43/// .
44#[cfg(feature = "resolve-config")]
45pub fn use_pyo3_cfgs() {
46    print_expected_cfgs();
47    for cargo_command in get().build_script_outputs() {
48        println!("{cargo_command}")
49    }
50}
51
52/// Adds linker arguments suitable for PyO3's `extension-module` feature.
53///
54/// This should be called from a build script.
55///
56/// The following link flags are added:
57/// - macOS: `-undefined dynamic_lookup`
58/// - wasm32-unknown-emscripten: `-sSIDE_MODULE=2 -sWASM_BIGINT`
59///
60/// All other platforms currently are no-ops, however this may change as necessary
61/// in future.
62pub fn add_extension_module_link_args() {
63    _add_extension_module_link_args(&impl_::target_triple_from_env(), std::io::stdout())
64}
65
66fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Write) {
67    if matches!(triple.operating_system, OperatingSystem::Darwin(_)) {
68        writeln!(writer, "cargo:rustc-cdylib-link-arg=-undefined").unwrap();
69        writeln!(writer, "cargo:rustc-cdylib-link-arg=dynamic_lookup").unwrap();
70    } else if triple == &Triple::from_str("wasm32-unknown-emscripten").unwrap() {
71        writeln!(writer, "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2").unwrap();
72        writeln!(writer, "cargo:rustc-cdylib-link-arg=-sWASM_BIGINT").unwrap();
73    }
74}
75
76/// Adds linker arguments suitable for linking against the Python framework on macOS.
77///
78/// This should be called from a build script.
79///
80/// The following link flags are added:
81/// - macOS: `-Wl,-rpath,<framework_prefix>`
82///
83/// All other platforms currently are no-ops.
84#[cfg(feature = "resolve-config")]
85pub fn add_python_framework_link_args() {
86    let target = impl_::target_triple_from_env();
87    _add_python_framework_link_args(
88        get(),
89        &target,
90        impl_::is_linking_libpython_for_target(&target),
91        std::io::stdout(),
92    )
93}
94
95#[cfg(feature = "resolve-config")]
96fn _add_python_framework_link_args(
97    interpreter_config: &InterpreterConfig,
98    triple: &Triple,
99    link_libpython: bool,
100    mut writer: impl std::io::Write,
101) {
102    if matches!(triple.operating_system, OperatingSystem::Darwin(_)) && link_libpython {
103        if let Some(framework_prefix) = interpreter_config.python_framework_prefix.as_ref() {
104            writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{framework_prefix}").unwrap();
105        }
106    }
107}
108
109/// Loads the configuration determined from the build environment.
110///
111/// Because this will never change in a given compilation run, this is cached in a `OnceLock`.
112#[cfg(feature = "resolve-config")]
113pub fn get() -> &'static InterpreterConfig {
114    static CONFIG: OnceLock<InterpreterConfig> = OnceLock::new();
115    CONFIG.get_or_init(|| {
116        // Check if we are in a build script and cross compiling to a different target.
117        let cross_compile_config_path = resolve_cross_compile_config_path();
118        let cross_compiling = cross_compile_config_path
119            .as_ref()
120            .map(|path| path.exists())
121            .unwrap_or(false);
122
123        // CONFIG_FILE is generated in build.rs, so its content can vary
124        #[allow(unknown_lints, clippy::const_is_empty)]
125        if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() {
126            interpreter_config
127        } else if let Some(interpreter_config) = config_from_pyo3_config_file_env() {
128            Ok(interpreter_config)
129        } else if cross_compiling {
130            InterpreterConfig::from_path(cross_compile_config_path.as_ref().unwrap())
131        } else {
132            InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))
133        }
134        .expect("failed to parse PyO3 config")
135    })
136}
137
138/// Build configuration provided by `PYO3_CONFIG_FILE`, inlined into the `pyo3-build-config` binary.
139#[cfg(feature = "resolve-config")]
140fn config_from_pyo3_config_file_env() -> Option<InterpreterConfig> {
141    #[doc(hidden)]
142    const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt"));
143
144    // CONFIG_FILE is generated in build.rs, so its content can vary
145    // TODO: `unknown_lints` allow not needed on MSRV 1.79+
146    #[allow(unknown_lints, clippy::const_is_empty)]
147    if !CONFIG_FILE.is_empty() {
148        let config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
149            .expect("contents of CONFIG_FILE should always be valid (generated by pyo3-build-config's build.rs)");
150        Some(config)
151    } else {
152        None
153    }
154}
155
156/// Build configuration discovered by `pyo3-build-config` build script. Not aware of
157/// cross-compilation settings. Not generated if `PYO3_CONFIG_FILE` is set.
158#[doc(hidden)]
159#[cfg(feature = "resolve-config")]
160const HOST_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config.txt"));
161
162/// Returns the path where PyO3's build.rs writes its cross compile configuration.
163///
164/// The config file will be named `$OUT_DIR/<triple>/pyo3-build-config.txt`.
165///
166/// Must be called from a build script, returns `None` if not.
167#[doc(hidden)]
168#[cfg(feature = "resolve-config")]
169fn resolve_cross_compile_config_path() -> Option<PathBuf> {
170    env::var_os("TARGET").map(|target| {
171        let mut path = PathBuf::from(env!("OUT_DIR"));
172        path.push(Path::new(&target));
173        path.push("pyo3-build-config.txt");
174        path
175    })
176}
177
178/// Helper to print a feature cfg with a minimum rust version required.
179fn print_feature_cfg(minor_version_required: u32, cfg: &str) {
180    let minor_version = rustc_minor_version().unwrap_or(0);
181
182    if minor_version >= minor_version_required {
183        println!("cargo:rustc-cfg={cfg}");
184    }
185
186    // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before
187    if minor_version >= 80 {
188        println!("cargo:rustc-check-cfg=cfg({cfg})");
189    }
190}
191
192/// Use certain features if we detect the compiler being used supports them.
193///
194/// Features may be removed or added as MSRV gets bumped or new features become available,
195/// so this function is unstable.
196#[doc(hidden)]
197pub fn print_feature_cfgs() {
198    print_feature_cfg(75, "return_position_impl_trait_in_traits");
199    print_feature_cfg(79, "c_str_lit");
200    // Actually this is available on 1.78, but we should avoid
201    // https://github.com/rust-lang/rust/issues/124651 just in case
202    print_feature_cfg(79, "diagnostic_namespace");
203    print_feature_cfg(83, "io_error_more");
204    print_feature_cfg(83, "mut_ref_in_const_fn");
205    print_feature_cfg(85, "fn_ptr_eq");
206    print_feature_cfg(86, "from_bytes_with_nul_error");
207}
208
209/// Registers `pyo3`s config names as reachable cfg expressions
210///
211/// - <https://github.com/rust-lang/cargo/pull/13571>
212/// - <https://doc.rust-lang.org/nightly/cargo/reference/build-scripts.html#rustc-check-cfg>
213#[doc(hidden)]
214pub fn print_expected_cfgs() {
215    if rustc_minor_version().is_some_and(|version| version < 80) {
216        // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before
217        return;
218    }
219
220    println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)");
221    println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)");
222    println!("cargo:rustc-check-cfg=cfg(PyPy)");
223    println!("cargo:rustc-check-cfg=cfg(GraalPy)");
224    println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))");
225    println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)");
226    println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)");
227
228    // allow `Py_3_*` cfgs from the minimum supported version up to the
229    // maximum minor version (+1 for development for the next)
230    for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 {
231        println!("cargo:rustc-check-cfg=cfg(Py_3_{i})");
232    }
233}
234
235/// Private exports used in PyO3's build.rs
236///
237/// Please don't use these - they could change at any time.
238#[doc(hidden)]
239#[cfg(feature = "resolve-config")]
240pub mod pyo3_build_script_impl {
241    use crate::errors::{Context, Result};
242
243    use super::*;
244
245    pub mod errors {
246        pub use crate::errors::*;
247    }
248    pub use crate::impl_::{
249        cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config,
250        target_triple_from_env, InterpreterConfig, PythonVersion,
251    };
252    pub enum BuildConfigSource {
253        /// Config was provided by `PYO3_CONFIG_FILE`.
254        ConfigFile,
255        /// Config was found by an interpreter on the host system.
256        Host,
257        /// Config was configured by cross-compilation settings.
258        CrossCompile,
259    }
260
261    pub struct BuildConfig {
262        pub interpreter_config: InterpreterConfig,
263        pub source: BuildConfigSource,
264    }
265
266    /// Gets the configuration for use from `pyo3-ffi`'s build script.
267    ///
268    /// Differs from `.get()` in three ways:
269    /// 1. The cargo_dep_env config is not yet available (exported by `pyo3-ffi`'s build script).
270    /// 1. If `PYO3_CONFIG_FILE` is set, lib name is fixed up and the windows import libs might be generated.
271    /// 2. The cross-compile config file is generated if necessary.
272    ///
273    /// Steps 2 and 3 are necessary because `pyo3-ffi`'s build script is the first code run which knows
274    /// the correct target triple.
275    pub fn resolve_build_config(target: &Triple) -> Result<BuildConfig> {
276        // CONFIG_FILE is generated in build.rs, so it's content can vary
277        #[allow(unknown_lints, clippy::const_is_empty)]
278        if let Some(mut interpreter_config) = config_from_pyo3_config_file_env() {
279            interpreter_config.apply_default_lib_name_to_config_file(target);
280            interpreter_config.generate_import_libs()?;
281            Ok(BuildConfig {
282                interpreter_config,
283                source: BuildConfigSource::ConfigFile,
284            })
285        } else if let Some(interpreter_config) = make_cross_compile_config()? {
286            // This is a cross compile and need to write the config file.
287            let path = resolve_cross_compile_config_path()
288                .expect("resolve_build_config() must be called from a build script");
289            let parent_dir = path.parent().ok_or_else(|| {
290                format!(
291                    "failed to resolve parent directory of config file {}",
292                    path.display()
293                )
294            })?;
295            std::fs::create_dir_all(parent_dir).with_context(|| {
296                format!(
297                    "failed to create config file directory {}",
298                    parent_dir.display()
299                )
300            })?;
301            interpreter_config.to_writer(&mut std::fs::File::create(&path).with_context(
302                || format!("failed to create config file at {}", path.display()),
303            )?)?;
304            Ok(BuildConfig {
305                interpreter_config,
306                source: BuildConfigSource::CrossCompile,
307            })
308        } else {
309            let interpreter_config = InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))?;
310            Ok(BuildConfig {
311                interpreter_config,
312                source: BuildConfigSource::Host,
313            })
314        }
315    }
316
317    /// Helper to generate an error message when the configured Python version is newer
318    /// than PyO3's current supported version.
319    pub struct MaximumVersionExceeded {
320        message: String,
321    }
322
323    impl MaximumVersionExceeded {
324        pub fn new(
325            interpreter_config: &InterpreterConfig,
326            supported_version: PythonVersion,
327        ) -> Self {
328            let implementation = match interpreter_config.implementation {
329                PythonImplementation::CPython => "Python",
330                PythonImplementation::PyPy => "PyPy",
331                PythonImplementation::GraalPy => "GraalPy",
332            };
333            let version = &interpreter_config.version;
334            let message = format!(
335                "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\
336                = help: this package is being built with PyO3 version {current_version}\n\
337                = help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\
338                = help: updating this package to the latest version of PyO3 may provide compatibility with this {implementation} version",
339                current_version = env!("CARGO_PKG_VERSION")
340            );
341            Self { message }
342        }
343
344        pub fn add_help(&mut self, help: &str) {
345            self.message.push_str("\n= help: ");
346            self.message.push_str(help);
347        }
348
349        pub fn finish(self) -> String {
350            self.message
351        }
352    }
353}
354
355fn rustc_minor_version() -> Option<u32> {
356    static RUSTC_MINOR_VERSION: OnceLock<Option<u32>> = OnceLock::new();
357    *RUSTC_MINOR_VERSION.get_or_init(|| {
358        let rustc = env::var_os("RUSTC")?;
359        let output = Command::new(rustc).arg("--version").output().ok()?;
360        let version = core::str::from_utf8(&output.stdout).ok()?;
361        let mut pieces = version.split('.');
362        if pieces.next() != Some("rustc 1") {
363            return None;
364        }
365        pieces.next()?.parse().ok()
366    })
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn extension_module_link_args() {
375        let mut buf = Vec::new();
376
377        // Does nothing on non-mac
378        _add_extension_module_link_args(
379            &Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
380            &mut buf,
381        );
382        assert_eq!(buf, Vec::new());
383
384        _add_extension_module_link_args(
385            &Triple::from_str("x86_64-apple-darwin").unwrap(),
386            &mut buf,
387        );
388        assert_eq!(
389            std::str::from_utf8(&buf).unwrap(),
390            "cargo:rustc-cdylib-link-arg=-undefined\n\
391             cargo:rustc-cdylib-link-arg=dynamic_lookup\n"
392        );
393
394        buf.clear();
395        _add_extension_module_link_args(
396            &Triple::from_str("wasm32-unknown-emscripten").unwrap(),
397            &mut buf,
398        );
399        assert_eq!(
400            std::str::from_utf8(&buf).unwrap(),
401            "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2\n\
402             cargo:rustc-cdylib-link-arg=-sWASM_BIGINT\n"
403        );
404    }
405
406    #[cfg(feature = "resolve-config")]
407    #[test]
408    fn python_framework_link_args() {
409        let mut buf = Vec::new();
410
411        let interpreter_config = InterpreterConfig {
412            implementation: PythonImplementation::CPython,
413            version: PythonVersion {
414                major: 3,
415                minor: 13,
416            },
417            shared: true,
418            abi3: false,
419            lib_name: None,
420            lib_dir: None,
421            executable: None,
422            pointer_width: None,
423            build_flags: BuildFlags::default(),
424            suppress_build_script_link_lines: false,
425            extra_build_script_lines: vec![],
426            python_framework_prefix: Some(
427                "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(),
428            ),
429        };
430        // Does nothing on non-mac
431        _add_python_framework_link_args(
432            &interpreter_config,
433            &Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
434            true,
435            &mut buf,
436        );
437        assert_eq!(buf, Vec::new());
438
439        _add_python_framework_link_args(
440            &interpreter_config,
441            &Triple::from_str("x86_64-apple-darwin").unwrap(),
442            true,
443            &mut buf,
444        );
445        assert_eq!(
446            std::str::from_utf8(&buf).unwrap(),
447            "cargo:rustc-link-arg=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Library/Frameworks\n"
448        );
449    }
450
451    #[test]
452    #[cfg(feature = "resolve-config")]
453    fn test_maximum_version_exceeded_formatting() {
454        let interpreter_config = InterpreterConfig {
455            implementation: PythonImplementation::CPython,
456            version: PythonVersion {
457                major: 3,
458                minor: 13,
459            },
460            shared: true,
461            abi3: false,
462            lib_name: None,
463            lib_dir: None,
464            executable: None,
465            pointer_width: None,
466            build_flags: BuildFlags::default(),
467            suppress_build_script_link_lines: false,
468            extra_build_script_lines: vec![],
469            python_framework_prefix: None,
470        };
471        let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new(
472            &interpreter_config,
473            PythonVersion {
474                major: 3,
475                minor: 12,
476            },
477        );
478        error.add_help("this is a help message");
479        let error = error.finish();
480        let expected = concat!("\
481            the configured Python version (3.13) is newer than PyO3's maximum supported version (3.12)\n\
482            = help: this package is being built with PyO3 version ", env!("CARGO_PKG_VERSION"), "\n\
483            = help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\
484            = help: updating this package to the latest version of PyO3 may provide compatibility with this Python version\n\
485            = help: this is a help message"
486        );
487        assert_eq!(error, expected);
488    }
489}