1#![warn(elided_lifetimes_in_paths, unused_lifetimes)]
9
10mod errors;
11mod impl_;
12
13#[cfg(feature = "resolve-config")]
14use std::{
15 io::Cursor,
16 path::{Path, PathBuf},
17};
18
19use std::{env, process::Command, str::FromStr, sync::OnceLock};
20
21pub use impl_::{
22 cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags,
23 CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple,
24};
25
26use target_lexicon::OperatingSystem;
27
28#[doc = concat!("[see PyO3's guide](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution/multiple_python_versions.html)")]
43#[cfg(feature = "resolve-config")]
45pub fn use_pyo3_cfgs() {
46 print_expected_cfgs();
47 for cargo_command in get().build_script_outputs() {
48 println!("{cargo_command}")
49 }
50}
51
52pub fn add_extension_module_link_args() {
63 _add_extension_module_link_args(&impl_::target_triple_from_env(), std::io::stdout())
64}
65
66fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Write) {
67 if matches!(triple.operating_system, OperatingSystem::Darwin(_)) {
68 writeln!(writer, "cargo:rustc-cdylib-link-arg=-undefined").unwrap();
69 writeln!(writer, "cargo:rustc-cdylib-link-arg=dynamic_lookup").unwrap();
70 } else if triple == &Triple::from_str("wasm32-unknown-emscripten").unwrap() {
71 writeln!(writer, "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2").unwrap();
72 writeln!(writer, "cargo:rustc-cdylib-link-arg=-sWASM_BIGINT").unwrap();
73 }
74}
75
76#[doc = concat!("[See PyO3's guide](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution#dynamically-embedding-the-python-interpreter)")]
90#[cfg(feature = "resolve-config")]
92pub fn add_libpython_rpath_link_args() {
93 let target = impl_::target_triple_from_env();
94 _add_libpython_rpath_link_args(
95 get(),
96 impl_::is_linking_libpython_for_target(&target),
97 std::io::stdout(),
98 )
99}
100
101#[cfg(feature = "resolve-config")]
102fn _add_libpython_rpath_link_args(
103 interpreter_config: &InterpreterConfig,
104 is_linking_libpython: bool,
105 mut writer: impl std::io::Write,
106) {
107 if is_linking_libpython {
108 if let Some(lib_dir) = interpreter_config.lib_dir.as_ref() {
109 writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}").unwrap();
110 }
111 }
112}
113
114#[cfg(feature = "resolve-config")]
123pub fn add_python_framework_link_args() {
124 let target = impl_::target_triple_from_env();
125 _add_python_framework_link_args(
126 get(),
127 &target,
128 impl_::is_linking_libpython_for_target(&target),
129 std::io::stdout(),
130 )
131}
132
133#[cfg(feature = "resolve-config")]
134fn _add_python_framework_link_args(
135 interpreter_config: &InterpreterConfig,
136 triple: &Triple,
137 link_libpython: bool,
138 mut writer: impl std::io::Write,
139) {
140 if matches!(triple.operating_system, OperatingSystem::Darwin(_)) && link_libpython {
141 if let Some(framework_prefix) = interpreter_config.python_framework_prefix.as_ref() {
142 writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{framework_prefix}").unwrap();
143 }
144 }
145}
146
147#[cfg(feature = "resolve-config")]
151pub fn get() -> &'static InterpreterConfig {
152 static CONFIG: OnceLock<InterpreterConfig> = OnceLock::new();
153 CONFIG.get_or_init(|| {
154 let cross_compile_config_path = resolve_cross_compile_config_path();
156 let cross_compiling = cross_compile_config_path
157 .as_ref()
158 .map(|path| path.exists())
159 .unwrap_or(false);
160
161 #[allow(
162 clippy::const_is_empty,
163 reason = "CONFIG_FILE is generated in build.rs, content can vary"
164 )]
165 if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() {
166 interpreter_config
167 } else if let Some(interpreter_config) = config_from_pyo3_config_file_env() {
168 Ok(interpreter_config)
169 } else if cross_compiling {
170 InterpreterConfig::from_path(cross_compile_config_path.as_ref().unwrap())
171 } else {
172 InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))
173 }
174 .expect("failed to parse PyO3 config")
175 })
176}
177
178#[cfg(feature = "resolve-config")]
180fn config_from_pyo3_config_file_env() -> Option<InterpreterConfig> {
181 #[doc(hidden)]
182 const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt"));
183
184 #[allow(
185 clippy::const_is_empty,
186 reason = "CONFIG_FILE is generated in build.rs, content can vary"
187 )]
188 if !CONFIG_FILE.is_empty() {
189 let config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
190 .expect("contents of CONFIG_FILE should always be valid (generated by pyo3-build-config's build.rs)");
191 Some(config)
192 } else {
193 None
194 }
195}
196
197#[doc(hidden)]
200#[cfg(feature = "resolve-config")]
201const HOST_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config.txt"));
202
203#[doc(hidden)]
209#[cfg(feature = "resolve-config")]
210fn resolve_cross_compile_config_path() -> Option<PathBuf> {
211 env::var_os("TARGET").map(|target| {
212 let mut path = PathBuf::from(env!("OUT_DIR"));
213 path.push(Path::new(&target));
214 path.push("pyo3-build-config.txt");
215 path
216 })
217}
218
219fn print_feature_cfg(minor_version_required: u32, cfg: &str) {
221 let minor_version = rustc_minor_version().unwrap_or(0);
222
223 if minor_version >= minor_version_required {
224 println!("cargo:rustc-cfg={cfg}");
225 }
226
227 if minor_version >= 80 {
229 println!("cargo:rustc-check-cfg=cfg({cfg})");
230 }
231}
232
233#[doc(hidden)]
238pub fn print_feature_cfgs() {
239 print_feature_cfg(85, "fn_ptr_eq");
240 print_feature_cfg(86, "from_bytes_with_nul_error");
241}
242
243#[doc(hidden)]
248pub fn print_expected_cfgs() {
249 if rustc_minor_version().is_some_and(|version| version < 80) {
250 return;
252 }
253
254 println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)");
255 println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)");
256 println!("cargo:rustc-check-cfg=cfg(PyPy)");
257 println!("cargo:rustc-check-cfg=cfg(GraalPy)");
258 println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))");
259 println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)");
260 println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)");
261
262 for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 {
265 println!("cargo:rustc-check-cfg=cfg(Py_3_{i})");
266 }
267}
268
269#[doc(hidden)]
273#[cfg(feature = "resolve-config")]
274pub mod pyo3_build_script_impl {
275 use crate::errors::{Context, Result};
276
277 use super::*;
278
279 pub mod errors {
280 pub use crate::errors::*;
281 }
282 pub use crate::impl_::{
283 cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config,
284 target_triple_from_env, InterpreterConfig, PythonVersion,
285 };
286 pub enum BuildConfigSource {
287 ConfigFile,
289 Host,
291 CrossCompile,
293 }
294
295 pub struct BuildConfig {
296 pub interpreter_config: InterpreterConfig,
297 pub source: BuildConfigSource,
298 }
299
300 pub fn resolve_build_config(target: &Triple) -> Result<BuildConfig> {
310 #[allow(
311 clippy::const_is_empty,
312 reason = "CONFIG_FILE is generated in build.rs, content can vary"
313 )]
314 if let Some(mut interpreter_config) = config_from_pyo3_config_file_env() {
315 interpreter_config.apply_default_lib_name_to_config_file(target);
316 interpreter_config.generate_import_libs()?;
317 Ok(BuildConfig {
318 interpreter_config,
319 source: BuildConfigSource::ConfigFile,
320 })
321 } else if let Some(interpreter_config) = make_cross_compile_config()? {
322 let path = resolve_cross_compile_config_path()
324 .expect("resolve_build_config() must be called from a build script");
325 let parent_dir = path.parent().ok_or_else(|| {
326 format!(
327 "failed to resolve parent directory of config file {}",
328 path.display()
329 )
330 })?;
331 std::fs::create_dir_all(parent_dir).with_context(|| {
332 format!(
333 "failed to create config file directory {}",
334 parent_dir.display()
335 )
336 })?;
337 interpreter_config.to_writer(&mut std::fs::File::create(&path).with_context(
338 || format!("failed to create config file at {}", path.display()),
339 )?)?;
340 Ok(BuildConfig {
341 interpreter_config,
342 source: BuildConfigSource::CrossCompile,
343 })
344 } else {
345 let interpreter_config = InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))?;
346 Ok(BuildConfig {
347 interpreter_config,
348 source: BuildConfigSource::Host,
349 })
350 }
351 }
352
353 pub struct MaximumVersionExceeded {
356 message: String,
357 }
358
359 impl MaximumVersionExceeded {
360 pub fn new(
361 interpreter_config: &InterpreterConfig,
362 supported_version: PythonVersion,
363 ) -> Self {
364 let implementation = match interpreter_config.implementation {
365 PythonImplementation::CPython => "Python",
366 PythonImplementation::PyPy => "PyPy",
367 PythonImplementation::GraalPy => "GraalPy",
368 };
369 let version = &interpreter_config.version;
370 let message = format!(
371 "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\
372 = help: this package is being built with PyO3 version {current_version}\n\
373 = help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\
374 = help: updating this package to the latest version of PyO3 may provide compatibility with this {implementation} version",
375 current_version = env!("CARGO_PKG_VERSION")
376 );
377 Self { message }
378 }
379
380 pub fn add_help(&mut self, help: &str) {
381 self.message.push_str("\n= help: ");
382 self.message.push_str(help);
383 }
384
385 pub fn finish(self) -> String {
386 self.message
387 }
388 }
389}
390
391fn rustc_minor_version() -> Option<u32> {
392 static RUSTC_MINOR_VERSION: OnceLock<Option<u32>> = OnceLock::new();
393 *RUSTC_MINOR_VERSION.get_or_init(|| {
394 let rustc = env::var_os("RUSTC")?;
395 let output = Command::new(rustc).arg("--version").output().ok()?;
396 let version = core::str::from_utf8(&output.stdout).ok()?;
397 let mut pieces = version.split('.');
398 if pieces.next() != Some("rustc 1") {
399 return None;
400 }
401 pieces.next()?.parse().ok()
402 })
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn extension_module_link_args() {
411 let mut buf = Vec::new();
412
413 _add_extension_module_link_args(
415 &Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
416 &mut buf,
417 );
418 assert_eq!(buf, Vec::new());
419
420 _add_extension_module_link_args(
421 &Triple::from_str("x86_64-apple-darwin").unwrap(),
422 &mut buf,
423 );
424 assert_eq!(
425 std::str::from_utf8(&buf).unwrap(),
426 "cargo:rustc-cdylib-link-arg=-undefined\n\
427 cargo:rustc-cdylib-link-arg=dynamic_lookup\n"
428 );
429
430 buf.clear();
431 _add_extension_module_link_args(
432 &Triple::from_str("wasm32-unknown-emscripten").unwrap(),
433 &mut buf,
434 );
435 assert_eq!(
436 std::str::from_utf8(&buf).unwrap(),
437 "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2\n\
438 cargo:rustc-cdylib-link-arg=-sWASM_BIGINT\n"
439 );
440 }
441
442 #[cfg(feature = "resolve-config")]
443 #[test]
444 fn python_framework_link_args() {
445 let mut buf = Vec::new();
446
447 let interpreter_config = InterpreterConfig {
448 implementation: PythonImplementation::CPython,
449 version: PythonVersion {
450 major: 3,
451 minor: 13,
452 },
453 shared: true,
454 abi3: false,
455 lib_name: None,
456 lib_dir: None,
457 executable: None,
458 pointer_width: None,
459 build_flags: BuildFlags::default(),
460 suppress_build_script_link_lines: false,
461 extra_build_script_lines: vec![],
462 python_framework_prefix: Some(
463 "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(),
464 ),
465 };
466 _add_python_framework_link_args(
468 &interpreter_config,
469 &Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
470 true,
471 &mut buf,
472 );
473 assert_eq!(buf, Vec::new());
474
475 _add_python_framework_link_args(
476 &interpreter_config,
477 &Triple::from_str("x86_64-apple-darwin").unwrap(),
478 true,
479 &mut buf,
480 );
481 assert_eq!(
482 std::str::from_utf8(&buf).unwrap(),
483 "cargo:rustc-link-arg=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Library/Frameworks\n"
484 );
485 }
486
487 #[test]
488 #[cfg(feature = "resolve-config")]
489 fn test_maximum_version_exceeded_formatting() {
490 let interpreter_config = InterpreterConfig {
491 implementation: PythonImplementation::CPython,
492 version: PythonVersion {
493 major: 3,
494 minor: 13,
495 },
496 shared: true,
497 abi3: false,
498 lib_name: None,
499 lib_dir: None,
500 executable: None,
501 pointer_width: None,
502 build_flags: BuildFlags::default(),
503 suppress_build_script_link_lines: false,
504 extra_build_script_lines: vec![],
505 python_framework_prefix: None,
506 };
507 let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new(
508 &interpreter_config,
509 PythonVersion {
510 major: 3,
511 minor: 12,
512 },
513 );
514 error.add_help("this is a help message");
515 let error = error.finish();
516 let expected = concat!("\
517 the configured Python version (3.13) is newer than PyO3's maximum supported version (3.12)\n\
518 = help: this package is being built with PyO3 version ", env!("CARGO_PKG_VERSION"), "\n\
519 = help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\
520 = help: updating this package to the latest version of PyO3 may provide compatibility with this Python version\n\
521 = help: this is a help message"
522 );
523 assert_eq!(error, expected);
524 }
525}