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