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