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 PyForge's guide](https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution/multiple_python_versions.html)")]
42#[cfg(feature = "resolve-config")]
44pub fn use_pyo3_cfgs() {
45 print_expected_cfgs();
46 for cargo_command in get().build_script_outputs() {
47 println!("{cargo_command}")
48 }
49}
50
51pub fn add_extension_module_link_args() {
62 _add_extension_module_link_args(
63 &impl_::target_triple_from_env(),
64 std::io::stdout(),
65 rustc_minor_version(),
66 )
67}
68
69fn _add_extension_module_link_args(
70 triple: &Triple,
71 mut writer: impl std::io::Write,
72 rustc_minor_version: Option<u32>,
73) {
74 if matches!(triple.operating_system, OperatingSystem::Darwin(_)) {
75 writeln!(writer, "cargo:rustc-cdylib-link-arg=-undefined").unwrap();
76 writeln!(writer, "cargo:rustc-cdylib-link-arg=dynamic_lookup").unwrap();
77 } else if triple == &Triple::from_str("wasm32-unknown-emscripten").unwrap()
78 && rustc_minor_version.is_some_and(|version| version < 95)
79 {
80 writeln!(writer, "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2").unwrap();
81 writeln!(writer, "cargo:rustc-cdylib-link-arg=-sWASM_BIGINT").unwrap();
82 }
83}
84
85#[doc = concat!("[See PyForge's guide](https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution#dynamically-embedding-the-python-interpreter)")]
99#[cfg(feature = "resolve-config")]
101pub fn add_libpython_rpath_link_args() {
102 let target = impl_::target_triple_from_env();
103 _add_libpython_rpath_link_args(
104 get(),
105 impl_::is_linking_libpython_for_target(&target),
106 std::io::stdout(),
107 )
108}
109
110#[cfg(feature = "resolve-config")]
111fn _add_libpython_rpath_link_args(
112 interpreter_config: &InterpreterConfig,
113 is_linking_libpython: bool,
114 mut writer: impl std::io::Write,
115) {
116 if is_linking_libpython {
117 if let Some(lib_dir) = interpreter_config.lib_dir.as_ref() {
118 writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}").unwrap();
119 }
120 }
121}
122
123#[cfg(feature = "resolve-config")]
132pub fn add_python_framework_link_args() {
133 let target = impl_::target_triple_from_env();
134 _add_python_framework_link_args(
135 get(),
136 &target,
137 impl_::is_linking_libpython_for_target(&target),
138 std::io::stdout(),
139 )
140}
141
142#[cfg(feature = "resolve-config")]
143fn _add_python_framework_link_args(
144 interpreter_config: &InterpreterConfig,
145 triple: &Triple,
146 link_libpython: bool,
147 mut writer: impl std::io::Write,
148) {
149 if matches!(triple.operating_system, OperatingSystem::Darwin(_)) && link_libpython {
150 if let Some(framework_prefix) = interpreter_config.python_framework_prefix.as_ref() {
151 writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{framework_prefix}").unwrap();
152 }
153 }
154}
155
156#[cfg(feature = "resolve-config")]
160pub fn get() -> &'static InterpreterConfig {
161 static CONFIG: OnceLock<InterpreterConfig> = OnceLock::new();
162 CONFIG.get_or_init(|| {
163 let cross_compile_config_path = resolve_cross_compile_config_path();
165 let cross_compiling = cross_compile_config_path
166 .as_ref()
167 .map(|path| path.exists())
168 .unwrap_or(false);
169
170 #[allow(
171 clippy::const_is_empty,
172 reason = "CONFIG_FILE is generated in build.rs, content can vary"
173 )]
174 if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() {
175 interpreter_config
176 } else if let Some(interpreter_config) = config_from_pyo3_config_file_env() {
177 Ok(interpreter_config)
178 } else if cross_compiling {
179 InterpreterConfig::from_path(cross_compile_config_path.as_ref().unwrap())
180 } else {
181 InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))
182 }
183 .expect("failed to parse PyForge config")
184 })
185}
186
187#[cfg(feature = "resolve-config")]
189fn config_from_pyo3_config_file_env() -> Option<InterpreterConfig> {
190 #[doc(hidden)]
191 const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt"));
192
193 #[allow(
194 clippy::const_is_empty,
195 reason = "CONFIG_FILE is generated in build.rs, content can vary"
196 )]
197 if !CONFIG_FILE.is_empty() {
198 let config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))
199 .expect("contents of CONFIG_FILE should always be valid (generated by pyo3-build-config's build.rs)");
200 Some(config)
201 } else {
202 None
203 }
204}
205
206#[doc(hidden)]
209#[cfg(feature = "resolve-config")]
210const HOST_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config.txt"));
211
212#[doc(hidden)]
218#[cfg(feature = "resolve-config")]
219fn resolve_cross_compile_config_path() -> Option<PathBuf> {
220 env::var_os("TARGET").map(|target| {
221 let mut path = PathBuf::from(env!("OUT_DIR"));
222 path.push(Path::new(&target));
223 path.push("pyo3-build-config.txt");
224 path
225 })
226}
227
228fn print_feature_cfg(minor_version_required: u32, cfg: &str) {
230 let minor_version = rustc_minor_version().unwrap_or(0);
231
232 if minor_version >= minor_version_required {
233 println!("cargo:rustc-cfg={cfg}");
234 }
235
236 if minor_version >= 80 {
238 println!("cargo:rustc-check-cfg=cfg({cfg})");
239 }
240}
241
242#[doc(hidden)]
247pub fn print_feature_cfgs() {
248 print_feature_cfg(85, "fn_ptr_eq");
249 print_feature_cfg(86, "from_bytes_with_nul_error");
250}
251
252#[doc(hidden)]
257pub fn print_expected_cfgs() {
258 if rustc_minor_version().is_some_and(|version| version < 80) {
259 return;
261 }
262
263 println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)");
264 println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)");
265 println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))");
266 println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)");
267 println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)");
268
269 for i in 8..=impl_::ABI3_MAX_MINOR + 1 {
271 println!("cargo:rustc-check-cfg=cfg(Py_3_{i})");
272 }
273
274 let mut dll_names = vec!["python3".to_string(), "python3_d".to_string()];
276 for i in 8..=impl_::ABI3_MAX_MINOR + 1 {
277 dll_names.push(format!("python3{i}"));
278 dll_names.push(format!("python3{i}_d"));
279 if i >= 13 {
280 dll_names.push(format!("python3{i}t"));
281 dll_names.push(format!("python3{i}t_d"));
282 }
283 }
284 let values = dll_names
286 .iter()
287 .map(|n| format!("\"{n}\""))
288 .collect::<Vec<_>>()
289 .join(", ");
290 println!("cargo:rustc-check-cfg=cfg(pyo3_dll, values({values}))");
291}
292
293#[doc(hidden)]
297#[cfg(feature = "resolve-config")]
298pub mod pyo3_build_script_impl {
299 use crate::errors::{Context, Result};
300
301 use super::*;
302
303 pub mod errors {
304 pub use crate::errors::*;
305 }
306 pub use crate::impl_::{
307 cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config,
308 target_triple_from_env, InterpreterConfig, PythonVersion,
309 };
310 pub enum BuildConfigSource {
311 ConfigFile,
313 Host,
315 CrossCompile,
317 }
318
319 pub struct BuildConfig {
320 pub interpreter_config: InterpreterConfig,
321 pub source: BuildConfigSource,
322 }
323
324 pub fn resolve_build_config(target: &Triple) -> Result<BuildConfig> {
334 #[allow(
335 clippy::const_is_empty,
336 reason = "CONFIG_FILE is generated in build.rs, content can vary"
337 )]
338 if let Some(mut interpreter_config) = config_from_pyo3_config_file_env() {
339 interpreter_config.apply_default_lib_name_to_config_file(target);
340 Ok(BuildConfig {
341 interpreter_config,
342 source: BuildConfigSource::ConfigFile,
343 })
344 } else if let Some(interpreter_config) = make_cross_compile_config()? {
345 let path = resolve_cross_compile_config_path()
347 .expect("resolve_build_config() must be called from a build script");
348 let parent_dir = path.parent().ok_or_else(|| {
349 format!(
350 "failed to resolve parent directory of config file {}",
351 path.display()
352 )
353 })?;
354 std::fs::create_dir_all(parent_dir).with_context(|| {
355 format!(
356 "failed to create config file directory {}",
357 parent_dir.display()
358 )
359 })?;
360 interpreter_config.to_writer(&mut std::fs::File::create(&path).with_context(
361 || format!("failed to create config file at {}", path.display()),
362 )?)?;
363 Ok(BuildConfig {
364 interpreter_config,
365 source: BuildConfigSource::CrossCompile,
366 })
367 } else {
368 let interpreter_config = InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))?;
369 Ok(BuildConfig {
370 interpreter_config,
371 source: BuildConfigSource::Host,
372 })
373 }
374 }
375
376 pub struct MaximumVersionExceeded {
379 message: String,
380 }
381
382 impl MaximumVersionExceeded {
383 pub fn new(
384 interpreter_config: &InterpreterConfig,
385 supported_version: PythonVersion,
386 ) -> Self {
387 let implementation = match interpreter_config.implementation {
388 PythonImplementation::CPython => "Python",
389 PythonImplementation::PyPy => "PyPy",
390 PythonImplementation::GraalPy => "GraalPy",
391 };
392 let version = &interpreter_config.version;
393 let message = format!(
394 "the configured {implementation} version ({version}) is newer than PyForge's maximum supported version ({supported_version})\n\
395 = help: this package is being built with PyForge version {current_version}\n\
396 = help: check https://crates.io/crates/pyo3 for the latest PyForge version available\n\
397 = help: updating this package to the latest version of PyForge may provide compatibility with this {implementation} version",
398 current_version = env!("CARGO_PKG_VERSION")
399 );
400 Self { message }
401 }
402
403 pub fn add_help(&mut self, help: &str) {
404 self.message.push_str("\n= help: ");
405 self.message.push_str(help);
406 }
407
408 pub fn finish(self) -> String {
409 self.message
410 }
411 }
412}
413
414fn rustc_minor_version() -> Option<u32> {
415 static RUSTC_MINOR_VERSION: OnceLock<Option<u32>> = OnceLock::new();
416 *RUSTC_MINOR_VERSION.get_or_init(|| {
417 let rustc = env::var_os("RUSTC")?;
418 let output = Command::new(rustc).arg("--version").output().ok()?;
419 let version = core::str::from_utf8(&output.stdout).ok()?;
420 let mut pieces = version.split('.');
421 if pieces.next() != Some("rustc 1") {
422 return None;
423 }
424 pieces.next()?.parse().ok()
425 })
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn extension_module_link_args() {
434 let mut buf = Vec::new();
435
436 _add_extension_module_link_args(
438 &Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
439 &mut buf,
440 None,
441 );
442 assert_eq!(buf, Vec::new());
443
444 _add_extension_module_link_args(
445 &Triple::from_str("x86_64-apple-darwin").unwrap(),
446 &mut buf,
447 None,
448 );
449 assert_eq!(
450 std::str::from_utf8(&buf).unwrap(),
451 "cargo:rustc-cdylib-link-arg=-undefined\n\
452 cargo:rustc-cdylib-link-arg=dynamic_lookup\n"
453 );
454
455 buf.clear();
456 _add_extension_module_link_args(
457 &Triple::from_str("wasm32-unknown-emscripten").unwrap(),
458 &mut buf,
459 Some(94),
460 );
461 assert_eq!(
462 std::str::from_utf8(&buf).unwrap(),
463 "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2\n\
464 cargo:rustc-cdylib-link-arg=-sWASM_BIGINT\n"
465 );
466 buf.clear();
467 _add_extension_module_link_args(
468 &Triple::from_str("wasm32-unknown-emscripten").unwrap(),
469 &mut buf,
470 Some(95),
471 );
472 assert_eq!(std::str::from_utf8(&buf).unwrap(), "");
473 }
474
475 #[cfg(feature = "resolve-config")]
476 #[test]
477 fn python_framework_link_args() {
478 let mut buf = Vec::new();
479
480 let interpreter_config = InterpreterConfig {
481 implementation: PythonImplementation::CPython,
482 version: PythonVersion {
483 major: 3,
484 minor: 13,
485 },
486 shared: true,
487 abi3: false,
488 lib_name: None,
489 lib_dir: None,
490 executable: None,
491 pointer_width: None,
492 build_flags: BuildFlags::default(),
493 suppress_build_script_link_lines: false,
494 extra_build_script_lines: vec![],
495 python_framework_prefix: Some(
496 "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(),
497 ),
498 };
499 _add_python_framework_link_args(
501 &interpreter_config,
502 &Triple::from_str("x86_64-pc-windows-msvc").unwrap(),
503 true,
504 &mut buf,
505 );
506 assert_eq!(buf, Vec::new());
507
508 _add_python_framework_link_args(
509 &interpreter_config,
510 &Triple::from_str("x86_64-apple-darwin").unwrap(),
511 true,
512 &mut buf,
513 );
514 assert_eq!(
515 std::str::from_utf8(&buf).unwrap(),
516 "cargo:rustc-link-arg=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Library/Frameworks\n"
517 );
518 }
519
520 #[test]
521 #[cfg(feature = "resolve-config")]
522 fn test_maximum_version_exceeded_formatting() {
523 let interpreter_config = InterpreterConfig {
524 implementation: PythonImplementation::CPython,
525 version: PythonVersion {
526 major: 3,
527 minor: 13,
528 },
529 shared: true,
530 abi3: false,
531 lib_name: None,
532 lib_dir: None,
533 executable: None,
534 pointer_width: None,
535 build_flags: BuildFlags::default(),
536 suppress_build_script_link_lines: false,
537 extra_build_script_lines: vec![],
538 python_framework_prefix: None,
539 };
540 let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new(
541 &interpreter_config,
542 PythonVersion {
543 major: 3,
544 minor: 12,
545 },
546 );
547 error.add_help("this is a help message");
548 let error = error.finish();
549 let expected = concat!("\
550 the configured Python version (3.13) is newer than PyForge's maximum supported version (3.12)\n\
551 = help: this package is being built with PyForge version ", env!("CARGO_PKG_VERSION"), "\n\
552 = help: check https://crates.io/crates/pyo3 for the latest PyForge version available\n\
553 = help: updating this package to the latest version of PyForge may provide compatibility with this Python version\n\
554 = help: this is a help message"
555 );
556 assert_eq!(error, expected);
557 }
558}