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