diff --git a/newsfragments/5606.changed.md b/newsfragments/5606.changed.md new file mode 100644 index 00000000000..502908ef0b9 --- /dev/null +++ b/newsfragments/5606.changed.md @@ -0,0 +1 @@ +If the `sysconfigdata` defines `PYTHONFRAMEWORK`, and the build is configured to link the against Python library, the link will be performed against that framework, using `PYTHONFRAMEWORKPREFIX` as a framework search path. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 7cf2d0eb600..b1a14dd92d2 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -98,6 +98,15 @@ pub struct InterpreterConfig { /// Serialized to `version`. pub version: PythonVersion, + /// The name of the Python framework (if available) + /// + /// If present (and not empty), the named framework will be used for + /// linking using the `python_framework_prefix` as the framework search + /// path. This overrides the use of `shared` and `lib_name` for linking. + /// + /// Serialized to `framework`. + pub framework: Option, + /// Whether link library is shared. /// /// Serialized to `shared`. @@ -242,6 +251,10 @@ def print_if_set(varname, value): if value is not None: print(varname, value) +def print_if_not_empty(varname, value): + if value: + print(varname, value) + # Windows always uses shared linking WINDOWS = platform.system() == "Windows" @@ -255,8 +268,9 @@ SHARED = bool(get_config_var("Py_ENABLE_SHARED")) print("implementation", platform.python_implementation()) print("version_major", sys.version_info[0]) print("version_minor", sys.version_info[1]) +print_if_not_empty("framework", get_config_var("PYTHONFRAMEWORK")) print("shared", PYPY or GRAALPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED) -print("python_framework_prefix", FRAMEWORK_PREFIX) +print_if_not_empty("python_framework_prefix", FRAMEWORK_PREFIX) print_if_set("ld_version", get_config_var("LDVERSION")) print_if_set("libdir", get_config_var("LIBDIR")) print_if_set("base_prefix", base_prefix) @@ -293,6 +307,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) ); }; + let framework = map.get("framework").cloned(); let shared = map["shared"].as_str() == "True"; let python_framework_prefix = map.get("python_framework_prefix").cloned(); @@ -360,6 +375,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) Ok(InterpreterConfig { version, implementation, + framework, shared, abi3, lib_name: Some(lib_name), @@ -402,13 +418,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) Some("0") | Some("false") | Some("False") => false, _ => bail!("expected a bool (1/true/True or 0/false/False) for Py_ENABLE_SHARED"), }; - // macOS framework packages use shared linking (PYTHONFRAMEWORK is the framework name, hence the empty check) - let framework = match sysconfigdata.get_value("PYTHONFRAMEWORK") { - Some(s) => !s.is_empty(), - _ => false, - }; - let python_framework_prefix = sysconfigdata - .get_value("PYTHONFRAMEWORKPREFIX") + // macOS framework packages use shared linking (PYTHONFRAMEWORK is the + // framework name, hence the empty check) Empty values are converted to + // None. + let framework = get_key!(sysconfigdata, "PYTHONFRAMEWORK") + .ok() + .filter(|s| !s.is_empty()) + .map(str::to_string); + let python_framework_prefix = get_key!(sysconfigdata, "PYTHONFRAMEWORKPREFIX") + .ok() + .filter(|s| !s.is_empty()) .map(str::to_string); let lib_dir = get_key!(sysconfigdata, "LIBDIR").ok().map(str::to_string); let gil_disabled = match sysconfigdata.get_value("Py_GIL_DISABLED") { @@ -429,11 +448,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .map(|bytes_width: u32| bytes_width * 8) .ok(); let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); + let shared = shared || framework.is_some(); Ok(InterpreterConfig { implementation, version, - shared: shared || framework, + framework, + shared, abi3, lib_dir, lib_name, @@ -510,6 +531,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut implementation = None; let mut version = None; + let mut framework = None; let mut shared = None; let mut abi3 = None; let mut lib_name = None; @@ -535,6 +557,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) match key { "implementation" => parse_value!(implementation, value), "version" => parse_value!(version, value), + "framework" => parse_value!(framework, value), "shared" => parse_value!(shared, value), "abi3" => parse_value!(abi3, value), "lib_name" => parse_value!(lib_name, value), @@ -561,6 +584,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) Ok(InterpreterConfig { implementation, version, + framework, shared: shared.unwrap_or(true), abi3, lib_name, @@ -674,6 +698,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) write_line!(implementation)?; write_line!(version)?; + write_option_line!(framework)?; write_line!(shared)?; write_line!(abi3)?; write_option_line!(lib_name)?; @@ -1625,6 +1650,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2084,6 +2112,7 @@ mod tests { implementation: PythonImplementation::PyPy, lib_dir: None, lib_name: None, + framework: None, shared: true, version: PythonVersion { major: 3, @@ -2109,6 +2138,7 @@ mod tests { implementation: PythonImplementation::CPython, lib_name: Some("lib_name".into()), lib_dir: Some("lib_dir\\n".into()), + framework: None, shared: true, version: MINIMUM_SUPPORTED_VERSION, suppress_build_script_link_lines: true, @@ -2131,6 +2161,7 @@ mod tests { InterpreterConfig { version: PythonVersion { major: 3, minor: 7 }, implementation: PythonImplementation::CPython, + framework: None, shared: true, abi3: false, lib_name: None, @@ -2154,6 +2185,7 @@ mod tests { InterpreterConfig { version: PythonVersion { major: 3, minor: 7 }, implementation: PythonImplementation::CPython, + framework: None, shared: true, abi3: false, lib_name: None, @@ -2262,6 +2294,7 @@ mod tests { implementation: PythonImplementation::CPython, lib_dir: Some("/usr/lib".into()), lib_name: Some("python3.7m".into()), + framework: None, shared: true, version: PythonVersion::PY37, suppress_build_script_link_lines: false, @@ -2279,6 +2312,7 @@ mod tests { // PYTHONFRAMEWORK should override Py_ENABLE_SHARED sysconfigdata.insert("Py_ENABLE_SHARED", "0"); sysconfigdata.insert("PYTHONFRAMEWORK", "Python"); + sysconfigdata.insert("PYTHONFRAMEWORKPREFIX", "/Library/Frameworks"); sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.7m"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); @@ -2292,11 +2326,12 @@ mod tests { implementation: PythonImplementation::CPython, lib_dir: Some("/usr/lib".into()), lib_name: Some("python3.7m".into()), + framework: Some("Python".into()), shared: true, version: PythonVersion::PY37, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], - python_framework_prefix: None, + python_framework_prefix: Some("/Library/Frameworks".into()), } ); @@ -2306,6 +2341,7 @@ mod tests { // An empty PYTHONFRAMEWORK means it is not a framework sysconfigdata.insert("Py_ENABLE_SHARED", "0"); sysconfigdata.insert("PYTHONFRAMEWORK", ""); + sysconfigdata.insert("PYTHONFRAMEWORKPREFIX", ""); sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.7m"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); @@ -2319,6 +2355,7 @@ mod tests { implementation: PythonImplementation::CPython, lib_dir: Some("/usr/lib".into()), lib_name: Some("python3.7m".into()), + framework: None, shared: false, version: PythonVersion::PY37, suppress_build_script_link_lines: false, @@ -2338,6 +2375,7 @@ mod tests { InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 7 }, + framework: None, shared: true, abi3: true, lib_name: Some("python3".into()), @@ -2362,6 +2400,7 @@ mod tests { InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, + framework: None, shared: true, abi3: true, lib_name: None, @@ -2397,6 +2436,7 @@ mod tests { InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 7 }, + framework: None, shared: true, abi3: false, lib_name: Some("python37".into()), @@ -2432,6 +2472,7 @@ mod tests { InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 8 }, + framework: None, shared: true, abi3: false, lib_name: Some("python38".into()), @@ -2467,6 +2508,7 @@ mod tests { InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, + framework: None, shared: true, abi3: false, lib_name: Some("python3.9".into()), @@ -2504,6 +2546,7 @@ mod tests { major: 3, minor: 11 }, + framework: None, shared: true, abi3: false, lib_name: Some("pypy3.11-c".into()), @@ -2897,6 +2940,7 @@ mod tests { implementation: PythonImplementation::CPython, lib_dir: None, lib_name: None, + framework: None, shared: true, version: PythonVersion { major: 3, minor: 7 }, suppress_build_script_link_lines: false, @@ -2920,6 +2964,7 @@ mod tests { implementation: PythonImplementation::CPython, lib_dir: None, lib_name: None, + framework: None, shared: true, version: PythonVersion { major: 3, minor: 7 }, suppress_build_script_link_lines: false, @@ -2985,6 +3030,7 @@ mod tests { implementation: PythonImplementation::CPython, lib_dir: interpreter_config.lib_dir.to_owned(), lib_name: interpreter_config.lib_name.to_owned(), + framework: None, shared: true, version: interpreter_config.version, suppress_build_script_link_lines: false, @@ -3115,6 +3161,7 @@ mod tests { major: 3, minor: 11, }, + framework: None, shared: true, abi3: false, lib_name: Some("python3".into()), @@ -3159,6 +3206,7 @@ mod tests { let interpreter_config = InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, + framework: None, shared: true, abi3: true, lib_name: Some("python3".into()), @@ -3207,6 +3255,7 @@ mod tests { major: 3, minor: 13, }, + framework: None, shared: true, abi3: false, lib_name: Some("python3".into()), @@ -3241,6 +3290,7 @@ mod tests { let interpreter_config = InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 7 }, + framework: None, shared: true, abi3: false, lib_name: Some("python3".into()), @@ -3296,6 +3346,7 @@ mod tests { let mut config = InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, + framework: None, shared: true, abi3: false, lib_name: None, diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 0544376e3b4..f93f347c3da 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -412,6 +412,7 @@ mod tests { major: 3, minor: 13, }, + framework: None, shared: true, abi3: false, lib_name: None, @@ -455,6 +456,7 @@ mod tests { major: 3, minor: 13, }, + framework: None, shared: true, abi3: false, lib_name: None, diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 3e2354f2d38..a0f81fa5a5b 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -150,7 +150,9 @@ fn emit_link_config(build_config: &BuildConfig) -> Result<()> { println!( "cargo:rustc-link-lib={link_model}{alias}{lib_name}", - link_model = if interpreter_config.shared { + link_model = if interpreter_config.framework.is_some() { + "framework=" + } else if interpreter_config.shared { "" } else { "static=" @@ -160,11 +162,20 @@ fn emit_link_config(build_config: &BuildConfig) -> Result<()> { } else { "" }, - lib_name = interpreter_config.lib_name.as_ref().ok_or( - "attempted to link to Python shared library but config does not contain lib_name" - )?, + lib_name = if let Some(framework) = &interpreter_config.framework { + framework + } else { + interpreter_config.lib_name.as_ref().ok_or( + "attempted to link to Python shared library but config does not contain lib_name", + )? + }, ); + if interpreter_config.framework.is_some() { + if let Some(framework_prefix) = &interpreter_config.python_framework_prefix { + println!("cargo:rustc-link-search=framework={framework_prefix}"); + } + } if let Some(lib_dir) = &interpreter_config.lib_dir { println!("cargo:rustc-link-search=native={lib_dir}"); } else if matches!(build_config.source, BuildConfigSource::CrossCompile) {