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" ] |
7 | mod import_lib; |
8 | |
9 | #[cfg (test)] |
10 | use std::cell::RefCell; |
11 | use 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 | |
23 | pub use target_lexicon::Triple; |
24 | |
25 | use target_lexicon::{Architecture, Environment, OperatingSystem}; |
26 | |
27 | use crate::{ |
28 | bail, ensure, |
29 | errors::{Context, Error, Result}, |
30 | warn, |
31 | }; |
32 | |
33 | /// Minimum Python version PyO3 supports. |
34 | pub(crate) const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 }; |
35 | |
36 | /// GraalPy may implement the same CPython version over multiple releases. |
37 | const 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. |
43 | pub(crate) const ABI3_MAX_MINOR: u8 = 13; |
44 | |
45 | #[cfg (test)] |
46 | thread_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. |
53 | pub fn cargo_env_var(var: &str) -> Option<String> { |
54 | env::var_os(key:var).map(|os_string: OsString| 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. |
59 | pub 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(key: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. |
75 | pub 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(msg:"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))] |
90 | pub 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 | |
174 | impl 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. |
213 | from __future__ import print_function |
214 | |
215 | import os.path |
216 | import platform |
217 | import struct |
218 | import sys |
219 | from sysconfig import get_config_var, get_platform |
220 | |
221 | PYPY = platform.python_implementation() == "PyPy" |
222 | GRAALPY = platform.python_implementation() == "GraalVM" |
223 | |
224 | if 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. |
231 | base_prefix = getattr(sys, "base_prefix", None) |
232 | |
233 | if 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")) |
238 | else: |
239 | ANACONDA = False |
240 | |
241 | def print_if_set(varname, value): |
242 | if value is not None: |
243 | print(varname, value) |
244 | |
245 | # Windows always uses shared linking |
246 | WINDOWS = platform.system() == "Windows" |
247 | |
248 | # macOS framework packages use shared linking |
249 | FRAMEWORK = bool(get_config_var("PYTHONFRAMEWORK")) |
250 | FRAMEWORK_PREFIX = get_config_var("PYTHONFRAMEWORKPREFIX") |
251 | |
252 | # unix-style shared library enabled |
253 | SHARED = bool(get_config_var("Py_ENABLE_SHARED")) |
254 | |
255 | print("implementation", platform.python_implementation()) |
256 | print("version_major", sys.version_info[0]) |
257 | print("version_minor", sys.version_info[1]) |
258 | print("shared", PYPY or GRAALPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED) |
259 | print("python_framework_prefix", FRAMEWORK_PREFIX) |
260 | print_if_set("ld_version", get_config_var("LDVERSION")) |
261 | print_if_set("libdir", get_config_var("LIBDIR")) |
262 | print_if_set("base_prefix", base_prefix) |
263 | print("executable", sys.executable) |
264 | print("calcsize_pointer", struct.calcsize("P")) |
265 | print("mingw", get_platform().startswith("mingw")) |
266 | print("ext_suffix", get_config_var("EXT_SUFFIX")) |
267 | print("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)] |
740 | pub struct PythonVersion { |
741 | pub major: u8, |
742 | pub minor: u8, |
743 | } |
744 | |
745 | impl 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 | |
757 | impl 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 | |
763 | impl FromStr for PythonVersion { |
764 | type Err = crate::errors::Error; |
765 | |
766 | fn from_str(value: &str) -> Result<Self, Self::Err> { |
767 | let mut split: SplitN<'_, char> = value.splitn(n:2, pat:'.' ); |
768 | let (major: &str, minor: &str) = ( |
769 | split |
770 | .next() |
771 | .expect(msg:"first splitn value should always be present" ), |
772 | split.next().ok_or(err:"expected major.minor version" )?, |
773 | ); |
774 | Ok(Self { |
775 | major: major.parse().context(message:"failed to parse major version" )?, |
776 | minor: minor.parse().context(message:"failed to parse minor version" )?, |
777 | }) |
778 | } |
779 | } |
780 | |
781 | #[derive (Debug, Copy, Clone, PartialEq, Eq)] |
782 | pub enum PythonImplementation { |
783 | CPython, |
784 | PyPy, |
785 | GraalPy, |
786 | } |
787 | |
788 | impl 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 | |
813 | impl 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 | |
823 | impl 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. |
839 | fn 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. |
846 | fn is_abi3() -> bool { |
847 | cargo_env_var("CARGO_FEATURE_ABI3" ).is_some() |
848 | || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY" ).map_or(default:false, |os_str: OsString| 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. |
854 | pub fn get_abi3_version() -> Option<PythonVersion> { |
855 | let minor_version: Option = (MINIMUM_SUPPORTED_VERSION.minor..=ABI3_MAX_MINOR) |
856 | .find(|i: &u8| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3 {}" , i)).is_some()); |
857 | minor_version.map(|minor: u8| 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. |
863 | pub 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. |
870 | pub 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. |
877 | fn 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. |
890 | fn require_libdir_for_target(target: &Triple) -> bool { |
891 | let is_generating_libpython: bool = 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)] |
903 | pub 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 | |
920 | impl 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 |
984 | struct 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 | |
995 | impl 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. |
1089 | pub fn cross_compiling_from_to( |
1090 | host: &Triple, |
1091 | target: &Triple, |
1092 | ) -> Result<Option<CrossCompileConfig>> { |
1093 | let env_vars: CrossCompileEnvVars = 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. |
1102 | pub fn cross_compiling_from_cargo_env() -> Result<Option<CrossCompileConfig>> { |
1103 | let env_vars: CrossCompileEnvVars = CrossCompileEnvVars::from_env(); |
1104 | let host: Triple = Triple::host(); |
1105 | let target: Triple = 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)] |
1112 | pub 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 | |
1121 | impl Display for BuildFlag { |
1122 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
1123 | match self { |
1124 | BuildFlag::Other(flag: &String) => write!(f, " {}" , flag), |
1125 | _ => write!(f, " {:?}" , self), |
1126 | } |
1127 | } |
1128 | } |
1129 | |
1130 | impl 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: &str => 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 |
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)] |
1159 | pub struct BuildFlags(pub HashSet<BuildFlag>); |
1160 | |
1161 | impl 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 | |
1234 | impl Display for BuildFlags { |
1235 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
1236 | let mut first: bool = true; |
1237 | for flag: &BuildFlag 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 | |
1249 | impl 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 = HashSet::new(); |
1254 | for flag: &str in value.split_terminator(',' ) { |
1255 | flags.insert(flag.parse().unwrap()); |
1256 | } |
1257 | Ok(BuildFlags(flags)) |
1258 | } |
1259 | } |
1260 | |
1261 | fn parse_script_output(output: &str) -> HashMap<String, String> { |
1262 | outputimpl Iterator |
1263 | .lines() |
1264 | .filter_map(|line: &str| { |
1265 | let mut i: SplitN<'_, char> = line.splitn(n:2, pat:' ' ); |
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. |
1274 | pub struct Sysconfigdata(HashMap<String, String>); |
1275 | |
1276 | impl 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). |
1299 | pub fn parse_sysconfigdata(sysconfigdata_path: impl AsRef<Path>) -> Result<Sysconfigdata> { |
1300 | let sysconfigdata_path: &Path = sysconfigdata_path.as_ref(); |
1301 | let mut script: String = 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#" |
1308 | for key, val in build_time_vars.items(): |
1309 | print(key, val) |
1310 | "# ; |
1311 | |
1312 | let output: String = run_python_script(&find_interpreter()?, &script)?; |
1313 | |
1314 | Ok(Sysconfigdata(parse_script_output(&output))) |
1315 | } |
1316 | |
1317 | fn starts_with(entry: &DirEntry, pat: &str) -> bool { |
1318 | let name: OsString = entry.file_name(); |
1319 | name.to_string_lossy().starts_with(pat) |
1320 | } |
1321 | fn ends_with(entry: &DirEntry, pat: &str) -> bool { |
1322 | let name: OsString = entry.file_name(); |
1323 | name.to_string_lossy().ends_with(pat) |
1324 | } |
1325 | |
1326 | /// Finds the sysconfigdata file when the target Python library directory is set. |
1327 | /// |
1328 | /// Returns `None` if the library directory is not available, and a runtime error |
1329 | /// when no or multiple sysconfigdata files are found. |
1330 | fn find_sysconfigdata(cross: &CrossCompileConfig) -> Result<Option<PathBuf>> { |
1331 | let mut sysconfig_paths = find_all_sysconfigdata(cross)?; |
1332 | if sysconfig_paths.is_empty() { |
1333 | if let Some(lib_dir) = cross.lib_dir.as_ref() { |
1334 | bail!("Could not find _sysconfigdata*.py in {}" , lib_dir.display()); |
1335 | } else { |
1336 | // Continue with the default configuration when PYO3_CROSS_LIB_DIR is not set. |
1337 | return Ok(None); |
1338 | } |
1339 | } else if sysconfig_paths.len() > 1 { |
1340 | let mut error_msg = String::from( |
1341 | "Detected multiple possible Python versions. Please set either the \ |
1342 | PYO3_CROSS_PYTHON_VERSION variable to the wanted version or the \ |
1343 | _PYTHON_SYSCONFIGDATA_NAME variable to the wanted sysconfigdata file name. \n\n\ |
1344 | sysconfigdata files found:" , |
1345 | ); |
1346 | for path in sysconfig_paths { |
1347 | use std::fmt::Write; |
1348 | write!(&mut error_msg, " \n\t{}" , path.display()).unwrap(); |
1349 | } |
1350 | bail!(" {}\n" , error_msg); |
1351 | } |
1352 | |
1353 | Ok(Some(sysconfig_paths.remove(0))) |
1354 | } |
1355 | |
1356 | /// Finds `_sysconfigdata*.py` files for detected Python interpreters. |
1357 | /// |
1358 | /// From the python source for `_sysconfigdata*.py` is always going to be located at |
1359 | /// `build/lib.{PLATFORM}-{PY_MINOR_VERSION}` when built from source. The [exact line][1] is defined as: |
1360 | /// |
1361 | /// ```py |
1362 | /// pybuilddir = 'build/lib.%s-%s' % (get_platform(), sys.version_info[:2]) |
1363 | /// ``` |
1364 | /// |
1365 | /// Where get_platform returns a kebab-case formatted string containing the os, the architecture and |
1366 | /// possibly the os' kernel version (not the case on linux). However, when installed using a package |
1367 | /// manager, the `_sysconfigdata*.py` file is installed in the `${PREFIX}/lib/python3.Y/` directory. |
1368 | /// The `_sysconfigdata*.py` is generally in a sub-directory of the location of `libpython3.Y.so`. |
1369 | /// So we must find the file in the following possible locations: |
1370 | /// |
1371 | /// ```sh |
1372 | /// # distribution from package manager, (lib_dir may or may not include lib/) |
1373 | /// ${INSTALL_PREFIX}/lib/python3.Y/_sysconfigdata*.py |
1374 | /// ${INSTALL_PREFIX}/lib/libpython3.Y.so |
1375 | /// ${INSTALL_PREFIX}/lib/python3.Y/config-3.Y-${HOST_TRIPLE}/libpython3.Y.so |
1376 | /// |
1377 | /// # Built from source from host |
1378 | /// ${CROSS_COMPILED_LOCATION}/build/lib.linux-x86_64-Y/_sysconfigdata*.py |
1379 | /// ${CROSS_COMPILED_LOCATION}/libpython3.Y.so |
1380 | /// |
1381 | /// # if cross compiled, kernel release is only present on certain OS targets. |
1382 | /// ${CROSS_COMPILED_LOCATION}/build/lib.{OS}(-{OS-KERNEL-RELEASE})?-{ARCH}-Y/_sysconfigdata*.py |
1383 | /// ${CROSS_COMPILED_LOCATION}/libpython3.Y.so |
1384 | /// |
1385 | /// # PyPy includes a similar file since v73 |
1386 | /// ${INSTALL_PREFIX}/lib/pypy3.Y/_sysconfigdata.py |
1387 | /// ${INSTALL_PREFIX}/lib_pypy/_sysconfigdata.py |
1388 | /// ``` |
1389 | /// |
1390 | /// [1]: https://github.com/python/cpython/blob/3.5/Lib/sysconfig.py#L389 |
1391 | /// |
1392 | /// Returns an empty vector when the target Python library directory |
1393 | /// is not set via `PYO3_CROSS_LIB_DIR`. |
1394 | pub fn find_all_sysconfigdata(cross: &CrossCompileConfig) -> Result<Vec<PathBuf>> { |
1395 | let sysconfig_paths = if let Some(lib_dir) = cross.lib_dir.as_ref() { |
1396 | search_lib_dir(lib_dir, cross).with_context(|| { |
1397 | format!( |
1398 | "failed to search the lib dir at 'PYO3_CROSS_LIB_DIR= {}'" , |
1399 | lib_dir.display() |
1400 | ) |
1401 | })? |
1402 | } else { |
1403 | return Ok(Vec::new()); |
1404 | }; |
1405 | |
1406 | let sysconfig_name = env_var("_PYTHON_SYSCONFIGDATA_NAME" ); |
1407 | let mut sysconfig_paths = sysconfig_paths |
1408 | .iter() |
1409 | .filter_map(|p| { |
1410 | let canonical = fs::canonicalize(p).ok(); |
1411 | match &sysconfig_name { |
1412 | Some(_) => canonical.filter(|p| p.file_stem() == sysconfig_name.as_deref()), |
1413 | None => canonical, |
1414 | } |
1415 | }) |
1416 | .collect::<Vec<PathBuf>>(); |
1417 | |
1418 | sysconfig_paths.sort(); |
1419 | sysconfig_paths.dedup(); |
1420 | |
1421 | Ok(sysconfig_paths) |
1422 | } |
1423 | |
1424 | fn is_pypy_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool { |
1425 | let pypy_version_pat: String = if let Some(v: &PythonVersion) = v { |
1426 | format!("pypy {}" , v) |
1427 | } else { |
1428 | "pypy3." .into() |
1429 | }; |
1430 | path == "lib_pypy" || path.starts_with(&pypy_version_pat) |
1431 | } |
1432 | |
1433 | fn is_graalpy_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool { |
1434 | let graalpy_version_pat: String = if let Some(v: &PythonVersion) = v { |
1435 | format!("graalpy {}" , v) |
1436 | } else { |
1437 | "graalpy2" .into() |
1438 | }; |
1439 | path == "lib_graalpython" || path.starts_with(&graalpy_version_pat) |
1440 | } |
1441 | |
1442 | fn is_cpython_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool { |
1443 | let cpython_version_pat: String = if let Some(v: &PythonVersion) = v { |
1444 | format!("python {}" , v) |
1445 | } else { |
1446 | "python3." .into() |
1447 | }; |
1448 | path.starts_with(&cpython_version_pat) |
1449 | } |
1450 | |
1451 | /// recursive search for _sysconfigdata, returns all possibilities of sysconfigdata paths |
1452 | fn search_lib_dir(path: impl AsRef<Path>, cross: &CrossCompileConfig) -> Result<Vec<PathBuf>> { |
1453 | let mut sysconfig_paths = vec![]; |
1454 | for f in fs::read_dir(path.as_ref()).with_context(|| { |
1455 | format!( |
1456 | "failed to list the entries in ' {}'" , |
1457 | path.as_ref().display() |
1458 | ) |
1459 | })? { |
1460 | sysconfig_paths.extend(match &f { |
1461 | // Python 3.7+ sysconfigdata with platform specifics |
1462 | Ok(f) if starts_with(f, "_sysconfigdata_" ) && ends_with(f, "py" ) => vec![f.path()], |
1463 | Ok(f) if f.metadata().map_or(false, |metadata| metadata.is_dir()) => { |
1464 | let file_name = f.file_name(); |
1465 | let file_name = file_name.to_string_lossy(); |
1466 | if file_name == "build" || file_name == "lib" { |
1467 | search_lib_dir(f.path(), cross)? |
1468 | } else if file_name.starts_with("lib." ) { |
1469 | // check if right target os |
1470 | if !file_name.contains(&cross.target.operating_system.to_string()) { |
1471 | continue; |
1472 | } |
1473 | // Check if right arch |
1474 | if !file_name.contains(&cross.target.architecture.to_string()) { |
1475 | continue; |
1476 | } |
1477 | search_lib_dir(f.path(), cross)? |
1478 | } else if is_cpython_lib_dir(&file_name, &cross.version) |
1479 | || is_pypy_lib_dir(&file_name, &cross.version) |
1480 | || is_graalpy_lib_dir(&file_name, &cross.version) |
1481 | { |
1482 | search_lib_dir(f.path(), cross)? |
1483 | } else { |
1484 | continue; |
1485 | } |
1486 | } |
1487 | _ => continue, |
1488 | }); |
1489 | } |
1490 | // If we got more than one file, only take those that contain the arch name. |
1491 | // For ubuntu 20.04 with host architecture x86_64 and a foreign architecture of armhf |
1492 | // this reduces the number of candidates to 1: |
1493 | // |
1494 | // $ find /usr/lib/python3.8/ -name '_sysconfigdata*.py' -not -lname '*' |
1495 | // /usr/lib/python3.8/_sysconfigdata__x86_64-linux-gnu.py |
1496 | // /usr/lib/python3.8/_sysconfigdata__arm-linux-gnueabihf.py |
1497 | if sysconfig_paths.len() > 1 { |
1498 | let temp = sysconfig_paths |
1499 | .iter() |
1500 | .filter(|p| { |
1501 | p.to_string_lossy() |
1502 | .contains(&cross.target.architecture.to_string()) |
1503 | }) |
1504 | .cloned() |
1505 | .collect::<Vec<PathBuf>>(); |
1506 | if !temp.is_empty() { |
1507 | sysconfig_paths = temp; |
1508 | } |
1509 | } |
1510 | |
1511 | Ok(sysconfig_paths) |
1512 | } |
1513 | |
1514 | /// Find cross compilation information from sysconfigdata file |
1515 | /// |
1516 | /// first find sysconfigdata file which follows the pattern [`_sysconfigdata_{abi}_{platform}_{multiarch}`][1] |
1517 | /// |
1518 | /// [1]: https://github.com/python/cpython/blob/3.8/Lib/sysconfig.py#L348 |
1519 | /// |
1520 | /// Returns `None` when the target Python library directory is not set. |
1521 | fn cross_compile_from_sysconfigdata( |
1522 | cross_compile_config: &CrossCompileConfig, |
1523 | ) -> Result<Option<InterpreterConfig>> { |
1524 | if let Some(path: PathBuf) = find_sysconfigdata(cross_compile_config)? { |
1525 | let data: Sysconfigdata = parse_sysconfigdata(path)?; |
1526 | let mut config: InterpreterConfig = InterpreterConfig::from_sysconfigdata(&data)?; |
1527 | if let Some(cross_lib_dir: String) = cross_compile_config.lib_dir_string() { |
1528 | config.lib_dir = Some(cross_lib_dir) |
1529 | } |
1530 | |
1531 | Ok(Some(config)) |
1532 | } else { |
1533 | Ok(None) |
1534 | } |
1535 | } |
1536 | |
1537 | /// Generates "default" cross compilation information for the target. |
1538 | /// |
1539 | /// This should work for most CPython extension modules when targeting |
1540 | /// Windows, macOS and Linux. |
1541 | /// |
1542 | /// Must be called from a PyO3 crate build script. |
1543 | #[allow (unused_mut)] |
1544 | fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result<InterpreterConfig> { |
1545 | let version = cross_compile_config |
1546 | .version |
1547 | .or_else(get_abi3_version) |
1548 | .ok_or_else(|| |
1549 | format!( |
1550 | "PYO3_CROSS_PYTHON_VERSION or an abi3-py3* feature must be specified \ |
1551 | when cross-compiling and PYO3_CROSS_LIB_DIR is not set. \n\ |
1552 | = help: see the PyO3 user guide for more information: https://pyo3.rs/v {}/building-and-distribution.html#cross-compiling" , |
1553 | env!("CARGO_PKG_VERSION" ) |
1554 | ) |
1555 | )?; |
1556 | |
1557 | let abi3 = is_abi3(); |
1558 | let implementation = cross_compile_config |
1559 | .implementation |
1560 | .unwrap_or(PythonImplementation::CPython); |
1561 | let gil_disabled = cross_compile_config.abiflags.as_deref() == Some("t" ); |
1562 | |
1563 | let lib_name = default_lib_name_for_target( |
1564 | version, |
1565 | implementation, |
1566 | abi3, |
1567 | gil_disabled, |
1568 | &cross_compile_config.target, |
1569 | ); |
1570 | |
1571 | let mut lib_dir = cross_compile_config.lib_dir_string(); |
1572 | |
1573 | // Auto generate python3.dll import libraries for Windows targets. |
1574 | #[cfg (feature = "python3-dll-a" )] |
1575 | if lib_dir.is_none() { |
1576 | let py_version = if implementation == PythonImplementation::CPython && abi3 && !gil_disabled |
1577 | { |
1578 | None |
1579 | } else { |
1580 | Some(version) |
1581 | }; |
1582 | lib_dir = self::import_lib::generate_import_lib( |
1583 | &cross_compile_config.target, |
1584 | cross_compile_config |
1585 | .implementation |
1586 | .unwrap_or(PythonImplementation::CPython), |
1587 | py_version, |
1588 | None, |
1589 | )?; |
1590 | } |
1591 | |
1592 | Ok(InterpreterConfig { |
1593 | implementation, |
1594 | version, |
1595 | shared: true, |
1596 | abi3, |
1597 | lib_name, |
1598 | lib_dir, |
1599 | executable: None, |
1600 | pointer_width: None, |
1601 | build_flags: BuildFlags::default(), |
1602 | suppress_build_script_link_lines: false, |
1603 | extra_build_script_lines: vec![], |
1604 | python_framework_prefix: None, |
1605 | }) |
1606 | } |
1607 | |
1608 | /// Generates "default" interpreter configuration when compiling "abi3" extensions |
1609 | /// without a working Python interpreter. |
1610 | /// |
1611 | /// `version` specifies the minimum supported Stable ABI CPython version. |
1612 | /// |
1613 | /// This should work for most CPython extension modules when compiling on |
1614 | /// Windows, macOS and Linux. |
1615 | /// |
1616 | /// Must be called from a PyO3 crate build script. |
1617 | fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result<InterpreterConfig> { |
1618 | // FIXME: PyPy & GraalPy do not support the Stable ABI. |
1619 | let implementation = PythonImplementation::CPython; |
1620 | let abi3 = true; |
1621 | |
1622 | let lib_name = if host.operating_system == OperatingSystem::Windows { |
1623 | Some(default_lib_name_windows( |
1624 | version, |
1625 | implementation, |
1626 | abi3, |
1627 | false, |
1628 | false, |
1629 | false, |
1630 | )?) |
1631 | } else { |
1632 | None |
1633 | }; |
1634 | |
1635 | Ok(InterpreterConfig { |
1636 | implementation, |
1637 | version, |
1638 | shared: true, |
1639 | abi3, |
1640 | lib_name, |
1641 | lib_dir: None, |
1642 | executable: None, |
1643 | pointer_width: None, |
1644 | build_flags: BuildFlags::default(), |
1645 | suppress_build_script_link_lines: false, |
1646 | extra_build_script_lines: vec![], |
1647 | python_framework_prefix: None, |
1648 | }) |
1649 | } |
1650 | |
1651 | /// Detects the cross compilation target interpreter configuration from all |
1652 | /// available sources (PyO3 environment variables, Python sysconfigdata, etc.). |
1653 | /// |
1654 | /// Returns the "default" target interpreter configuration for Windows and |
1655 | /// when no target Python interpreter is found. |
1656 | /// |
1657 | /// Must be called from a PyO3 crate build script. |
1658 | fn load_cross_compile_config( |
1659 | cross_compile_config: CrossCompileConfig, |
1660 | ) -> Result<InterpreterConfig> { |
1661 | let windows = cross_compile_config.target.operating_system == OperatingSystem::Windows; |
1662 | |
1663 | let config = if windows || !have_python_interpreter() { |
1664 | // Load the defaults for Windows even when `PYO3_CROSS_LIB_DIR` is set |
1665 | // since it has no sysconfigdata files in it. |
1666 | // Also, do not try to look for sysconfigdata when `PYO3_NO_PYTHON` variable is set. |
1667 | default_cross_compile(&cross_compile_config)? |
1668 | } else if let Some(config) = cross_compile_from_sysconfigdata(&cross_compile_config)? { |
1669 | // Try to find and parse sysconfigdata files on other targets. |
1670 | config |
1671 | } else { |
1672 | // Fall back to the defaults when nothing else can be done. |
1673 | default_cross_compile(&cross_compile_config)? |
1674 | }; |
1675 | |
1676 | if config.lib_name.is_some() && config.lib_dir.is_none() { |
1677 | warn!( |
1678 | "The output binary will link to libpython, \ |
1679 | but PYO3_CROSS_LIB_DIR environment variable is not set. \ |
1680 | Ensure that the target Python library directory is \ |
1681 | in the rustc native library search path." |
1682 | ); |
1683 | } |
1684 | |
1685 | Ok(config) |
1686 | } |
1687 | |
1688 | // These contains only the limited ABI symbols. |
1689 | const WINDOWS_ABI3_LIB_NAME: &str = "python3" ; |
1690 | const WINDOWS_ABI3_DEBUG_LIB_NAME: &str = "python3_d" ; |
1691 | |
1692 | fn default_lib_name_for_target( |
1693 | version: PythonVersion, |
1694 | implementation: PythonImplementation, |
1695 | abi3: bool, |
1696 | gil_disabled: bool, |
1697 | target: &Triple, |
1698 | ) -> Option<String> { |
1699 | if target.operating_system == OperatingSystem::Windows { |
1700 | Some( |
1701 | default_lib_name_windowsResult(version, implementation, abi3, mingw:false, debug:false, gil_disabled) |
1702 | .unwrap(), |
1703 | ) |
1704 | } else if is_linking_libpython_for_target(target) { |
1705 | Some(default_lib_name_unix(version, implementation, ld_version:None, gil_disabled).unwrap()) |
1706 | } else { |
1707 | None |
1708 | } |
1709 | } |
1710 | |
1711 | fn default_lib_name_windows( |
1712 | version: PythonVersion, |
1713 | implementation: PythonImplementation, |
1714 | abi3: bool, |
1715 | mingw: bool, |
1716 | debug: bool, |
1717 | gil_disabled: bool, |
1718 | ) -> Result<String> { |
1719 | if debug && version < PythonVersion::PY310 { |
1720 | // CPython bug: linking against python3_d.dll raises error |
1721 | // https://github.com/python/cpython/issues/101614 |
1722 | Ok(format!("python {}{}_d" , version.major, version.minor)) |
1723 | } else if abi3 && !(gil_disabled || implementation.is_pypy() || implementation.is_graalpy()) { |
1724 | if debug { |
1725 | Ok(WINDOWS_ABI3_DEBUG_LIB_NAME.to_owned()) |
1726 | } else { |
1727 | Ok(WINDOWS_ABI3_LIB_NAME.to_owned()) |
1728 | } |
1729 | } else if mingw { |
1730 | ensure!( |
1731 | !gil_disabled, |
1732 | "MinGW free-threaded builds are not currently tested or supported" |
1733 | ); |
1734 | // https://packages.msys2.org/base/mingw-w64-python |
1735 | Ok(format!("python {}. {}" , version.major, version.minor)) |
1736 | } else if gil_disabled { |
1737 | 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); |
1738 | if debug { |
1739 | Ok(format!("python {}{}t_d" , version.major, version.minor)) |
1740 | } else { |
1741 | Ok(format!("python {}{}t" , version.major, version.minor)) |
1742 | } |
1743 | } else if debug { |
1744 | Ok(format!("python {}{}_d" , version.major, version.minor)) |
1745 | } else { |
1746 | Ok(format!("python {}{}" , version.major, version.minor)) |
1747 | } |
1748 | } |
1749 | |
1750 | fn default_lib_name_unix( |
1751 | version: PythonVersion, |
1752 | implementation: PythonImplementation, |
1753 | ld_version: Option<&str>, |
1754 | gil_disabled: bool, |
1755 | ) -> Result<String> { |
1756 | match implementation { |
1757 | PythonImplementation::CPython => match ld_version { |
1758 | Some(ld_version) => Ok(format!("python {}" , ld_version)), |
1759 | None => { |
1760 | if version > PythonVersion::PY37 { |
1761 | // PEP 3149 ABI version tags are finally gone |
1762 | if gil_disabled { |
1763 | 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); |
1764 | Ok(format!("python {}. {}t" , version.major, version.minor)) |
1765 | } else { |
1766 | Ok(format!("python {}. {}" , version.major, version.minor)) |
1767 | } |
1768 | } else { |
1769 | // Work around https://bugs.python.org/issue36707 |
1770 | Ok(format!("python {}. {}m" , version.major, version.minor)) |
1771 | } |
1772 | } |
1773 | }, |
1774 | PythonImplementation::PyPy => match ld_version { |
1775 | Some(ld_version) => Ok(format!("pypy {}-c" , ld_version)), |
1776 | None => Ok(format!("pypy {}. {}-c" , version.major, version.minor)), |
1777 | }, |
1778 | |
1779 | PythonImplementation::GraalPy => Ok("python-native" .to_string()), |
1780 | } |
1781 | } |
1782 | |
1783 | /// Run a python script using the specified interpreter binary. |
1784 | fn run_python_script(interpreter: &Path, script: &str) -> Result<String> { |
1785 | run_python_script_with_envs(interpreter, script, envs:std::iter::empty::<(&str, &str)>()) |
1786 | } |
1787 | |
1788 | /// Run a python script using the specified interpreter binary with additional environment |
1789 | /// variables (e.g. PYTHONPATH) set. |
1790 | fn run_python_script_with_envs<I, K, V>(interpreter: &Path, script: &str, envs: I) -> Result<String> |
1791 | where |
1792 | I: IntoIterator<Item = (K, V)>, |
1793 | K: AsRef<OsStr>, |
1794 | V: AsRef<OsStr>, |
1795 | { |
1796 | let out = Command::new(interpreter) |
1797 | .env("PYTHONIOENCODING" , "utf-8" ) |
1798 | .envs(envs) |
1799 | .stdin(Stdio::piped()) |
1800 | .stdout(Stdio::piped()) |
1801 | .stderr(Stdio::inherit()) |
1802 | .spawn() |
1803 | .and_then(|mut child| { |
1804 | child |
1805 | .stdin |
1806 | .as_mut() |
1807 | .expect("piped stdin" ) |
1808 | .write_all(script.as_bytes())?; |
1809 | child.wait_with_output() |
1810 | }); |
1811 | |
1812 | match out { |
1813 | Err(err) => bail!( |
1814 | "failed to run the Python interpreter at {}: {}" , |
1815 | interpreter.display(), |
1816 | err |
1817 | ), |
1818 | Ok(ok) if !ok.status.success() => bail!("Python script failed" ), |
1819 | Ok(ok) => Ok(String::from_utf8(ok.stdout) |
1820 | .context("failed to parse Python script output as utf-8" )?), |
1821 | } |
1822 | } |
1823 | |
1824 | fn venv_interpreter(virtual_env: &OsStr, windows: bool) -> PathBuf { |
1825 | if windows { |
1826 | Path::new(virtual_env).join("Scripts" ).join(path:"python.exe" ) |
1827 | } else { |
1828 | Path::new(virtual_env).join("bin" ).join(path:"python" ) |
1829 | } |
1830 | } |
1831 | |
1832 | fn conda_env_interpreter(conda_prefix: &OsStr, windows: bool) -> PathBuf { |
1833 | if windows { |
1834 | Path::new(conda_prefix).join(path:"python.exe" ) |
1835 | } else { |
1836 | Path::new(conda_prefix).join("bin" ).join(path:"python" ) |
1837 | } |
1838 | } |
1839 | |
1840 | fn get_env_interpreter() -> Option<PathBuf> { |
1841 | match (env_var("VIRTUAL_ENV" ), env_var("CONDA_PREFIX" )) { |
1842 | // Use cfg rather than CARGO_CFG_TARGET_OS because this affects where files are located on the |
1843 | // build host |
1844 | (Some(dir: OsString), None) => Some(venv_interpreter(&dir, windows:cfg!(windows))), |
1845 | (None, Some(dir: OsString)) => Some(conda_env_interpreter(&dir, windows:cfg!(windows))), |
1846 | (Some(_), Some(_)) => { |
1847 | warn!( |
1848 | "Both VIRTUAL_ENV and CONDA_PREFIX are set. PyO3 will ignore both of these for \ |
1849 | locating the Python interpreter until you unset one of them." |
1850 | ); |
1851 | None |
1852 | } |
1853 | (None, None) => None, |
1854 | } |
1855 | } |
1856 | |
1857 | /// Attempts to locate a python interpreter. |
1858 | /// |
1859 | /// Locations are checked in the order listed: |
1860 | /// 1. If `PYO3_PYTHON` is set, this interpreter is used. |
1861 | /// 2. If in a virtualenv, that environment's interpreter is used. |
1862 | /// 3. `python`, if this is functional a Python 3.x interpreter |
1863 | /// 4. `python3`, as above |
1864 | pub fn find_interpreter() -> Result<PathBuf> { |
1865 | // Trigger rebuilds when `PYO3_ENVIRONMENT_SIGNATURE` env var value changes |
1866 | // See https://github.com/PyO3/pyo3/issues/2724 |
1867 | println!("cargo:rerun-if-env-changed=PYO3_ENVIRONMENT_SIGNATURE" ); |
1868 | |
1869 | if let Some(exe) = env_var("PYO3_PYTHON" ) { |
1870 | Ok(exe.into()) |
1871 | } else if let Some(env_interpreter) = get_env_interpreter() { |
1872 | Ok(env_interpreter) |
1873 | } else { |
1874 | println!("cargo:rerun-if-env-changed=PATH" ); |
1875 | ["python" , "python3" ] |
1876 | .iter() |
1877 | .find(|bin| { |
1878 | if let Ok(out) = Command::new(bin).arg("--version" ).output() { |
1879 | // begin with `Python 3.X.X :: additional info` |
1880 | out.stdout.starts_with(b"Python 3" ) |
1881 | || out.stderr.starts_with(b"Python 3" ) |
1882 | || out.stdout.starts_with(b"GraalPy 3" ) |
1883 | } else { |
1884 | false |
1885 | } |
1886 | }) |
1887 | .map(PathBuf::from) |
1888 | .ok_or_else(|| "no Python 3.x interpreter found" .into()) |
1889 | } |
1890 | } |
1891 | |
1892 | /// Locates and extracts the build host Python interpreter configuration. |
1893 | /// |
1894 | /// Lowers the configured Python version to `abi3_version` if required. |
1895 | fn get_host_interpreter(abi3_version: Option<PythonVersion>) -> Result<InterpreterConfig> { |
1896 | let interpreter_path: PathBuf = find_interpreter()?; |
1897 | |
1898 | let mut interpreter_config: InterpreterConfig = InterpreterConfig::from_interpreter(interpreter_path)?; |
1899 | interpreter_config.fixup_for_abi3_version(abi3_version)?; |
1900 | |
1901 | Ok(interpreter_config) |
1902 | } |
1903 | |
1904 | /// Generates an interpreter config suitable for cross-compilation. |
1905 | /// |
1906 | /// This must be called from PyO3's build script, because it relies on environment variables such as |
1907 | /// CARGO_CFG_TARGET_OS which aren't available at any other time. |
1908 | pub fn make_cross_compile_config() -> Result<Option<InterpreterConfig>> { |
1909 | let interpreter_config: Option = if let Some(cross_config: CrossCompileConfig) = cross_compiling_from_cargo_env()? { |
1910 | let mut interpreter_config: InterpreterConfig = load_cross_compile_config(cross_config)?; |
1911 | interpreter_config.fixup_for_abi3_version(get_abi3_version())?; |
1912 | Some(interpreter_config) |
1913 | } else { |
1914 | None |
1915 | }; |
1916 | |
1917 | Ok(interpreter_config) |
1918 | } |
1919 | |
1920 | /// Generates an interpreter config which will be hard-coded into the pyo3-build-config crate. |
1921 | /// Only used by `pyo3-build-config` build script. |
1922 | #[allow (dead_code, unused_mut)] |
1923 | pub fn make_interpreter_config() -> Result<InterpreterConfig> { |
1924 | let host = Triple::host(); |
1925 | let abi3_version = get_abi3_version(); |
1926 | |
1927 | // See if we can safely skip the Python interpreter configuration detection. |
1928 | // Unix "abi3" extension modules can usually be built without any interpreter. |
1929 | let need_interpreter = abi3_version.is_none() || require_libdir_for_target(&host); |
1930 | |
1931 | if have_python_interpreter() { |
1932 | match get_host_interpreter(abi3_version) { |
1933 | Ok(interpreter_config) => return Ok(interpreter_config), |
1934 | // Bail if the interpreter configuration is required to build. |
1935 | Err(e) if need_interpreter => return Err(e), |
1936 | _ => { |
1937 | // Fall back to the "abi3" defaults just as if `PYO3_NO_PYTHON` |
1938 | // environment variable was set. |
1939 | warn!("Compiling without a working Python interpreter." ); |
1940 | } |
1941 | } |
1942 | } else { |
1943 | ensure!( |
1944 | abi3_version.is_some(), |
1945 | "An abi3-py3* feature must be specified when compiling without a Python interpreter." |
1946 | ); |
1947 | }; |
1948 | |
1949 | let mut interpreter_config = default_abi3_config(&host, abi3_version.unwrap())?; |
1950 | |
1951 | // Auto generate python3.dll import libraries for Windows targets. |
1952 | #[cfg (feature = "python3-dll-a" )] |
1953 | { |
1954 | let gil_disabled = interpreter_config |
1955 | .build_flags |
1956 | .0 |
1957 | .contains(&BuildFlag::Py_GIL_DISABLED); |
1958 | let py_version = if interpreter_config.implementation == PythonImplementation::CPython |
1959 | && interpreter_config.abi3 |
1960 | && !gil_disabled |
1961 | { |
1962 | None |
1963 | } else { |
1964 | Some(interpreter_config.version) |
1965 | }; |
1966 | interpreter_config.lib_dir = self::import_lib::generate_import_lib( |
1967 | &host, |
1968 | interpreter_config.implementation, |
1969 | py_version, |
1970 | None, |
1971 | )?; |
1972 | } |
1973 | |
1974 | Ok(interpreter_config) |
1975 | } |
1976 | |
1977 | fn escape(bytes: &[u8]) -> String { |
1978 | let mut escaped: String = String::with_capacity(2 * bytes.len()); |
1979 | |
1980 | for byte: &u8 in bytes { |
1981 | const LUT: &[u8; 16] = b"0123456789abcdef" ; |
1982 | |
1983 | escaped.push(LUT[(byte >> 4) as usize] as char); |
1984 | escaped.push(LUT[(byte & 0x0F) as usize] as char); |
1985 | } |
1986 | |
1987 | escaped |
1988 | } |
1989 | |
1990 | fn unescape(escaped: &str) -> Vec<u8> { |
1991 | assert!(escaped.len() % 2 == 0, "invalid hex encoding" ); |
1992 | |
1993 | let mut bytes: Vec = Vec::with_capacity(escaped.len() / 2); |
1994 | |
1995 | for chunk: &[u8] in escaped.as_bytes().chunks_exact(chunk_size:2) { |
1996 | fn unhex(hex: u8) -> u8 { |
1997 | match hex { |
1998 | b'a' ..=b'f' => hex - b'a' + 10, |
1999 | b'0' ..=b'9' => hex - b'0' , |
2000 | _ => panic!("invalid hex encoding" ), |
2001 | } |
2002 | } |
2003 | |
2004 | bytes.push((unhex(hex:chunk[0]) << 4) | unhex(hex:chunk[1])); |
2005 | } |
2006 | |
2007 | bytes |
2008 | } |
2009 | |
2010 | #[cfg (test)] |
2011 | mod tests { |
2012 | use target_lexicon::triple; |
2013 | |
2014 | use super::*; |
2015 | |
2016 | #[test ] |
2017 | fn test_config_file_roundtrip() { |
2018 | let config = InterpreterConfig { |
2019 | abi3: true, |
2020 | build_flags: BuildFlags::default(), |
2021 | pointer_width: Some(32), |
2022 | executable: Some("executable" .into()), |
2023 | implementation: PythonImplementation::CPython, |
2024 | lib_name: Some("lib_name" .into()), |
2025 | lib_dir: Some("lib_dir" .into()), |
2026 | shared: true, |
2027 | version: MINIMUM_SUPPORTED_VERSION, |
2028 | suppress_build_script_link_lines: true, |
2029 | extra_build_script_lines: vec!["cargo:test1" .to_string(), "cargo:test2" .to_string()], |
2030 | python_framework_prefix: None, |
2031 | }; |
2032 | let mut buf: Vec<u8> = Vec::new(); |
2033 | config.to_writer(&mut buf).unwrap(); |
2034 | |
2035 | assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap()); |
2036 | |
2037 | // And some different options, for variety |
2038 | |
2039 | let config = InterpreterConfig { |
2040 | abi3: false, |
2041 | build_flags: { |
2042 | let mut flags = HashSet::new(); |
2043 | flags.insert(BuildFlag::Py_DEBUG); |
2044 | flags.insert(BuildFlag::Other(String::from("Py_SOME_FLAG" ))); |
2045 | BuildFlags(flags) |
2046 | }, |
2047 | pointer_width: None, |
2048 | executable: None, |
2049 | implementation: PythonImplementation::PyPy, |
2050 | lib_dir: None, |
2051 | lib_name: None, |
2052 | shared: true, |
2053 | version: PythonVersion { |
2054 | major: 3, |
2055 | minor: 10, |
2056 | }, |
2057 | suppress_build_script_link_lines: false, |
2058 | extra_build_script_lines: vec![], |
2059 | python_framework_prefix: None, |
2060 | }; |
2061 | let mut buf: Vec<u8> = Vec::new(); |
2062 | config.to_writer(&mut buf).unwrap(); |
2063 | |
2064 | assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap()); |
2065 | } |
2066 | |
2067 | #[test ] |
2068 | fn test_config_file_roundtrip_with_escaping() { |
2069 | let config = InterpreterConfig { |
2070 | abi3: true, |
2071 | build_flags: BuildFlags::default(), |
2072 | pointer_width: Some(32), |
2073 | executable: Some("executable" .into()), |
2074 | implementation: PythonImplementation::CPython, |
2075 | lib_name: Some("lib_name" .into()), |
2076 | lib_dir: Some("lib_dir \\n" .into()), |
2077 | shared: true, |
2078 | version: MINIMUM_SUPPORTED_VERSION, |
2079 | suppress_build_script_link_lines: true, |
2080 | extra_build_script_lines: vec!["cargo:test1" .to_string(), "cargo:test2" .to_string()], |
2081 | python_framework_prefix: None, |
2082 | }; |
2083 | let mut buf: Vec<u8> = Vec::new(); |
2084 | config.to_writer(&mut buf).unwrap(); |
2085 | |
2086 | let buf = unescape(&escape(&buf)); |
2087 | |
2088 | assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap()); |
2089 | } |
2090 | |
2091 | #[test ] |
2092 | fn test_config_file_defaults() { |
2093 | // Only version is required |
2094 | assert_eq!( |
2095 | InterpreterConfig::from_reader("version=3.7" .as_bytes()).unwrap(), |
2096 | InterpreterConfig { |
2097 | version: PythonVersion { major: 3, minor: 7 }, |
2098 | implementation: PythonImplementation::CPython, |
2099 | shared: true, |
2100 | abi3: false, |
2101 | lib_name: None, |
2102 | lib_dir: None, |
2103 | executable: None, |
2104 | pointer_width: None, |
2105 | build_flags: BuildFlags::default(), |
2106 | suppress_build_script_link_lines: false, |
2107 | extra_build_script_lines: vec![], |
2108 | python_framework_prefix: None, |
2109 | } |
2110 | ) |
2111 | } |
2112 | |
2113 | #[test ] |
2114 | fn test_config_file_unknown_keys() { |
2115 | // ext_suffix is unknown to pyo3-build-config, but it shouldn't error |
2116 | assert_eq!( |
2117 | InterpreterConfig::from_reader("version=3.7 \next_suffix=.python37.so" .as_bytes()) |
2118 | .unwrap(), |
2119 | InterpreterConfig { |
2120 | version: PythonVersion { major: 3, minor: 7 }, |
2121 | implementation: PythonImplementation::CPython, |
2122 | shared: true, |
2123 | abi3: false, |
2124 | lib_name: None, |
2125 | lib_dir: None, |
2126 | executable: None, |
2127 | pointer_width: None, |
2128 | build_flags: BuildFlags::default(), |
2129 | suppress_build_script_link_lines: false, |
2130 | extra_build_script_lines: vec![], |
2131 | python_framework_prefix: None, |
2132 | } |
2133 | ) |
2134 | } |
2135 | |
2136 | #[test ] |
2137 | fn build_flags_default() { |
2138 | assert_eq!(BuildFlags::default(), BuildFlags::new()); |
2139 | } |
2140 | |
2141 | #[test ] |
2142 | fn build_flags_from_sysconfigdata() { |
2143 | let mut sysconfigdata = Sysconfigdata::new(); |
2144 | |
2145 | assert_eq!( |
2146 | BuildFlags::from_sysconfigdata(&sysconfigdata).0, |
2147 | HashSet::new() |
2148 | ); |
2149 | |
2150 | for flag in &BuildFlags::ALL { |
2151 | sysconfigdata.insert(flag.to_string(), "0" .into()); |
2152 | } |
2153 | |
2154 | assert_eq!( |
2155 | BuildFlags::from_sysconfigdata(&sysconfigdata).0, |
2156 | HashSet::new() |
2157 | ); |
2158 | |
2159 | let mut expected_flags = HashSet::new(); |
2160 | for flag in &BuildFlags::ALL { |
2161 | sysconfigdata.insert(flag.to_string(), "1" .into()); |
2162 | expected_flags.insert(flag.clone()); |
2163 | } |
2164 | |
2165 | assert_eq!( |
2166 | BuildFlags::from_sysconfigdata(&sysconfigdata).0, |
2167 | expected_flags |
2168 | ); |
2169 | } |
2170 | |
2171 | #[test ] |
2172 | fn build_flags_fixup() { |
2173 | let mut build_flags = BuildFlags::new(); |
2174 | |
2175 | build_flags = build_flags.fixup(); |
2176 | assert!(build_flags.0.is_empty()); |
2177 | |
2178 | build_flags.0.insert(BuildFlag::Py_DEBUG); |
2179 | |
2180 | build_flags = build_flags.fixup(); |
2181 | |
2182 | // Py_DEBUG implies Py_REF_DEBUG |
2183 | assert!(build_flags.0.contains(&BuildFlag::Py_REF_DEBUG)); |
2184 | } |
2185 | |
2186 | #[test ] |
2187 | fn parse_script_output() { |
2188 | let output = "foo bar \nbar foobar \n\n" ; |
2189 | let map = super::parse_script_output(output); |
2190 | assert_eq!(map.len(), 2); |
2191 | assert_eq!(map["foo" ], "bar" ); |
2192 | assert_eq!(map["bar" ], "foobar" ); |
2193 | } |
2194 | |
2195 | #[test ] |
2196 | fn config_from_interpreter() { |
2197 | // Smoke test to just see whether this works |
2198 | // |
2199 | // PyO3's CI is dependent on Python being installed, so this should be reliable. |
2200 | assert!(make_interpreter_config().is_ok()) |
2201 | } |
2202 | |
2203 | #[test ] |
2204 | fn config_from_empty_sysconfigdata() { |
2205 | let sysconfigdata = Sysconfigdata::new(); |
2206 | assert!(InterpreterConfig::from_sysconfigdata(&sysconfigdata).is_err()); |
2207 | } |
2208 | |
2209 | #[test ] |
2210 | fn config_from_sysconfigdata() { |
2211 | let mut sysconfigdata = Sysconfigdata::new(); |
2212 | // these are the minimal values required such that InterpreterConfig::from_sysconfigdata |
2213 | // does not error |
2214 | sysconfigdata.insert("SOABI" , "cpython-37m-x86_64-linux-gnu" ); |
2215 | sysconfigdata.insert("VERSION" , "3.7" ); |
2216 | sysconfigdata.insert("Py_ENABLE_SHARED" , "1" ); |
2217 | sysconfigdata.insert("LIBDIR" , "/usr/lib" ); |
2218 | sysconfigdata.insert("LDVERSION" , "3.7m" ); |
2219 | sysconfigdata.insert("SIZEOF_VOID_P" , "8" ); |
2220 | assert_eq!( |
2221 | InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), |
2222 | InterpreterConfig { |
2223 | abi3: false, |
2224 | build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), |
2225 | pointer_width: Some(64), |
2226 | executable: None, |
2227 | implementation: PythonImplementation::CPython, |
2228 | lib_dir: Some("/usr/lib" .into()), |
2229 | lib_name: Some("python3.7m" .into()), |
2230 | shared: true, |
2231 | version: PythonVersion::PY37, |
2232 | suppress_build_script_link_lines: false, |
2233 | extra_build_script_lines: vec![], |
2234 | python_framework_prefix: None, |
2235 | } |
2236 | ); |
2237 | } |
2238 | |
2239 | #[test ] |
2240 | fn config_from_sysconfigdata_framework() { |
2241 | let mut sysconfigdata = Sysconfigdata::new(); |
2242 | sysconfigdata.insert("SOABI" , "cpython-37m-x86_64-linux-gnu" ); |
2243 | sysconfigdata.insert("VERSION" , "3.7" ); |
2244 | // PYTHONFRAMEWORK should override Py_ENABLE_SHARED |
2245 | sysconfigdata.insert("Py_ENABLE_SHARED" , "0" ); |
2246 | sysconfigdata.insert("PYTHONFRAMEWORK" , "Python" ); |
2247 | sysconfigdata.insert("LIBDIR" , "/usr/lib" ); |
2248 | sysconfigdata.insert("LDVERSION" , "3.7m" ); |
2249 | sysconfigdata.insert("SIZEOF_VOID_P" , "8" ); |
2250 | assert_eq!( |
2251 | InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), |
2252 | InterpreterConfig { |
2253 | abi3: false, |
2254 | build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), |
2255 | pointer_width: Some(64), |
2256 | executable: None, |
2257 | implementation: PythonImplementation::CPython, |
2258 | lib_dir: Some("/usr/lib" .into()), |
2259 | lib_name: Some("python3.7m" .into()), |
2260 | shared: true, |
2261 | version: PythonVersion::PY37, |
2262 | suppress_build_script_link_lines: false, |
2263 | extra_build_script_lines: vec![], |
2264 | python_framework_prefix: None, |
2265 | } |
2266 | ); |
2267 | |
2268 | sysconfigdata = Sysconfigdata::new(); |
2269 | sysconfigdata.insert("SOABI" , "cpython-37m-x86_64-linux-gnu" ); |
2270 | sysconfigdata.insert("VERSION" , "3.7" ); |
2271 | // An empty PYTHONFRAMEWORK means it is not a framework |
2272 | sysconfigdata.insert("Py_ENABLE_SHARED" , "0" ); |
2273 | sysconfigdata.insert("PYTHONFRAMEWORK" , "" ); |
2274 | sysconfigdata.insert("LIBDIR" , "/usr/lib" ); |
2275 | sysconfigdata.insert("LDVERSION" , "3.7m" ); |
2276 | sysconfigdata.insert("SIZEOF_VOID_P" , "8" ); |
2277 | assert_eq!( |
2278 | InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), |
2279 | InterpreterConfig { |
2280 | abi3: false, |
2281 | build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), |
2282 | pointer_width: Some(64), |
2283 | executable: None, |
2284 | implementation: PythonImplementation::CPython, |
2285 | lib_dir: Some("/usr/lib" .into()), |
2286 | lib_name: Some("python3.7m" .into()), |
2287 | shared: false, |
2288 | version: PythonVersion::PY37, |
2289 | suppress_build_script_link_lines: false, |
2290 | extra_build_script_lines: vec![], |
2291 | python_framework_prefix: None, |
2292 | } |
2293 | ); |
2294 | } |
2295 | |
2296 | #[test ] |
2297 | fn windows_hardcoded_abi3_compile() { |
2298 | let host = triple!("x86_64-pc-windows-msvc" ); |
2299 | let min_version = "3.7" .parse().unwrap(); |
2300 | |
2301 | assert_eq!( |
2302 | default_abi3_config(&host, min_version).unwrap(), |
2303 | InterpreterConfig { |
2304 | implementation: PythonImplementation::CPython, |
2305 | version: PythonVersion { major: 3, minor: 7 }, |
2306 | shared: true, |
2307 | abi3: true, |
2308 | lib_name: Some("python3" .into()), |
2309 | lib_dir: None, |
2310 | executable: None, |
2311 | pointer_width: None, |
2312 | build_flags: BuildFlags::default(), |
2313 | suppress_build_script_link_lines: false, |
2314 | extra_build_script_lines: vec![], |
2315 | python_framework_prefix: None, |
2316 | } |
2317 | ); |
2318 | } |
2319 | |
2320 | #[test ] |
2321 | fn unix_hardcoded_abi3_compile() { |
2322 | let host = triple!("x86_64-unknown-linux-gnu" ); |
2323 | let min_version = "3.9" .parse().unwrap(); |
2324 | |
2325 | assert_eq!( |
2326 | default_abi3_config(&host, min_version).unwrap(), |
2327 | InterpreterConfig { |
2328 | implementation: PythonImplementation::CPython, |
2329 | version: PythonVersion { major: 3, minor: 9 }, |
2330 | shared: true, |
2331 | abi3: true, |
2332 | lib_name: None, |
2333 | lib_dir: None, |
2334 | executable: None, |
2335 | pointer_width: None, |
2336 | build_flags: BuildFlags::default(), |
2337 | suppress_build_script_link_lines: false, |
2338 | extra_build_script_lines: vec![], |
2339 | python_framework_prefix: None, |
2340 | } |
2341 | ); |
2342 | } |
2343 | |
2344 | #[test ] |
2345 | fn windows_hardcoded_cross_compile() { |
2346 | let env_vars = CrossCompileEnvVars { |
2347 | pyo3_cross: None, |
2348 | pyo3_cross_lib_dir: Some("C: \\some \\path" .into()), |
2349 | pyo3_cross_python_implementation: None, |
2350 | pyo3_cross_python_version: Some("3.7" .into()), |
2351 | }; |
2352 | |
2353 | let host = triple!("x86_64-unknown-linux-gnu" ); |
2354 | let target = triple!("i686-pc-windows-msvc" ); |
2355 | let cross_config = |
2356 | CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target) |
2357 | .unwrap() |
2358 | .unwrap(); |
2359 | |
2360 | assert_eq!( |
2361 | default_cross_compile(&cross_config).unwrap(), |
2362 | InterpreterConfig { |
2363 | implementation: PythonImplementation::CPython, |
2364 | version: PythonVersion { major: 3, minor: 7 }, |
2365 | shared: true, |
2366 | abi3: false, |
2367 | lib_name: Some("python37" .into()), |
2368 | lib_dir: Some("C: \\some \\path" .into()), |
2369 | executable: None, |
2370 | pointer_width: None, |
2371 | build_flags: BuildFlags::default(), |
2372 | suppress_build_script_link_lines: false, |
2373 | extra_build_script_lines: vec![], |
2374 | python_framework_prefix: None, |
2375 | } |
2376 | ); |
2377 | } |
2378 | |
2379 | #[test ] |
2380 | fn mingw_hardcoded_cross_compile() { |
2381 | let env_vars = CrossCompileEnvVars { |
2382 | pyo3_cross: None, |
2383 | pyo3_cross_lib_dir: Some("/usr/lib/mingw" .into()), |
2384 | pyo3_cross_python_implementation: None, |
2385 | pyo3_cross_python_version: Some("3.8" .into()), |
2386 | }; |
2387 | |
2388 | let host = triple!("x86_64-unknown-linux-gnu" ); |
2389 | let target = triple!("i686-pc-windows-gnu" ); |
2390 | let cross_config = |
2391 | CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target) |
2392 | .unwrap() |
2393 | .unwrap(); |
2394 | |
2395 | assert_eq!( |
2396 | default_cross_compile(&cross_config).unwrap(), |
2397 | InterpreterConfig { |
2398 | implementation: PythonImplementation::CPython, |
2399 | version: PythonVersion { major: 3, minor: 8 }, |
2400 | shared: true, |
2401 | abi3: false, |
2402 | lib_name: Some("python38" .into()), |
2403 | lib_dir: Some("/usr/lib/mingw" .into()), |
2404 | executable: None, |
2405 | pointer_width: None, |
2406 | build_flags: BuildFlags::default(), |
2407 | suppress_build_script_link_lines: false, |
2408 | extra_build_script_lines: vec![], |
2409 | python_framework_prefix: None, |
2410 | } |
2411 | ); |
2412 | } |
2413 | |
2414 | #[test ] |
2415 | fn unix_hardcoded_cross_compile() { |
2416 | let env_vars = CrossCompileEnvVars { |
2417 | pyo3_cross: None, |
2418 | pyo3_cross_lib_dir: Some("/usr/arm64/lib" .into()), |
2419 | pyo3_cross_python_implementation: None, |
2420 | pyo3_cross_python_version: Some("3.9" .into()), |
2421 | }; |
2422 | |
2423 | let host = triple!("x86_64-unknown-linux-gnu" ); |
2424 | let target = triple!("aarch64-unknown-linux-gnu" ); |
2425 | let cross_config = |
2426 | CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target) |
2427 | .unwrap() |
2428 | .unwrap(); |
2429 | |
2430 | assert_eq!( |
2431 | default_cross_compile(&cross_config).unwrap(), |
2432 | InterpreterConfig { |
2433 | implementation: PythonImplementation::CPython, |
2434 | version: PythonVersion { major: 3, minor: 9 }, |
2435 | shared: true, |
2436 | abi3: false, |
2437 | lib_name: Some("python3.9" .into()), |
2438 | lib_dir: Some("/usr/arm64/lib" .into()), |
2439 | executable: None, |
2440 | pointer_width: None, |
2441 | build_flags: BuildFlags::default(), |
2442 | suppress_build_script_link_lines: false, |
2443 | extra_build_script_lines: vec![], |
2444 | python_framework_prefix: None, |
2445 | } |
2446 | ); |
2447 | } |
2448 | |
2449 | #[test ] |
2450 | fn pypy_hardcoded_cross_compile() { |
2451 | let env_vars = CrossCompileEnvVars { |
2452 | pyo3_cross: None, |
2453 | pyo3_cross_lib_dir: None, |
2454 | pyo3_cross_python_implementation: Some("PyPy" .into()), |
2455 | pyo3_cross_python_version: Some("3.10" .into()), |
2456 | }; |
2457 | |
2458 | let triple = triple!("x86_64-unknown-linux-gnu" ); |
2459 | let cross_config = |
2460 | CrossCompileConfig::try_from_env_vars_host_target(env_vars, &triple, &triple) |
2461 | .unwrap() |
2462 | .unwrap(); |
2463 | |
2464 | assert_eq!( |
2465 | default_cross_compile(&cross_config).unwrap(), |
2466 | InterpreterConfig { |
2467 | implementation: PythonImplementation::PyPy, |
2468 | version: PythonVersion { |
2469 | major: 3, |
2470 | minor: 10 |
2471 | }, |
2472 | shared: true, |
2473 | abi3: false, |
2474 | lib_name: Some("pypy3.10-c" .into()), |
2475 | lib_dir: None, |
2476 | executable: None, |
2477 | pointer_width: None, |
2478 | build_flags: BuildFlags::default(), |
2479 | suppress_build_script_link_lines: false, |
2480 | extra_build_script_lines: vec![], |
2481 | python_framework_prefix: None, |
2482 | } |
2483 | ); |
2484 | } |
2485 | |
2486 | #[test ] |
2487 | fn default_lib_name_windows() { |
2488 | use PythonImplementation::*; |
2489 | assert_eq!( |
2490 | super::default_lib_name_windows( |
2491 | PythonVersion { major: 3, minor: 9 }, |
2492 | CPython, |
2493 | false, |
2494 | false, |
2495 | false, |
2496 | false, |
2497 | ) |
2498 | .unwrap(), |
2499 | "python39" , |
2500 | ); |
2501 | assert!(super::default_lib_name_windows( |
2502 | PythonVersion { major: 3, minor: 9 }, |
2503 | CPython, |
2504 | false, |
2505 | false, |
2506 | false, |
2507 | true, |
2508 | ) |
2509 | .is_err()); |
2510 | assert_eq!( |
2511 | super::default_lib_name_windows( |
2512 | PythonVersion { major: 3, minor: 9 }, |
2513 | CPython, |
2514 | true, |
2515 | false, |
2516 | false, |
2517 | false, |
2518 | ) |
2519 | .unwrap(), |
2520 | "python3" , |
2521 | ); |
2522 | assert_eq!( |
2523 | super::default_lib_name_windows( |
2524 | PythonVersion { major: 3, minor: 9 }, |
2525 | CPython, |
2526 | false, |
2527 | true, |
2528 | false, |
2529 | false, |
2530 | ) |
2531 | .unwrap(), |
2532 | "python3.9" , |
2533 | ); |
2534 | assert_eq!( |
2535 | super::default_lib_name_windows( |
2536 | PythonVersion { major: 3, minor: 9 }, |
2537 | CPython, |
2538 | true, |
2539 | true, |
2540 | false, |
2541 | false, |
2542 | ) |
2543 | .unwrap(), |
2544 | "python3" , |
2545 | ); |
2546 | assert_eq!( |
2547 | super::default_lib_name_windows( |
2548 | PythonVersion { major: 3, minor: 9 }, |
2549 | PyPy, |
2550 | true, |
2551 | false, |
2552 | false, |
2553 | false, |
2554 | ) |
2555 | .unwrap(), |
2556 | "python39" , |
2557 | ); |
2558 | assert_eq!( |
2559 | super::default_lib_name_windows( |
2560 | PythonVersion { major: 3, minor: 9 }, |
2561 | CPython, |
2562 | false, |
2563 | false, |
2564 | true, |
2565 | false, |
2566 | ) |
2567 | .unwrap(), |
2568 | "python39_d" , |
2569 | ); |
2570 | // abi3 debug builds on windows use version-specific lib on 3.9 and older |
2571 | // to workaround https://github.com/python/cpython/issues/101614 |
2572 | assert_eq!( |
2573 | super::default_lib_name_windows( |
2574 | PythonVersion { major: 3, minor: 9 }, |
2575 | CPython, |
2576 | true, |
2577 | false, |
2578 | true, |
2579 | false, |
2580 | ) |
2581 | .unwrap(), |
2582 | "python39_d" , |
2583 | ); |
2584 | assert_eq!( |
2585 | super::default_lib_name_windows( |
2586 | PythonVersion { |
2587 | major: 3, |
2588 | minor: 10 |
2589 | }, |
2590 | CPython, |
2591 | true, |
2592 | false, |
2593 | true, |
2594 | false, |
2595 | ) |
2596 | .unwrap(), |
2597 | "python3_d" , |
2598 | ); |
2599 | // Python versions older than 3.13 don't support gil_disabled |
2600 | assert!(super::default_lib_name_windows( |
2601 | PythonVersion { |
2602 | major: 3, |
2603 | minor: 12, |
2604 | }, |
2605 | CPython, |
2606 | false, |
2607 | false, |
2608 | false, |
2609 | true, |
2610 | ) |
2611 | .is_err()); |
2612 | // mingw and free-threading are incompatible (until someone adds support) |
2613 | assert!(super::default_lib_name_windows( |
2614 | PythonVersion { |
2615 | major: 3, |
2616 | minor: 12, |
2617 | }, |
2618 | CPython, |
2619 | false, |
2620 | true, |
2621 | false, |
2622 | true, |
2623 | ) |
2624 | .is_err()); |
2625 | assert_eq!( |
2626 | super::default_lib_name_windows( |
2627 | PythonVersion { |
2628 | major: 3, |
2629 | minor: 13 |
2630 | }, |
2631 | CPython, |
2632 | false, |
2633 | false, |
2634 | false, |
2635 | true, |
2636 | ) |
2637 | .unwrap(), |
2638 | "python313t" , |
2639 | ); |
2640 | assert_eq!( |
2641 | super::default_lib_name_windows( |
2642 | PythonVersion { |
2643 | major: 3, |
2644 | minor: 13 |
2645 | }, |
2646 | CPython, |
2647 | true, // abi3 true should not affect the free-threaded lib name |
2648 | false, |
2649 | false, |
2650 | true, |
2651 | ) |
2652 | .unwrap(), |
2653 | "python313t" , |
2654 | ); |
2655 | assert_eq!( |
2656 | super::default_lib_name_windows( |
2657 | PythonVersion { |
2658 | major: 3, |
2659 | minor: 13 |
2660 | }, |
2661 | CPython, |
2662 | false, |
2663 | false, |
2664 | true, |
2665 | true, |
2666 | ) |
2667 | .unwrap(), |
2668 | "python313t_d" , |
2669 | ); |
2670 | } |
2671 | |
2672 | #[test ] |
2673 | fn default_lib_name_unix() { |
2674 | use PythonImplementation::*; |
2675 | // Defaults to python3.7m for CPython 3.7 |
2676 | assert_eq!( |
2677 | super::default_lib_name_unix( |
2678 | PythonVersion { major: 3, minor: 7 }, |
2679 | CPython, |
2680 | None, |
2681 | false |
2682 | ) |
2683 | .unwrap(), |
2684 | "python3.7m" , |
2685 | ); |
2686 | // Defaults to pythonX.Y for CPython 3.8+ |
2687 | assert_eq!( |
2688 | super::default_lib_name_unix( |
2689 | PythonVersion { major: 3, minor: 8 }, |
2690 | CPython, |
2691 | None, |
2692 | false |
2693 | ) |
2694 | .unwrap(), |
2695 | "python3.8" , |
2696 | ); |
2697 | assert_eq!( |
2698 | super::default_lib_name_unix( |
2699 | PythonVersion { major: 3, minor: 9 }, |
2700 | CPython, |
2701 | None, |
2702 | false |
2703 | ) |
2704 | .unwrap(), |
2705 | "python3.9" , |
2706 | ); |
2707 | // Can use ldversion to override for CPython |
2708 | assert_eq!( |
2709 | super::default_lib_name_unix( |
2710 | PythonVersion { major: 3, minor: 9 }, |
2711 | CPython, |
2712 | Some("3.7md" ), |
2713 | false |
2714 | ) |
2715 | .unwrap(), |
2716 | "python3.7md" , |
2717 | ); |
2718 | |
2719 | // PyPy 3.9 includes ldversion |
2720 | assert_eq!( |
2721 | super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, None, false) |
2722 | .unwrap(), |
2723 | "pypy3.9-c" , |
2724 | ); |
2725 | |
2726 | assert_eq!( |
2727 | super::default_lib_name_unix( |
2728 | PythonVersion { major: 3, minor: 9 }, |
2729 | PyPy, |
2730 | Some("3.9d" ), |
2731 | false |
2732 | ) |
2733 | .unwrap(), |
2734 | "pypy3.9d-c" , |
2735 | ); |
2736 | |
2737 | // free-threading adds a t suffix |
2738 | assert_eq!( |
2739 | super::default_lib_name_unix( |
2740 | PythonVersion { |
2741 | major: 3, |
2742 | minor: 13 |
2743 | }, |
2744 | CPython, |
2745 | None, |
2746 | true |
2747 | ) |
2748 | .unwrap(), |
2749 | "python3.13t" , |
2750 | ); |
2751 | // 3.12 and older are incompatible with gil_disabled |
2752 | assert!(super::default_lib_name_unix( |
2753 | PythonVersion { |
2754 | major: 3, |
2755 | minor: 12, |
2756 | }, |
2757 | CPython, |
2758 | None, |
2759 | true, |
2760 | ) |
2761 | .is_err()); |
2762 | } |
2763 | |
2764 | #[test ] |
2765 | fn parse_cross_python_version() { |
2766 | let env_vars = CrossCompileEnvVars { |
2767 | pyo3_cross: None, |
2768 | pyo3_cross_lib_dir: None, |
2769 | pyo3_cross_python_version: Some("3.9" .into()), |
2770 | pyo3_cross_python_implementation: None, |
2771 | }; |
2772 | |
2773 | assert_eq!( |
2774 | env_vars.parse_version().unwrap(), |
2775 | (Some(PythonVersion { major: 3, minor: 9 }), None), |
2776 | ); |
2777 | |
2778 | let env_vars = CrossCompileEnvVars { |
2779 | pyo3_cross: None, |
2780 | pyo3_cross_lib_dir: None, |
2781 | pyo3_cross_python_version: None, |
2782 | pyo3_cross_python_implementation: None, |
2783 | }; |
2784 | |
2785 | assert_eq!(env_vars.parse_version().unwrap(), (None, None)); |
2786 | |
2787 | let env_vars = CrossCompileEnvVars { |
2788 | pyo3_cross: None, |
2789 | pyo3_cross_lib_dir: None, |
2790 | pyo3_cross_python_version: Some("3.13t" .into()), |
2791 | pyo3_cross_python_implementation: None, |
2792 | }; |
2793 | |
2794 | assert_eq!( |
2795 | env_vars.parse_version().unwrap(), |
2796 | ( |
2797 | Some(PythonVersion { |
2798 | major: 3, |
2799 | minor: 13 |
2800 | }), |
2801 | Some("t" .into()) |
2802 | ), |
2803 | ); |
2804 | |
2805 | let env_vars = CrossCompileEnvVars { |
2806 | pyo3_cross: None, |
2807 | pyo3_cross_lib_dir: None, |
2808 | pyo3_cross_python_version: Some("100" .into()), |
2809 | pyo3_cross_python_implementation: None, |
2810 | }; |
2811 | |
2812 | assert!(env_vars.parse_version().is_err()); |
2813 | } |
2814 | |
2815 | #[test ] |
2816 | fn interpreter_version_reduced_to_abi3() { |
2817 | let mut config = InterpreterConfig { |
2818 | abi3: true, |
2819 | build_flags: BuildFlags::default(), |
2820 | pointer_width: None, |
2821 | executable: None, |
2822 | implementation: PythonImplementation::CPython, |
2823 | lib_dir: None, |
2824 | lib_name: None, |
2825 | shared: true, |
2826 | version: PythonVersion { major: 3, minor: 7 }, |
2827 | suppress_build_script_link_lines: false, |
2828 | extra_build_script_lines: vec![], |
2829 | python_framework_prefix: None, |
2830 | }; |
2831 | |
2832 | config |
2833 | .fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 7 })) |
2834 | .unwrap(); |
2835 | assert_eq!(config.version, PythonVersion { major: 3, minor: 7 }); |
2836 | } |
2837 | |
2838 | #[test ] |
2839 | fn abi3_version_cannot_be_higher_than_interpreter() { |
2840 | let mut config = InterpreterConfig { |
2841 | abi3: true, |
2842 | build_flags: BuildFlags::new(), |
2843 | pointer_width: None, |
2844 | executable: None, |
2845 | implementation: PythonImplementation::CPython, |
2846 | lib_dir: None, |
2847 | lib_name: None, |
2848 | shared: true, |
2849 | version: PythonVersion { major: 3, minor: 7 }, |
2850 | suppress_build_script_link_lines: false, |
2851 | extra_build_script_lines: vec![], |
2852 | python_framework_prefix: None, |
2853 | }; |
2854 | |
2855 | assert!(config |
2856 | .fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 8 })) |
2857 | .unwrap_err() |
2858 | .to_string() |
2859 | .contains( |
2860 | "cannot set a minimum Python version 3.8 higher than the interpreter version 3.7" |
2861 | )); |
2862 | } |
2863 | |
2864 | #[test ] |
2865 | #[cfg (all( |
2866 | target_os = "linux" , |
2867 | target_arch = "x86_64" , |
2868 | feature = "resolve-config" |
2869 | ))] |
2870 | fn parse_sysconfigdata() { |
2871 | // A best effort attempt to get test coverage for the sysconfigdata parsing. |
2872 | // Might not complete successfully depending on host installation; that's ok as long as |
2873 | // CI demonstrates this path is covered! |
2874 | |
2875 | let interpreter_config = crate::get(); |
2876 | |
2877 | let lib_dir = match &interpreter_config.lib_dir { |
2878 | Some(lib_dir) => Path::new(lib_dir), |
2879 | // Don't know where to search for sysconfigdata; never mind. |
2880 | None => return, |
2881 | }; |
2882 | |
2883 | let cross = CrossCompileConfig { |
2884 | lib_dir: Some(lib_dir.into()), |
2885 | version: Some(interpreter_config.version), |
2886 | implementation: Some(interpreter_config.implementation), |
2887 | target: triple!("x86_64-unknown-linux-gnu" ), |
2888 | abiflags: if interpreter_config.is_free_threaded() { |
2889 | Some("t" .into()) |
2890 | } else { |
2891 | None |
2892 | }, |
2893 | }; |
2894 | |
2895 | let sysconfigdata_path = match find_sysconfigdata(&cross) { |
2896 | Ok(Some(path)) => path, |
2897 | // Couldn't find a matching sysconfigdata; never mind! |
2898 | _ => return, |
2899 | }; |
2900 | let sysconfigdata = super::parse_sysconfigdata(sysconfigdata_path).unwrap(); |
2901 | let parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); |
2902 | |
2903 | assert_eq!( |
2904 | parsed_config, |
2905 | InterpreterConfig { |
2906 | abi3: false, |
2907 | build_flags: BuildFlags(interpreter_config.build_flags.0.clone()), |
2908 | pointer_width: Some(64), |
2909 | executable: None, |
2910 | implementation: PythonImplementation::CPython, |
2911 | lib_dir: interpreter_config.lib_dir.to_owned(), |
2912 | lib_name: interpreter_config.lib_name.to_owned(), |
2913 | shared: true, |
2914 | version: interpreter_config.version, |
2915 | suppress_build_script_link_lines: false, |
2916 | extra_build_script_lines: vec![], |
2917 | python_framework_prefix: None, |
2918 | } |
2919 | ) |
2920 | } |
2921 | |
2922 | #[test ] |
2923 | fn test_venv_interpreter() { |
2924 | let base = OsStr::new("base" ); |
2925 | assert_eq!( |
2926 | venv_interpreter(base, true), |
2927 | PathBuf::from_iter(&["base" , "Scripts" , "python.exe" ]) |
2928 | ); |
2929 | assert_eq!( |
2930 | venv_interpreter(base, false), |
2931 | PathBuf::from_iter(&["base" , "bin" , "python" ]) |
2932 | ); |
2933 | } |
2934 | |
2935 | #[test ] |
2936 | fn test_conda_env_interpreter() { |
2937 | let base = OsStr::new("base" ); |
2938 | assert_eq!( |
2939 | conda_env_interpreter(base, true), |
2940 | PathBuf::from_iter(&["base" , "python.exe" ]) |
2941 | ); |
2942 | assert_eq!( |
2943 | conda_env_interpreter(base, false), |
2944 | PathBuf::from_iter(&["base" , "bin" , "python" ]) |
2945 | ); |
2946 | } |
2947 | |
2948 | #[test ] |
2949 | fn test_not_cross_compiling_from_to() { |
2950 | assert!(cross_compiling_from_to( |
2951 | &triple!("x86_64-unknown-linux-gnu" ), |
2952 | &triple!("x86_64-unknown-linux-gnu" ), |
2953 | ) |
2954 | .unwrap() |
2955 | .is_none()); |
2956 | |
2957 | assert!(cross_compiling_from_to( |
2958 | &triple!("x86_64-apple-darwin" ), |
2959 | &triple!("x86_64-apple-darwin" ) |
2960 | ) |
2961 | .unwrap() |
2962 | .is_none()); |
2963 | |
2964 | assert!(cross_compiling_from_to( |
2965 | &triple!("aarch64-apple-darwin" ), |
2966 | &triple!("x86_64-apple-darwin" ) |
2967 | ) |
2968 | .unwrap() |
2969 | .is_none()); |
2970 | |
2971 | assert!(cross_compiling_from_to( |
2972 | &triple!("x86_64-apple-darwin" ), |
2973 | &triple!("aarch64-apple-darwin" ) |
2974 | ) |
2975 | .unwrap() |
2976 | .is_none()); |
2977 | |
2978 | assert!(cross_compiling_from_to( |
2979 | &triple!("x86_64-pc-windows-msvc" ), |
2980 | &triple!("i686-pc-windows-msvc" ) |
2981 | ) |
2982 | .unwrap() |
2983 | .is_none()); |
2984 | |
2985 | assert!(cross_compiling_from_to( |
2986 | &triple!("x86_64-unknown-linux-gnu" ), |
2987 | &triple!("x86_64-unknown-linux-musl" ) |
2988 | ) |
2989 | .unwrap() |
2990 | .is_none()); |
2991 | } |
2992 | |
2993 | #[test ] |
2994 | fn test_is_cross_compiling_from_to() { |
2995 | assert!(cross_compiling_from_to( |
2996 | &triple!("x86_64-pc-windows-msvc" ), |
2997 | &triple!("aarch64-pc-windows-msvc" ) |
2998 | ) |
2999 | .unwrap() |
3000 | .is_some()); |
3001 | } |
3002 | |
3003 | #[test ] |
3004 | fn test_run_python_script() { |
3005 | // as above, this should be okay in CI where Python is presumed installed |
3006 | let interpreter = make_interpreter_config() |
3007 | .expect("could not get InterpreterConfig from installed interpreter" ); |
3008 | let out = interpreter |
3009 | .run_python_script("print(2 + 2)" ) |
3010 | .expect("failed to run Python script" ); |
3011 | assert_eq!(out.trim_end(), "4" ); |
3012 | } |
3013 | |
3014 | #[test ] |
3015 | fn test_run_python_script_with_envs() { |
3016 | // as above, this should be okay in CI where Python is presumed installed |
3017 | let interpreter = make_interpreter_config() |
3018 | .expect("could not get InterpreterConfig from installed interpreter" ); |
3019 | let out = interpreter |
3020 | .run_python_script_with_envs( |
3021 | "import os; print(os.getenv('PYO3_TEST'))" , |
3022 | vec![("PYO3_TEST" , "42" )], |
3023 | ) |
3024 | .expect("failed to run Python script" ); |
3025 | assert_eq!(out.trim_end(), "42" ); |
3026 | } |
3027 | |
3028 | #[test ] |
3029 | fn test_build_script_outputs_base() { |
3030 | let interpreter_config = InterpreterConfig { |
3031 | implementation: PythonImplementation::CPython, |
3032 | version: PythonVersion { major: 3, minor: 9 }, |
3033 | shared: true, |
3034 | abi3: false, |
3035 | lib_name: Some("python3" .into()), |
3036 | lib_dir: None, |
3037 | executable: None, |
3038 | pointer_width: None, |
3039 | build_flags: BuildFlags::default(), |
3040 | suppress_build_script_link_lines: false, |
3041 | extra_build_script_lines: vec![], |
3042 | python_framework_prefix: None, |
3043 | }; |
3044 | assert_eq!( |
3045 | interpreter_config.build_script_outputs(), |
3046 | [ |
3047 | "cargo:rustc-cfg=Py_3_7" .to_owned(), |
3048 | "cargo:rustc-cfg=Py_3_8" .to_owned(), |
3049 | "cargo:rustc-cfg=Py_3_9" .to_owned(), |
3050 | ] |
3051 | ); |
3052 | |
3053 | let interpreter_config = InterpreterConfig { |
3054 | implementation: PythonImplementation::PyPy, |
3055 | ..interpreter_config |
3056 | }; |
3057 | assert_eq!( |
3058 | interpreter_config.build_script_outputs(), |
3059 | [ |
3060 | "cargo:rustc-cfg=Py_3_7" .to_owned(), |
3061 | "cargo:rustc-cfg=Py_3_8" .to_owned(), |
3062 | "cargo:rustc-cfg=Py_3_9" .to_owned(), |
3063 | "cargo:rustc-cfg=PyPy" .to_owned(), |
3064 | ] |
3065 | ); |
3066 | } |
3067 | |
3068 | #[test ] |
3069 | fn test_build_script_outputs_abi3() { |
3070 | let interpreter_config = InterpreterConfig { |
3071 | implementation: PythonImplementation::CPython, |
3072 | version: PythonVersion { major: 3, minor: 9 }, |
3073 | shared: true, |
3074 | abi3: true, |
3075 | lib_name: Some("python3" .into()), |
3076 | lib_dir: None, |
3077 | executable: None, |
3078 | pointer_width: None, |
3079 | build_flags: BuildFlags::default(), |
3080 | suppress_build_script_link_lines: false, |
3081 | extra_build_script_lines: vec![], |
3082 | python_framework_prefix: None, |
3083 | }; |
3084 | |
3085 | assert_eq!( |
3086 | interpreter_config.build_script_outputs(), |
3087 | [ |
3088 | "cargo:rustc-cfg=Py_3_7" .to_owned(), |
3089 | "cargo:rustc-cfg=Py_3_8" .to_owned(), |
3090 | "cargo:rustc-cfg=Py_3_9" .to_owned(), |
3091 | "cargo:rustc-cfg=Py_LIMITED_API" .to_owned(), |
3092 | ] |
3093 | ); |
3094 | |
3095 | let interpreter_config = InterpreterConfig { |
3096 | implementation: PythonImplementation::PyPy, |
3097 | ..interpreter_config |
3098 | }; |
3099 | assert_eq!( |
3100 | interpreter_config.build_script_outputs(), |
3101 | [ |
3102 | "cargo:rustc-cfg=Py_3_7" .to_owned(), |
3103 | "cargo:rustc-cfg=Py_3_8" .to_owned(), |
3104 | "cargo:rustc-cfg=Py_3_9" .to_owned(), |
3105 | "cargo:rustc-cfg=PyPy" .to_owned(), |
3106 | "cargo:rustc-cfg=Py_LIMITED_API" .to_owned(), |
3107 | ] |
3108 | ); |
3109 | } |
3110 | |
3111 | #[test ] |
3112 | fn test_build_script_outputs_gil_disabled() { |
3113 | let mut build_flags = BuildFlags::default(); |
3114 | build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); |
3115 | let interpreter_config = InterpreterConfig { |
3116 | implementation: PythonImplementation::CPython, |
3117 | version: PythonVersion { |
3118 | major: 3, |
3119 | minor: 13, |
3120 | }, |
3121 | shared: true, |
3122 | abi3: false, |
3123 | lib_name: Some("python3" .into()), |
3124 | lib_dir: None, |
3125 | executable: None, |
3126 | pointer_width: None, |
3127 | build_flags, |
3128 | suppress_build_script_link_lines: false, |
3129 | extra_build_script_lines: vec![], |
3130 | python_framework_prefix: None, |
3131 | }; |
3132 | |
3133 | assert_eq!( |
3134 | interpreter_config.build_script_outputs(), |
3135 | [ |
3136 | "cargo:rustc-cfg=Py_3_7" .to_owned(), |
3137 | "cargo:rustc-cfg=Py_3_8" .to_owned(), |
3138 | "cargo:rustc-cfg=Py_3_9" .to_owned(), |
3139 | "cargo:rustc-cfg=Py_3_10" .to_owned(), |
3140 | "cargo:rustc-cfg=Py_3_11" .to_owned(), |
3141 | "cargo:rustc-cfg=Py_3_12" .to_owned(), |
3142 | "cargo:rustc-cfg=Py_3_13" .to_owned(), |
3143 | "cargo:rustc-cfg=Py_GIL_DISABLED" .to_owned(), |
3144 | ] |
3145 | ); |
3146 | } |
3147 | |
3148 | #[test ] |
3149 | fn test_build_script_outputs_debug() { |
3150 | let mut build_flags = BuildFlags::default(); |
3151 | build_flags.0.insert(BuildFlag::Py_DEBUG); |
3152 | let interpreter_config = InterpreterConfig { |
3153 | implementation: PythonImplementation::CPython, |
3154 | version: PythonVersion { major: 3, minor: 7 }, |
3155 | shared: true, |
3156 | abi3: false, |
3157 | lib_name: Some("python3" .into()), |
3158 | lib_dir: None, |
3159 | executable: None, |
3160 | pointer_width: None, |
3161 | build_flags, |
3162 | suppress_build_script_link_lines: false, |
3163 | extra_build_script_lines: vec![], |
3164 | python_framework_prefix: None, |
3165 | }; |
3166 | |
3167 | assert_eq!( |
3168 | interpreter_config.build_script_outputs(), |
3169 | [ |
3170 | "cargo:rustc-cfg=Py_3_7" .to_owned(), |
3171 | "cargo:rustc-cfg=py_sys_config= \"Py_DEBUG \"" .to_owned(), |
3172 | ] |
3173 | ); |
3174 | } |
3175 | |
3176 | #[test ] |
3177 | fn test_find_sysconfigdata_in_invalid_lib_dir() { |
3178 | let e = find_all_sysconfigdata(&CrossCompileConfig { |
3179 | lib_dir: Some(PathBuf::from("/abc/123/not/a/real/path" )), |
3180 | version: None, |
3181 | implementation: None, |
3182 | target: triple!("x86_64-unknown-linux-gnu" ), |
3183 | abiflags: None, |
3184 | }) |
3185 | .unwrap_err(); |
3186 | |
3187 | // actual error message is platform-dependent, so just check the context we add |
3188 | assert!(e.report().to_string().starts_with( |
3189 | "failed to search the lib dir at 'PYO3_CROSS_LIB_DIR=/abc/123/not/a/real/path' \n\ |
3190 | caused by: \n \ |
3191 | - 0: failed to list the entries in '/abc/123/not/a/real/path' \n \ |
3192 | - 1: \ |
3193 | " |
3194 | )); |
3195 | } |
3196 | |
3197 | #[test ] |
3198 | fn test_from_pyo3_config_file_env_rebuild() { |
3199 | READ_ENV_VARS.with(|vars| vars.borrow_mut().clear()); |
3200 | let _ = InterpreterConfig::from_pyo3_config_file_env(); |
3201 | // it's possible that other env vars were also read, hence just checking for contains |
3202 | READ_ENV_VARS.with(|vars| assert!(vars.borrow().contains(&"PYO3_CONFIG_FILE" .to_string()))); |
3203 | } |
3204 | } |
3205 | |