pyo3_build_config/
impl_.rs

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