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