| 1 | //! Configuration used by PyO3 for conditional support of varying Python versions. |
| 2 | //! |
| 3 | //! This crate exposes functionality to be called from build scripts to simplify building crates |
| 4 | //! which depend on PyO3. |
| 5 | //! |
| 6 | //! It used internally by the PyO3 crate's build script to apply the same configuration. |
| 7 | |
| 8 | #![warn (elided_lifetimes_in_paths, unused_lifetimes)] |
| 9 | |
| 10 | mod errors; |
| 11 | mod impl_; |
| 12 | |
| 13 | #[cfg (feature = "resolve-config" )] |
| 14 | use std::{ |
| 15 | io::Cursor, |
| 16 | path::{Path, PathBuf}, |
| 17 | }; |
| 18 | |
| 19 | use std::{env, process::Command, str::FromStr}; |
| 20 | |
| 21 | use once_cell::sync::OnceCell; |
| 22 | |
| 23 | pub use impl_::{ |
| 24 | cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, |
| 25 | CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple, |
| 26 | }; |
| 27 | use target_lexicon::OperatingSystem; |
| 28 | |
| 29 | /// Adds all the [`#[cfg]` flags](index.html) to the current compilation. |
| 30 | /// |
| 31 | /// This should be called from a build script. |
| 32 | /// |
| 33 | /// The full list of attributes added are the following: |
| 34 | /// |
| 35 | /// | Flag | Description | |
| 36 | /// | ---- | ----------- | |
| 37 | /// | `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_7)]` marks code which can run on Python 3.7 **and newer**. | |
| 38 | /// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. | |
| 39 | /// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. | |
| 40 | /// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. | |
| 41 | /// |
| 42 | /// For examples of how to use these attributes, |
| 43 | #[doc = concat!("[see PyO3's guide](https://pyo3.rs/v" , env!("CARGO_PKG_VERSION" ), "/building-and-distribution/multiple_python_versions.html)" )] |
| 44 | /// . |
| 45 | #[cfg (feature = "resolve-config" )] |
| 46 | pub fn use_pyo3_cfgs() { |
| 47 | print_expected_cfgs(); |
| 48 | for cargo_command: String in get().build_script_outputs() { |
| 49 | println!(" {}" , cargo_command) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | /// Adds linker arguments suitable for PyO3's `extension-module` feature. |
| 54 | /// |
| 55 | /// This should be called from a build script. |
| 56 | /// |
| 57 | /// The following link flags are added: |
| 58 | /// - macOS: `-undefined dynamic_lookup` |
| 59 | /// - wasm32-unknown-emscripten: `-sSIDE_MODULE=2 -sWASM_BIGINT` |
| 60 | /// |
| 61 | /// All other platforms currently are no-ops, however this may change as necessary |
| 62 | /// in future. |
| 63 | pub fn add_extension_module_link_args() { |
| 64 | _add_extension_module_link_args(&impl_::target_triple_from_env(), writer:std::io::stdout()) |
| 65 | } |
| 66 | |
| 67 | fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Write) { |
| 68 | if matches!(triple.operating_system, OperatingSystem::Darwin(_)) { |
| 69 | writeln!(writer, "cargo:rustc-cdylib-link-arg=-undefined" ).unwrap(); |
| 70 | writeln!(writer, "cargo:rustc-cdylib-link-arg=dynamic_lookup" ).unwrap(); |
| 71 | } else if triple == &Triple::from_str("wasm32-unknown-emscripten" ).unwrap() { |
| 72 | writeln!(writer, "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2" ).unwrap(); |
| 73 | writeln!(writer, "cargo:rustc-cdylib-link-arg=-sWASM_BIGINT" ).unwrap(); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | /// Adds linker arguments suitable for linking against the Python framework on macOS. |
| 78 | /// |
| 79 | /// This should be called from a build script. |
| 80 | /// |
| 81 | /// The following link flags are added: |
| 82 | /// - macOS: `-Wl,-rpath,<framework_prefix>` |
| 83 | /// |
| 84 | /// All other platforms currently are no-ops. |
| 85 | #[cfg (feature = "resolve-config" )] |
| 86 | pub fn add_python_framework_link_args() { |
| 87 | let interpreter_config: InterpreterConfig = pyo3_build_script_impl::resolve_interpreter_config().unwrap(); |
| 88 | _add_python_framework_link_args( |
| 89 | &interpreter_config, |
| 90 | &impl_::target_triple_from_env(), |
| 91 | link_libpython:impl_::is_linking_libpython(), |
| 92 | writer:std::io::stdout(), |
| 93 | ) |
| 94 | } |
| 95 | |
| 96 | #[cfg (feature = "resolve-config" )] |
| 97 | fn _add_python_framework_link_args( |
| 98 | interpreter_config: &InterpreterConfig, |
| 99 | triple: &Triple, |
| 100 | link_libpython: bool, |
| 101 | mut writer: impl std::io::Write, |
| 102 | ) { |
| 103 | if matches!(triple.operating_system, OperatingSystem::Darwin(_)) && link_libpython { |
| 104 | if let Some(framework_prefix: &String) = interpreter_config.python_framework_prefix.as_ref() { |
| 105 | writelnResult<(), Error>!( |
| 106 | writer, |
| 107 | "cargo:rustc-link-arg=-Wl,-rpath, {}" , |
| 108 | framework_prefix |
| 109 | ) |
| 110 | .unwrap(); |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | /// Loads the configuration determined from the build environment. |
| 116 | /// |
| 117 | /// Because this will never change in a given compilation run, this is cached in a `once_cell`. |
| 118 | #[cfg (feature = "resolve-config" )] |
| 119 | pub fn get() -> &'static InterpreterConfig { |
| 120 | static CONFIG: OnceCell<InterpreterConfig> = OnceCell::new(); |
| 121 | CONFIG.get_or_init(|| { |
| 122 | // Check if we are in a build script and cross compiling to a different target. |
| 123 | let cross_compile_config_path: Option = resolve_cross_compile_config_path(); |
| 124 | let cross_compiling: bool = cross_compile_config_path |
| 125 | .as_ref() |
| 126 | .map(|path| path.exists()) |
| 127 | .unwrap_or(default:false); |
| 128 | |
| 129 | // CONFIG_FILE is generated in build.rs, so it's content can vary |
| 130 | #[allow (unknown_lints, clippy::const_is_empty)] |
| 131 | if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() { |
| 132 | interpreter_config |
| 133 | } else if !CONFIG_FILE.is_empty() { |
| 134 | InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE)) |
| 135 | } else if cross_compiling { |
| 136 | InterpreterConfig::from_path(cross_compile_config_path.as_ref().unwrap()) |
| 137 | } else { |
| 138 | InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG)) |
| 139 | } |
| 140 | .expect(msg:"failed to parse PyO3 config" ) |
| 141 | }) |
| 142 | } |
| 143 | |
| 144 | /// Build configuration provided by `PYO3_CONFIG_FILE`. May be empty if env var not set. |
| 145 | #[doc (hidden)] |
| 146 | #[cfg (feature = "resolve-config" )] |
| 147 | const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR" ) , "/pyo3-build-config-file.txt" )); |
| 148 | |
| 149 | /// Build configuration discovered by `pyo3-build-config` build script. Not aware of |
| 150 | /// cross-compilation settings. |
| 151 | #[doc (hidden)] |
| 152 | #[cfg (feature = "resolve-config" )] |
| 153 | const HOST_CONFIG: &str = include_str!(concat!(env!("OUT_DIR" ) , "/pyo3-build-config.txt" )); |
| 154 | |
| 155 | /// Returns the path where PyO3's build.rs writes its cross compile configuration. |
| 156 | /// |
| 157 | /// The config file will be named `$OUT_DIR/<triple>/pyo3-build-config.txt`. |
| 158 | /// |
| 159 | /// Must be called from a build script, returns `None` if not. |
| 160 | #[doc (hidden)] |
| 161 | #[cfg (feature = "resolve-config" )] |
| 162 | fn resolve_cross_compile_config_path() -> Option<PathBuf> { |
| 163 | env::var_os(key:"TARGET" ).map(|target: OsString| { |
| 164 | let mut path: PathBuf = PathBuf::from(env!("OUT_DIR" )); |
| 165 | path.push(Path::new(&target)); |
| 166 | path.push(path:"pyo3-build-config.txt" ); |
| 167 | path |
| 168 | }) |
| 169 | } |
| 170 | |
| 171 | /// Helper to print a feature cfg with a minimum rust version required. |
| 172 | fn print_feature_cfg(minor_version_required: u32, cfg: &str) { |
| 173 | let minor_version: u32 = rustc_minor_version().unwrap_or(default:0); |
| 174 | |
| 175 | if minor_version >= minor_version_required { |
| 176 | println!("cargo:rustc-cfg= {}" , cfg); |
| 177 | } |
| 178 | |
| 179 | // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before |
| 180 | if minor_version >= 80 { |
| 181 | println!("cargo:rustc-check-cfg=cfg( {})" , cfg); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | /// Use certain features if we detect the compiler being used supports them. |
| 186 | /// |
| 187 | /// Features may be removed or added as MSRV gets bumped or new features become available, |
| 188 | /// so this function is unstable. |
| 189 | #[doc (hidden)] |
| 190 | pub fn print_feature_cfgs() { |
| 191 | print_feature_cfg(minor_version_required:70, cfg:"rustc_has_once_lock" ); |
| 192 | print_feature_cfg(minor_version_required:70, cfg:"cargo_toml_lints" ); |
| 193 | print_feature_cfg(minor_version_required:71, cfg:"rustc_has_extern_c_unwind" ); |
| 194 | print_feature_cfg(minor_version_required:74, cfg:"invalid_from_utf8_lint" ); |
| 195 | print_feature_cfg(minor_version_required:79, cfg:"c_str_lit" ); |
| 196 | // Actually this is available on 1.78, but we should avoid |
| 197 | // https://github.com/rust-lang/rust/issues/124651 just in case |
| 198 | print_feature_cfg(minor_version_required:79, cfg:"diagnostic_namespace" ); |
| 199 | print_feature_cfg(minor_version_required:83, cfg:"io_error_more" ); |
| 200 | print_feature_cfg(minor_version_required:85, cfg:"fn_ptr_eq" ); |
| 201 | } |
| 202 | |
| 203 | /// Registers `pyo3`s config names as reachable cfg expressions |
| 204 | /// |
| 205 | /// - <https://github.com/rust-lang/cargo/pull/13571> |
| 206 | /// - <https://doc.rust-lang.org/nightly/cargo/reference/build-scripts.html#rustc-check-cfg> |
| 207 | #[doc (hidden)] |
| 208 | pub fn print_expected_cfgs() { |
| 209 | if rustc_minor_version().map_or(default:false, |version: u32| version < 80) { |
| 210 | // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before |
| 211 | return; |
| 212 | } |
| 213 | |
| 214 | println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)" ); |
| 215 | println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)" ); |
| 216 | println!("cargo:rustc-check-cfg=cfg(PyPy)" ); |
| 217 | println!("cargo:rustc-check-cfg=cfg(GraalPy)" ); |
| 218 | println!("cargo:rustc-check-cfg=cfg(py_sys_config, values( \"Py_DEBUG \", \"Py_REF_DEBUG \", \"Py_TRACE_REFS \", \"COUNT_ALLOCS \"))" ); |
| 219 | println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)" ); |
| 220 | println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)" ); |
| 221 | |
| 222 | // allow `Py_3_*` cfgs from the minimum supported version up to the |
| 223 | // maximum minor version (+1 for development for the next) |
| 224 | // FIXME: support cfg(Py_3_14) as well due to PyGILState_Ensure |
| 225 | for i: u8 in impl_::MINIMUM_SUPPORTED_VERSION.minor..=std::cmp::max(v1:14, v2:impl_::ABI3_MAX_MINOR + 1) { |
| 226 | println!("cargo:rustc-check-cfg=cfg(Py_3_ {i})" ); |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | /// Private exports used in PyO3's build.rs |
| 231 | /// |
| 232 | /// Please don't use these - they could change at any time. |
| 233 | #[doc (hidden)] |
| 234 | pub mod pyo3_build_script_impl { |
| 235 | #[cfg (feature = "resolve-config" )] |
| 236 | use crate::errors::{Context, Result}; |
| 237 | |
| 238 | #[cfg (feature = "resolve-config" )] |
| 239 | use super::*; |
| 240 | |
| 241 | pub mod errors { |
| 242 | pub use crate::errors::*; |
| 243 | } |
| 244 | pub use crate::impl_::{ |
| 245 | cargo_env_var, env_var, is_linking_libpython, make_cross_compile_config, InterpreterConfig, |
| 246 | PythonVersion, |
| 247 | }; |
| 248 | |
| 249 | /// Gets the configuration for use from PyO3's build script. |
| 250 | /// |
| 251 | /// Differs from .get() above only in the cross-compile case, where PyO3's build script is |
| 252 | /// required to generate a new config (as it's the first build script which has access to the |
| 253 | /// correct value for CARGO_CFG_TARGET_OS). |
| 254 | #[cfg (feature = "resolve-config" )] |
| 255 | pub fn resolve_interpreter_config() -> Result<InterpreterConfig> { |
| 256 | // CONFIG_FILE is generated in build.rs, so it's content can vary |
| 257 | #[allow (unknown_lints, clippy::const_is_empty)] |
| 258 | if !CONFIG_FILE.is_empty() { |
| 259 | let mut interperter_config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))?; |
| 260 | interperter_config.generate_import_libs()?; |
| 261 | Ok(interperter_config) |
| 262 | } else if let Some(interpreter_config) = make_cross_compile_config()? { |
| 263 | // This is a cross compile and need to write the config file. |
| 264 | let path = resolve_cross_compile_config_path() |
| 265 | .expect("resolve_interpreter_config() must be called from a build script" ); |
| 266 | let parent_dir = path.parent().ok_or_else(|| { |
| 267 | format!( |
| 268 | "failed to resolve parent directory of config file {}" , |
| 269 | path.display() |
| 270 | ) |
| 271 | })?; |
| 272 | std::fs::create_dir_all(parent_dir).with_context(|| { |
| 273 | format!( |
| 274 | "failed to create config file directory {}" , |
| 275 | parent_dir.display() |
| 276 | ) |
| 277 | })?; |
| 278 | interpreter_config.to_writer(&mut std::fs::File::create(&path).with_context( |
| 279 | || format!("failed to create config file at {}" , path.display()), |
| 280 | )?)?; |
| 281 | Ok(interpreter_config) |
| 282 | } else { |
| 283 | InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG)) |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | fn rustc_minor_version() -> Option<u32> { |
| 289 | static RUSTC_MINOR_VERSION: OnceCell<Option<u32>> = OnceCell::new(); |
| 290 | *RUSTC_MINOR_VERSION.get_or_init(|| { |
| 291 | let rustc: OsString = env::var_os(key:"RUSTC" )?; |
| 292 | let output: Output = Command::new(program:rustc).arg("--version" ).output().ok()?; |
| 293 | let version: &str = core::str::from_utf8(&output.stdout).ok()?; |
| 294 | let mut pieces: Split<'_, char> = version.split('.' ); |
| 295 | if pieces.next() != Some("rustc 1" ) { |
| 296 | return None; |
| 297 | } |
| 298 | pieces.next()?.parse().ok() |
| 299 | }) |
| 300 | } |
| 301 | |
| 302 | #[cfg (test)] |
| 303 | mod tests { |
| 304 | use super::*; |
| 305 | |
| 306 | #[test ] |
| 307 | fn extension_module_link_args() { |
| 308 | let mut buf = Vec::new(); |
| 309 | |
| 310 | // Does nothing on non-mac |
| 311 | _add_extension_module_link_args( |
| 312 | &Triple::from_str("x86_64-pc-windows-msvc" ).unwrap(), |
| 313 | &mut buf, |
| 314 | ); |
| 315 | assert_eq!(buf, Vec::new()); |
| 316 | |
| 317 | _add_extension_module_link_args( |
| 318 | &Triple::from_str("x86_64-apple-darwin" ).unwrap(), |
| 319 | &mut buf, |
| 320 | ); |
| 321 | assert_eq!( |
| 322 | std::str::from_utf8(&buf).unwrap(), |
| 323 | "cargo:rustc-cdylib-link-arg=-undefined \n\ |
| 324 | cargo:rustc-cdylib-link-arg=dynamic_lookup \n" |
| 325 | ); |
| 326 | |
| 327 | buf.clear(); |
| 328 | _add_extension_module_link_args( |
| 329 | &Triple::from_str("wasm32-unknown-emscripten" ).unwrap(), |
| 330 | &mut buf, |
| 331 | ); |
| 332 | assert_eq!( |
| 333 | std::str::from_utf8(&buf).unwrap(), |
| 334 | "cargo:rustc-cdylib-link-arg=-sSIDE_MODULE=2 \n\ |
| 335 | cargo:rustc-cdylib-link-arg=-sWASM_BIGINT \n" |
| 336 | ); |
| 337 | } |
| 338 | |
| 339 | #[cfg (feature = "resolve-config" )] |
| 340 | #[test ] |
| 341 | fn python_framework_link_args() { |
| 342 | let mut buf = Vec::new(); |
| 343 | |
| 344 | let interpreter_config = InterpreterConfig { |
| 345 | implementation: PythonImplementation::CPython, |
| 346 | version: PythonVersion { |
| 347 | major: 3, |
| 348 | minor: 13, |
| 349 | }, |
| 350 | shared: true, |
| 351 | abi3: false, |
| 352 | lib_name: None, |
| 353 | lib_dir: None, |
| 354 | executable: None, |
| 355 | pointer_width: None, |
| 356 | build_flags: BuildFlags::default(), |
| 357 | suppress_build_script_link_lines: false, |
| 358 | extra_build_script_lines: vec![], |
| 359 | python_framework_prefix: Some( |
| 360 | "/Applications/Xcode.app/Contents/Developer/Library/Frameworks" .to_string(), |
| 361 | ), |
| 362 | }; |
| 363 | // Does nothing on non-mac |
| 364 | _add_python_framework_link_args( |
| 365 | &interpreter_config, |
| 366 | &Triple::from_str("x86_64-pc-windows-msvc" ).unwrap(), |
| 367 | true, |
| 368 | &mut buf, |
| 369 | ); |
| 370 | assert_eq!(buf, Vec::new()); |
| 371 | |
| 372 | _add_python_framework_link_args( |
| 373 | &interpreter_config, |
| 374 | &Triple::from_str("x86_64-apple-darwin" ).unwrap(), |
| 375 | true, |
| 376 | &mut buf, |
| 377 | ); |
| 378 | assert_eq!( |
| 379 | std::str::from_utf8(&buf).unwrap(), |
| 380 | "cargo:rustc-link-arg=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Library/Frameworks \n" |
| 381 | ); |
| 382 | } |
| 383 | } |
| 384 | |