Skip to main content

pyo3_build_config/
impl_.rs

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