Skip to main content

pyo3_build_config/
impl_.rs

1#[cfg(test)]
2use std::cell::RefCell;
3use std::{
4    collections::{HashMap, HashSet},
5    env,
6    ffi::{OsStr, OsString},
7    fmt::Display,
8    fs::{self, DirEntry},
9    io::{BufRead, BufReader, Read, Write},
10    path::{Path, PathBuf},
11    process::{Command, Stdio},
12    str::{self, FromStr},
13};
14
15pub use target_lexicon::Triple;
16
17use target_lexicon::{Architecture, Environment, OperatingSystem, Vendor};
18
19use crate::{
20    bail, ensure,
21    errors::{Context, Error, Result},
22    warn,
23};
24
25/// Minimum Python version PyO3 supports.
26pub(crate) const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 8 };
27
28pub(crate) const MINIMUM_SUPPORTED_VERSION_ABI3T: PythonVersion = PythonVersion {
29    major: 3,
30    minor: 15,
31};
32
33/// GraalPy may implement the same CPython version over multiple releases.
34const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion {
35    major: 25,
36    minor: 0,
37};
38
39/// Maximum Python version that can be used as minimum required Python version with abi3.
40pub(crate) const STABLE_ABI_MAX_MINOR: u8 = 15;
41
42#[cfg(test)]
43thread_local! {
44    static READ_ENV_VARS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
45}
46
47/// Gets an environment variable owned by cargo.
48///
49/// Environment variables set by cargo are expected to be valid UTF8.
50pub fn cargo_env_var(var: &str) -> Option<String> {
51    env::var_os(var).map(|os_string| os_string.to_str().unwrap().into())
52}
53
54/// Gets an external environment variable, and registers the build script to rerun if
55/// the variable changes.
56pub fn env_var(var: &str) -> Option<OsString> {
57    println!("cargo:rerun-if-env-changed={var}");
58    #[cfg(test)]
59    {
60        READ_ENV_VARS.with(|env_vars| {
61            env_vars.borrow_mut().push(var.to_owned());
62        });
63    }
64    env::var_os(var)
65}
66
67/// Gets the compilation target triple from environment variables set by Cargo.
68///
69/// Must be called from a crate build script.
70pub fn target_triple_from_env() -> Triple {
71    env::var("TARGET")
72        .expect("target_triple_from_env() must be called from a build script")
73        .parse()
74        .expect("Unrecognized TARGET environment variable value")
75}
76
77fn sanitize_stable_abi_version(
78    stable_abi_version: Option<PythonVersion>,
79    version: PythonVersion,
80) -> Result<PythonVersion> {
81    if let Some(min_version) = stable_abi_version {
82        ensure!(
83            min_version <= version,
84            "cannot set a minimum Python version {} higher than the interpreter version {} \
85             (the minimum Python version is implied by the abi3-py3{} feature)",
86            min_version,
87            version,
88            min_version.minor
89        );
90        Ok(min_version)
91    } else {
92        Ok(version)
93    }
94}
95
96/// Selects which stable ABI (kind and minimum version) from the `abi3-py3*`
97/// and `abi3t-py3*` features (if any) applies to the given interpreter.
98///
99/// Interpreters which cannot target the requested stable ABI (e.g.
100/// free-threaded CPython before 3.15) get a version-specific build instead.
101/// A bare `abi3`/`abi3t` feature request resolves to the interpreter version.
102fn applicable_stable_abi(
103    implementation: PythonImplementation,
104    version: PythonVersion,
105    gil_disabled: bool,
106    abi3_version: Option<StableAbiVersion>,
107    abi3t_version: Option<StableAbiVersion>,
108) -> Option<(StableAbi, PythonVersion)> {
109    let exact = |requested: StableAbiVersion| match requested {
110        StableAbiVersion::Current => version,
111        StableAbiVersion::Target(target) => target,
112    };
113    let abi3 = abi3_version.map(|v| (StableAbi::Abi3, exact(v)));
114    let abi3t = abi3t_version.map(|v| (StableAbi::Abi3t, exact(v)));
115    let selected = if version >= MINIMUM_SUPPORTED_VERSION_ABI3T {
116        match gil_disabled {
117            false => abi3t.or(abi3),
118            true => abi3t,
119        }
120    } else {
121        match gil_disabled {
122            false => abi3,
123            true => None,
124        }
125    };
126    match implementation {
127        PythonImplementation::PyPy | PythonImplementation::GraalPy => {
128            selected.map(|(kind, _)| (kind, version))
129        }
130        _ => selected,
131    }
132}
133
134/// Like [`applicable_stable_abi`], but reads the `abi3-py3*`/`abi3t-py3*`
135/// cargo features and keeps the interpreter version rather than the feature
136/// minimum, so that `lib_name` matches the real libpython;
137/// `apply_build_env` lowers the ABI to the feature minimum afterwards.
138fn applicable_stable_abi_at_interpreter_version(
139    implementation: PythonImplementation,
140    version: PythonVersion,
141    gil_disabled: bool,
142) -> Option<(StableAbi, PythonVersion)> {
143    applicable_stable_abi(
144        implementation,
145        version,
146        gil_disabled,
147        get_abi3_version(),
148        get_abi3t_version(),
149    )
150    .map(|(kind, _)| (kind, version))
151}
152
153/// Configuration needed by PyO3 to build for the correct Python implementation.
154///
155/// The version and implementation fields correspond to the interpreter
156/// used to host a build. These need not be the same as the implementation and
157/// version fields set for the build target in the `target_abi` field.
158///
159/// Usually this is queried directly from the Python interpreter, or overridden using the
160/// `PYO3_CONFIG_FILE` environment variable.
161///
162/// When the `PYO3_NO_PYTHON` variable is set, or during cross compile situations, then alternative
163/// strategies are used to populate this type.
164#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
165pub struct InterpreterConfig {
166    /// The host Python implementation flavor.
167    ///
168    /// Serialized to `implementation`.
169    #[deprecated(
170        since = "0.29.0",
171        note = "please use `.implementation()` getter or `InterpreterConfigBuilder` instead"
172    )]
173    pub implementation: PythonImplementation,
174
175    /// The host Python `X.Y` version. e.g. `3.9`.
176    ///
177    /// Serialized to `version`.
178    #[deprecated(
179        since = "0.29.0",
180        note = "please use `.version()` getter or `InterpreterConfigBuilder` instead"
181    )]
182    pub version: PythonVersion,
183
184    /// Whether link library is shared.
185    ///
186    /// Serialized to `shared`.
187    #[deprecated(
188        since = "0.29.0",
189        note = "please use `.shared()` getter or `InterpreterConfigBuilder` instead"
190    )]
191    pub shared: bool,
192
193    target_abi: PythonAbi,
194
195    /// Serialized to `abi3`.
196    #[deprecated(since = "0.29.0", note = "please match against target_abi instead")]
197    pub abi3: bool,
198
199    /// The name of the link library defining Python.
200    ///
201    /// This effectively controls the `cargo:rustc-link-lib=<name>` value to
202    /// control how libpython is linked. Values should not contain the `lib`
203    /// prefix.
204    ///
205    /// Serialized to `lib_name`.
206    #[deprecated(
207        since = "0.29.0",
208        note = "please use `.lib_name()` getter or `InterpreterConfigBuilder` instead"
209    )]
210    pub lib_name: Option<String>,
211
212    /// The directory containing the Python library to link against.
213    ///
214    /// The effectively controls the `cargo:rustc-link-search=native=<path>` value
215    /// to add an additional library search path for the linker.
216    ///
217    /// Serialized to `lib_dir`.
218    #[deprecated(
219        since = "0.29.0",
220        note = "please use `.lib_dir()` getter or `InterpreterConfigBuilder` instead"
221    )]
222    pub lib_dir: Option<String>,
223
224    /// Path of host `python` executable.
225    ///
226    /// This is a valid executable capable of running on the host/building machine.
227    /// For configurations derived by invoking a Python interpreter, it was the
228    /// executable invoked.
229    ///
230    /// Serialized to `executable`.
231    #[deprecated(
232        since = "0.29.0",
233        note = "please use `.executable()` getter or `InterpreterConfigBuilder` instead"
234    )]
235    pub executable: Option<String>,
236
237    /// Width in bits of pointers on the target machine.
238    ///
239    /// Serialized to `pointer_width`.
240    #[deprecated(
241        since = "0.29.0",
242        note = "please use `.pointer_width()` getter or `InterpreterConfigBuilder` instead"
243    )]
244    pub pointer_width: Option<u32>,
245
246    /// Additional relevant Python build flags / configuration settings.
247    ///
248    /// Serialized to `build_flags`.
249    #[deprecated(
250        since = "0.29.0",
251        note = "please use `.build_flags()` getter or `InterpreterConfigBuilder` instead"
252    )]
253    pub build_flags: BuildFlags,
254
255    /// Whether to suppress emitting of `cargo:rustc-link-*` lines from the build script.
256    ///
257    /// Typically, `pyo3`'s build script will emit `cargo:rustc-link-lib=` and
258    /// `cargo:rustc-link-search=` lines derived from other fields in this struct. In
259    /// advanced building configurations, the default logic to derive these lines may not
260    /// be sufficient. This field can be set to `Some(true)` to suppress the emission
261    /// of these lines.
262    ///
263    /// If suppression is enabled, `extra_build_script_lines` should contain equivalent
264    /// functionality or else a build failure is likely.
265    #[deprecated(
266        since = "0.29.0",
267        note = "please use `.suppress_build_script_link_lines()` getter or `InterpreterConfigBuilder` instead"
268    )]
269    pub suppress_build_script_link_lines: bool,
270
271    /// Additional lines to `println!()` from Cargo build scripts.
272    ///
273    /// This field can be populated to enable the `pyo3` crate to emit additional lines from its
274    /// its Cargo build script.
275    ///
276    /// This crate doesn't populate this field itself. Rather, it is intended to be used with
277    /// externally provided config files to give them significant control over how the crate
278    /// is build/configured.
279    ///
280    /// Serialized to multiple `extra_build_script_line` values.
281    #[deprecated(
282        since = "0.29.0",
283        note = "please use `.extra_build_script_lines()` getter or `InterpreterConfigBuilder` instead"
284    )]
285    pub extra_build_script_lines: Vec<String>,
286    /// macOS Python3.framework requires special rpath handling
287    #[deprecated(
288        since = "0.29.0",
289        note = "please use `.python_framework_prefix()` getter or `InterpreterConfigBuilder` instead"
290    )]
291    pub python_framework_prefix: Option<String>,
292}
293
294// Should no longer be deprecated once the internal fields are private
295#[expect(deprecated, reason = "this impl block touches the internal fields")]
296impl InterpreterConfig {
297    /// The Python implementation flavor.
298    ///
299    /// Serialized to `implementation`.
300    pub fn implementation(&self) -> PythonImplementation {
301        self.implementation
302    }
303
304    /// Python `X.Y` version. e.g. `3.9`.
305    ///
306    /// Serialized to `version`.
307    pub fn version(&self) -> PythonVersion {
308        self.version
309    }
310
311    /// Whether link library is shared.
312    ///
313    /// Serialized to `shared`.
314    pub fn shared(&self) -> bool {
315        self.shared
316    }
317
318    /// The ABI to use for the compilation target.
319    /// See the documentation for the PythonAbi enum for more details.
320    ///
321    /// Serialized to `target_abi`.
322    pub fn target_abi(&self) -> PythonAbi {
323        self.target_abi
324    }
325
326    /// Whether linking against the stable/limited Python 3 API.
327    ///
328    #[deprecated(since = "0.29.0", note = "please use `target_abi()` instead")]
329    pub fn abi3(&self) -> bool {
330        matches!(self.target_abi.kind, PythonAbiKind::Stable(StableAbi::Abi3))
331    }
332
333    /// The name of the link library defining Python.
334    ///
335    /// This effectively controls the `cargo:rustc-link-lib=<name>` value to
336    /// control how libpython is linked. Values should not contain the `lib`
337    /// prefix.
338    ///
339    /// Serialized to `lib_name`.
340    pub fn lib_name(&self) -> Option<&str> {
341        self.lib_name.as_deref()
342    }
343
344    /// The directory containing the Python library to link against.
345    ///
346    /// The effectively controls the `cargo:rustc-link-search=native=<path>` value
347    /// to add an additional library search path for the linker.
348    ///
349    /// Serialized to `lib_dir`.
350    pub fn lib_dir(&self) -> Option<&str> {
351        self.lib_dir.as_deref()
352    }
353
354    /// Path of host `python` executable.
355    ///
356    /// This is a valid executable capable of running on the host/building machine.
357    /// For configurations derived by invoking a Python interpreter, it was the
358    /// executable invoked.
359    ///
360    /// Serialized to `executable`.
361    pub fn executable(&self) -> Option<&str> {
362        self.executable.as_deref()
363    }
364
365    /// Width in bits of pointers on the target machine.
366    ///
367    /// Serialized to `pointer_width`.
368    pub fn pointer_width(&self) -> Option<u32> {
369        self.pointer_width
370    }
371
372    /// Additional relevant Python build flags / configuration settings.
373    ///
374    /// Serialized to `build_flags`.
375    pub fn build_flags(&self) -> &BuildFlags {
376        &self.build_flags
377    }
378
379    /// Whether to suppress emitting of `cargo:rustc-link-*` lines from the build script.
380    pub fn suppress_build_script_link_lines(&self) -> bool {
381        self.suppress_build_script_link_lines
382    }
383
384    /// Additional lines to `println!()` from Cargo build scripts.
385    ///
386    /// Serialized to multiple `extra_build_script_line` values.
387    pub fn extra_build_script_lines(&self) -> &[String] {
388        &self.extra_build_script_lines
389    }
390
391    /// macOS Python3.framework prefix used for special rpath handling.
392    pub fn python_framework_prefix(&self) -> Option<&str> {
393        self.python_framework_prefix.as_deref()
394    }
395
396    #[doc(hidden)]
397    pub fn build_script_outputs(&self) -> Vec<String> {
398        // This should have been checked during pyo3-build-config build time.
399        assert!(self.target_abi.version() >= MINIMUM_SUPPORTED_VERSION);
400
401        let mut out = vec![];
402
403        for i in MINIMUM_SUPPORTED_VERSION.minor..=self.target_abi.version().minor {
404            out.push(format!("cargo:rustc-cfg=Py_3_{i}"));
405        }
406
407        match self.target_abi.implementation() {
408            PythonImplementation::CPython => {}
409            PythonImplementation::PyPy => out.push("cargo:rustc-cfg=PyPy".to_owned()),
410            PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()),
411            PythonImplementation::RustPython => out.push("cargo:rustc-cfg=RustPython".to_owned()),
412        }
413
414        match self.target_abi.kind() {
415            PythonAbiKind::Stable(kind) => {
416                out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned());
417                if kind == StableAbi::Abi3t {
418                    out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned());
419                }
420            }
421            PythonAbiKind::VersionSpecific(kind) => match kind {
422                GilUsed::FreeThreaded => {
423                    out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned());
424                }
425                GilUsed::GilEnabled => {}
426            },
427        }
428        for flag in &self.build_flags.0 {
429            match flag {
430                // already handled by target ABI logic above
431                BuildFlag::Py_GIL_DISABLED => continue,
432                flag => out.push(format!("cargo:rustc-cfg=py_sys_config=\"{flag}\"")),
433            }
434        }
435        out
436    }
437
438    fn from_interpreter(
439        interpreter: impl AsRef<Path>,
440        abi3_version: Option<StableAbiVersion>,
441        abi3t_version: Option<StableAbiVersion>,
442    ) -> Result<Self> {
443        const SCRIPT: &str = r#"
444# Allow the script to run on Python 2, so that nicer error can be printed later.
445from __future__ import print_function
446
447import os.path
448import platform
449import struct
450import sys
451from sysconfig import get_config_var, get_platform
452
453PYPY = platform.python_implementation() == "PyPy"
454GRAALPY = platform.python_implementation() == "GraalVM"
455
456if GRAALPY:
457    graalpy_ver = map(int, __graalpython__.get_graalvm_version().split('.'));
458    print("graalpy_major", next(graalpy_ver))
459    print("graalpy_minor", next(graalpy_ver))
460
461# sys.base_prefix is missing on Python versions older than 3.3; this allows the script to continue
462# so that the version mismatch can be reported in a nicer way later.
463base_prefix = getattr(sys, "base_prefix", None)
464
465if base_prefix:
466    # Anaconda based python distributions have a static python executable, but include
467    # the shared library. Use the shared library for embedding to avoid rust trying to
468    # LTO the static library (and failing with newer gcc's, because it is old).
469    ANACONDA = os.path.exists(os.path.join(base_prefix, "conda-meta"))
470else:
471    ANACONDA = False
472
473def print_if_set(varname, value):
474    if value is not None:
475        print(varname, value)
476
477# Windows always uses shared linking
478WINDOWS = platform.system() == "Windows"
479
480# macOS framework packages use shared linking
481FRAMEWORK = bool(get_config_var("PYTHONFRAMEWORK"))
482FRAMEWORK_PREFIX = get_config_var("PYTHONFRAMEWORKPREFIX")
483
484# unix-style shared library enabled
485SHARED = bool(get_config_var("Py_ENABLE_SHARED"))
486
487print("implementation", platform.python_implementation())
488print("version_major", sys.version_info[0])
489print("version_minor", sys.version_info[1])
490print("shared", PYPY or GRAALPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED)
491print("python_framework_prefix", FRAMEWORK_PREFIX)
492print_if_set("ld_version", get_config_var("LDVERSION"))
493print_if_set("libdir", get_config_var("LIBDIR"))
494print_if_set("base_prefix", base_prefix)
495print("executable", sys.executable)
496print("calcsize_pointer", struct.calcsize("P"))
497print("mingw", get_platform().startswith("mingw"))
498print("cygwin", get_platform().startswith("cygwin"))
499print("ext_suffix", get_config_var("EXT_SUFFIX"))
500print("gil_disabled", get_config_var("Py_GIL_DISABLED"))
501"#;
502        let output = run_python_script(interpreter.as_ref(), SCRIPT)?;
503        let map: HashMap<String, String> = parse_script_output(&output);
504
505        ensure!(
506            !map.is_empty(),
507            "broken Python interpreter: {}",
508            interpreter.as_ref().display()
509        );
510
511        if let Some(value) = map.get("graalpy_major") {
512            let graalpy_version = PythonVersion {
513                major: value
514                    .parse()
515                    .context("failed to parse GraalPy major version")?,
516                minor: map["graalpy_minor"]
517                    .parse()
518                    .context("failed to parse GraalPy minor version")?,
519            };
520            ensure!(
521                graalpy_version >= MINIMUM_SUPPORTED_VERSION_GRAALPY,
522                "At least GraalPy version {} needed, got {}",
523                MINIMUM_SUPPORTED_VERSION_GRAALPY,
524                graalpy_version
525            );
526        };
527
528        let shared = map["shared"].as_str() == "True";
529        let python_framework_prefix = map.get("python_framework_prefix").cloned();
530
531        let version = PythonVersion {
532            major: map["version_major"]
533                .parse()
534                .context("failed to parse major version")?,
535            minor: map["version_minor"]
536                .parse()
537                .context("failed to parse minor version")?,
538        };
539
540        let implementation = map["implementation"].parse()?;
541
542        let gil_disabled = match map["gil_disabled"].as_str() {
543            "1" => true,
544            "0" => false,
545            "None" => false,
546            _ => panic!("Unknown Py_GIL_DISABLED value"),
547        };
548
549        let stable_abi = applicable_stable_abi(
550            implementation,
551            version,
552            gil_disabled,
553            abi3_version,
554            abi3t_version,
555        );
556
557        let target_abi =
558            PythonAbi::from_stable_abi(implementation, version, stable_abi, gil_disabled)?;
559
560        let cygwin = map["cygwin"].as_str() == "True";
561
562        let lib_name = if cfg!(windows) {
563            default_lib_name_windows(
564                target_abi,
565                map["mingw"].as_str() == "True",
566                // This is the best heuristic currently available to detect debug build
567                // on Windows from sysconfig - e.g. ext_suffix may be
568                // `_d.cp312-win_amd64.pyd` for 3.12 debug build
569                map["ext_suffix"].starts_with("_d."),
570            )?
571        } else {
572            default_lib_name_unix(
573                target_abi,
574                cygwin,
575                map.get("ld_version").map(String::as_str),
576            )?
577        };
578
579        let lib_dir = if cfg!(windows) {
580            map.get("base_prefix")
581                .map(|base_prefix| format!("{base_prefix}\\libs"))
582        } else {
583            map.get("libdir").cloned()
584        };
585
586        // The reason we don't use platform.architecture() here is that it's not
587        // reliable on macOS. See https://stackoverflow.com/a/1405971/823869.
588        // Similarly, sys.maxsize is not reliable on Windows. See
589        // https://stackoverflow.com/questions/1405913/how-do-i-determine-if-my-python-shell-is-executing-in-32bit-or-64bit-mode-on-os/1405971#comment6209952_1405971
590        // and https://stackoverflow.com/a/3411134/823869.
591        let calcsize_pointer: u32 = map["calcsize_pointer"]
592            .parse()
593            .context("failed to parse calcsize_pointer")?;
594
595        InterpreterConfigBuilder::new(implementation, version)
596            .target_abi(target_abi)
597            .shared(shared)
598            .lib_name(lib_name)
599            .lib_dir(lib_dir)
600            .executable(map["executable"].clone())
601            .pointer_width(calcsize_pointer * 8)
602            .build_flags(BuildFlags::from_interpreter(interpreter)?)
603            .python_framework_prefix(python_framework_prefix)
604            .finalize()
605    }
606
607    /// Generate from parsed sysconfigdata file
608    ///
609    /// Use [`parse_sysconfigdata`] to generate a hash map of configuration values which may be
610    /// used to build an [`InterpreterConfig`].
611    pub fn from_sysconfigdata(sysconfigdata: &Sysconfigdata) -> Result<Self> {
612        macro_rules! get_key {
613            ($sysconfigdata:expr, $key:literal) => {
614                $sysconfigdata
615                    .get_value($key)
616                    .ok_or(concat!($key, " not found in sysconfigdata file"))
617            };
618        }
619
620        macro_rules! parse_key {
621            ($sysconfigdata:expr, $key:literal) => {
622                get_key!($sysconfigdata, $key)?
623                    .parse()
624                    .context(concat!("could not parse value of ", $key))
625            };
626        }
627
628        let soabi = get_key!(sysconfigdata, "SOABI")?;
629        let implementation = PythonImplementation::from_soabi(soabi)?;
630        let version = parse_key!(sysconfigdata, "VERSION")?;
631        let shared = match sysconfigdata.get_value("Py_ENABLE_SHARED") {
632            Some("1") | Some("true") | Some("True") => true,
633            Some("0") | Some("false") | Some("False") => false,
634            _ => bail!("expected a bool (1/true/True or 0/false/False) for Py_ENABLE_SHARED"),
635        };
636        // macOS framework packages use shared linking (PYTHONFRAMEWORK is the framework name, hence the empty check)
637        let framework = match sysconfigdata.get_value("PYTHONFRAMEWORK") {
638            Some(s) => !s.is_empty(),
639            _ => false,
640        };
641        let python_framework_prefix = sysconfigdata
642            .get_value("PYTHONFRAMEWORKPREFIX")
643            .map(str::to_string);
644        let lib_dir = get_key!(sysconfigdata, "LIBDIR").ok().map(str::to_string);
645        let gil_disabled = match sysconfigdata.get_value("Py_GIL_DISABLED") {
646            Some(value) => value == "1",
647            None => false,
648        };
649        let cygwin = soabi.ends_with("cygwin");
650        let stable_abi =
651            applicable_stable_abi_at_interpreter_version(implementation, version, gil_disabled);
652        let target_abi =
653            PythonAbi::from_stable_abi(implementation, version, stable_abi, gil_disabled)?;
654        let lib_name =
655            default_lib_name_unix(target_abi, cygwin, sysconfigdata.get_value("LDVERSION"))?;
656        let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P")
657            .map(|bytes_width: u32| bytes_width * 8)
658            .ok();
659        let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata);
660
661        InterpreterConfigBuilder::new(implementation, version)
662            .target_abi(target_abi)
663            .shared(shared || framework)
664            .pointer_width(pointer_width)
665            .lib_name(lib_name)
666            .lib_dir(lib_dir)
667            .python_framework_prefix(python_framework_prefix)
668            .build_flags(build_flags)
669            .finalize()
670    }
671
672    /// Import an externally-provided config file.
673    ///
674    /// The `abi3` features, if set, may apply an `abi3` constraint to the Python version.
675    pub(super) fn from_pyo3_config_file_env(target: &Triple) -> Option<Result<Self>> {
676        env_var("PYO3_CONFIG_FILE").map(|path| {
677            let path = Path::new(&path);
678            println!("cargo:rerun-if-changed={}", path.display());
679            // Absolute path is necessary because this build script is run with a cwd different to the
680            // original `cargo build` instruction.
681            ensure!(
682                path.is_absolute(),
683                "PYO3_CONFIG_FILE must be an absolute path"
684            );
685
686            let mut config = InterpreterConfig::from_path(path)
687                .context("failed to parse contents of PYO3_CONFIG_FILE")?
688                .apply_build_env()?;
689
690            // For config files which don't apply a lib name, apply a default which we can use
691            // for linking.
692            if config.lib_name.is_none() {
693                config.lib_name = Some(default_lib_name_for_target(config.target_abi, target));
694            }
695
696            Ok(config)
697        })
698    }
699
700    fn from_path(path: impl AsRef<Path>) -> Result<Self> {
701        let path = path.as_ref();
702        let config_file = std::fs::File::open(path)
703            .with_context(|| format!("failed to open PyO3 config file at {}", path.display()))?;
704        let reader = std::io::BufReader::new(config_file);
705        InterpreterConfig::from_reader(reader)
706    }
707
708    /// Environment variable populated via pyo3-ffi's build script
709    pub(crate) const PYO3_FFI_CONFIG_ENV_VAR: &str = "DEP_PYTHON_PYO3_CONFIG";
710    /// Environment variable populated via pyo3's build script by forwarding the value from pyo3-ffi
711    pub(crate) const PYO3_CONFIG_ENV_VAR: &str = "DEP_PYO3_PYTHON_PYO3_CONFIG";
712
713    pub(crate) fn from_cargo_dep_env() -> Option<Result<Self>> {
714        cargo_env_var(Self::PYO3_FFI_CONFIG_ENV_VAR)
715            .or_else(|| cargo_env_var(Self::PYO3_CONFIG_ENV_VAR))
716            .map(|buf| InterpreterConfig::from_reader(&*unescape(&buf)))
717    }
718
719    fn from_reader(reader: impl Read) -> Result<Self> {
720        let reader = BufReader::new(reader);
721        let lines = reader.lines();
722
723        macro_rules! parse_value {
724            ($variable:ident, $value:ident) => {
725                $variable = Some($value.trim().parse().context(format!(
726                    concat!(
727                        "failed to parse ",
728                        stringify!($variable),
729                        " from config value '{}'"
730                    ),
731                    $value
732                ))?)
733            };
734        }
735
736        let mut implementation = None;
737        let mut version = None;
738        let mut shared = None;
739        let mut target_abi = None;
740        // deprecated in the struct but we still allow it to support old config files
741        let mut abi3 = None;
742        let mut lib_name = None;
743        let mut lib_dir = None;
744        let mut executable = None;
745        let mut pointer_width = None;
746        let mut build_flags: Option<BuildFlags> = None;
747        let mut suppress_build_script_link_lines: Option<bool> = None;
748        let mut extra_build_script_lines = vec![];
749        let mut python_framework_prefix = None;
750
751        for (i, line) in lines.enumerate() {
752            let line = line.context("failed to read line from config")?;
753            let mut split = line.splitn(2, '=');
754            let (key, value) = (
755                split
756                    .next()
757                    .expect("first splitn value should always be present"),
758                split
759                    .next()
760                    .ok_or_else(|| format!("expected key=value pair on line {}", i + 1))?,
761            );
762            match key {
763                "implementation" => parse_value!(implementation, value),
764                "version" => parse_value!(version, value),
765                "shared" => parse_value!(shared, value),
766                "target_abi" => parse_value!(target_abi, value),
767                "abi3" => parse_value!(abi3, value),
768                "lib_name" => parse_value!(lib_name, value),
769                "lib_dir" => parse_value!(lib_dir, value),
770                "executable" => parse_value!(executable, value),
771                "pointer_width" => parse_value!(pointer_width, value),
772                "build_flags" => parse_value!(build_flags, value),
773                "suppress_build_script_link_lines" => {
774                    parse_value!(suppress_build_script_link_lines, value)
775                }
776                "extra_build_script_line" => {
777                    extra_build_script_lines.push(value.to_string());
778                }
779                "python_framework_prefix" => parse_value!(python_framework_prefix, value),
780                unknown => warn!("unknown config key `{}`", unknown),
781            }
782        }
783
784        let version = version.ok_or("missing value for version")?;
785        let implementation = implementation.unwrap_or(PythonImplementation::CPython);
786        let flags_contains_free_threaded = if let Some(ref flags) = build_flags {
787            flags.0.contains(&BuildFlag::Py_GIL_DISABLED)
788        } else {
789            false
790        };
791        let target_abi = if let Some(target_abi) = target_abi {
792            ensure!(
793                abi3.is_none(),
794                "Invalid config that sets both target_abi and abi3."
795            );
796            target_abi
797        } else if flags_contains_free_threaded {
798            // This fires even if is_abi3() is True for backward compatibility reasons
799            PythonAbiBuilder::new(implementation, version)
800                .free_threaded()
801                .finalize()?
802        } else if abi3 == Some(true) {
803            warn!("abi3 configuration file option is deprecated since pyo3 0.29, set target_abi instead");
804            PythonAbiBuilder::new(implementation, version)
805                .stable_abi(StableAbi::Abi3)
806                .finalize()?
807        } else {
808            PythonAbiBuilder::new(implementation, version).finalize()?
809        };
810
811        let builder = InterpreterConfigBuilder::new(implementation, version)
812            .target_abi(target_abi)
813            .shared(shared.unwrap_or(true))
814            .lib_name(lib_name)
815            .lib_dir(lib_dir)
816            .executable(executable)
817            .pointer_width(pointer_width)
818            .build_flags(build_flags.unwrap_or_default())
819            .suppress_build_script_link_lines(suppress_build_script_link_lines.unwrap_or(false))
820            .extra_build_script_lines(extra_build_script_lines)
821            .python_framework_prefix(python_framework_prefix);
822
823        builder.finalize()
824    }
825
826    #[doc(hidden)]
827    /// Serialize the `InterpreterConfig` and print it to the environment for Cargo to pass along
828    /// to dependent packages during build time.
829    ///
830    /// NB: writing to the cargo environment requires the
831    /// [`links`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key)
832    /// manifest key to be set. In this case that means this is called by the `pyo3-ffi` crate and
833    /// available for dependent package build scripts in `DEP_PYTHON_PYO3_CONFIG`. See
834    /// documentation for the
835    /// [`DEP_<name>_<key>`](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts)
836    /// environment variable.
837    pub fn to_cargo_dep_env(&self) -> Result<()> {
838        let mut buf = Vec::new();
839        self.to_writer(&mut buf)?;
840        // escape newlines in env var
841        println!("cargo:PYO3_CONFIG={}", escape(&buf));
842        Ok(())
843    }
844
845    #[doc(hidden)]
846    pub fn to_writer(&self, mut writer: impl Write) -> Result<()> {
847        macro_rules! write_line {
848            ($value:ident) => {
849                writeln!(writer, "{}={}", stringify!($value), self.$value).context(concat!(
850                    "failed to write ",
851                    stringify!($value),
852                    " to config"
853                ))
854            };
855        }
856
857        macro_rules! write_option_line {
858            ($value:ident) => {
859                if let Some(value) = &self.$value {
860                    writeln!(writer, "{}={}", stringify!($value), value).context(concat!(
861                        "failed to write ",
862                        stringify!($value),
863                        " to config"
864                    ))
865                } else {
866                    Ok(())
867                }
868            };
869        }
870
871        write_line!(implementation)?;
872        write_line!(version)?;
873        write_line!(shared)?;
874        write_line!(target_abi)?;
875        write_option_line!(lib_name)?;
876        write_option_line!(lib_dir)?;
877        write_option_line!(executable)?;
878        write_option_line!(pointer_width)?;
879        write_line!(build_flags)?;
880        write_option_line!(python_framework_prefix)?;
881        write_line!(suppress_build_script_link_lines)?;
882        for line in &self.extra_build_script_lines {
883            writeln!(writer, "extra_build_script_line={line}")
884                .context("failed to write extra_build_script_line")?;
885        }
886        Ok(())
887    }
888
889    /// Run a python script using the [`InterpreterConfig::executable`].
890    ///
891    /// # Panics
892    ///
893    /// This function will panic if the [`executable`](InterpreterConfig::executable) is `None`.
894    pub fn run_python_script(&self, script: &str) -> Result<String> {
895        run_python_script_with_envs(
896            Path::new(self.executable.as_ref().expect("no interpreter executable")),
897            script,
898            std::iter::empty::<(&str, &str)>(),
899        )
900    }
901
902    /// Run a python script using the [`InterpreterConfig::executable`] with additional
903    /// environment variables (e.g. PYTHONPATH) set.
904    ///
905    /// # Panics
906    ///
907    /// This function will panic if the [`executable`](InterpreterConfig::executable) is `None`.
908    pub fn run_python_script_with_envs<I, K, V>(&self, script: &str, envs: I) -> Result<String>
909    where
910        I: IntoIterator<Item = (K, V)>,
911        K: AsRef<OsStr>,
912        V: AsRef<OsStr>,
913    {
914        run_python_script_with_envs(
915            Path::new(self.executable.as_ref().expect("no interpreter executable")),
916            script,
917            envs,
918        )
919    }
920
921    pub fn is_free_threaded(&self) -> bool {
922        self.target_abi.kind().is_free_threaded()
923    }
924
925    fn apply_build_env(mut self) -> Result<InterpreterConfig> {
926        // the host `implementation` may differ from the `target_abi`
927        // implementation; the recomputed ABI must stay on the target
928        let implementation = self.target_abi.implementation;
929        let gil_disabled = self.target_abi.kind().is_free_threaded();
930        let stable_abi = applicable_stable_abi(
931            implementation,
932            self.version,
933            gil_disabled,
934            get_abi3_version(),
935            get_abi3t_version(),
936        );
937        self.target_abi =
938            PythonAbi::from_stable_abi(implementation, self.version, stable_abi, gil_disabled)?;
939        Ok(self)
940    }
941}
942
943#[cfg_attr(test, derive(Debug))]
944pub struct PythonAbiBuilder {
945    implementation: PythonImplementation,
946    version: PythonVersion,
947    kind: Option<PythonAbiKind>,
948}
949
950impl PythonAbiBuilder {
951    pub fn new(implementation: PythonImplementation, version: PythonVersion) -> PythonAbiBuilder {
952        PythonAbiBuilder {
953            implementation,
954            version,
955            kind: None,
956        }
957    }
958
959    pub fn stable_abi(self, kind: StableAbi) -> PythonAbiBuilder {
960        let mut build_version = self.version;
961        if self.version.minor > STABLE_ABI_MAX_MINOR {
962            warn!("Automatically falling back to {kind}-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported");
963            build_version.minor = STABLE_ABI_MAX_MINOR;
964        }
965
966        PythonAbiBuilder {
967            kind: Some(PythonAbiKind::Stable(kind)),
968            version: build_version,
969            ..self
970        }
971    }
972
973    pub fn free_threaded(self) -> PythonAbiBuilder {
974        PythonAbiBuilder {
975            kind: Some(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)),
976            ..self
977        }
978    }
979
980    pub fn finalize(self) -> Result<PythonAbi> {
981        // default to GIL-enabled version-specific ABI
982        let kind = self.kind.unwrap_or(match self.implementation {
983            PythonImplementation::RustPython => PythonAbiKind::Stable(StableAbi::Abi3t),
984            _ => PythonAbiKind::VersionSpecific(GilUsed::GilEnabled),
985        });
986        if matches!(self.implementation, PythonImplementation::RustPython) {
987            ensure!(matches!(kind, PythonAbiKind::Stable(StableAbi::Abi3t)),
988                    "RustPython only supports targeting abi3t, it does not allow targeting other Python ABIs. Currently targeting '{kind}'")
989        }
990        if matches!(kind, PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded))
991            && self.version
992                < (PythonVersion {
993                    major: 3,
994                    minor: 13,
995                })
996        {
997            bail!(
998                "Cannot target free-threaded builds for Python versions before 3.13, tried to build for {}", self.version
999            )
1000        }
1001        Ok(PythonAbi {
1002            implementation: self.implementation,
1003            kind,
1004            version: self.version,
1005        })
1006    }
1007}
1008
1009#[non_exhaustive]
1010#[derive(Copy, Clone, PartialEq, Eq)]
1011#[cfg_attr(test, derive(Debug))]
1012pub struct PythonAbi {
1013    implementation: PythonImplementation,
1014    kind: PythonAbiKind,
1015    version: PythonVersion,
1016}
1017
1018impl Display for PythonAbi {
1019    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1020        write!(f, "{}-{}-{}", self.implementation, self.kind, self.version)
1021    }
1022}
1023
1024impl FromStr for PythonAbi {
1025    type Err = crate::errors::Error;
1026
1027    fn from_str(value: &str) -> Result<Self, Self::Err> {
1028        let mut parts = value.splitn(3, '-');
1029        Ok(PythonAbi {
1030            implementation: parts
1031                .next()
1032                .ok_or_else(|| format!("Invalid ABI string representation: {value}"))?
1033                .parse()?,
1034            kind: parts
1035                .next()
1036                .ok_or_else(|| format!("Invalid ABI string representation: {value}"))?
1037                .parse()?,
1038            version: parts
1039                .next()
1040                .ok_or_else(|| format!("Invalid ABI string representation: {value}"))?
1041                .parse()?,
1042        })
1043    }
1044}
1045
1046impl PythonAbi {
1047    /// Constructs the ABI to target for an interpreter of `version`, given the
1048    /// stable ABI kind and minimum Python version to target, if any.
1049    ///
1050    /// Callers decide whether a stable ABI applies to the interpreter; this
1051    /// does not consult the `abi3`/`abi3t` cargo features. The minimum version
1052    /// must not exceed the interpreter version. Without a stable ABI the
1053    /// result is version-specific, free-threaded when `gil_disabled` is set.
1054    fn from_stable_abi(
1055        implementation: PythonImplementation,
1056        version: PythonVersion,
1057        stable_abi: Option<(StableAbi, PythonVersion)>,
1058        gil_disabled: bool,
1059    ) -> Result<PythonAbi> {
1060        let builder = match stable_abi {
1061            Some((kind, min_version)) => {
1062                ensure!(
1063                    min_version <= version,
1064                    "cannot set a minimum Python version {} higher than the interpreter version {} \
1065                     (the minimum Python version is implied by the {}-py3{} feature)",
1066                    min_version,
1067                    version,
1068                    kind,
1069                    min_version.minor
1070                );
1071                PythonAbiBuilder::new(implementation, min_version).stable_abi(kind)
1072            }
1073            None if gil_disabled => PythonAbiBuilder::new(implementation, version).free_threaded(),
1074            None => PythonAbiBuilder::new(implementation, version),
1075        };
1076        builder.finalize()
1077    }
1078
1079    pub fn from_build_env(
1080        implementation: PythonImplementation,
1081        version: PythonVersion,
1082        stable_abi_version: Option<PythonVersion>,
1083        gil_disabled: bool,
1084    ) -> Result<PythonAbi> {
1085        let builder = PythonAbiBuilder {
1086            implementation,
1087            version: sanitize_stable_abi_version(stable_abi_version, version)?,
1088            kind: None,
1089        };
1090        let builder = if get_abi3t_version().is_some() && version >= MINIMUM_SUPPORTED_VERSION_ABI3T
1091        {
1092            builder.stable_abi(StableAbi::Abi3t)
1093        } else if get_abi3_version().is_some() && !gil_disabled {
1094            builder.stable_abi(StableAbi::Abi3)
1095        } else if gil_disabled {
1096            builder.free_threaded()
1097        } else {
1098            builder
1099        };
1100        builder.finalize()
1101    }
1102
1103    /// The Python implementation flavor.
1104    ///
1105    /// Serialized to `implementation`.
1106    pub fn implementation(&self) -> PythonImplementation {
1107        self.implementation
1108    }
1109
1110    /// The ABI flavor
1111    ///
1112    /// Serialized to `kind`
1113    pub fn kind(&self) -> PythonAbiKind {
1114        self.kind
1115    }
1116
1117    /// Python `X.Y` version. e.g. `3.9`.
1118    ///
1119    /// Serialized to `version`.
1120    pub fn version(&self) -> PythonVersion {
1121        self.version
1122    }
1123}
1124
1125/// The "kind" of ABI.
1126///
1127/// Either a variety of stable ABI or a GIL-enabled or free-threaded
1128/// version-specific ABI.
1129#[derive(Clone, Copy, PartialEq, Eq)]
1130#[cfg_attr(test, derive(Debug))]
1131pub enum PythonAbiKind {
1132    /// One of the stable ABIs, which supports multiple Python versions
1133    Stable(StableAbi),
1134    /// Version specific ABI, which is different on the free-threaded build
1135    VersionSpecific(GilUsed),
1136}
1137
1138impl Display for PythonAbiKind {
1139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1140        match self {
1141            PythonAbiKind::Stable(stable_abi) => write!(f, "{stable_abi}"),
1142            PythonAbiKind::VersionSpecific(gil_used) => {
1143                write!(f, "{gil_used}")
1144            }
1145        }
1146    }
1147}
1148
1149impl FromStr for PythonAbiKind {
1150    type Err = crate::errors::Error;
1151
1152    fn from_str(value: &str) -> Result<Self, Self::Err> {
1153        match value {
1154            "abi3" => Ok(PythonAbiKind::Stable(StableAbi::Abi3)),
1155            "abi3t" => Ok(PythonAbiKind::Stable(StableAbi::Abi3t)),
1156            "free_threaded" => Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)),
1157            "gil_enabled" => Ok(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)),
1158            _ => Err(format!("Unrecognized ABI name: {value}").into()),
1159        }
1160    }
1161}
1162
1163impl PythonAbiKind {
1164    pub fn is_free_threaded(self) -> bool {
1165        match self {
1166            PythonAbiKind::VersionSpecific(gil_disabled) => gil_disabled == GilUsed::FreeThreaded,
1167            PythonAbiKind::Stable(StableAbi::Abi3) => false,
1168            PythonAbiKind::Stable(StableAbi::Abi3t) => true,
1169        }
1170    }
1171}
1172
1173/// The variety of stable ABI
1174#[derive(Clone, Copy, PartialEq, Eq)]
1175#[cfg_attr(test, derive(Debug))]
1176pub enum StableAbi {
1177    /// The original stable ABI, supporting Python 3.2 and up
1178    Abi3,
1179    /// The free-threaded stable ABI, supporting Python 3.15 and up
1180    Abi3t,
1181}
1182
1183impl Display for StableAbi {
1184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1185        match self {
1186            StableAbi::Abi3 => write!(f, "abi3"),
1187            StableAbi::Abi3t => write!(f, "abi3t"),
1188        }
1189    }
1190}
1191
1192/// Whether the ABI is for the GIL-enabled or free-threaded build.
1193#[derive(Clone, Copy, PartialEq, Eq)]
1194#[cfg_attr(test, derive(Debug))]
1195pub enum GilUsed {
1196    /// The original PyObject layout
1197    GilEnabled,
1198    /// The free-threaded PyObject layout
1199    FreeThreaded,
1200}
1201
1202impl Display for GilUsed {
1203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1204        match self {
1205            GilUsed::GilEnabled => write!(f, "gil_enabled"),
1206            GilUsed::FreeThreaded => write!(f, "free_threaded"),
1207        }
1208    }
1209}
1210
1211#[cfg_attr(test, derive(Debug))]
1212pub struct InterpreterConfigBuilder {
1213    implementation: PythonImplementation,
1214    version: PythonVersion,
1215    shared: bool,
1216    target_abi: Option<PythonAbi>,
1217    lib_name: Option<String>,
1218    lib_dir: Option<String>,
1219    executable: Option<String>,
1220    pointer_width: Option<u32>,
1221    build_flags: BuildFlags,
1222    suppress_build_script_link_lines: bool,
1223    extra_build_script_lines: Vec<String>,
1224    python_framework_prefix: Option<String>,
1225}
1226
1227impl InterpreterConfigBuilder {
1228    pub fn new(
1229        implementation: PythonImplementation,
1230        version: PythonVersion,
1231    ) -> InterpreterConfigBuilder {
1232        InterpreterConfigBuilder {
1233            implementation,
1234            version,
1235            shared: true,
1236            target_abi: None,
1237            lib_name: None,
1238            lib_dir: None,
1239            executable: None,
1240            pointer_width: None,
1241            build_flags: BuildFlags::default(),
1242            suppress_build_script_link_lines: false,
1243            extra_build_script_lines: vec![],
1244            python_framework_prefix: None,
1245        }
1246    }
1247
1248    pub fn target_abi(self, target_abi: PythonAbi) -> InterpreterConfigBuilder {
1249        InterpreterConfigBuilder {
1250            target_abi: Some(target_abi),
1251            ..self
1252        }
1253    }
1254
1255    pub fn stable_abi(self, kind: StableAbi) -> InterpreterConfigBuilder {
1256        let implementation = self.implementation;
1257        let version = self.version;
1258        self.target_abi(
1259            PythonAbiBuilder::new(implementation, version)
1260                .stable_abi(kind)
1261                .finalize()
1262                // Cannot panic
1263                .unwrap(),
1264        )
1265    }
1266
1267    pub fn free_threaded(self) -> Result<InterpreterConfigBuilder> {
1268        let implementation = self.implementation;
1269        let version = self.version;
1270        Ok(self.target_abi(
1271            PythonAbiBuilder::new(implementation, version)
1272                .free_threaded()
1273                .finalize()?,
1274        ))
1275    }
1276
1277    pub fn lib_name(mut self, lib_name: impl Into<Option<String>>) -> InterpreterConfigBuilder {
1278        self.lib_name = lib_name.into();
1279        self
1280    }
1281
1282    pub fn pointer_width(
1283        mut self,
1284        pointer_width: impl Into<Option<u32>>,
1285    ) -> InterpreterConfigBuilder {
1286        self.pointer_width = pointer_width.into();
1287        self
1288    }
1289
1290    pub fn executable(mut self, executable: impl Into<Option<String>>) -> InterpreterConfigBuilder {
1291        self.executable = executable.into();
1292        self
1293    }
1294
1295    pub fn suppress_build_script_link_lines(
1296        mut self,
1297        suppress_build_script_link_lines: bool,
1298    ) -> InterpreterConfigBuilder {
1299        self.suppress_build_script_link_lines = suppress_build_script_link_lines;
1300        self
1301    }
1302
1303    pub fn extra_build_script_lines(
1304        mut self,
1305        extra_build_script_lines: Vec<String>,
1306    ) -> InterpreterConfigBuilder {
1307        self.extra_build_script_lines = extra_build_script_lines;
1308        self
1309    }
1310
1311    pub fn lib_dir(mut self, lib_dir: impl Into<Option<String>>) -> InterpreterConfigBuilder {
1312        self.lib_dir = lib_dir.into();
1313        self
1314    }
1315
1316    pub fn shared(mut self, shared: bool) -> InterpreterConfigBuilder {
1317        self.shared = shared;
1318        self
1319    }
1320
1321    pub fn build_flags(mut self, build_flags: BuildFlags) -> InterpreterConfigBuilder {
1322        self.build_flags = build_flags;
1323        self
1324    }
1325
1326    pub fn python_framework_prefix(
1327        mut self,
1328        python_framework_prefix: impl Into<Option<String>>,
1329    ) -> InterpreterConfigBuilder {
1330        self.python_framework_prefix = python_framework_prefix.into();
1331        self
1332    }
1333
1334    pub fn finalize(self) -> Result<InterpreterConfig> {
1335        let mut build_flags = self.build_flags.clone();
1336        let py_gil_disabled = build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED);
1337        let target_abi = match (self.target_abi, py_gil_disabled) {
1338            // No target ABI set, no Py_GIL_DISABLED: default to GIL-enabled version-specific.
1339            (None, false) => PythonAbiBuilder::new(self.implementation, self.version).finalize()?,
1340            // No target ABI set, Py_GIL_DISABLED in build flags: infer free-threaded.
1341            (None, true) => PythonAbiBuilder::new(self.implementation, self.version)
1342                .free_threaded()
1343                .finalize()?,
1344            // Target ABI set, no Py_GIL_DISABLED: use as-is.
1345            (Some(target_abi), false) => target_abi,
1346            // Target ABI set + Py_GIL_DISABLED: reconcile.
1347            (Some(target_abi), true) => match target_abi.kind() {
1348                // abi3 + Py_GIL_DISABLED: the abi3 feature is a no-op on free-threaded
1349                // interpreters, so for backward compatibility fall back to a free-threaded
1350                // version-specific build.
1351                PythonAbiKind::Stable(StableAbi::Abi3) => {
1352                    let new_abi =
1353                        PythonAbiBuilder::new(target_abi.implementation(), target_abi.version())
1354                            .free_threaded()
1355                            .finalize()?;
1356                    warn!(
1357                        "Targeting an abi3 build but build_flags contains Py_GIL_DISABLED, \
1358                         falling back to a version-specific free-threaded build"
1359                    );
1360                    new_abi
1361                }
1362                // GIL-enabled version-specific + Py_GIL_DISABLED is contradictory.
1363                PythonAbiKind::VersionSpecific(GilUsed::GilEnabled) => bail!(
1364                    "build_flags contains Py_GIL_DISABLED but target_abi \
1365                     '{target_abi}' is not free-threaded"
1366                ),
1367                // Already free-threaded (Stable(Abi3t) or VersionSpecific(FreeThreaded)).
1368                _ => target_abi,
1369            },
1370        };
1371        if target_abi.kind().is_free_threaded() {
1372            build_flags.0.insert(BuildFlag::Py_GIL_DISABLED);
1373        }
1374        #[expect(
1375            deprecated,
1376            reason = "constructing an InterpreterConfig directly, need to write to fields"
1377        )]
1378        Ok(InterpreterConfig {
1379            implementation: self.implementation,
1380            version: self.version,
1381            shared: self.shared,
1382            target_abi,
1383            abi3: matches!(target_abi.kind(), PythonAbiKind::Stable(StableAbi::Abi3)),
1384            lib_name: self.lib_name,
1385            lib_dir: self.lib_dir,
1386            executable: self.executable,
1387            pointer_width: self.pointer_width,
1388            build_flags,
1389            suppress_build_script_link_lines: self.suppress_build_script_link_lines,
1390            extra_build_script_lines: self.extra_build_script_lines,
1391            python_framework_prefix: self.python_framework_prefix,
1392        })
1393    }
1394}
1395
1396#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1397pub struct PythonVersion {
1398    pub major: u8,
1399    pub minor: u8,
1400}
1401
1402impl PythonVersion {
1403    #[cfg(test)]
1404    pub(crate) const PY315: Self = PythonVersion {
1405        major: 3,
1406        minor: 15,
1407    };
1408    #[cfg(test)]
1409    pub(crate) const PY314: Self = PythonVersion {
1410        major: 3,
1411        minor: 14,
1412    };
1413    #[deprecated(
1414        since = "0.29.0",
1415        note = "please construct `PythonVersion` directly rather than use these constants"
1416    )]
1417    pub const PY313: Self = PythonVersion {
1418        major: 3,
1419        minor: 13,
1420    };
1421    #[deprecated(
1422        since = "0.29.0",
1423        note = "please construct `PythonVersion` directly rather than use these constants"
1424    )]
1425    pub const PY312: Self = PythonVersion {
1426        major: 3,
1427        minor: 12,
1428    };
1429    #[cfg(test)]
1430    const PY311: Self = PythonVersion {
1431        major: 3,
1432        minor: 11,
1433    };
1434    const PY310: Self = PythonVersion {
1435        major: 3,
1436        minor: 10,
1437    };
1438    #[cfg(test)]
1439    const PY39: Self = PythonVersion { major: 3, minor: 9 };
1440    #[cfg(test)]
1441    const PY38: Self = PythonVersion { major: 3, minor: 8 };
1442}
1443
1444impl Display for PythonVersion {
1445    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1446        write!(f, "{}.{}", self.major, self.minor)
1447    }
1448}
1449
1450impl FromStr for PythonVersion {
1451    type Err = crate::errors::Error;
1452
1453    fn from_str(value: &str) -> Result<Self, Self::Err> {
1454        let mut split = value.splitn(2, '.');
1455        let (major, minor) = (
1456            split
1457                .next()
1458                .expect("first splitn value should always be present"),
1459            split.next().ok_or("expected major.minor version")?,
1460        );
1461        Ok(Self {
1462            major: major.parse().context("failed to parse major version")?,
1463            minor: minor.parse().context("failed to parse minor version")?,
1464        })
1465    }
1466}
1467
1468#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1469pub enum PythonImplementation {
1470    CPython,
1471    PyPy,
1472    GraalPy,
1473    RustPython,
1474}
1475
1476impl PythonImplementation {
1477    fn is_pypy(self) -> bool {
1478        self == PythonImplementation::PyPy
1479    }
1480
1481    fn from_soabi(soabi: &str) -> Result<Self> {
1482        if soabi.starts_with("pypy") {
1483            Ok(PythonImplementation::PyPy)
1484        } else if soabi.starts_with("cpython") {
1485            Ok(PythonImplementation::CPython)
1486        } else if soabi.starts_with("graalpy") {
1487            Ok(PythonImplementation::GraalPy)
1488        } else {
1489            bail!("unsupported Python interpreter");
1490        }
1491    }
1492}
1493
1494impl Display for PythonImplementation {
1495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1496        match self {
1497            PythonImplementation::CPython => write!(f, "CPython"),
1498            PythonImplementation::PyPy => write!(f, "PyPy"),
1499            PythonImplementation::GraalPy => write!(f, "GraalVM"),
1500            PythonImplementation::RustPython => write!(f, "RustPython"),
1501        }
1502    }
1503}
1504
1505impl FromStr for PythonImplementation {
1506    type Err = Error;
1507    fn from_str(s: &str) -> Result<Self> {
1508        match s {
1509            "CPython" => Ok(PythonImplementation::CPython),
1510            "PyPy" => Ok(PythonImplementation::PyPy),
1511            "GraalVM" => Ok(PythonImplementation::GraalPy),
1512            "RustPython" => Ok(PythonImplementation::RustPython),
1513            _ => bail!("unknown interpreter: {}", s),
1514        }
1515    }
1516}
1517
1518/// Checks if we should look for a Python interpreter installation
1519/// to get the target interpreter configuration.
1520///
1521/// Returns `false` if `PYO3_NO_PYTHON` environment variable is set.
1522fn have_python_interpreter() -> bool {
1523    env_var("PYO3_NO_PYTHON").is_none()
1524}
1525
1526/// The target stable ABI version.
1527///
1528/// For abi3(t)-py* builds a specific version is chosen, otherwise the builds is
1529/// for the stable ABI exposed by the host python interpreter version.
1530#[derive(Debug, Copy, Clone)]
1531pub enum StableAbiVersion {
1532    Current,
1533    Target(PythonVersion),
1534}
1535
1536/// Gets the minimum supported Python version from PyO3 `abi3-py*` features.
1537///
1538/// Must be called from a PyO3 crate build script. Returns None if an `abi3-py*`
1539/// feature is activated that is unsupported or if no `abi3-py3*` feature is
1540/// active.
1541pub fn get_abi3_version() -> Option<StableAbiVersion> {
1542    let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=STABLE_ABI_MAX_MINOR)
1543        .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{i}")).is_some());
1544    minor_version.map_or(
1545        if cargo_env_var("CARGO_FEATURE_ABI3").is_some() {
1546            Some(StableAbiVersion::Current)
1547        } else {
1548            None
1549        },
1550        |minor| Some(StableAbiVersion::Target(PythonVersion { major: 3, minor })),
1551    )
1552}
1553
1554/// Gets the minimum supported Python version from PyO3 `abi3t-py*` features.
1555///
1556/// Must be called from a PyO3 crate build script. Returns None if an `abi3t-py*`
1557/// feature is activated that is unsupported or if no `abi3t-py3*` feature is
1558/// active.
1559pub fn get_abi3t_version() -> Option<StableAbiVersion> {
1560    let minor_version = (MINIMUM_SUPPORTED_VERSION_ABI3T.minor..=STABLE_ABI_MAX_MINOR)
1561        .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3T_PY3{i}")).is_some());
1562    minor_version.map_or(
1563        if cargo_env_var("CARGO_FEATURE_ABI3T").is_some() {
1564            Some(StableAbiVersion::Current)
1565        } else {
1566            None
1567        },
1568        |minor| Some(StableAbiVersion::Target(PythonVersion { major: 3, minor })),
1569    )
1570}
1571
1572/// Checks if the `extension-module` feature is enabled for the PyO3 crate.
1573///
1574/// This can be triggered either by:
1575/// - The `extension-module` Cargo feature (deprecated)
1576/// - Setting the `PYO3_BUILD_EXTENSION_MODULE` environment variable
1577///
1578/// Must be called from a PyO3 crate build script.
1579pub fn is_extension_module() -> bool {
1580    cargo_env_var("CARGO_FEATURE_EXTENSION_MODULE").is_some()
1581        || env_var("PYO3_BUILD_EXTENSION_MODULE").is_some()
1582}
1583
1584/// Checks if we need to link to `libpython` for the target.
1585///
1586/// Must be called from a PyO3 crate build script.
1587pub fn is_linking_libpython_for_target(target: &Triple) -> bool {
1588    target.operating_system == OperatingSystem::Windows
1589        // See https://github.com/PyO3/pyo3/issues/4068#issuecomment-2051159852
1590        || target.operating_system == OperatingSystem::Aix
1591        || target.environment == Environment::Android
1592        || target.environment == Environment::Androideabi
1593        || target.operating_system == OperatingSystem::Cygwin
1594        || matches!(target.operating_system, OperatingSystem::IOS(_))
1595        || !is_extension_module()
1596}
1597
1598/// Checks if we need to discover the Python library directory
1599/// to link the extension module binary.
1600///
1601/// Must be called from a PyO3 crate build script.
1602fn require_libdir_for_target(target: &Triple) -> bool {
1603    // With raw-dylib, Windows targets never need a lib dir — the compiler generates
1604    // import entries directly from `#[link(kind = "raw-dylib")]` attributes.
1605    if target.operating_system == OperatingSystem::Windows {
1606        return false;
1607    }
1608
1609    is_linking_libpython_for_target(target)
1610}
1611
1612/// Configuration needed by PyO3 to cross-compile for a target platform.
1613///
1614/// Usually this is collected from the environment (i.e. `PYO3_CROSS_*` and `CARGO_CFG_TARGET_*`)
1615/// when a cross-compilation configuration is detected.
1616#[derive(Debug, PartialEq, Eq)]
1617pub struct CrossCompileConfig {
1618    /// The directory containing the Python library to link against.
1619    pub lib_dir: Option<PathBuf>,
1620
1621    /// The version of the Python library to link against.
1622    version: Option<PythonVersion>,
1623
1624    /// The target Python implementation hint (CPython, PyPy, GraalPy, ...)
1625    implementation: Option<PythonImplementation>,
1626
1627    /// The compile target triple (e.g. aarch64-unknown-linux-gnu)
1628    target: Triple,
1629
1630    /// Python ABI flags, used to detect free-threaded Python builds.
1631    abiflags: Option<String>,
1632}
1633
1634impl CrossCompileConfig {
1635    /// Creates a new cross compile config struct from PyO3 environment variables
1636    /// and the build environment when cross compilation mode is detected.
1637    ///
1638    /// Returns `None` when not cross compiling.
1639    fn try_from_env_vars_host_target(
1640        env_vars: CrossCompileEnvVars,
1641        host: &Triple,
1642        target: &Triple,
1643    ) -> Result<Option<Self>> {
1644        if env_vars.any() || Self::is_cross_compiling_from_to(host, target) {
1645            let lib_dir = env_vars.lib_dir_path()?;
1646            let (version, abiflags) = env_vars.parse_version()?;
1647            let implementation = env_vars.parse_implementation()?;
1648            let target = target.clone();
1649
1650            Ok(Some(CrossCompileConfig {
1651                lib_dir,
1652                version,
1653                implementation,
1654                target,
1655                abiflags,
1656            }))
1657        } else {
1658            Ok(None)
1659        }
1660    }
1661
1662    /// Checks if compiling on `host` for `target` required "real" cross compilation.
1663    ///
1664    /// Returns `false` if the target Python interpreter can run on the host.
1665    fn is_cross_compiling_from_to(host: &Triple, target: &Triple) -> bool {
1666        // Not cross-compiling if arch-vendor-os is all the same
1667        // e.g. x86_64-unknown-linux-musl on x86_64-unknown-linux-gnu host
1668        //      x86_64-pc-windows-gnu on x86_64-pc-windows-msvc host
1669        let mut compatible = host.architecture == target.architecture
1670            && (host.vendor == target.vendor
1671                // Don't treat `-pc-` to `-win7-` as cross-compiling
1672                || (host.vendor == Vendor::Pc && target.vendor.as_str() == "win7"))
1673            && host.operating_system == target.operating_system;
1674
1675        // Not cross-compiling to compile for 32-bit Python from windows 64-bit
1676        compatible |= target.operating_system == OperatingSystem::Windows
1677            && host.operating_system == OperatingSystem::Windows
1678            && matches!(target.architecture, Architecture::X86_32(_))
1679            && host.architecture == Architecture::X86_64;
1680
1681        // Not cross-compiling to compile for x86-64 Python from macOS arm64 and vice versa
1682        compatible |= matches!(target.operating_system, OperatingSystem::Darwin(_))
1683            && matches!(host.operating_system, OperatingSystem::Darwin(_));
1684
1685        compatible |= matches!(target.operating_system, OperatingSystem::IOS(_));
1686
1687        !compatible
1688    }
1689
1690    /// Converts `lib_dir` member field to an UTF-8 string.
1691    ///
1692    /// The conversion can not fail because `PYO3_CROSS_LIB_DIR` variable
1693    /// is ensured contain a valid UTF-8 string.
1694    fn lib_dir_string(&self) -> Option<String> {
1695        self.lib_dir
1696            .as_ref()
1697            .map(|s| s.to_str().unwrap().to_owned())
1698    }
1699}
1700
1701/// PyO3-specific cross compile environment variable values
1702struct CrossCompileEnvVars {
1703    /// `PYO3_CROSS`
1704    pyo3_cross: Option<OsString>,
1705    /// `PYO3_CROSS_LIB_DIR`
1706    pyo3_cross_lib_dir: Option<OsString>,
1707    /// `PYO3_CROSS_PYTHON_VERSION`
1708    pyo3_cross_python_version: Option<OsString>,
1709    /// `PYO3_CROSS_PYTHON_IMPLEMENTATION`
1710    pyo3_cross_python_implementation: Option<OsString>,
1711}
1712
1713impl CrossCompileEnvVars {
1714    /// Grabs the PyO3 cross-compile variables from the environment.
1715    ///
1716    /// Registers the build script to rerun if any of the variables changes.
1717    fn from_env() -> Self {
1718        CrossCompileEnvVars {
1719            pyo3_cross: env_var("PYO3_CROSS"),
1720            pyo3_cross_lib_dir: env_var("PYO3_CROSS_LIB_DIR"),
1721            pyo3_cross_python_version: env_var("PYO3_CROSS_PYTHON_VERSION"),
1722            pyo3_cross_python_implementation: env_var("PYO3_CROSS_PYTHON_IMPLEMENTATION"),
1723        }
1724    }
1725
1726    /// Checks if any of the variables is set.
1727    fn any(&self) -> bool {
1728        self.pyo3_cross.is_some()
1729            || self.pyo3_cross_lib_dir.is_some()
1730            || self.pyo3_cross_python_version.is_some()
1731            || self.pyo3_cross_python_implementation.is_some()
1732    }
1733
1734    /// Parses `PYO3_CROSS_PYTHON_VERSION` environment variable value
1735    /// into `PythonVersion` and ABI flags.
1736    fn parse_version(&self) -> Result<(Option<PythonVersion>, Option<String>)> {
1737        match self.pyo3_cross_python_version.as_ref() {
1738            Some(os_string) => {
1739                let utf8_str = os_string
1740                    .to_str()
1741                    .ok_or("PYO3_CROSS_PYTHON_VERSION is not valid a UTF-8 string")?;
1742                let (utf8_str, abiflags) = if let Some(version) = utf8_str.strip_suffix('t') {
1743                    (version, Some("t".to_string()))
1744                } else {
1745                    (utf8_str, None)
1746                };
1747                let version = utf8_str
1748                    .parse()
1749                    .context("failed to parse PYO3_CROSS_PYTHON_VERSION")?;
1750                Ok((Some(version), abiflags))
1751            }
1752            None => Ok((None, None)),
1753        }
1754    }
1755
1756    /// Parses `PYO3_CROSS_PYTHON_IMPLEMENTATION` environment variable value
1757    /// into `PythonImplementation`.
1758    fn parse_implementation(&self) -> Result<Option<PythonImplementation>> {
1759        let implementation = self
1760            .pyo3_cross_python_implementation
1761            .as_ref()
1762            .map(|os_string| {
1763                let utf8_str = os_string
1764                    .to_str()
1765                    .ok_or("PYO3_CROSS_PYTHON_IMPLEMENTATION is not valid a UTF-8 string")?;
1766                utf8_str
1767                    .parse()
1768                    .context("failed to parse PYO3_CROSS_PYTHON_IMPLEMENTATION")
1769            })
1770            .transpose()?;
1771
1772        Ok(implementation)
1773    }
1774
1775    /// Converts the stored `PYO3_CROSS_LIB_DIR` variable value (if any)
1776    /// into a `PathBuf` instance.
1777    ///
1778    /// Ensures that the path is a valid UTF-8 string.
1779    fn lib_dir_path(&self) -> Result<Option<PathBuf>> {
1780        let lib_dir = self.pyo3_cross_lib_dir.as_ref().map(PathBuf::from);
1781
1782        if let Some(dir) = lib_dir.as_ref() {
1783            ensure!(
1784                dir.to_str().is_some(),
1785                "PYO3_CROSS_LIB_DIR variable value is not a valid UTF-8 string"
1786            );
1787        }
1788
1789        Ok(lib_dir)
1790    }
1791}
1792
1793/// Detect whether we are cross compiling and return an assembled CrossCompileConfig if so.
1794///
1795/// This function relies on PyO3 cross-compiling environment variables:
1796///
1797/// * `PYO3_CROSS`: If present, forces PyO3 to configure as a cross-compilation.
1798/// * `PYO3_CROSS_LIB_DIR`: If present, must be set to the directory containing
1799///   the target's libpython DSO and the associated `_sysconfigdata*.py` file for
1800///   Unix-like targets, or the Python DLL import libraries for the Windows target.
1801/// * `PYO3_CROSS_PYTHON_VERSION`: Major and minor version (e.g. 3.9) of the target Python
1802///   installation. This variable is only needed if PyO3 cannot determine the version to target
1803///   from `abi3-py3*` features, or if there are multiple versions of Python present in
1804///   `PYO3_CROSS_LIB_DIR`.
1805///
1806/// See the [PyO3 User Guide](https://pyo3.rs/) for more info on cross-compiling.
1807pub fn cross_compiling_from_to(
1808    host: &Triple,
1809    target: &Triple,
1810) -> Result<Option<CrossCompileConfig>> {
1811    let env_vars = CrossCompileEnvVars::from_env();
1812    CrossCompileConfig::try_from_env_vars_host_target(env_vars, host, target)
1813}
1814
1815#[allow(non_camel_case_types)]
1816#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1817pub enum BuildFlag {
1818    Py_DEBUG,
1819    Py_REF_DEBUG,
1820    #[deprecated(since = "0.29.0", note = "no longer supported by PyO3")]
1821    Py_TRACE_REFS,
1822    Py_GIL_DISABLED,
1823    COUNT_ALLOCS,
1824    Other(String),
1825}
1826
1827impl Display for BuildFlag {
1828    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1829        match self {
1830            BuildFlag::Other(flag) => write!(f, "{flag}"),
1831            _ => write!(f, "{self:?}"),
1832        }
1833    }
1834}
1835
1836impl FromStr for BuildFlag {
1837    type Err = std::convert::Infallible;
1838    fn from_str(s: &str) -> Result<Self, Self::Err> {
1839        match s {
1840            "Py_DEBUG" => Ok(BuildFlag::Py_DEBUG),
1841            "Py_REF_DEBUG" => Ok(BuildFlag::Py_REF_DEBUG),
1842            "Py_GIL_DISABLED" => Ok(BuildFlag::Py_GIL_DISABLED),
1843            "COUNT_ALLOCS" => Ok(BuildFlag::COUNT_ALLOCS),
1844            other => Ok(BuildFlag::Other(other.to_owned())),
1845        }
1846    }
1847}
1848
1849/// A list of python interpreter compile-time preprocessor defines.
1850///
1851/// PyO3 will pick these up and pass to rustc via `--cfg=py_sys_config={varname}`;
1852/// this allows using them conditional cfg attributes in the .rs files, so
1853///
1854/// ```rust,no_run
1855/// #[cfg(py_sys_config="{varname}")]
1856/// # struct Foo;
1857/// ```
1858///
1859/// is the equivalent of `#ifdef {varname}` in C.
1860///
1861/// see Misc/SpecialBuilds.txt in the python source for what these mean.
1862#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
1863#[derive(Clone, Default)]
1864pub struct BuildFlags(pub HashSet<BuildFlag>);
1865
1866impl BuildFlags {
1867    const ALL: [BuildFlag; 4] = [
1868        BuildFlag::Py_DEBUG,
1869        BuildFlag::Py_REF_DEBUG,
1870        BuildFlag::Py_GIL_DISABLED,
1871        BuildFlag::COUNT_ALLOCS,
1872    ];
1873
1874    pub fn new() -> Self {
1875        BuildFlags(HashSet::new())
1876    }
1877
1878    fn from_sysconfigdata(config_map: &Sysconfigdata) -> Self {
1879        Self(
1880            BuildFlags::ALL
1881                .iter()
1882                .filter(|flag| config_map.get_value(flag.to_string()) == Some("1"))
1883                .cloned()
1884                .collect(),
1885        )
1886        .fixup()
1887    }
1888
1889    /// Examine python's compile flags to pass to cfg by launching
1890    /// the interpreter and printing variables of interest from
1891    /// sysconfig.get_config_vars.
1892    fn from_interpreter(interpreter: impl AsRef<Path>) -> Result<Self> {
1893        // sysconfig is missing all the flags on windows for Python 3.12 and
1894        // older, so we can't actually query the interpreter directly for its
1895        // build flags on those versions.
1896        if cfg!(windows) {
1897            let script = String::from("import sys;print(sys.version_info < (3, 13))");
1898            let stdout = run_python_script(interpreter.as_ref(), &script)?;
1899            if stdout.trim_end() == "True" {
1900                return Ok(Self::new());
1901            }
1902        }
1903
1904        let mut script = String::from("import sysconfig\n");
1905        script.push_str("config = sysconfig.get_config_vars()\n");
1906
1907        for k in &BuildFlags::ALL {
1908            use std::fmt::Write;
1909            writeln!(&mut script, "print(config.get('{k}', '0'))").unwrap();
1910        }
1911
1912        let stdout = run_python_script(interpreter.as_ref(), &script)?;
1913        let split_stdout: Vec<&str> = stdout.trim_end().lines().collect();
1914        ensure!(
1915            split_stdout.len() == BuildFlags::ALL.len(),
1916            "Python stdout len didn't return expected number of lines: {}",
1917            split_stdout.len()
1918        );
1919        let flags = BuildFlags::ALL
1920            .iter()
1921            .zip(split_stdout)
1922            .filter(|(_, flag_value)| *flag_value == "1")
1923            .map(|(flag, _)| flag.clone())
1924            .collect();
1925
1926        Ok(Self(flags).fixup())
1927    }
1928
1929    fn fixup(mut self) -> Self {
1930        if self.0.contains(&BuildFlag::Py_DEBUG) {
1931            self.0.insert(BuildFlag::Py_REF_DEBUG);
1932        }
1933
1934        self
1935    }
1936}
1937
1938impl Display for BuildFlags {
1939    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1940        let mut first = true;
1941        for flag in &self.0 {
1942            if first {
1943                first = false;
1944            } else {
1945                write!(f, ",")?;
1946            }
1947            write!(f, "{flag}")?;
1948        }
1949        Ok(())
1950    }
1951}
1952
1953impl FromStr for BuildFlags {
1954    type Err = std::convert::Infallible;
1955
1956    fn from_str(value: &str) -> Result<Self, Self::Err> {
1957        let mut flags = HashSet::new();
1958        for flag in value.split_terminator(',') {
1959            flags.insert(flag.parse().unwrap());
1960        }
1961        Ok(BuildFlags(flags))
1962    }
1963}
1964
1965fn parse_script_output(output: &str) -> HashMap<String, String> {
1966    output
1967        .lines()
1968        .filter_map(|line| {
1969            let mut i = line.splitn(2, ' ');
1970            Some((i.next()?.into(), i.next()?.into()))
1971        })
1972        .collect()
1973}
1974
1975/// Parsed data from Python sysconfigdata file
1976///
1977/// A hash map of all values from a sysconfigdata file.
1978pub struct Sysconfigdata(HashMap<String, String>);
1979
1980impl Sysconfigdata {
1981    pub fn get_value<S: AsRef<str>>(&self, k: S) -> Option<&str> {
1982        self.0.get(k.as_ref()).map(String::as_str)
1983    }
1984
1985    #[cfg(test)]
1986    fn new() -> Self {
1987        Sysconfigdata(HashMap::new())
1988    }
1989
1990    #[cfg(test)]
1991    fn insert<S: Into<String>>(&mut self, k: S, v: S) {
1992        self.0.insert(k.into(), v.into());
1993    }
1994}
1995
1996/// Parse sysconfigdata file
1997///
1998/// The sysconfigdata is simply a dictionary containing all the build time variables used for the
1999/// python executable and library. This function necessitates a python interpreter on the host
2000/// machine to work. Here it is read into a `Sysconfigdata` (hash map), which can be turned into an
2001/// [`InterpreterConfig`] using
2002/// [`from_sysconfigdata`](InterpreterConfig::from_sysconfigdata).
2003pub fn parse_sysconfigdata(sysconfigdata_path: impl AsRef<Path>) -> Result<Sysconfigdata> {
2004    let sysconfigdata_path = sysconfigdata_path.as_ref();
2005    let mut script = fs::read_to_string(sysconfigdata_path).with_context(|| {
2006        format!(
2007            "failed to read config from {}",
2008            sysconfigdata_path.display()
2009        )
2010    })?;
2011    script += r#"
2012for key, val in build_time_vars.items():
2013    # (ana)conda(-forge) built Pythons are statically linked but ship the shared library with them.
2014    # We detect them based on the magic prefix directory they have encoded in their builds.
2015    if key == "Py_ENABLE_SHARED" and "_h_env_placehold" in build_time_vars.get("prefix"):
2016        val = 1
2017    print(key, val)
2018"#;
2019
2020    let output = run_python_script(&find_interpreter()?, &script)?;
2021
2022    Ok(Sysconfigdata(parse_script_output(&output)))
2023}
2024
2025fn starts_with(entry: &DirEntry, pat: &str) -> bool {
2026    let name = entry.file_name();
2027    name.to_string_lossy().starts_with(pat)
2028}
2029fn ends_with(entry: &DirEntry, pat: &str) -> bool {
2030    let name = entry.file_name();
2031    name.to_string_lossy().ends_with(pat)
2032}
2033
2034/// Finds the sysconfigdata file when the target Python library directory is set.
2035///
2036/// Returns `None` if the library directory is not available, and a runtime error
2037/// when no or multiple sysconfigdata files are found.
2038fn find_sysconfigdata(cross: &CrossCompileConfig) -> Result<Option<PathBuf>> {
2039    let mut sysconfig_paths = find_all_sysconfigdata(cross)?;
2040    if sysconfig_paths.is_empty() {
2041        if let Some(lib_dir) = cross.lib_dir.as_ref() {
2042            bail!("Could not find _sysconfigdata*.py in {}", lib_dir.display());
2043        } else {
2044            // Continue with the default configuration when PYO3_CROSS_LIB_DIR is not set.
2045            return Ok(None);
2046        }
2047    } else if sysconfig_paths.len() > 1 {
2048        let mut error_msg = String::from(
2049            "Detected multiple possible Python versions. Please set either the \
2050            PYO3_CROSS_PYTHON_VERSION variable to the wanted version or the \
2051            _PYTHON_SYSCONFIGDATA_NAME variable to the wanted sysconfigdata file name.\n\n\
2052            sysconfigdata files found:",
2053        );
2054        for path in sysconfig_paths {
2055            use std::fmt::Write;
2056            write!(&mut error_msg, "\n\t{}", path.display()).unwrap();
2057        }
2058        bail!("{}\n", error_msg);
2059    }
2060
2061    Ok(Some(sysconfig_paths.remove(0)))
2062}
2063
2064/// Finds `_sysconfigdata*.py` files for detected Python interpreters.
2065///
2066/// From the python source for `_sysconfigdata*.py` is always going to be located at
2067/// `build/lib.{PLATFORM}-{PY_MINOR_VERSION}` when built from source. The [exact line][1] is defined as:
2068///
2069/// ```py
2070/// pybuilddir = 'build/lib.%s-%s' % (get_platform(), sys.version_info[:2])
2071/// ```
2072///
2073/// Where get_platform returns a kebab-case formatted string containing the os, the architecture and
2074/// possibly the os' kernel version (not the case on linux). However, when installed using a package
2075/// manager, the `_sysconfigdata*.py` file is installed in the `${PREFIX}/lib/python3.Y/` directory.
2076/// The `_sysconfigdata*.py` is generally in a sub-directory of the location of `libpython3.Y.so`.
2077/// So we must find the file in the following possible locations:
2078///
2079/// ```sh
2080/// # distribution from package manager, (lib_dir may or may not include lib/)
2081/// ${INSTALL_PREFIX}/lib/python3.Y/_sysconfigdata*.py
2082/// ${INSTALL_PREFIX}/lib/libpython3.Y.so
2083/// ${INSTALL_PREFIX}/lib/python3.Y/config-3.Y-${HOST_TRIPLE}/libpython3.Y.so
2084///
2085/// # Built from source from host
2086/// ${CROSS_COMPILED_LOCATION}/build/lib.linux-x86_64-Y/_sysconfigdata*.py
2087/// ${CROSS_COMPILED_LOCATION}/libpython3.Y.so
2088///
2089/// # if cross compiled, kernel release is only present on certain OS targets.
2090/// ${CROSS_COMPILED_LOCATION}/build/lib.{OS}(-{OS-KERNEL-RELEASE})?-{ARCH}-Y/_sysconfigdata*.py
2091/// ${CROSS_COMPILED_LOCATION}/libpython3.Y.so
2092///
2093/// # PyPy includes a similar file since v73
2094/// ${INSTALL_PREFIX}/lib/pypy3.Y/_sysconfigdata.py
2095/// ${INSTALL_PREFIX}/lib_pypy/_sysconfigdata.py
2096/// ```
2097///
2098/// [1]: https://github.com/python/cpython/blob/3.5/Lib/sysconfig.py#L389
2099///
2100/// Returns an empty vector when the target Python library directory
2101/// is not set via `PYO3_CROSS_LIB_DIR`.
2102pub fn find_all_sysconfigdata(cross: &CrossCompileConfig) -> Result<Vec<PathBuf>> {
2103    let sysconfig_paths = if let Some(lib_dir) = cross.lib_dir.as_ref() {
2104        search_lib_dir(lib_dir, cross).with_context(|| {
2105            format!(
2106                "failed to search the lib dir at 'PYO3_CROSS_LIB_DIR={}'",
2107                lib_dir.display()
2108            )
2109        })?
2110    } else {
2111        return Ok(Vec::new());
2112    };
2113
2114    let sysconfig_name = env_var("_PYTHON_SYSCONFIGDATA_NAME");
2115    let mut sysconfig_paths = sysconfig_paths
2116        .iter()
2117        .filter_map(|p| {
2118            let canonical = fs::canonicalize(p).ok();
2119            match &sysconfig_name {
2120                Some(_) => canonical.filter(|p| p.file_stem() == sysconfig_name.as_deref()),
2121                None => canonical,
2122            }
2123        })
2124        .collect::<Vec<PathBuf>>();
2125
2126    sysconfig_paths.sort();
2127    sysconfig_paths.dedup();
2128
2129    Ok(sysconfig_paths)
2130}
2131
2132fn is_pypy_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
2133    let pypy_version_pat = if let Some(v) = v {
2134        format!("pypy{v}")
2135    } else {
2136        "pypy3.".into()
2137    };
2138    path == "lib_pypy" || path.starts_with(&pypy_version_pat)
2139}
2140
2141fn is_graalpy_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
2142    let graalpy_version_pat = if let Some(v) = v {
2143        format!("graalpy{v}")
2144    } else {
2145        "graalpy2".into()
2146    };
2147    path == "lib_graalpython" || path.starts_with(&graalpy_version_pat)
2148}
2149
2150fn is_cpython_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
2151    let cpython_version_pat = if let Some(v) = v {
2152        format!("python{v}")
2153    } else {
2154        "python3.".into()
2155    };
2156    path.starts_with(&cpython_version_pat)
2157}
2158
2159/// recursive search for _sysconfigdata, returns all possibilities of sysconfigdata paths
2160fn search_lib_dir(path: impl AsRef<Path>, cross: &CrossCompileConfig) -> Result<Vec<PathBuf>> {
2161    let mut sysconfig_paths = vec![];
2162    for f in fs::read_dir(path.as_ref()).with_context(|| {
2163        format!(
2164            "failed to list the entries in '{}'",
2165            path.as_ref().display()
2166        )
2167    })? {
2168        sysconfig_paths.extend(match &f {
2169            // Python 3.7+ sysconfigdata with platform specifics
2170            Ok(f) if starts_with(f, "_sysconfigdata_") && ends_with(f, "py") => vec![f.path()],
2171            Ok(f) if f.metadata().is_ok_and(|metadata| metadata.is_dir()) => {
2172                let file_name = f.file_name();
2173                let file_name = file_name.to_string_lossy();
2174                if file_name == "build" || file_name == "lib" {
2175                    search_lib_dir(f.path(), cross)?
2176                } else if file_name.starts_with("lib.") {
2177                    // check if right target os
2178                    if !file_name.contains(&cross.target.operating_system.to_string()) {
2179                        continue;
2180                    }
2181                    // Check if right arch
2182                    if !file_name.contains(&cross.target.architecture.to_string()) {
2183                        continue;
2184                    }
2185                    search_lib_dir(f.path(), cross)?
2186                } else if is_cpython_lib_dir(&file_name, &cross.version)
2187                    || is_pypy_lib_dir(&file_name, &cross.version)
2188                    || is_graalpy_lib_dir(&file_name, &cross.version)
2189                {
2190                    search_lib_dir(f.path(), cross)?
2191                } else {
2192                    continue;
2193                }
2194            }
2195            _ => continue,
2196        });
2197    }
2198    // If we got more than one file, only take those that contain the arch name.
2199    // For ubuntu 20.04 with host architecture x86_64 and a foreign architecture of armhf
2200    // this reduces the number of candidates to 1:
2201    //
2202    // $ find /usr/lib/python3.8/ -name '_sysconfigdata*.py' -not -lname '*'
2203    //  /usr/lib/python3.8/_sysconfigdata__x86_64-linux-gnu.py
2204    //  /usr/lib/python3.8/_sysconfigdata__arm-linux-gnueabihf.py
2205    if sysconfig_paths.len() > 1 {
2206        let temp = sysconfig_paths
2207            .iter()
2208            .filter(|p| {
2209                p.to_string_lossy()
2210                    .contains(&cross.target.architecture.to_string())
2211            })
2212            .cloned()
2213            .collect::<Vec<PathBuf>>();
2214        if !temp.is_empty() {
2215            sysconfig_paths = temp;
2216        }
2217    }
2218
2219    Ok(sysconfig_paths)
2220}
2221
2222/// Find cross compilation information from sysconfigdata file
2223///
2224/// first find sysconfigdata file which follows the pattern [`_sysconfigdata_{abi}_{platform}_{multiarch}`][1]
2225///
2226/// [1]: https://github.com/python/cpython/blob/3.8/Lib/sysconfig.py#L348
2227///
2228/// Returns `None` when the target Python library directory is not set.
2229fn cross_compile_from_sysconfigdata(
2230    cross_compile_config: &CrossCompileConfig,
2231) -> Result<Option<InterpreterConfig>> {
2232    if let Some(path) = find_sysconfigdata(cross_compile_config)? {
2233        let data = parse_sysconfigdata(path)?;
2234        let mut config = InterpreterConfig::from_sysconfigdata(&data)?;
2235        #[expect(deprecated, reason = "modifying config inline")]
2236        if let Some(cross_lib_dir) = cross_compile_config.lib_dir_string() {
2237            config.lib_dir = Some(cross_lib_dir)
2238        }
2239
2240        Ok(Some(config))
2241    } else {
2242        Ok(None)
2243    }
2244}
2245
2246fn exact_stable_abi_version(version: Option<StableAbiVersion>) -> Option<PythonVersion> {
2247    version.and_then(|v| match v {
2248        StableAbiVersion::Current => None,
2249        StableAbiVersion::Target(inner) => Some(inner),
2250    })
2251}
2252
2253/// Generates "default" cross compilation information for the target.
2254///
2255/// This should work for most CPython extension modules when targeting
2256/// Windows, macOS and Linux.
2257///
2258/// Must be called from a PyO3 crate build script.
2259fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result<InterpreterConfig> {
2260    let version = cross_compile_config
2261        .version
2262        .or_else(|| exact_stable_abi_version(get_abi3_version()))
2263        .or_else(|| exact_stable_abi_version(get_abi3t_version()))
2264        .ok_or_else(||
2265            format!(
2266                "PYO3_CROSS_PYTHON_VERSION or either an abi3-py3* or abi3t-py3* feature must be specified \
2267                when cross-compiling and PYO3_CROSS_LIB_DIR is not set.\n\
2268                = help: see the PyO3 user guide for more information: https://pyo3.rs/v{}/building-and-distribution.html#cross-compiling",
2269                env!("CARGO_PKG_VERSION")
2270            )
2271        )?;
2272    let gil_disabled = cross_compile_config.abiflags.as_deref() == Some("t");
2273
2274    let implementation = cross_compile_config
2275        .implementation
2276        .unwrap_or(PythonImplementation::CPython);
2277
2278    let stable_abi =
2279        applicable_stable_abi_at_interpreter_version(implementation, version, gil_disabled);
2280
2281    let target_abi = PythonAbi::from_stable_abi(implementation, version, stable_abi, gil_disabled)?;
2282
2283    let lib_name = default_lib_name_for_target(target_abi, &cross_compile_config.target);
2284
2285    let lib_dir = cross_compile_config.lib_dir_string();
2286
2287    InterpreterConfigBuilder::new(implementation, version)
2288        .target_abi(target_abi)
2289        .lib_name(lib_name)
2290        .lib_dir(lib_dir)
2291        .finalize()
2292}
2293
2294/// Generates "default" interpreter configuration when compiling stable ABI extensions
2295/// without a working Python interpreter.
2296///
2297/// `abi3_version` or `abi3t_version` specifies the minimum supported Stable ABI
2298/// CPython version and which stable ABI to target.
2299///
2300/// This should work for most CPython extension modules when compiling on
2301/// Windows, macOS and Linux.
2302///
2303/// Must be called from a PyO3 crate build script.
2304fn default_stable_abi_config(
2305    host: &Triple,
2306    abi3_version: Option<PythonVersion>,
2307    abi3t_version: Option<PythonVersion>,
2308) -> Result<InterpreterConfig> {
2309    if abi3_version.is_none() && abi3t_version.is_none() {
2310        bail!("Neither abi3 or abi3t features are enabled")
2311    }
2312    let (stable_abi, version) = if let Some(version) = abi3_version {
2313        (StableAbi::Abi3, version)
2314    } else if let Some(version) = abi3t_version {
2315        (StableAbi::Abi3t, version)
2316    } else {
2317        unreachable!();
2318    };
2319
2320    if stable_abi == StableAbi::Abi3t && version < MINIMUM_SUPPORTED_VERSION_ABI3T {
2321        bail!("Cannot target an abi3t version below {MINIMUM_SUPPORTED_VERSION_ABI3T}")
2322    }
2323
2324    // FIXME: PyPy & GraalPy do not support the Stable ABI.
2325    let target_abi = PythonAbiBuilder::new(PythonImplementation::CPython, version)
2326        .stable_abi(stable_abi)
2327        .finalize()?;
2328    let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version)
2329        .target_abi(target_abi);
2330    if host.operating_system == OperatingSystem::Windows {
2331        builder.lib_name(default_lib_name_windows(target_abi, false, false)?)
2332    } else {
2333        builder
2334    }
2335    .finalize()
2336}
2337
2338/// Detects the cross compilation target interpreter configuration from all
2339/// available sources (PyO3 environment variables, Python sysconfigdata, etc.).
2340///
2341/// Returns the "default" target interpreter configuration for Windows and
2342/// when no target Python interpreter is found.
2343///
2344/// Must be called from a PyO3 crate build script.
2345fn load_cross_compile_config(
2346    cross_compile_config: CrossCompileConfig,
2347) -> Result<InterpreterConfig> {
2348    let windows = cross_compile_config.target.operating_system == OperatingSystem::Windows;
2349
2350    let config = if windows || !have_python_interpreter() {
2351        // Load the defaults for Windows even when `PYO3_CROSS_LIB_DIR` is set
2352        // since it has no sysconfigdata files in it.
2353        // Also, do not try to look for sysconfigdata when `PYO3_NO_PYTHON` variable is set.
2354        default_cross_compile(&cross_compile_config)?
2355    } else if let Some(config) = cross_compile_from_sysconfigdata(&cross_compile_config)? {
2356        // Try to find and parse sysconfigdata files on other targets.
2357        config
2358    } else {
2359        // Fall back to the defaults when nothing else can be done.
2360        default_cross_compile(&cross_compile_config)?
2361    };
2362
2363    Ok(config)
2364}
2365
2366// These contains only the limited ABI symbols.
2367const WINDOWS_STABLE_ABI_LIB_NAME: &str = "python3";
2368const WINDOWS_STABLE_ABI_DEBUG_LIB_NAME: &str = "python3_d";
2369
2370/// Generates the default library name for the target platform.
2371#[allow(dead_code)]
2372fn default_lib_name_for_target(abi: PythonAbi, target: &Triple) -> String {
2373    if target.operating_system == OperatingSystem::Windows {
2374        default_lib_name_windows(abi, false, false).unwrap()
2375    } else {
2376        default_lib_name_unix(
2377            abi,
2378            target.operating_system == OperatingSystem::Cygwin,
2379            None,
2380        )
2381        .unwrap()
2382    }
2383}
2384
2385fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result<String> {
2386    // mingw formats lib names like unix, and uses a "lib" prefix. We could let the linker
2387    // handle "lib" prefix, but that means the `raw-dylib` name is incorrect (where the
2388    // "lib" prefix is not automatically added).
2389    if mingw {
2390        let mut lib_name = default_lib_name_unix(abi, true, None)?;
2391        lib_name.insert_str(0, "lib");
2392        return Ok(lib_name);
2393    }
2394
2395    if abi.implementation.is_pypy() {
2396        // PyPy on Windows ships `libpypy3.X-c.dll` (e.g. `libpypy3.11-c.dll`),
2397        // not CPython's `pythonXY.dll`. With raw-dylib linking we need the real
2398        // DLL name rather than the import-library alias.
2399        Ok(format!(
2400            "libpypy{}.{}-c",
2401            abi.version.major, abi.version.minor
2402        ))
2403    } else if debug && abi.version < PythonVersion::PY310 {
2404        // CPython bug: linking against python3_d.dll raises error
2405        // https://github.com/python/cpython/issues/101614
2406        Ok(format!(
2407            "python{}{}_d",
2408            abi.version.major, abi.version.minor
2409        ))
2410    } else if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3)
2411        || abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t)
2412    {
2413        let mut lib_name = if debug {
2414            WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned()
2415        } else {
2416            WINDOWS_STABLE_ABI_LIB_NAME.to_owned()
2417        };
2418        if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) {
2419            lib_name = lib_name.replace("python3", "python3t");
2420        }
2421        Ok(lib_name)
2422    } else if abi.kind().is_free_threaded() {
2423        #[expect(deprecated, reason = "using constant internally")]
2424        {
2425            ensure!(abi.version() >= PythonVersion::PY313, "Cannot compile extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor);
2426        }
2427        if debug {
2428            Ok(format!(
2429                "python{}{}t_d",
2430                abi.version.major, abi.version.minor
2431            ))
2432        } else {
2433            Ok(format!("python{}{}t", abi.version.major, abi.version.minor))
2434        }
2435    } else if debug {
2436        Ok(format!(
2437            "python{}{}_d",
2438            abi.version.major, abi.version.minor
2439        ))
2440    } else {
2441        Ok(format!("python{}{}", abi.version.major, abi.version.minor))
2442    }
2443}
2444
2445fn default_lib_name_unix(
2446    abi: PythonAbi,
2447    use_stable_abi_lib: bool,
2448    ld_version: Option<&str>,
2449) -> Result<String> {
2450    match abi.implementation {
2451        PythonImplementation::CPython => match ld_version {
2452            Some(ld_version) => Ok(format!("python{ld_version}")),
2453            None => match abi.kind {
2454                PythonAbiKind::Stable(StableAbi::Abi3) if use_stable_abi_lib => {
2455                    Ok("python3".to_string())
2456                }
2457                PythonAbiKind::Stable(StableAbi::Abi3t) if use_stable_abi_lib => {
2458                    Ok("python3t".to_string())
2459                }
2460                _ => {
2461                    if abi.kind.is_free_threaded() {
2462                        #[expect(deprecated, reason = "using constant internally")]
2463                        {
2464                            ensure!(abi.version >= PythonVersion::PY313, "Cannot compile extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor);
2465                        }
2466                        Ok(format!(
2467                            "python{}.{}t",
2468                            abi.version.major, abi.version.minor
2469                        ))
2470                    } else {
2471                        Ok(format!("python{}.{}", abi.version.major, abi.version.minor))
2472                    }
2473                }
2474            },
2475        },
2476        PythonImplementation::PyPy => match ld_version {
2477            Some(ld_version) => Ok(format!("pypy{ld_version}-c")),
2478            None => Ok(format!("pypy{}.{}-c", abi.version.major, abi.version.minor)),
2479        },
2480
2481        PythonImplementation::GraalPy => Ok("python-native".to_string()),
2482        PythonImplementation::RustPython => Ok("rustpython_capi".to_string()),
2483    }
2484}
2485
2486/// Run a python script using the specified interpreter binary.
2487fn run_python_script(interpreter: &Path, script: &str) -> Result<String> {
2488    run_python_script_with_envs(interpreter, script, std::iter::empty::<(&str, &str)>())
2489}
2490
2491/// Run a python script using the specified interpreter binary with additional environment
2492/// variables (e.g. PYTHONPATH) set.
2493fn run_python_script_with_envs<I, K, V>(interpreter: &Path, script: &str, envs: I) -> Result<String>
2494where
2495    I: IntoIterator<Item = (K, V)>,
2496    K: AsRef<OsStr>,
2497    V: AsRef<OsStr>,
2498{
2499    let out = Command::new(interpreter)
2500        .env("PYTHONIOENCODING", "utf-8")
2501        .envs(envs)
2502        .stdin(Stdio::piped())
2503        .stdout(Stdio::piped())
2504        .stderr(Stdio::inherit())
2505        .spawn()
2506        .and_then(|mut child| {
2507            child
2508                .stdin
2509                .as_mut()
2510                .expect("piped stdin")
2511                .write_all(script.as_bytes())?;
2512            child.wait_with_output()
2513        });
2514
2515    match out {
2516        Err(err) => bail!(
2517            "failed to run the Python interpreter at {}: {}",
2518            interpreter.display(),
2519            err
2520        ),
2521        Ok(ok) if !ok.status.success() => bail!("Python script failed"),
2522        Ok(ok) => Ok(String::from_utf8(ok.stdout)
2523            .context("failed to parse Python script output as utf-8")?),
2524    }
2525}
2526
2527fn venv_interpreter(virtual_env: &OsStr, windows: bool) -> PathBuf {
2528    let venv = Path::new(virtual_env);
2529    // Rebuild if the virtual environment configuration changes
2530    println!(
2531        "cargo:rerun-if-changed={}",
2532        venv.join("pyvenv.cfg").display()
2533    );
2534    if windows {
2535        venv.join("Scripts").join("python.exe")
2536    } else {
2537        venv.join("bin").join("python")
2538    }
2539}
2540
2541fn conda_env_interpreter(conda_prefix: &OsStr, windows: bool) -> PathBuf {
2542    if windows {
2543        Path::new(conda_prefix).join("python.exe")
2544    } else {
2545        Path::new(conda_prefix).join("bin").join("python")
2546    }
2547}
2548
2549fn get_env_interpreter() -> Option<PathBuf> {
2550    match (env_var("VIRTUAL_ENV"), env_var("CONDA_PREFIX")) {
2551        // Use cfg rather than CARGO_CFG_TARGET_OS because this affects where files are located on the
2552        // build host
2553        (Some(dir), None) => Some(venv_interpreter(&dir, cfg!(windows))),
2554        (None, Some(dir)) => Some(conda_env_interpreter(&dir, cfg!(windows))),
2555        (Some(_), Some(_)) => {
2556            warn!(
2557                "Both VIRTUAL_ENV and CONDA_PREFIX are set. PyO3 will ignore both of these for \
2558                 locating the Python interpreter until you unset one of them."
2559            );
2560            None
2561        }
2562        (None, None) => None,
2563    }
2564}
2565
2566/// Attempts to locate a python interpreter.
2567///
2568/// Locations are checked in the order listed:
2569///   1. If `PYO3_PYTHON` is set, this interpreter is used.
2570///   2. If in a virtualenv, that environment's interpreter is used.
2571///   3. `python`, if this is functional a Python 3.x interpreter
2572///   4. `python3`, as above
2573pub fn find_interpreter() -> Result<PathBuf> {
2574    // Trigger rebuilds when `PYO3_ENVIRONMENT_SIGNATURE` env var value changes
2575    // See https://github.com/PyO3/pyo3/issues/2724
2576    println!("cargo:rerun-if-env-changed=PYO3_ENVIRONMENT_SIGNATURE");
2577
2578    if let Some(exe) = env_var("PYO3_PYTHON") {
2579        Ok(exe.into())
2580    } else if let Some(env_interpreter) = get_env_interpreter() {
2581        Ok(env_interpreter)
2582    } else {
2583        println!("cargo:rerun-if-env-changed=PATH");
2584        ["python", "python3"]
2585            .iter()
2586            .find(|bin| {
2587                if let Ok(out) = Command::new(bin).arg("--version").output() {
2588                    // begin with `Python 3.X.X :: additional info`
2589                    out.stdout.starts_with(b"Python 3")
2590                        || out.stderr.starts_with(b"Python 3")
2591                        || out.stdout.starts_with(b"GraalPy 3")
2592                } else {
2593                    false
2594                }
2595            })
2596            .map(PathBuf::from)
2597            .ok_or_else(|| "no Python 3.x interpreter found".into())
2598    }
2599}
2600
2601/// Locates and extracts the build host Python interpreter configuration.
2602///
2603/// Lowers the configured Python version to `abi3_version` or `abi3t_version` if required.
2604fn get_host_interpreter(
2605    abi3_version: Option<StableAbiVersion>,
2606    abi3t_version: Option<StableAbiVersion>,
2607) -> Result<InterpreterConfig> {
2608    let interpreter_path = find_interpreter()?;
2609
2610    let interpreter_config =
2611        InterpreterConfig::from_interpreter(interpreter_path, abi3_version, abi3t_version)?;
2612
2613    Ok(interpreter_config)
2614}
2615
2616/// Generates an interpreter config suitable for cross-compilation.
2617///
2618/// This must be called from PyO3's build script, because it relies on environment variables such as
2619/// CARGO_CFG_TARGET_OS which aren't available at any other time.
2620pub fn make_cross_compile_config(target: &Triple) -> Result<Option<InterpreterConfig>> {
2621    let interpreter_config =
2622        if let Some(cross_config) = cross_compiling_from_to(&Triple::host(), target)? {
2623            Some(load_cross_compile_config(cross_config)?.apply_build_env()?)
2624        } else {
2625            None
2626        };
2627
2628    Ok(interpreter_config)
2629}
2630
2631/// Generates an interpreter config suitable for the build host.
2632pub fn make_interpreter_config() -> Result<InterpreterConfig> {
2633    let host = Triple::host();
2634    let abi3_version = get_abi3_version();
2635    let abi3t_version = get_abi3t_version();
2636
2637    // See if we can safely skip the Python interpreter configuration detection.
2638    // Unix stable ABI extension modules can usually be built without any interpreter.
2639    let need_interpreter =
2640        (abi3_version.is_none() && abi3t_version.is_none()) || require_libdir_for_target(&host);
2641
2642    if have_python_interpreter() {
2643        match get_host_interpreter(abi3_version, abi3t_version) {
2644            Ok(interpreter_config) => return Ok(interpreter_config),
2645            // Bail if the interpreter configuration is required to build.
2646            Err(e) if need_interpreter => return Err(e),
2647            _ => {
2648                // Fall back to the stable ABI just as if `PYO3_NO_PYTHON`
2649                // environment variable was set.
2650                warn!("Compiling without a working Python interpreter.");
2651            }
2652        }
2653    }
2654
2655    let interpreter_config = default_stable_abi_config(
2656        &host,
2657        exact_stable_abi_version(abi3_version),
2658        exact_stable_abi_version(abi3t_version),
2659    )?;
2660
2661    Ok(interpreter_config)
2662}
2663
2664pub(crate) fn escape(bytes: &[u8]) -> String {
2665    let mut escaped = String::with_capacity(2 * bytes.len());
2666
2667    for byte in bytes {
2668        const LUT: &[u8; 16] = b"0123456789abcdef";
2669
2670        escaped.push(LUT[(byte >> 4) as usize] as char);
2671        escaped.push(LUT[(byte & 0x0F) as usize] as char);
2672    }
2673
2674    escaped
2675}
2676
2677fn unescape(escaped: &str) -> Vec<u8> {
2678    assert_eq!(escaped.len() % 2, 0, "invalid hex encoding");
2679
2680    let mut bytes = Vec::with_capacity(escaped.len() / 2);
2681
2682    for chunk in escaped.as_bytes().chunks_exact(2) {
2683        fn unhex(hex: u8) -> u8 {
2684            match hex {
2685                b'a'..=b'f' => hex - b'a' + 10,
2686                b'0'..=b'9' => hex - b'0',
2687                _ => panic!("invalid hex encoding"),
2688            }
2689        }
2690
2691        bytes.push((unhex(chunk[0]) << 4) | unhex(chunk[1]));
2692    }
2693
2694    bytes
2695}
2696
2697#[cfg(test)]
2698// can remove this expect when fields are private
2699#[expect(deprecated, reason = "accessing config fields directly for testing")]
2700mod tests {
2701    use target_lexicon::triple;
2702
2703    use super::*;
2704
2705    #[test]
2706    fn test_config_file_roundtrip() {
2707        let implementation = PythonImplementation::CPython;
2708        let version = MINIMUM_SUPPORTED_VERSION;
2709        let config = InterpreterConfigBuilder::new(implementation, version)
2710            .stable_abi(StableAbi::Abi3)
2711            .pointer_width(32)
2712            .executable("executable".to_string())
2713            .lib_dir("lib_name".to_string())
2714            .lib_name("lib_name".to_string())
2715            .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()])
2716            .finalize()
2717            .unwrap();
2718        let mut buf: Vec<u8> = Vec::new();
2719        config.to_writer(&mut buf).unwrap();
2720
2721        assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap());
2722
2723        // And some different options, for variety
2724        let version = PythonVersion::PY310;
2725        let implementation = PythonImplementation::PyPy;
2726        let build_flags = {
2727            let mut flags = HashSet::new();
2728            flags.insert(BuildFlag::Py_DEBUG);
2729            flags.insert(BuildFlag::Other(String::from("Py_SOME_FLAG")));
2730            BuildFlags(flags)
2731        };
2732        let config = InterpreterConfigBuilder::new(implementation, version)
2733            .build_flags(build_flags)
2734            .finalize()
2735            .unwrap();
2736
2737        let mut buf: Vec<u8> = Vec::new();
2738        config.to_writer(&mut buf).unwrap();
2739
2740        assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap());
2741    }
2742
2743    #[test]
2744    fn test_config_file_roundtrip_with_escaping() {
2745        let implementation = PythonImplementation::CPython;
2746        let version = MINIMUM_SUPPORTED_VERSION;
2747        let config = InterpreterConfigBuilder::new(implementation, version)
2748            .stable_abi(StableAbi::Abi3)
2749            .pointer_width(32)
2750            .executable("executable".to_string())
2751            .lib_name("lib_name".to_string())
2752            .lib_dir("lib_dir\\n".to_string())
2753            .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()])
2754            .finalize()
2755            .unwrap();
2756        let mut buf: Vec<u8> = Vec::new();
2757        config.to_writer(&mut buf).unwrap();
2758
2759        let buf = unescape(&escape(&buf));
2760
2761        assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap());
2762    }
2763
2764    #[test]
2765    fn test_config_file_defaults() {
2766        // Only version is required
2767        let implementation = PythonImplementation::CPython;
2768        let version = PythonVersion::PY38;
2769        assert_eq!(
2770            InterpreterConfig::from_reader("version=3.8".as_bytes()).unwrap(),
2771            InterpreterConfigBuilder::new(implementation, version,)
2772                .finalize()
2773                .unwrap()
2774        )
2775    }
2776
2777    #[test]
2778    fn test_config_file_unknown_keys() {
2779        // ext_suffix is unknown to pyo3-build-config, but it shouldn't error
2780        let implementation = PythonImplementation::CPython;
2781        let version = PythonVersion::PY38;
2782        assert_eq!(
2783            InterpreterConfig::from_reader("version=3.8\next_suffix=.python38.so".as_bytes())
2784                .unwrap(),
2785            InterpreterConfigBuilder::new(implementation, version,)
2786                .finalize()
2787                .unwrap()
2788        )
2789    }
2790
2791    #[test]
2792    fn test_config_file_invalid_keys() {
2793        assert!(
2794            InterpreterConfig::from_reader("version=3.14\ntarget_abi=foo-bar-baz".as_bytes())
2795                .is_err()
2796        );
2797        assert!(InterpreterConfig::from_reader(
2798            "version=3.14\ntarget_abi=CPython-bar-baz".as_bytes()
2799        )
2800        .is_err());
2801        assert!(InterpreterConfig::from_reader(
2802            "version=3.14\ntarget_abi=CPython-abi3-baz".as_bytes()
2803        )
2804        .is_err());
2805    }
2806
2807    #[test]
2808    fn gil_disabled_config_file_corner_cases() {
2809        let implementation = PythonImplementation::CPython;
2810        let version = PythonVersion::PY313;
2811        // Legacy: build_flags=Py_GIL_DISABLED with no target_abi infers free-threaded.
2812        assert_eq!(
2813            InterpreterConfig::from_reader("version=3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes())
2814                .unwrap(),
2815            InterpreterConfigBuilder::new(implementation, version)
2816                .free_threaded()
2817                .unwrap()
2818                .finalize()
2819                .unwrap()
2820        );
2821        // Canonical: target_abi=free_threaded.
2822        assert_eq!(
2823            InterpreterConfig::from_reader(
2824                "version=3.13\ntarget_abi=CPython-free_threaded-3.13".as_bytes()
2825            )
2826            .unwrap(),
2827            InterpreterConfigBuilder::new(implementation, version)
2828                .free_threaded()
2829                .unwrap()
2830                .finalize()
2831                .unwrap()
2832        );
2833        // target_abi=gil_enabled with build_flags=Py_GIL_DISABLED is inconsistent and rejected.
2834        assert!(InterpreterConfig::from_reader(
2835            "version=3.13\ntarget_abi=CPython-gil_enabled-3.13\nbuild_flags=Py_GIL_DISABLED"
2836                .as_bytes()
2837        )
2838        .is_err());
2839        // build_flags=Py_GIL_DISABLED on a builder without target_abi is ok
2840        let mut flags = BuildFlags::default();
2841        flags.0.insert(BuildFlag::Py_GIL_DISABLED);
2842        assert!(InterpreterConfigBuilder::new(implementation, version)
2843            .build_flags(flags)
2844            .finalize()
2845            .unwrap()
2846            .target_abi
2847            .kind
2848            .is_free_threaded());
2849
2850        let mut flags = BuildFlags::default();
2851        flags.0.insert(BuildFlag::Py_GIL_DISABLED);
2852        assert!(
2853            InterpreterConfigBuilder::new(implementation, PythonVersion::PY312)
2854                .build_flags(flags)
2855                .finalize()
2856                .is_err()
2857        );
2858
2859        let mut flags = BuildFlags::default();
2860        flags.0.insert(BuildFlag::Py_GIL_DISABLED);
2861        assert!(
2862            InterpreterConfigBuilder::new(implementation, PythonVersion::PY312)
2863                .stable_abi(StableAbi::Abi3)
2864                .build_flags(flags)
2865                .finalize()
2866                .is_err()
2867        );
2868
2869        assert!(
2870            InterpreterConfigBuilder::new(implementation, PythonVersion::PY38)
2871                .free_threaded()
2872                .is_err()
2873        );
2874    }
2875
2876    #[test]
2877    fn abi3_from_old_config_file() {
2878        let implementation = PythonImplementation::CPython;
2879        let version = PythonVersion::PY313;
2880        assert_eq!(
2881            InterpreterConfig::from_reader("version=3.13\nabi3=true".as_bytes()).unwrap(),
2882            InterpreterConfigBuilder::new(implementation, version)
2883                .stable_abi(StableAbi::Abi3)
2884                .finalize()
2885                .unwrap()
2886        );
2887    }
2888
2889    #[test]
2890    fn test_target_abi_and_abi3() {
2891        assert!(InterpreterConfig::from_reader(
2892            "version=3.13\nabi3=true\ntarget_abi=CPython-abi3-3.13".as_bytes()
2893        )
2894        .unwrap_err()
2895        .to_string()
2896        .contains("Invalid config"),);
2897    }
2898
2899    #[test]
2900    fn build_flags_default() {
2901        assert_eq!(BuildFlags::default(), BuildFlags::new());
2902    }
2903
2904    #[test]
2905    fn build_flags_from_sysconfigdata() {
2906        let mut sysconfigdata = Sysconfigdata::new();
2907
2908        assert_eq!(
2909            BuildFlags::from_sysconfigdata(&sysconfigdata).0,
2910            HashSet::new()
2911        );
2912
2913        for flag in &BuildFlags::ALL {
2914            sysconfigdata.insert(flag.to_string(), "0".into());
2915        }
2916
2917        assert_eq!(
2918            BuildFlags::from_sysconfigdata(&sysconfigdata).0,
2919            HashSet::new()
2920        );
2921
2922        let mut expected_flags = HashSet::new();
2923        for flag in &BuildFlags::ALL {
2924            sysconfigdata.insert(flag.to_string(), "1".into());
2925            expected_flags.insert(flag.clone());
2926        }
2927
2928        assert_eq!(
2929            BuildFlags::from_sysconfigdata(&sysconfigdata).0,
2930            expected_flags
2931        );
2932    }
2933
2934    #[test]
2935    fn build_flags_fixup() {
2936        let mut build_flags = BuildFlags::new();
2937
2938        build_flags = build_flags.fixup();
2939        assert!(build_flags.0.is_empty());
2940
2941        build_flags.0.insert(BuildFlag::Py_DEBUG);
2942
2943        build_flags = build_flags.fixup();
2944
2945        // Py_DEBUG implies Py_REF_DEBUG
2946        assert!(build_flags.0.contains(&BuildFlag::Py_REF_DEBUG));
2947    }
2948
2949    #[test]
2950    fn parse_script_output() {
2951        let output = "foo bar\nbar foobar\n\n";
2952        let map = super::parse_script_output(output);
2953        assert_eq!(map.len(), 2);
2954        assert_eq!(map["foo"], "bar");
2955        assert_eq!(map["bar"], "foobar");
2956    }
2957
2958    #[test]
2959    fn config_from_interpreter() {
2960        // Smoke test to just see whether this works
2961        //
2962        // PyO3's CI is dependent on Python being installed, so this should be reliable.
2963        assert!(make_interpreter_config().is_ok())
2964    }
2965
2966    #[test]
2967    fn config_from_empty_sysconfigdata() {
2968        let sysconfigdata = Sysconfigdata::new();
2969        assert!(InterpreterConfig::from_sysconfigdata(&sysconfigdata).is_err());
2970    }
2971
2972    #[test]
2973    fn config_from_sysconfigdata() {
2974        let mut sysconfigdata = Sysconfigdata::new();
2975        // these are the minimal values required such that InterpreterConfig::from_sysconfigdata
2976        // does not error
2977        sysconfigdata.insert("SOABI", "cpython-38-x86_64-linux-gnu");
2978        sysconfigdata.insert("VERSION", "3.8");
2979        sysconfigdata.insert("Py_ENABLE_SHARED", "1");
2980        sysconfigdata.insert("LIBDIR", "/usr/lib");
2981        sysconfigdata.insert("LDVERSION", "3.8");
2982        sysconfigdata.insert("SIZEOF_VOID_P", "8");
2983        let implementation = PythonImplementation::CPython;
2984        let version = PythonVersion::PY38;
2985        assert_eq!(
2986            InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(),
2987            InterpreterConfigBuilder::new(implementation, version,)
2988                .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata))
2989                .lib_dir("/usr/lib".to_string())
2990                .lib_name("python3.8".to_string())
2991                .pointer_width(64)
2992                .finalize()
2993                .unwrap()
2994        );
2995    }
2996
2997    #[test]
2998    fn config_from_sysconfigdata_framework() {
2999        let mut sysconfigdata = Sysconfigdata::new();
3000        sysconfigdata.insert("SOABI", "cpython-38-x86_64-linux-gnu");
3001        sysconfigdata.insert("VERSION", "3.8");
3002        // PYTHONFRAMEWORK should override Py_ENABLE_SHARED
3003        sysconfigdata.insert("Py_ENABLE_SHARED", "0");
3004        sysconfigdata.insert("PYTHONFRAMEWORK", "Python");
3005        sysconfigdata.insert("LIBDIR", "/usr/lib");
3006        sysconfigdata.insert("LDVERSION", "3.8");
3007        sysconfigdata.insert("SIZEOF_VOID_P", "8");
3008        let implementation = PythonImplementation::CPython;
3009        let version = PythonVersion::PY38;
3010        assert_eq!(
3011            InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(),
3012            InterpreterConfigBuilder::new(implementation, version,)
3013                .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata))
3014                .lib_dir("/usr/lib".to_string())
3015                .lib_name("python3.8".to_string())
3016                .pointer_width(64)
3017                .finalize()
3018                .unwrap()
3019        );
3020
3021        sysconfigdata = Sysconfigdata::new();
3022        sysconfigdata.insert("SOABI", "cpython-38-x86_64-linux-gnu");
3023        sysconfigdata.insert("VERSION", "3.8");
3024        // An empty PYTHONFRAMEWORK means it is not a framework
3025        sysconfigdata.insert("Py_ENABLE_SHARED", "0");
3026        sysconfigdata.insert("PYTHONFRAMEWORK", "");
3027        sysconfigdata.insert("LIBDIR", "/usr/lib");
3028        sysconfigdata.insert("LDVERSION", "3.8");
3029        sysconfigdata.insert("SIZEOF_VOID_P", "8");
3030        let implementation = PythonImplementation::CPython;
3031        let version = PythonVersion::PY38;
3032        assert_eq!(
3033            InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(),
3034            InterpreterConfigBuilder::new(implementation, version,)
3035                .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata))
3036                .lib_dir("/usr/lib".to_string())
3037                .lib_name("python3.8".to_string())
3038                .pointer_width(64)
3039                .shared(false)
3040                .finalize()
3041                .unwrap()
3042        );
3043    }
3044
3045    #[test]
3046    fn windows_hardcoded_abi3_compile() {
3047        let host = triple!("x86_64-pc-windows-msvc");
3048        let implementation = PythonImplementation::CPython;
3049        let version = PythonVersion::PY38;
3050        let config = InterpreterConfigBuilder::new(implementation, version)
3051            .stable_abi(StableAbi::Abi3)
3052            .lib_name("python3".to_string())
3053            .finalize()
3054            .unwrap();
3055        assert_eq!(
3056            default_stable_abi_config(&host, Some(version), None).unwrap(),
3057            config
3058        );
3059    }
3060
3061    #[test]
3062    fn windows_hardcoded_abi3t_compile() {
3063        let host = triple!("x86_64-pc-windows-msvc");
3064        let implementation = PythonImplementation::CPython;
3065        let version = PythonVersion::PY315;
3066        let config = InterpreterConfigBuilder::new(implementation, version)
3067            .stable_abi(StableAbi::Abi3t)
3068            .lib_name("python3t".to_string())
3069            .finalize()
3070            .unwrap();
3071        assert_eq!(
3072            default_stable_abi_config(&host, None, Some(version)).unwrap(),
3073            config
3074        );
3075    }
3076
3077    #[test]
3078    fn unix_hardcoded_abi3_compile() {
3079        let host = triple!("x86_64-unknown-linux-gnu");
3080        let implementation = PythonImplementation::CPython;
3081        let version = PythonVersion::PY39;
3082        let config = InterpreterConfigBuilder::new(implementation, version)
3083            .stable_abi(StableAbi::Abi3)
3084            .finalize()
3085            .unwrap();
3086        assert_eq!(
3087            default_stable_abi_config(&host, Some(version), None).unwrap(),
3088            config
3089        );
3090    }
3091
3092    #[test]
3093    fn unix_hardcoded_abi3t_compile() {
3094        let host = triple!("x86_64-unknown-linux-gnu");
3095        let implementation = PythonImplementation::CPython;
3096        let version = PythonVersion::PY315;
3097        let config = InterpreterConfigBuilder::new(implementation, version)
3098            .stable_abi(StableAbi::Abi3t)
3099            .finalize()
3100            .unwrap();
3101        assert_eq!(
3102            default_stable_abi_config(&host, None, Some(version)).unwrap(),
3103            config
3104        );
3105    }
3106
3107    #[test]
3108    fn default_stable_abi_config_corner_cases() {
3109        let host = triple!("x86_64-unknown-linux-gnu");
3110        let py315 = Some("3.15".parse().unwrap());
3111        let py39 = Some("3.9".parse().unwrap());
3112        let implementation = PythonImplementation::CPython;
3113        let version = PythonVersion::PY39;
3114        let config = InterpreterConfigBuilder::new(implementation, version)
3115            .stable_abi(StableAbi::Abi3)
3116            .finalize()
3117            .unwrap();
3118        assert_eq!(
3119            default_stable_abi_config(&host, py39, py315).unwrap(),
3120            config
3121        );
3122        assert!(default_stable_abi_config(&host, None, py39).is_err());
3123    }
3124
3125    #[test]
3126    fn windows_hardcoded_cross_compile() {
3127        let env_vars = CrossCompileEnvVars {
3128            pyo3_cross: None,
3129            pyo3_cross_lib_dir: Some("C:\\some\\path".into()),
3130            pyo3_cross_python_implementation: None,
3131            pyo3_cross_python_version: Some("3.8".into()),
3132        };
3133
3134        let host = triple!("x86_64-unknown-linux-gnu");
3135        let target = triple!("i686-pc-windows-msvc");
3136        let cross_config =
3137            CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target)
3138                .unwrap()
3139                .unwrap();
3140
3141        let implementation = PythonImplementation::CPython;
3142        let version = PythonVersion::PY38;
3143        let config = InterpreterConfigBuilder::new(implementation, version)
3144            .lib_name("python38".to_string())
3145            .lib_dir("C:\\some\\path".to_string())
3146            .finalize()
3147            .unwrap();
3148        assert_eq!(default_cross_compile(&cross_config).unwrap(), config);
3149    }
3150
3151    #[test]
3152    fn mingw_hardcoded_cross_compile() {
3153        let env_vars = CrossCompileEnvVars {
3154            pyo3_cross: None,
3155            pyo3_cross_lib_dir: Some("/usr/lib/mingw".into()),
3156            pyo3_cross_python_implementation: None,
3157            pyo3_cross_python_version: Some("3.8".into()),
3158        };
3159
3160        let host = triple!("x86_64-unknown-linux-gnu");
3161        let target = triple!("i686-pc-windows-gnu");
3162        let cross_config =
3163            CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target)
3164                .unwrap()
3165                .unwrap();
3166
3167        let implementation = PythonImplementation::CPython;
3168        let version = PythonVersion::PY38;
3169        let config = InterpreterConfigBuilder::new(implementation, version)
3170            .lib_name("python38".to_string())
3171            .lib_dir("/usr/lib/mingw".to_string())
3172            .finalize()
3173            .unwrap();
3174        assert_eq!(default_cross_compile(&cross_config).unwrap(), config);
3175    }
3176
3177    #[test]
3178    fn unix_hardcoded_cross_compile() {
3179        let env_vars = CrossCompileEnvVars {
3180            pyo3_cross: None,
3181            pyo3_cross_lib_dir: Some("/usr/arm64/lib".into()),
3182            pyo3_cross_python_implementation: None,
3183            pyo3_cross_python_version: Some("3.9".into()),
3184        };
3185
3186        let host = triple!("x86_64-unknown-linux-gnu");
3187        let target = triple!("aarch64-unknown-linux-gnu");
3188        let cross_config =
3189            CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target)
3190                .unwrap()
3191                .unwrap();
3192
3193        let implementation = PythonImplementation::CPython;
3194        let version = PythonVersion::PY39;
3195        let config = InterpreterConfigBuilder::new(implementation, version)
3196            .lib_name("python3.9".to_string())
3197            .lib_dir("/usr/arm64/lib".to_string())
3198            .finalize()
3199            .unwrap();
3200        assert_eq!(default_cross_compile(&cross_config).unwrap(), config);
3201    }
3202
3203    #[test]
3204    fn pypy_hardcoded_cross_compile() {
3205        let env_vars = CrossCompileEnvVars {
3206            pyo3_cross: None,
3207            pyo3_cross_lib_dir: None,
3208            pyo3_cross_python_implementation: Some("PyPy".into()),
3209            pyo3_cross_python_version: Some("3.11".into()),
3210        };
3211
3212        let triple = triple!("x86_64-unknown-linux-gnu");
3213        let cross_config =
3214            CrossCompileConfig::try_from_env_vars_host_target(env_vars, &triple, &triple)
3215                .unwrap()
3216                .unwrap();
3217
3218        let implementation = PythonImplementation::PyPy;
3219        let version = PythonVersion::PY311;
3220        let config = InterpreterConfigBuilder::new(implementation, version)
3221            .lib_name("pypy3.11-c".to_string())
3222            .finalize()
3223            .unwrap();
3224        assert_eq!(default_cross_compile(&cross_config).unwrap(), config);
3225    }
3226
3227    // 3.14t cross-compile must produce a version-specific free-threaded ABI:
3228    // 3.14 is below MINIMUM_SUPPORTED_VERSION_ABI3T (3.15) so abi3t is unavailable,
3229    // and the free-threaded build does not support abi3 either.
3230    #[test]
3231    fn unix_free_threaded_pre_315_cross_compile() {
3232        let env_vars = CrossCompileEnvVars {
3233            pyo3_cross: None,
3234            pyo3_cross_lib_dir: None,
3235            pyo3_cross_python_implementation: None,
3236            pyo3_cross_python_version: Some("3.14t".into()),
3237        };
3238
3239        let host = triple!("x86_64-unknown-linux-gnu");
3240        let target = triple!("aarch64-unknown-linux-gnu");
3241        let cross_config =
3242            CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target)
3243                .unwrap()
3244                .unwrap();
3245
3246        let implementation = PythonImplementation::CPython;
3247        let version = PythonVersion::PY314;
3248        let config = InterpreterConfigBuilder::new(implementation, version)
3249            .free_threaded()
3250            .unwrap()
3251            .lib_name("python3.14t".to_string())
3252            .finalize()
3253            .unwrap();
3254        let result = default_cross_compile(&cross_config).unwrap();
3255        assert_eq!(result, config);
3256        assert_eq!(
3257            result.target_abi.kind(),
3258            PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)
3259        );
3260    }
3261
3262    #[test]
3263    fn windows_free_threaded_pre_315_cross_compile() {
3264        let env_vars = CrossCompileEnvVars {
3265            pyo3_cross: None,
3266            pyo3_cross_lib_dir: None,
3267            pyo3_cross_python_implementation: None,
3268            pyo3_cross_python_version: Some("3.14t".into()),
3269        };
3270
3271        let host = triple!("x86_64-unknown-linux-gnu");
3272        let target = triple!("x86_64-pc-windows-msvc");
3273        let cross_config =
3274            CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target)
3275                .unwrap()
3276                .unwrap();
3277
3278        let implementation = PythonImplementation::CPython;
3279        let version = PythonVersion::PY314;
3280        let config = InterpreterConfigBuilder::new(implementation, version)
3281            .free_threaded()
3282            .unwrap()
3283            .lib_name("python314t".to_string())
3284            .finalize()
3285            .unwrap();
3286        let result = default_cross_compile(&cross_config).unwrap();
3287        assert_eq!(result, config);
3288        assert_eq!(
3289            result.target_abi.kind(),
3290            PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)
3291        );
3292    }
3293
3294    // PYO3_CROSS_PYTHON_VERSION=3.15t with no abi3t-py3* feature active still
3295    // produces a version-specific free-threaded ABI rather than abi3t.
3296    #[test]
3297    fn unix_free_threaded_315_cross_compile() {
3298        let env_vars = CrossCompileEnvVars {
3299            pyo3_cross: None,
3300            pyo3_cross_lib_dir: None,
3301            pyo3_cross_python_implementation: None,
3302            pyo3_cross_python_version: Some("3.15t".into()),
3303        };
3304
3305        let host = triple!("x86_64-unknown-linux-gnu");
3306        let target = triple!("aarch64-unknown-linux-gnu");
3307        let cross_config =
3308            CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target)
3309                .unwrap()
3310                .unwrap();
3311
3312        let implementation = PythonImplementation::CPython;
3313        let version = PythonVersion::PY315;
3314        let config = InterpreterConfigBuilder::new(implementation, version)
3315            .free_threaded()
3316            .unwrap()
3317            .lib_name("python3.15t".to_string())
3318            .finalize()
3319            .unwrap();
3320        let result = default_cross_compile(&cross_config).unwrap();
3321        assert_eq!(result, config);
3322        assert_eq!(
3323            result.target_abi.kind(),
3324            PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)
3325        );
3326    }
3327
3328    #[test]
3329    fn default_lib_name_windows() {
3330        assert_eq!(
3331            super::default_lib_name_windows(
3332                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39)
3333                    .finalize()
3334                    .unwrap(),
3335                false,
3336                false,
3337            )
3338            .unwrap(),
3339            "python39",
3340        );
3341        // free-threaded Python 3.9 builds should be impossible
3342        assert!(
3343            PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39)
3344                .free_threaded()
3345                .finalize()
3346                .is_err()
3347        );
3348        assert_eq!(
3349            super::default_lib_name_windows(
3350                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39)
3351                    .stable_abi(StableAbi::Abi3)
3352                    .finalize()
3353                    .unwrap(),
3354                false,
3355                false,
3356            )
3357            .unwrap(),
3358            "python3",
3359        );
3360        assert_eq!(
3361            super::default_lib_name_windows(
3362                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39)
3363                    .finalize()
3364                    .unwrap(),
3365                true,
3366                false,
3367            )
3368            .unwrap(),
3369            "libpython3.9",
3370        );
3371        assert_eq!(
3372            super::default_lib_name_windows(
3373                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39)
3374                    .stable_abi(StableAbi::Abi3)
3375                    .finalize()
3376                    .unwrap(),
3377                true,
3378                false,
3379            )
3380            .unwrap(),
3381            "libpython3",
3382        );
3383        assert_eq!(
3384            super::default_lib_name_windows(
3385                PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39)
3386                    .stable_abi(StableAbi::Abi3)
3387                    .finalize()
3388                    .unwrap(),
3389                false,
3390                false,
3391            )
3392            .unwrap(),
3393            "libpypy3.9-c",
3394        );
3395        assert_eq!(
3396            super::default_lib_name_windows(
3397                PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311)
3398                    .stable_abi(StableAbi::Abi3)
3399                    .finalize()
3400                    .unwrap(),
3401                false,
3402                false,
3403            )
3404            .unwrap(),
3405            "libpypy3.11-c",
3406        );
3407        assert_eq!(
3408            super::default_lib_name_windows(
3409                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310)
3410                    .stable_abi(StableAbi::Abi3)
3411                    .finalize()
3412                    .unwrap(),
3413                false,
3414                true,
3415            )
3416            .unwrap(),
3417            "python3_d",
3418        );
3419        // abi3 debug builds on windows use version-specific lib on 3.9 and older
3420        // to workaround https://github.com/python/cpython/issues/101614
3421        assert_eq!(
3422            super::default_lib_name_windows(
3423                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39)
3424                    .stable_abi(StableAbi::Abi3)
3425                    .finalize()
3426                    .unwrap(),
3427                false,
3428                true,
3429            )
3430            .unwrap(),
3431            "python39_d",
3432        );
3433        assert_eq!(
3434            super::default_lib_name_windows(
3435                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310)
3436                    .stable_abi(StableAbi::Abi3)
3437                    .finalize()
3438                    .unwrap(),
3439                false,
3440                true,
3441            )
3442            .unwrap(),
3443            "python3_d",
3444        );
3445        assert_eq!(
3446            super::default_lib_name_windows(
3447                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313)
3448                    .free_threaded()
3449                    .finalize()
3450                    .unwrap(),
3451                false,
3452                false,
3453            )
3454            .unwrap(),
3455            "python313t",
3456        );
3457        assert_eq!(
3458            super::default_lib_name_windows(
3459                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313)
3460                    .free_threaded()
3461                    .finalize()
3462                    .unwrap(),
3463                false,
3464                true,
3465            )
3466            .unwrap(),
3467            "python313t_d",
3468        );
3469        assert_eq!(
3470            super::default_lib_name_windows(
3471                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY315)
3472                    .stable_abi(StableAbi::Abi3t)
3473                    .finalize()
3474                    .unwrap(),
3475                false,
3476                false,
3477            )
3478            .unwrap(),
3479            "python3t",
3480        );
3481        assert_eq!(
3482            super::default_lib_name_windows(
3483                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY315)
3484                    .stable_abi(StableAbi::Abi3t)
3485                    .finalize()
3486                    .unwrap(),
3487                false,
3488                true,
3489            )
3490            .unwrap(),
3491            "python3t_d",
3492        );
3493    }
3494
3495    #[test]
3496    fn default_lib_name_unix() {
3497        // Defaults to pythonX.Y for CPython 3.8+
3498        assert_eq!(
3499            super::default_lib_name_unix(
3500                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38)
3501                    .finalize()
3502                    .unwrap(),
3503                false,
3504                None,
3505            )
3506            .unwrap(),
3507            "python3.8",
3508        );
3509        assert_eq!(
3510            super::default_lib_name_unix(
3511                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39)
3512                    .finalize()
3513                    .unwrap(),
3514                false,
3515                None,
3516            )
3517            .unwrap(),
3518            "python3.9",
3519        );
3520        // Can use ldversion to override for CPython
3521        assert_eq!(
3522            super::default_lib_name_unix(
3523                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39)
3524                    .finalize()
3525                    .unwrap(),
3526                false,
3527                Some("3.8d"),
3528            )
3529            .unwrap(),
3530            "python3.8d",
3531        );
3532
3533        // PyPy 3.11 includes ldversion
3534        assert_eq!(
3535            super::default_lib_name_unix(
3536                PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311)
3537                    .finalize()
3538                    .unwrap(),
3539                false,
3540                None,
3541            )
3542            .unwrap(),
3543            "pypy3.11-c",
3544        );
3545
3546        assert_eq!(
3547            super::default_lib_name_unix(
3548                PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39)
3549                    .finalize()
3550                    .unwrap(),
3551                false,
3552                Some("3.11d"),
3553            )
3554            .unwrap(),
3555            "pypy3.11d-c",
3556        );
3557
3558        // free-threading adds a t suffix
3559        assert_eq!(
3560            super::default_lib_name_unix(
3561                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313)
3562                    .free_threaded()
3563                    .finalize()
3564                    .unwrap(),
3565                false,
3566                None,
3567            )
3568            .unwrap(),
3569            "python3.13t",
3570        );
3571        // cygwin abi3 links to unversioned libpython
3572        assert_eq!(
3573            super::default_lib_name_unix(
3574                PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313)
3575                    .stable_abi(StableAbi::Abi3)
3576                    .finalize()
3577                    .unwrap(),
3578                true,
3579                None,
3580            )
3581            .unwrap(),
3582            "python3",
3583        );
3584    }
3585
3586    #[test]
3587    fn abi_builder_error_paths() {
3588        let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39)
3589            .free_threaded()
3590            .finalize();
3591
3592        assert!(builder.is_err());
3593        assert!(builder.unwrap_err().to_string().contains("Cannot target"));
3594
3595        assert_eq!(
3596            PythonAbiBuilder::new(
3597                PythonImplementation::CPython,
3598                PythonVersion {
3599                    major: 3,
3600                    minor: 16,
3601                },
3602            )
3603            .stable_abi(StableAbi::Abi3)
3604            .finalize()
3605            .unwrap()
3606            .version
3607            .minor,
3608            STABLE_ABI_MAX_MINOR
3609        );
3610
3611        assert!("invalid".parse::<PythonAbi>().is_err());
3612        assert!("CPython-invalid".parse::<PythonAbi>().is_err());
3613        assert!("CPython-free_threaded-invalid"
3614            .parse::<PythonAbi>()
3615            .is_err());
3616
3617        let builder = PythonAbiBuilder::new(PythonImplementation::RustPython, PythonVersion::PY315)
3618            .free_threaded();
3619        let res = builder.finalize();
3620
3621        assert!(res.is_err());
3622        assert!(res
3623            .unwrap_err()
3624            .to_string()
3625            .contains("RustPython only supports targeting abi3t"));
3626    }
3627
3628    #[test]
3629    fn parse_cross_python_version() {
3630        let env_vars = CrossCompileEnvVars {
3631            pyo3_cross: None,
3632            pyo3_cross_lib_dir: None,
3633            pyo3_cross_python_version: Some("3.9".into()),
3634            pyo3_cross_python_implementation: None,
3635        };
3636
3637        assert_eq!(
3638            env_vars.parse_version().unwrap(),
3639            (Some(PythonVersion { major: 3, minor: 9 }), None),
3640        );
3641
3642        let env_vars = CrossCompileEnvVars {
3643            pyo3_cross: None,
3644            pyo3_cross_lib_dir: None,
3645            pyo3_cross_python_version: None,
3646            pyo3_cross_python_implementation: None,
3647        };
3648
3649        assert_eq!(env_vars.parse_version().unwrap(), (None, None));
3650
3651        let env_vars = CrossCompileEnvVars {
3652            pyo3_cross: None,
3653            pyo3_cross_lib_dir: None,
3654            pyo3_cross_python_version: Some("3.13t".into()),
3655            pyo3_cross_python_implementation: None,
3656        };
3657
3658        assert_eq!(
3659            env_vars.parse_version().unwrap(),
3660            (
3661                Some(PythonVersion {
3662                    major: 3,
3663                    minor: 13
3664                }),
3665                Some("t".into())
3666            ),
3667        );
3668
3669        let env_vars = CrossCompileEnvVars {
3670            pyo3_cross: None,
3671            pyo3_cross_lib_dir: None,
3672            pyo3_cross_python_version: Some("100".into()),
3673            pyo3_cross_python_implementation: None,
3674        };
3675
3676        assert!(env_vars.parse_version().is_err());
3677    }
3678
3679    #[test]
3680    fn target_abi3_version_different_from_host() {
3681        let implementation = PythonImplementation::CPython;
3682        let host_version = PythonVersion::PY39;
3683        let target_version = PythonVersion::PY38;
3684        let config = InterpreterConfigBuilder::new(implementation, host_version)
3685            .target_abi(
3686                PythonAbiBuilder::new(implementation, target_version)
3687                    .stable_abi(StableAbi::Abi3)
3688                    .finalize()
3689                    .unwrap(),
3690            )
3691            .finalize()
3692            .unwrap();
3693        assert_eq!(config.target_abi.version(), target_version);
3694        assert_eq!(config.version, host_version);
3695    }
3696
3697    #[test]
3698    fn stable_abi_applicability() {
3699        use PythonImplementation::*;
3700        let abi3 = Some(StableAbiVersion::Target(PythonVersion::PY310));
3701        let abi3t = Some(StableAbiVersion::Target(PythonVersion::PY315));
3702
3703        // 3.14t cannot target any stable ABI, so the features are ignored
3704        // rather than raising an error
3705        assert_eq!(
3706            applicable_stable_abi(CPython, PythonVersion::PY314, true, abi3, abi3t),
3707            None
3708        );
3709        // GIL-enabled below 3.15: only abi3 applies
3710        assert_eq!(
3711            applicable_stable_abi(CPython, PythonVersion::PY314, false, abi3, abi3t),
3712            Some((StableAbi::Abi3, PythonVersion::PY310))
3713        );
3714        assert_eq!(
3715            applicable_stable_abi(CPython, PythonVersion::PY314, false, None, abi3t),
3716            None
3717        );
3718        // 3.15+ GIL-enabled: abi3t preferred over abi3
3719        assert_eq!(
3720            applicable_stable_abi(CPython, PythonVersion::PY315, false, abi3, abi3t),
3721            Some((StableAbi::Abi3t, PythonVersion::PY315))
3722        );
3723        assert_eq!(
3724            applicable_stable_abi(CPython, PythonVersion::PY315, false, abi3, None),
3725            Some((StableAbi::Abi3, PythonVersion::PY310))
3726        );
3727        // 3.15+ free-threaded: only abi3t applies
3728        assert_eq!(
3729            applicable_stable_abi(CPython, PythonVersion::PY315, true, abi3, abi3t),
3730            Some((StableAbi::Abi3t, PythonVersion::PY315))
3731        );
3732        assert_eq!(
3733            applicable_stable_abi(CPython, PythonVersion::PY315, true, abi3, None),
3734            None
3735        );
3736        // a bare abi3/abi3t feature resolves to the interpreter version
3737        assert_eq!(
3738            applicable_stable_abi(
3739                CPython,
3740                PythonVersion::PY314,
3741                false,
3742                Some(StableAbiVersion::Current),
3743                None
3744            ),
3745            Some((StableAbi::Abi3, PythonVersion::PY314))
3746        );
3747        assert_eq!(
3748            applicable_stable_abi(
3749                CPython,
3750                PythonVersion::PY315,
3751                true,
3752                None,
3753                Some(StableAbiVersion::Current)
3754            ),
3755            Some((StableAbi::Abi3t, PythonVersion::PY315))
3756        );
3757        // PyPy and GraalPy: the kind applies but the version is never lowered
3758        assert_eq!(
3759            applicable_stable_abi(PyPy, PythonVersion::PY311, false, abi3, abi3t),
3760            Some((StableAbi::Abi3, PythonVersion::PY311))
3761        );
3762        assert_eq!(
3763            applicable_stable_abi(GraalPy, PythonVersion::PY311, false, abi3, abi3t),
3764            Some((StableAbi::Abi3, PythonVersion::PY311))
3765        );
3766        assert_eq!(
3767            applicable_stable_abi(PyPy, PythonVersion::PY311, false, None, abi3t),
3768            None
3769        );
3770    }
3771
3772    #[test]
3773    fn apply_build_env_preserves_target_implementation() {
3774        // the host `implementation` may differ from the `target_abi`
3775        // implementation; recomputing the target ABI from the build
3776        // environment must not switch it to the host's
3777        let config = InterpreterConfig::from_reader(
3778            "implementation=CPython\nversion=3.11\ntarget_abi=PyPy-gil_enabled-3.11".as_bytes(),
3779        )
3780        .unwrap()
3781        .apply_build_env()
3782        .unwrap();
3783        assert_eq!(
3784            config.target_abi.implementation(),
3785            PythonImplementation::PyPy
3786        );
3787        assert_eq!(
3788            config.target_abi.kind(),
3789            PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)
3790        );
3791        assert_eq!(config.target_abi.version(), PythonVersion::PY311);
3792    }
3793
3794    #[test]
3795    fn python_abi_from_stable_abi() {
3796        let implementation = PythonImplementation::CPython;
3797
3798        // no stable ABI: version-specific, free-threaded per gil_disabled
3799        let abi =
3800            PythonAbi::from_stable_abi(implementation, PythonVersion::PY314, None, true).unwrap();
3801        assert_eq!(
3802            abi.kind(),
3803            PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)
3804        );
3805        assert_eq!(abi.version(), PythonVersion::PY314);
3806
3807        let abi =
3808            PythonAbi::from_stable_abi(implementation, PythonVersion::PY314, None, false).unwrap();
3809        assert_eq!(
3810            abi.kind(),
3811            PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)
3812        );
3813
3814        // stable ABI: targets the minimum version
3815        let abi = PythonAbi::from_stable_abi(
3816            implementation,
3817            PythonVersion::PY314,
3818            Some((StableAbi::Abi3, PythonVersion::PY310)),
3819            false,
3820        )
3821        .unwrap();
3822        assert_eq!(abi.kind(), PythonAbiKind::Stable(StableAbi::Abi3));
3823        assert_eq!(abi.version(), PythonVersion::PY310);
3824
3825        // a minimum above the interpreter version errors, naming the right feature
3826        let error = PythonAbi::from_stable_abi(
3827            implementation,
3828            PythonVersion::PY314,
3829            Some((StableAbi::Abi3t, PythonVersion::PY315)),
3830            true,
3831        )
3832        .unwrap_err();
3833        assert!(error.to_string().contains(
3834            "cannot set a minimum Python version 3.15 higher than the interpreter version 3.14 \
3835             (the minimum Python version is implied by the abi3t-py315 feature)"
3836        ));
3837    }
3838
3839    #[test]
3840    fn config_file_applies_build_env() {
3841        // no abi3/abi3t cargo features are set when running tests, so
3842        // apply_build_env preserves the version-specific target ABI
3843        let config = InterpreterConfig::from_reader(
3844            "version=3.14\ntarget_abi=CPython-free_threaded-3.14\nbuild_flags=Py_GIL_DISABLED"
3845                .as_bytes(),
3846        )
3847        .unwrap()
3848        .apply_build_env()
3849        .unwrap();
3850        assert_eq!(
3851            config.target_abi.kind(),
3852            PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)
3853        );
3854        assert_eq!(config.target_abi.version(), PythonVersion::PY314);
3855
3856        // a stable ABI recorded in the config file is recomputed from the
3857        // (unset) features, so the result is version-specific
3858        let config =
3859            InterpreterConfig::from_reader("version=3.12\ntarget_abi=CPython-abi3-3.10".as_bytes())
3860                .unwrap()
3861                .apply_build_env()
3862                .unwrap();
3863        assert_eq!(
3864            config.target_abi.kind(),
3865            PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)
3866        );
3867        assert_eq!(config.target_abi.version(), PythonVersion::PY312);
3868    }
3869
3870    #[test]
3871    fn abi3_version_cannot_be_higher_than_interpreter() {
3872        if !have_python_interpreter() {
3873            return;
3874        }
3875
3876        let host_interpreter = get_host_interpreter(None, None).unwrap();
3877        let host_version = host_interpreter.version;
3878        let host_free_threaded = host_interpreter.target_abi.kind.is_free_threaded();
3879
3880        // skip these tests on 3.14t, pypy, and graalpy because they don't support any stable ABI
3881        if matches!(
3882            host_interpreter.implementation,
3883            PythonImplementation::PyPy | PythonImplementation::GraalPy
3884        ) || ((host_version == PythonVersion::PY314) && host_free_threaded)
3885        {
3886            return;
3887        }
3888
3889        let interpreter = get_host_interpreter(
3890            Some(StableAbiVersion::Target(PythonVersion {
3891                major: 3,
3892                minor: 45,
3893            })),
3894            None,
3895        );
3896        if !host_free_threaded {
3897            assert!(interpreter.unwrap_err().to_string().contains(
3898                "cannot set a minimum Python version 3.45 higher than the interpreter version"
3899            ));
3900            if host_version >= PythonVersion::PY313 {
3901                let interpreter = get_host_interpreter(
3902                    Some(StableAbiVersion::Target(PythonVersion::PY313)),
3903                    None,
3904                );
3905                assert_eq!(
3906                    interpreter.unwrap().target_abi.version(),
3907                    PythonVersion::PY313
3908                );
3909            }
3910        }
3911
3912        // If both features abi3 and abi3t features are active, the feature that "wins" depends on the host Python version
3913        if host_version >= PythonVersion::PY313 {
3914            let interpreter = get_host_interpreter(
3915                Some(StableAbiVersion::Target(PythonVersion::PY313)),
3916                Some(StableAbiVersion::Target(PythonVersion::PY315)),
3917            )
3918            .unwrap();
3919            assert_eq!(
3920                interpreter.target_abi.version(),
3921                if host_version >= PythonVersion::PY315 {
3922                    PythonVersion::PY315
3923                } else {
3924                    PythonVersion::PY313
3925                }
3926            );
3927        }
3928    }
3929
3930    #[test]
3931    #[cfg(all(target_os = "linux", target_arch = "x86_64",))]
3932    fn parse_sysconfigdata() {
3933        // A best effort attempt to get test coverage for the sysconfigdata parsing.
3934        // Might not complete successfully depending on host installation; that's ok as long as
3935        // CI demonstrates this path is covered!
3936
3937        let Ok(interpreter_config) = make_interpreter_config() else {
3938            // Couldn't get an interpreter config, won't be able to test a matching sysconfigdata,
3939            // never mind. (This is intended for coverage, don't mind if it fails if it doesn't run.)
3940            return;
3941        };
3942
3943        let lib_dir = match &interpreter_config.lib_dir {
3944            Some(lib_dir) => Path::new(lib_dir),
3945            // Don't know where to search for sysconfigdata; never mind.
3946            None => return,
3947        };
3948
3949        let cross = CrossCompileConfig {
3950            lib_dir: Some(lib_dir.into()),
3951            version: Some(interpreter_config.version),
3952            implementation: Some(interpreter_config.implementation),
3953            target: triple!("x86_64-unknown-linux-gnu"),
3954            abiflags: if interpreter_config.target_abi.kind().is_free_threaded() {
3955                Some("t".into())
3956            } else {
3957                None
3958            },
3959        };
3960
3961        let sysconfigdata_path = match find_sysconfigdata(&cross) {
3962            Ok(Some(path)) => path,
3963            // Couldn't find a matching sysconfigdata; never mind!
3964            _ => return,
3965        };
3966        let sysconfigdata = super::parse_sysconfigdata(sysconfigdata_path).unwrap();
3967        let mut parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap();
3968
3969        // Workaround case where empty `PYTHONFRAMEWORKPREFIX` is returned as empty string instead of None,
3970        // which causes the assert_eq! below to fail.
3971        //
3972        // TODO: probably should deprecate using this variable at all, seemingly only used in `add_python_framework_link_args`
3973        // which is probably a strictly worse version of `add_libpython_rpath_link_args`.
3974        if parsed_config.python_framework_prefix.as_deref() == Some("") {
3975            parsed_config.python_framework_prefix = None;
3976        }
3977
3978        assert_eq!(parsed_config.implementation, PythonImplementation::CPython);
3979        assert_eq!(
3980            parsed_config,
3981            InterpreterConfigBuilder::new(
3982                interpreter_config.implementation,
3983                interpreter_config.version,
3984            )
3985            .build_flags(interpreter_config.build_flags().clone())
3986            .pointer_width(64)
3987            .lib_dir(interpreter_config.lib_dir().map(str::to_owned))
3988            .lib_name(interpreter_config.lib_name().map(str::to_owned))
3989            .finalize()
3990            .unwrap()
3991        )
3992    }
3993
3994    #[test]
3995    fn test_venv_interpreter() {
3996        let base = OsStr::new("base");
3997        assert_eq!(
3998            venv_interpreter(base, true),
3999            PathBuf::from_iter(&["base", "Scripts", "python.exe"])
4000        );
4001        assert_eq!(
4002            venv_interpreter(base, false),
4003            PathBuf::from_iter(&["base", "bin", "python"])
4004        );
4005    }
4006
4007    #[test]
4008    fn test_conda_env_interpreter() {
4009        let base = OsStr::new("base");
4010        assert_eq!(
4011            conda_env_interpreter(base, true),
4012            PathBuf::from_iter(&["base", "python.exe"])
4013        );
4014        assert_eq!(
4015            conda_env_interpreter(base, false),
4016            PathBuf::from_iter(&["base", "bin", "python"])
4017        );
4018    }
4019
4020    #[test]
4021    fn test_not_cross_compiling_from_to() {
4022        assert!(cross_compiling_from_to(
4023            &triple!("x86_64-unknown-linux-gnu"),
4024            &triple!("x86_64-unknown-linux-gnu"),
4025        )
4026        .unwrap()
4027        .is_none());
4028
4029        assert!(cross_compiling_from_to(
4030            &triple!("x86_64-apple-darwin"),
4031            &triple!("x86_64-apple-darwin")
4032        )
4033        .unwrap()
4034        .is_none());
4035
4036        assert!(cross_compiling_from_to(
4037            &triple!("aarch64-apple-darwin"),
4038            &triple!("x86_64-apple-darwin")
4039        )
4040        .unwrap()
4041        .is_none());
4042
4043        assert!(cross_compiling_from_to(
4044            &triple!("x86_64-apple-darwin"),
4045            &triple!("aarch64-apple-darwin")
4046        )
4047        .unwrap()
4048        .is_none());
4049
4050        assert!(cross_compiling_from_to(
4051            &triple!("x86_64-pc-windows-msvc"),
4052            &triple!("i686-pc-windows-msvc")
4053        )
4054        .unwrap()
4055        .is_none());
4056
4057        assert!(cross_compiling_from_to(
4058            &triple!("x86_64-unknown-linux-gnu"),
4059            &triple!("x86_64-unknown-linux-musl")
4060        )
4061        .unwrap()
4062        .is_none());
4063
4064        assert!(cross_compiling_from_to(
4065            &triple!("x86_64-pc-windows-msvc"),
4066            &triple!("x86_64-win7-windows-msvc"),
4067        )
4068        .unwrap()
4069        .is_none());
4070    }
4071
4072    #[test]
4073    fn test_is_cross_compiling_from_to() {
4074        assert!(cross_compiling_from_to(
4075            &triple!("x86_64-pc-windows-msvc"),
4076            &triple!("aarch64-pc-windows-msvc")
4077        )
4078        .unwrap()
4079        .is_some());
4080    }
4081
4082    #[test]
4083    fn test_run_python_script() {
4084        // as above, this should be okay in CI where Python is presumed installed
4085        let interpreter = make_interpreter_config()
4086            .expect("could not get InterpreterConfig from installed interpreter");
4087        let out = interpreter
4088            .run_python_script("print(2 + 2)")
4089            .expect("failed to run Python script");
4090        assert_eq!(out.trim_end(), "4");
4091    }
4092
4093    #[test]
4094    fn test_run_python_script_with_envs() {
4095        // as above, this should be okay in CI where Python is presumed installed
4096        let interpreter = make_interpreter_config()
4097            .expect("could not get InterpreterConfig from installed interpreter");
4098        let out = interpreter
4099            .run_python_script_with_envs(
4100                "import os; print(os.getenv('PYO3_TEST'))",
4101                vec![("PYO3_TEST", "42")],
4102            )
4103            .expect("failed to run Python script");
4104        assert_eq!(out.trim_end(), "42");
4105    }
4106
4107    #[test]
4108    fn test_build_script_outputs_base() {
4109        let implementation = PythonImplementation::CPython;
4110        let version = PythonVersion::PY311;
4111        let interpreter_config = InterpreterConfigBuilder::new(implementation, version)
4112            .finalize()
4113            .unwrap();
4114        assert_eq!(
4115            interpreter_config.build_script_outputs(),
4116            [
4117                "cargo:rustc-cfg=Py_3_8".to_owned(),
4118                "cargo:rustc-cfg=Py_3_9".to_owned(),
4119                "cargo:rustc-cfg=Py_3_10".to_owned(),
4120                "cargo:rustc-cfg=Py_3_11".to_owned(),
4121            ]
4122        );
4123
4124        let interpreter_config = InterpreterConfigBuilder::new(PythonImplementation::PyPy, version)
4125            .finalize()
4126            .unwrap();
4127        assert_eq!(
4128            interpreter_config.build_script_outputs(),
4129            [
4130                "cargo:rustc-cfg=Py_3_8".to_owned(),
4131                "cargo:rustc-cfg=Py_3_9".to_owned(),
4132                "cargo:rustc-cfg=Py_3_10".to_owned(),
4133                "cargo:rustc-cfg=Py_3_11".to_owned(),
4134                "cargo:rustc-cfg=PyPy".to_owned(),
4135            ]
4136        );
4137
4138        let interpreter_config =
4139            InterpreterConfigBuilder::new(PythonImplementation::RustPython, version)
4140                .finalize()
4141                .unwrap();
4142        assert_eq!(
4143            interpreter_config.build_script_outputs(),
4144            [
4145                "cargo:rustc-cfg=Py_3_8".to_owned(),
4146                "cargo:rustc-cfg=Py_3_9".to_owned(),
4147                "cargo:rustc-cfg=Py_3_10".to_owned(),
4148                "cargo:rustc-cfg=Py_3_11".to_owned(),
4149                "cargo:rustc-cfg=RustPython".to_owned(),
4150                "cargo:rustc-cfg=Py_LIMITED_API".to_owned(),
4151                "cargo:rustc-cfg=Py_GIL_DISABLED".to_owned(),
4152            ]
4153        );
4154    }
4155
4156    #[test]
4157    fn test_build_script_outputs_abi3() {
4158        let implementation = PythonImplementation::CPython;
4159        let version = PythonVersion::PY39;
4160        let interpreter_config = InterpreterConfigBuilder::new(implementation, version)
4161            .stable_abi(StableAbi::Abi3)
4162            .finalize()
4163            .unwrap();
4164
4165        assert_eq!(
4166            interpreter_config.build_script_outputs(),
4167            [
4168                "cargo:rustc-cfg=Py_3_8".to_owned(),
4169                "cargo:rustc-cfg=Py_3_9".to_owned(),
4170                "cargo:rustc-cfg=Py_LIMITED_API".to_owned(),
4171            ]
4172        );
4173
4174        let interpreter_config = InterpreterConfigBuilder::new(PythonImplementation::PyPy, version)
4175            .stable_abi(StableAbi::Abi3)
4176            .finalize()
4177            .unwrap();
4178        assert_eq!(
4179            interpreter_config.build_script_outputs(),
4180            [
4181                "cargo:rustc-cfg=Py_3_8".to_owned(),
4182                "cargo:rustc-cfg=Py_3_9".to_owned(),
4183                "cargo:rustc-cfg=PyPy".to_owned(),
4184                "cargo:rustc-cfg=Py_LIMITED_API".to_owned(),
4185            ]
4186        );
4187
4188        let interpreter_config =
4189            InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY315)
4190                .stable_abi(StableAbi::Abi3)
4191                .finalize()
4192                .unwrap();
4193        assert_eq!(
4194            interpreter_config.build_script_outputs(),
4195            [
4196                "cargo:rustc-cfg=Py_3_8".to_owned(),
4197                "cargo:rustc-cfg=Py_3_9".to_owned(),
4198                "cargo:rustc-cfg=Py_3_10".to_owned(),
4199                "cargo:rustc-cfg=Py_3_11".to_owned(),
4200                "cargo:rustc-cfg=Py_3_12".to_owned(),
4201                "cargo:rustc-cfg=Py_3_13".to_owned(),
4202                "cargo:rustc-cfg=Py_3_14".to_owned(),
4203                "cargo:rustc-cfg=Py_3_15".to_owned(),
4204                "cargo:rustc-cfg=Py_LIMITED_API".to_owned(),
4205            ]
4206        );
4207    }
4208
4209    #[test]
4210    fn test_build_script_outputs_gil_disabled() {
4211        let interpreter_config =
4212            InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY313)
4213                .free_threaded()
4214                .unwrap()
4215                .finalize()
4216                .unwrap();
4217        assert_eq!(
4218            interpreter_config.build_script_outputs(),
4219            [
4220                "cargo:rustc-cfg=Py_3_8".to_owned(),
4221                "cargo:rustc-cfg=Py_3_9".to_owned(),
4222                "cargo:rustc-cfg=Py_3_10".to_owned(),
4223                "cargo:rustc-cfg=Py_3_11".to_owned(),
4224                "cargo:rustc-cfg=Py_3_12".to_owned(),
4225                "cargo:rustc-cfg=Py_3_13".to_owned(),
4226                "cargo:rustc-cfg=Py_GIL_DISABLED".to_owned(),
4227            ]
4228        );
4229    }
4230
4231    #[test]
4232    fn test_interpreter_config_builder_gil_disabled_flag() {
4233        let builder = InterpreterConfigBuilder::new(
4234            PythonImplementation::CPython,
4235            PythonVersion {
4236                major: 3,
4237                minor: 14,
4238            },
4239        );
4240        let mut flags = BuildFlags::new();
4241        flags.0.insert(BuildFlag::Py_GIL_DISABLED);
4242        let config = builder
4243            .stable_abi(StableAbi::Abi3)
4244            .build_flags(flags)
4245            .finalize()
4246            .unwrap();
4247        // build flags win due to backward compatibility (abi3 feature is a no-op on ft builds)
4248        assert!(config.target_abi.kind() == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded));
4249
4250        // The reconciliation is order-independent: build_flags first, then stable_abi(Abi3)
4251        // produces the same result as the previous ordering.
4252        let builder = InterpreterConfigBuilder::new(
4253            PythonImplementation::CPython,
4254            PythonVersion {
4255                major: 3,
4256                minor: 14,
4257            },
4258        );
4259        let mut flags = BuildFlags::new();
4260        flags.0.insert(BuildFlag::Py_GIL_DISABLED);
4261        let config = builder
4262            .build_flags(flags)
4263            .stable_abi(StableAbi::Abi3)
4264            .finalize()
4265            .unwrap();
4266        assert!(config.target_abi.kind() == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded));
4267
4268        // Explicit GIL-enabled target with Py_GIL_DISABLED in build flags is contradictory and
4269        // is rejected at finalize regardless of the order in which the setters were called.
4270        let builder = InterpreterConfigBuilder::new(
4271            PythonImplementation::CPython,
4272            PythonVersion {
4273                major: 3,
4274                minor: 14,
4275            },
4276        );
4277        let target_abi = PythonAbiBuilder::new(
4278            PythonImplementation::CPython,
4279            PythonVersion {
4280                major: 3,
4281                minor: 14,
4282            },
4283        )
4284        .finalize()
4285        .unwrap();
4286        let mut flags = BuildFlags::new();
4287        flags.0.insert(BuildFlag::Py_GIL_DISABLED);
4288        assert!(builder
4289            .target_abi(target_abi)
4290            .build_flags(flags)
4291            .finalize()
4292            .is_err());
4293
4294        let builder = InterpreterConfigBuilder::new(
4295            PythonImplementation::CPython,
4296            PythonVersion {
4297                major: 3,
4298                minor: 14,
4299            },
4300        );
4301        let config = builder.free_threaded().unwrap().finalize().unwrap();
4302        assert!(config.target_abi.kind().is_free_threaded());
4303        assert!(config.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED));
4304    }
4305
4306    #[test]
4307    fn test_build_script_outputs_debug() {
4308        let mut build_flags = BuildFlags::default();
4309        build_flags.0.insert(BuildFlag::Py_DEBUG);
4310        let implementation = PythonImplementation::CPython;
4311        let version = PythonVersion::PY38;
4312        let interpreter_config = InterpreterConfigBuilder::new(implementation, version)
4313            .build_flags(build_flags)
4314            .finalize()
4315            .unwrap();
4316        assert_eq!(
4317            interpreter_config.build_script_outputs(),
4318            [
4319                "cargo:rustc-cfg=Py_3_8".to_owned(),
4320                "cargo:rustc-cfg=py_sys_config=\"Py_DEBUG\"".to_owned(),
4321            ]
4322        );
4323    }
4324
4325    #[test]
4326    fn test_find_sysconfigdata_in_invalid_lib_dir() {
4327        let e = find_all_sysconfigdata(&CrossCompileConfig {
4328            lib_dir: Some(PathBuf::from("/abc/123/not/a/real/path")),
4329            version: None,
4330            implementation: None,
4331            target: triple!("x86_64-unknown-linux-gnu"),
4332            abiflags: None,
4333        })
4334        .unwrap_err();
4335
4336        // actual error message is platform-dependent, so just check the context we add
4337        assert!(e.report().to_string().starts_with(
4338            "failed to search the lib dir at 'PYO3_CROSS_LIB_DIR=/abc/123/not/a/real/path'\n\
4339            caused by:\n  \
4340              - 0: failed to list the entries in '/abc/123/not/a/real/path'\n  \
4341              - 1: \
4342            "
4343        ));
4344    }
4345
4346    #[test]
4347    fn test_from_pyo3_config_file_env_rebuild() {
4348        READ_ENV_VARS.with(|vars| vars.borrow_mut().clear());
4349        let _ = InterpreterConfig::from_pyo3_config_file_env(&Triple::host());
4350        // it's possible that other env vars were also read, hence just checking for contains
4351        READ_ENV_VARS.with(|vars| assert!(vars.borrow().contains(&"PYO3_CONFIG_FILE".to_string())));
4352    }
4353
4354    #[test]
4355    fn test_default_lib_name_for_target() {
4356        let cpython = PythonImplementation::CPython;
4357        let pypy = PythonImplementation::PyPy;
4358        let py39 = PythonVersion::PY39;
4359        let py311 = PythonVersion {
4360            major: 3,
4361            minor: 11,
4362        };
4363        let py313 = PythonVersion {
4364            major: 3,
4365            minor: 13,
4366        };
4367        let cpy39 = PythonAbiBuilder::new(cpython, py39).finalize().unwrap();
4368        let pypy311 = PythonAbiBuilder::new(pypy, py311).finalize().unwrap();
4369        let cpy313t = PythonAbiBuilder::new(cpython, py313)
4370            .free_threaded()
4371            .finalize()
4372            .unwrap();
4373        let cpy313_abi3 = PythonAbiBuilder::new(cpython, py313)
4374            .stable_abi(StableAbi::Abi3)
4375            .finalize()
4376            .unwrap();
4377
4378        let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap();
4379        let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap();
4380        let win_arm64 = Triple::from_str("aarch64-pc-windows-msvc").unwrap();
4381
4382        let lib_name = default_lib_name_for_target(cpy39, &unix);
4383        assert_eq!(lib_name, "python3.9");
4384
4385        let lib_name = default_lib_name_for_target(cpy39, &win_x64);
4386        assert_eq!(lib_name, "python39");
4387
4388        let lib_name = default_lib_name_for_target(cpy39, &win_arm64);
4389        assert_eq!(lib_name, "python39");
4390
4391        // PyPy
4392        let lib_name = default_lib_name_for_target(pypy311, &unix);
4393        assert_eq!(lib_name, "pypy3.11-c");
4394
4395        let lib_name = default_lib_name_for_target(pypy311, &win_x64);
4396        assert_eq!(lib_name, "libpypy3.11-c");
4397
4398        // Free-threaded
4399        let lib_name = default_lib_name_for_target(cpy313t, &unix);
4400        assert_eq!(lib_name, "python3.13t");
4401
4402        let lib_name = default_lib_name_for_target(cpy313t, &win_x64);
4403        assert_eq!(lib_name, "python313t");
4404
4405        let lib_name = default_lib_name_for_target(cpy313t, &win_arm64);
4406        assert_eq!(lib_name, "python313t");
4407
4408        // abi3
4409        let lib_name = default_lib_name_for_target(cpy313_abi3, &unix);
4410        assert_eq!(lib_name, "python3.13");
4411
4412        let lib_name = default_lib_name_for_target(cpy313_abi3, &win_x64);
4413        assert_eq!(lib_name, "python3");
4414
4415        let lib_name = default_lib_name_for_target(cpy313_abi3, &win_arm64);
4416        assert_eq!(lib_name, "python3");
4417    }
4418}