Skip to content
17 changes: 14 additions & 3 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1362,17 +1362,28 @@ def _check_raw_dylib_macro(session: nox.Session):
min_minor = int(min_version.split(".")[1])
max_minor = int(max_version.split(".")[1])

# Build the set of DLL names that default_lib_name_windows can produce
expected_dlls = {"python3", "python3_d"}
# Build the set of raw-dylib link names that default_lib_name_windows can produce
expected_dlls = {
"python3",
"python3_d",
"libpython3",
"libpython3_d",
}
for minor in range(min_minor, max_minor + 1):
expected_dlls.add(f"python3{minor}")
expected_dlls.add(f"python3{minor}_d")
expected_dlls.add(f"libpython3.{minor}")
expected_dlls.add(f"libpython3.{minor}_d")
if minor >= 13:
expected_dlls.add(f"python3{minor}t")
expected_dlls.add(f"python3{minor}t_d")
expected_dlls.add(f"libpython3.{minor}t")
expected_dlls.add(f"libpython3.{minor}t_d")
if minor >= 15:
expected_dlls.add("python3t")
expected_dlls.add("python3t_d")
expected_dlls.add("libpython3t")
expected_dlls.add("libpython3t_d")

# PyPy DLL names (libpypy3.X-c.dll)
pypy_min, pypy_max = _parse_supported_interpreter_version("pypy")
Expand All @@ -1383,7 +1394,7 @@ def _check_raw_dylib_macro(session: nox.Session):

# Parse the DLL name list in the extern_libpython!(@impl ...) invocation
lib_rs = (PYO3_DIR / "pyo3-ffi" / "src" / "impl_" / "macros.rs").read_text()
found_dlls = set(re.findall(r'"((?:python|libpypy)[^"]+)"', lib_rs))
found_dlls = set(re.findall(r'"((?:python|libpython|libpypy)[^"]+)"', lib_rs))

missing = expected_dlls - found_dlls
extra = found_dlls - expected_dlls
Expand Down
72 changes: 58 additions & 14 deletions pyo3-build-config/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2302,7 +2302,12 @@ const WINDOWS_STABLE_ABI_DEBUG_LIB_NAME: &str = "python3_d";
#[allow(dead_code)]
fn default_lib_name_for_target(abi: PythonAbi, target: &Triple) -> String {
if target.operating_system == OperatingSystem::Windows {
default_lib_name_windows(abi, false, false).unwrap()
default_lib_name_windows(
abi,
matches!(target.environment, Environment::Gnu | Environment::GnuLlvm),
false,
)
.unwrap()
} else {
default_lib_name_unix(
abi,
Expand All @@ -2314,6 +2319,9 @@ fn default_lib_name_for_target(abi: PythonAbi, target: &Triple) -> String {
}

fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result<String> {
// MSYS2 MinGW-style Windows targets ship libpython with a `lib` prefix.
let lib_prefix = if mingw { "lib" } else { "" };

if abi.implementation.is_pypy() {
// PyPy on Windows ships `libpypy3.X-c.dll` (e.g. `libpypy3.11-c.dll`),
// not CPython's `pythonXY.dll`. With raw-dylib linking we need the real
Expand All @@ -2326,8 +2334,8 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result<
// CPython bug: linking against python3_d.dll raises error
// https://github.com/python/cpython/issues/101614
Ok(format!(
"python{}{}_d",
abi.version.major, abi.version.minor
"{}python{}{}_d",
lib_prefix, abi.version.major, abi.version.minor
))
} else if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3)
|| abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t)
Expand All @@ -2340,34 +2348,43 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result<
if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) {
lib_name = lib_name.replace("python3", "python3t");
}
Ok(lib_name)
Ok(format!("{}{}", lib_prefix, lib_name))
} else if mingw {
ensure!(
!abi.kind.is_free_threaded(),
"MinGW free-threaded builds are not currently tested or supported"
);
// https://packages.msys2.org/base/mingw-w64-python
Ok(format!("python{}.{}", abi.version.major, abi.version.minor))
Ok(format!(
"{}python{}.{}",
lib_prefix, abi.version.major, abi.version.minor
))
} else if abi.kind().is_free_threaded() {
#[expect(deprecated, reason = "using constant internally")]
{
ensure!(abi.version() >= PythonVersion::PY313, "Cannot compile extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor);
}
if debug {
Ok(format!(
"python{}{}t_d",
abi.version.major, abi.version.minor
"{}python{}{}t_d",
lib_prefix, abi.version.major, abi.version.minor
))
} else {
Ok(format!("python{}{}t", abi.version.major, abi.version.minor))
Ok(format!(
"{}python{}{}t",
lib_prefix, abi.version.major, abi.version.minor
))
}
} else if debug {
Ok(format!(
"python{}{}_d",
abi.version.major, abi.version.minor
"{}python{}{}_d",
lib_prefix, abi.version.major, abi.version.minor
))
} else {
Ok(format!("python{}{}", abi.version.major, abi.version.minor))
Ok(format!(
"{}python{}{}",
lib_prefix, abi.version.major, abi.version.minor
))
}
}

Expand Down Expand Up @@ -3091,7 +3108,7 @@ mod tests {
let implementation = PythonImplementation::CPython;
let version = PythonVersion::PY39;
let config = InterpreterConfigBuilder::new(implementation, version)
.lib_name("python39".to_string())
.lib_name("libpython3.9".to_string())
.lib_dir("/usr/lib/mingw".to_string())
.finalize()
.unwrap();
Expand Down Expand Up @@ -3290,7 +3307,7 @@ mod tests {
false,
)
.unwrap(),
"python3.9",
"libpython3.9",
);
assert_eq!(
super::default_lib_name_windows(
Expand All @@ -3302,7 +3319,7 @@ mod tests {
false,
)
.unwrap(),
"python3",
"libpython3",
);
assert_eq!(
super::default_lib_name_windows(
Expand Down Expand Up @@ -4114,8 +4131,20 @@ mod tests {
.unwrap();

let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap();
let win_gnu = Triple::from_str("x86_64-pc-windows-gnu").unwrap();
let win_gnu_x86 = Triple::from_str("i686-pc-windows-gnu").unwrap();
let win_gnullvm = Triple::from_str("x86_64-pc-windows-gnullvm").unwrap();
let win_gnullvm_x86 = Triple::from_str("i686-pc-windows-gnullvm").unwrap();
let win_gnullvm_arm64 = Triple::from_str("aarch64-pc-windows-gnullvm").unwrap();
let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap();
let win_arm64 = Triple::from_str("aarch64-pc-windows-msvc").unwrap();
let windows_gnu_like = [
&win_gnu,
&win_gnu_x86,
&win_gnullvm,
&win_gnullvm_x86,
&win_gnullvm_arm64,
];

let lib_name = default_lib_name_for_target(cpy39, &unix);
assert_eq!(lib_name, "python3.9");
Expand All @@ -4126,13 +4155,23 @@ mod tests {
let lib_name = default_lib_name_for_target(cpy39, &win_arm64);
assert_eq!(lib_name, "python39");

for target in windows_gnu_like {
let lib_name = default_lib_name_for_target(cpy39, target);
assert_eq!(lib_name, "libpython3.9");
}

// PyPy
let lib_name = default_lib_name_for_target(pypy311, &unix);
assert_eq!(lib_name, "pypy3.11-c");

let lib_name = default_lib_name_for_target(pypy311, &win_x64);
assert_eq!(lib_name, "libpypy3.11-c");

for target in windows_gnu_like {
let lib_name = default_lib_name_for_target(pypy311, target);
assert_eq!(lib_name, "libpypy3.11-c");
}

// Free-threaded
let lib_name = default_lib_name_for_target(cpy313t, &unix);
assert_eq!(lib_name, "python3.13t");
Expand All @@ -4150,6 +4189,11 @@ mod tests {
let lib_name = default_lib_name_for_target(cpy313_abi3, &win_x64);
assert_eq!(lib_name, "python3");

for target in windows_gnu_like {
let lib_name = default_lib_name_for_target(cpy313_abi3, target);
assert_eq!(lib_name, "libpython3");
}

let lib_name = default_lib_name_for_target(cpy313_abi3, &win_arm64);
assert_eq!(lib_name, "python3");
}
Expand Down
16 changes: 12 additions & 4 deletions pyo3-build-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,21 @@ pub fn print_expected_cfgs() {
"python3_d".to_string(),
"python3t".to_string(),
"python3t_d".to_string(),
"libpython3".to_string(),
"libpython3_d".to_string(),
"libpython3t".to_string(),
"libpython3t_d".to_string(),
];
for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::STABLE_ABI_MAX_MINOR + 1 {
dll_names.push(format!("python3{i}"));
dll_names.push(format!("python3{i}_d"));
dll_names.push(format!("libpython3.{i}"));
dll_names.push(format!("libpython3.{i}_d"));
if i >= 13 {
dll_names.push(format!("python3{i}t"));
dll_names.push(format!("python3{i}t_d"));
dll_names.push(format!("libpython3.{i}t"));
dll_names.push(format!("libpython3.{i}t_d"));
}
}
// PyPy DLL names (libpypy3.X-c.dll)
Expand Down Expand Up @@ -324,7 +332,7 @@ pub mod pyo3_build_script_impl {
target.architecture,
Architecture::Wasm32 | Architecture::Wasm64
);
let is_emscripten = target.operating_system == target_lexicon::OperatingSystem::Emscripten;
let is_emscripten = target.operating_system == OperatingSystem::Emscripten;
// webassembly targets generally don't support rpath, emscripten is the only exception currently aware of:
// https://github.com/emscripten-core/emscripten/issues/22126
if is_linking_libpython && (!is_wasm || is_emscripten) {
Expand Down Expand Up @@ -468,15 +476,15 @@ mod tests {
interpreter_config.to_writer(&mut buf).unwrap();
let config_string = escape(&buf);
// SAFETY: no other tests use `crate::get()`
unsafe { std::env::set_var(InterpreterConfig::PYO3_FFI_CONFIG_ENV_VAR, &config_string) };
unsafe { env::set_var(InterpreterConfig::PYO3_FFI_CONFIG_ENV_VAR, &config_string) };

assert_eq!(get_inner(), interpreter_config);

// Repeat with PyO3 env var
// SAFETY: no other tests use `crate::get()`
unsafe {
std::env::remove_var(InterpreterConfig::PYO3_FFI_CONFIG_ENV_VAR);
std::env::set_var(InterpreterConfig::PYO3_CONFIG_ENV_VAR, &config_string)
env::remove_var(InterpreterConfig::PYO3_FFI_CONFIG_ENV_VAR);
env::set_var(InterpreterConfig::PYO3_CONFIG_ENV_VAR, &config_string)
}

assert_eq!(get_inner(), interpreter_config);
Expand Down
21 changes: 21 additions & 0 deletions pyo3-ffi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ fn ensure_target_pointer_width(interpreter_config: &InterpreterConfig) -> Result
fn emit_link_config(build_config: &BuildConfig) -> Result<()> {
let interpreter_config = &build_config.interpreter_config;
let target_os = cargo_env_var("CARGO_CFG_TARGET_OS").unwrap();
let target_env = cargo_env_var("CARGO_CFG_TARGET_ENV");
let target_arch = cargo_env_var("CARGO_CFG_TARGET_ARCH");

let lib_name = interpreter_config
.lib_name()
Expand All @@ -208,6 +210,25 @@ fn emit_link_config(build_config: &BuildConfig) -> Result<()> {
// Python interpreter on Windows is not supported by this path (and is not
// officially supported by CPython on Windows).
println!("cargo:rustc-cfg=pyo3_dll=\"{lib_name}\"");

let is_i686_pc_windows_gnu = matches!(target_env.as_deref(), Some("gnu"))
&& matches!(target_arch.as_deref(), Some("x86"));

if is_i686_pc_windows_gnu {
let import_lib_name = lib_name.strip_prefix("lib").unwrap_or(lib_name);
println!("cargo:rustc-link-lib={import_lib_name}");

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) {
warn!(
"The output binary will link to libpython, \
but PYO3_CROSS_LIB_DIR environment variable is not set. \
Ensure that the target Python library directory is \
in the rustc native library search path."
);
}

@chirizxc chirizxc Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could use target-lexicon as a build-dependency for pyo3-ffi as well; I think that would make the code a little more readable:

fn emit_link_config(build_config: &BuildConfig) -> Result<()> {
    let interpreter_config = &build_config.interpreter_config;
    let target = target_triple_from_env();

    let lib_name = interpreter_config
        .lib_name()
        .ok_or("attempted to link to Python shared library but config does not contain lib_name")?;

    if target.operating_system == target_lexicon::OperatingSystem::Windows {
        // Use raw-dylib linking: emit a cfg so that `extern_libpython!` picks the
        // right `#[link(name = "...", kind = "raw-dylib")]` attribute at compile time.
        // This eliminates the need for import libraries (.lib files) entirely.
        //
        // Note: raw-dylib is inherently dynamic linking. Static embedding of the
        // Python interpreter on Windows is not supported by this path (and is not
        // officially supported by CPython on Windows).
        println!("cargo:rustc-cfg=pyo3_dll=\"{lib_name}\"");
        let is_i686_pc_windows_gnu = target.environment == target_lexicon::Environment::Gnu
            && matches!(target.architecture, target_lexicon::Architecture::X86_32(_));

}
} else {
println!(
"cargo:rustc-link-lib={link_model}{lib_name}",
Expand Down
27 changes: 14 additions & 13 deletions pyo3-ffi/src/impl_/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ macro_rules! extern_libpython_items {
/// Helper macro to declare `extern` blocks that link against libpython on Windows
/// using `raw-dylib`, eliminating the need for import libraries.
///
/// The build script sets a `pyo3_dll` cfg value to the target DLL name (e.g. `python312`),
/// The build script sets a `pyo3_dll` cfg value to the raw-dylib link name
/// (e.g. `python312` or `libpython3.12`),
/// and this macro expands to the appropriate `#[link(name = "...", kind = "raw-dylib")]`
/// attribute for that DLL.
///
Expand All @@ -257,21 +258,21 @@ macro_rules! extern_libpython {
($abi:literal { $($body:tt)* }) => {
extern_libpython!(@impl $abi { $($body)* }
// abi3
"python3", "python3_d",
"python3", "python3_d", "libpython3", "libpython3_d",
// abi3t
"python3t", "python3t_d",
"python3t", "python3t_d", "libpython3t", "libpython3t_d",
// Python 3.9 - 3.15
"python39", "python39_d",
"python310", "python310_d",
"python311", "python311_d",
"python312", "python312_d",
"python313", "python313_d",
"python314", "python314_d",
"python315", "python315_d",
"python39", "python39_d", "libpython3.9", "libpython3.9_d",
"python310", "python310_d", "libpython3.10", "libpython3.10_d",
"python311", "python311_d", "libpython3.11", "libpython3.11_d",
"python312", "python312_d", "libpython3.12", "libpython3.12_d",
"python313", "python313_d", "libpython3.13", "libpython3.13_d",
"python314", "python314_d", "libpython3.14", "libpython3.14_d",
"python315", "python315_d", "libpython3.15", "libpython3.15_d",
// free-threaded builds (3.13+)
"python313t", "python313t_d",
"python314t", "python314t_d",
"python315t", "python315t_d",
"python313t", "python313t_d", "libpython3.13t", "libpython3.13t_d",
"python314t", "python314t_d", "libpython3.14t", "libpython3.14t_d",
"python315t", "python315t_d", "libpython3.15t", "libpython3.15t_d",
// PyPy (DLL is libpypy3.X-c.dll, not pythonXY.dll)
"libpypy3.11-c",
);
Expand Down
Loading