Skip to content

Commit 1143c9b

Browse files
committed
fix: auto-generate .def file for zig + windows-gnu to export PyInit symbol
When using zig as the linker for windows-gnu targets, the `PyInit_<module>` symbol is not exported, causing Python to fail to import the module. This generates a `.def` file in `target/maturin/` that explicitly exports the symbol, passed to the linker via `-C link-arg`. Also refactors `target/maturin` directory creation into a shared `ensure_target_maturin_dir()` helper used by compile and repair. Fixes #922
1 parent 896b648 commit 1143c9b

File tree

2 files changed

+46
-5
lines changed

2 files changed

+46
-5
lines changed

src/build_context/repair.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,7 @@ impl BuildContext {
321321
/// reflink-or-copy directly; the concurrent-modification window is
322322
/// unlikely in cross-device setups.
323323
pub(super) fn stage_artifact(&self, artifact: &mut BuildArtifact) -> Result<()> {
324-
let maturin_build = self.target_dir.join(env!("CARGO_PKG_NAME"));
325-
fs::create_dir_all(&maturin_build)?;
324+
let maturin_build = crate::compile::ensure_target_maturin_dir(&self.target_dir);
326325
let artifact_path = &artifact.path;
327326
let new_artifact_path = maturin_build.join(artifact_path.file_name().unwrap());
328327
// Remove any stale file at the destination so that `fs::rename`

src/compile.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ fn cargo_build_command(
265265
bridge_model,
266266
&context.module_name,
267267
python_interpreter,
268+
#[cfg(feature = "zig")]
269+
context.zig,
270+
#[cfg(feature = "zig")]
271+
&context.target_dir,
268272
);
269273

270274
if context.strip {
@@ -325,14 +329,16 @@ fn configure_bin_lib_flags(
325329
}
326330
}
327331

328-
/// Configure platform-specific linker arguments (macOS PyO3, Emscripten).
332+
/// Configure platform-specific linker arguments (macOS PyO3, Emscripten, Windows GNU + zig).
329333
fn configure_platform_linker_args(
330334
cargo_rustc: &mut cargo_options::Rustc,
331335
rustflags: &mut cargo_config2::Flags,
332336
target: &Target,
333337
bridge_model: &BridgeModel,
334338
module_name: &str,
335339
python_interpreter: Option<&PythonInterpreter>,
340+
#[cfg(feature = "zig")] zig: bool,
341+
#[cfg(feature = "zig")] target_dir: &Path,
336342
) {
337343
if target.is_macos() {
338344
if let BridgeModel::PyO3 { .. } = bridge_model {
@@ -346,6 +352,35 @@ fn configure_platform_linker_args(
346352
} else if target.is_emscripten() {
347353
configure_emscripten_args(cargo_rustc, rustflags, target);
348354
}
355+
356+
// When using zig as the linker for windows-gnu targets, the PyInit symbol
357+
// is not exported. Work around this by generating a .def file that explicitly
358+
// exports the symbol.
359+
// See https://github.com/PyO3/maturin/issues/922
360+
#[cfg(feature = "zig")]
361+
if zig
362+
&& target.is_windows()
363+
&& !target.is_msvc()
364+
&& matches!(
365+
bridge_model,
366+
BridgeModel::PyO3 { .. } | BridgeModel::Cffi | BridgeModel::UniFfi
367+
)
368+
{
369+
let py_init = format!("PyInit_{module_name}");
370+
let maturin_dir = ensure_target_maturin_dir(target_dir);
371+
let def_path = maturin_dir.join(format!("{module_name}.def"));
372+
let def_contents = format!("LIBRARY {module_name}\nEXPORTS\n {py_init}\n");
373+
if let Err(e) = fs::write(&def_path, def_contents) {
374+
eprintln!("⚠️ Warning: Failed to write .def file for zig windows-gnu workaround: {e}");
375+
} else {
376+
debug!(
377+
"Generated .def file at {} for zig windows-gnu export workaround",
378+
def_path.display()
379+
);
380+
rustflags.push("-C");
381+
rustflags.push(&format!("link-arg={}", def_path.display()));
382+
}
383+
}
349384
}
350385

351386
/// Configure macOS-specific linker arguments for PyO3 builds.
@@ -632,12 +667,11 @@ fn configure_pyo3_env(
632667
build_command.env("PYTHON_SYS_EXECUTABLE", &interpreter.executable);
633668
} else if bridge_model.is_pyo3() && env::var_os("PYO3_CONFIG_FILE").is_none() {
634669
let pyo3_config = interpreter.pyo3_config_file();
635-
let maturin_target_dir = context.target_dir.join(env!("CARGO_PKG_NAME"));
670+
let maturin_target_dir = ensure_target_maturin_dir(&context.target_dir);
636671
let config_file = maturin_target_dir.join(format!(
637672
"pyo3-config-{}-{}.{}.txt",
638673
target_triple, interpreter.major, interpreter.minor
639674
));
640-
fs::create_dir_all(&maturin_target_dir)?;
641675
// We don't want to rewrite the file every time as that will make cargo
642676
// trigger a rebuild of the project every time
643677
let existing_pyo3_config = fs::read_to_string(&config_file).unwrap_or_default();
@@ -930,6 +964,14 @@ pub fn warn_missing_py_init(artifact: &Path, module_name: &str) -> Result<()> {
930964
Ok(())
931965
}
932966

967+
/// Ensures the `maturin` subdirectory inside the target directory exists
968+
/// and returns its path. This directory is used for maturin-generated artifacts.
969+
pub(crate) fn ensure_target_maturin_dir(target_dir: &Path) -> PathBuf {
970+
let dir = target_dir.join(env!("CARGO_PKG_NAME"));
971+
let _ = fs::create_dir_all(&dir);
972+
dir
973+
}
974+
933975
fn pyo3_version(cargo_metadata: &cargo_metadata::Metadata) -> Option<(u64, u64, u64)> {
934976
let packages: HashMap<&str, &cargo_metadata::Package> = cargo_metadata
935977
.packages

0 commit comments

Comments
 (0)