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 | |