pyo3_build_config/
impl_.rs

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