pyo3_build_config/
impl_.rs

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