Skip to main content

pyforge_build_config/
impl_.rs

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