pyo3_build_config/
impl_.rs

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