python3_dll_a/
lib.rs

1//! Standalone `python3(y).dll` import library generator
2//! ====================================================
3//!
4//! Generates import libraries for the Python DLL
5//! (either `python3.dll` or `python3y.dll`)
6//! for MinGW-w64 and MSVC (cross-)compile targets.
7//!
8//! This crate **does not require** Python 3 distribution files
9//! to be present on the (cross-)compile host system.
10//!
11//! This crate uses the binutils `dlltool` program to generate
12//! the Python DLL import libraries for MinGW-w64 targets.
13//! Setting `PYO3_MINGW_DLLTOOL` environment variable overrides
14//! the default `dlltool` command name for the target.
15//!
16//! **Note:** MSVC cross-compile targets require either LLVM binutils
17//! or Zig to be available on the host system.
18//! More specifically, `python3-dll-a` requires `llvm-dlltool` executable
19//! to be present in `PATH` when targeting `*-pc-windows-msvc` from Linux.
20//!
21//! Alternatively, `ZIG_COMMAND` environment variable may be set to e.g. `"zig"`
22//! or `"python -m ziglang"`, then `zig dlltool` will be used in place
23//! of `llvm-dlltool` (or MinGW binutils).
24//!
25//! PyO3 integration
26//! ----------------
27//!
28//! Since version **0.16.5**, the `pyo3` crate implements support
29//! for both the Stable ABI and version-specific Python DLL import
30//! library generation via its new `generate-import-lib` feature.
31//!
32//! In this configuration, `python3-dll-a` becomes a `pyo3` crate dependency
33//! and is automatically invoked by its build script in both native
34//! and cross compilation scenarios.
35//!
36//! ### Example `Cargo.toml` usage for an `abi3` PyO3 extension module
37//!
38//! ```toml
39//! [dependencies]
40//! pyo3 = { version = "0.16.5", features = ["extension-module", "abi3-py37", "generate-import-lib"] }
41//! ```
42//!
43//! ### Example `Cargo.toml` usage for a standard PyO3 extension module
44//!
45//! ```toml
46//! [dependencies]
47//! pyo3 = { version = "0.16.5", features = ["extension-module", "generate-import-lib"] }
48//! ```
49//!
50//! Standalone build script usage
51//! -----------------------------
52//!
53//! If an older `pyo3` crate version is used, or a different Python bindings
54//! library is required, `python3-dll-a` can be used directly
55//! from the crate build script.
56//!
57//! The examples below assume using an older version of PyO3.
58//!
59//! ### Example `build.rs` script for an `abi3` PyO3 extension
60//!
61//! The following cargo build script can be used to cross-compile Stable ABI
62//! PyO3 extension modules for Windows (64/32-bit x86 or 64-bit ARM)
63//! using either MinGW-w64 or MSVC target environment ABI:
64//!
65//! ```no_run
66//! fn main() {
67//!     if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" {
68//!         let cross_lib_dir = std::env::var_os("PYO3_CROSS_LIB_DIR")
69//!             .expect("PYO3_CROSS_LIB_DIR is not set when cross-compiling");
70//!         let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
71//!         let env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();
72//!
73//!         let libdir = std::path::Path::new(&cross_lib_dir);
74//!         python3_dll_a::generate_implib_for_target(libdir, &arch, &env)
75//!             .expect("python3.dll import library generator failed");
76//!     }
77//! }
78//! ```
79//!
80//! A compatible `python3.dll` import library file named `python3.dll.a`
81//! or `python3.lib` will be automatically created in the directory
82//! pointed by the `PYO3_CROSS_LIB_DIR` environment variable.
83//!
84//! ### Example `cargo build` invocation
85//!
86//! ```sh
87//! PYO3_CROSS_LIB_DIR=target/python3-dll cargo build --target x86_64-pc-windows-gnu
88//! ```
89//!
90//! Generating version-specific `python3y.dll` import libraries
91//! -----------------------------------------------------------
92//!
93//! As an advanced feature, `python3-dll-a` can generate Python version
94//! specific import libraries such as `python39.lib` or `python313t.lib`.
95//!
96//! See the [`ImportLibraryGenerator`] builder API description for details.
97
98#![deny(missing_docs)]
99#![allow(clippy::needless_doctest_main)]
100#![allow(clippy::uninlined_format_args)]
101
102use std::env;
103use std::fs::{create_dir_all, write};
104use std::io::{Error, ErrorKind, Result};
105use std::path::{Path, PathBuf};
106use std::process::Command;
107
108/// Import library file extension for the GNU environment ABI (MinGW-w64)
109const IMPLIB_EXT_GNU: &str = ".dll.a";
110
111/// Import library file extension for the MSVC environment ABI
112const IMPLIB_EXT_MSVC: &str = ".lib";
113
114/// Canonical MinGW-w64 `dlltool` program name
115const DLLTOOL_GNU: &str = "x86_64-w64-mingw32-dlltool";
116
117/// Canonical MinGW-w64 `dlltool` program name (32-bit version)
118const DLLTOOL_GNU_32: &str = "i686-w64-mingw32-dlltool";
119
120/// Canonical `dlltool` program name for the MSVC environment ABI (LLVM dlltool)
121const DLLTOOL_MSVC: &str = "llvm-dlltool";
122
123/// Canonical `lib` program name for the MSVC environment ABI (MSVC lib.exe)
124#[cfg(windows)]
125const LIB_MSVC: &str = "lib.exe";
126
127/// Python interpreter implementations
128#[derive(Debug, Clone, Copy)]
129pub enum PythonImplementation {
130    /// CPython
131    CPython,
132    /// PyPy
133    PyPy,
134}
135
136/// Windows import library generator for Python
137///
138/// Generates `python3.dll` or `pythonXY.dll` import library directly from the
139/// embedded Python ABI definitions data for the specified compile target.
140///
141/// ABI-tagged versioned Python DLLs such as `python313t.dll` are also supported
142/// via an optional ABI flags string parameter.
143///
144/// Example usage
145/// -------------
146///
147/// ```no_run
148/// # use std::path::Path;
149/// # use python3_dll_a::ImportLibraryGenerator;
150/// // Generate `python3.dll.a` in "target/python3-dll-a"
151/// ImportLibraryGenerator::new("x86_64", "gnu")
152///     .generate(Path::new("target/python3-dll-a"))
153///     .unwrap();
154///
155/// // Generate `python3.lib` in "target/python3-lib"
156/// ImportLibraryGenerator::new("x86_64", "msvc")
157///     .generate(Path::new("target/python3-lib"))
158///     .unwrap();
159///
160/// // Generate `python39.dll.a` in "target/python3-dll-a"
161/// ImportLibraryGenerator::new("x86_64", "gnu")
162///     .version(Some((3, 9)))
163///     .generate(Path::new("target/python3-dll-a"))
164///     .unwrap();
165///
166/// // Generate `python38.lib` in "target/python3-lib"
167/// ImportLibraryGenerator::new("x86_64", "msvc")
168///     .version(Some((3, 8)))
169///     .generate(Path::new("target/python3-lib"))
170///     .unwrap();
171///
172/// // Generate `python313t.lib` in "target/python3-lib"
173/// ImportLibraryGenerator::new("x86_64", "msvc")
174///     .version(Some((3, 13)))
175///     .abiflags(Some("t"))
176///     .generate(Path::new("target/python3-lib"))
177///     .unwrap();
178/// ```
179#[derive(Debug, Clone)]
180pub struct ImportLibraryGenerator {
181    /// The compile target architecture name (as in `CARGO_CFG_TARGET_ARCH`)
182    arch: String,
183    // The compile target environment ABI name (as in `CARGO_CFG_TARGET_ENV`)
184    env: String,
185    /// Major and minor Python version (for `pythonXY.dll` only)
186    version: Option<(u8, u8)>,
187    /// Python interpreter implementation
188    implementation: PythonImplementation,
189    /// Optional Python ABI flags
190    ///
191    /// For example, `"t"` stands for the free-threaded CPython v3.13 build
192    /// aka CPython `3.13t`.
193    abiflags: Option<String>,
194}
195
196impl ImportLibraryGenerator {
197    /// Creates a new import library generator for the specified compile target.
198    ///
199    /// The compile target architecture name (as in `CARGO_CFG_TARGET_ARCH`)
200    /// is passed in `arch`.
201    ///
202    /// The compile target environment ABI name (as in `CARGO_CFG_TARGET_ENV`)
203    /// is passed in `env`.
204    #[must_use]
205    pub fn new(arch: &str, env: &str) -> Self {
206        ImportLibraryGenerator {
207            arch: arch.to_string(),
208            env: env.to_string(),
209            version: None,
210            implementation: PythonImplementation::CPython,
211            abiflags: None,
212        }
213    }
214
215    /// Sets major and minor version for the `pythonXY.dll` import library.
216    ///
217    /// The version-agnostic `python3.dll` is generated by default.
218    pub fn version(&mut self, version: Option<(u8, u8)>) -> &mut Self {
219        self.version = version;
220        self
221    }
222
223    /// Sets the ABI flags for the `pythonXY<abi>.dll` import library.
224    ///
225    /// For example, `"t"` stands for the free-threaded CPython v3.13 build
226    /// aka CPython `3.13t`.
227    /// In this case, `python313t.dll` import library will be generated.
228    ///
229    /// The untagged versioned `pythonXY.dll` import library
230    /// is generated by default.
231    pub fn abiflags(&mut self, flags: Option<&str>) -> &mut Self {
232        self.abiflags = flags.map(ToOwned::to_owned);
233        self
234    }
235
236    /// Sets Python interpreter implementation
237    pub fn implementation(&mut self, implementation: PythonImplementation) -> &mut Self {
238        self.implementation = implementation;
239        self
240    }
241
242    /// Generates the Python DLL import library in `out_dir`.
243    ///
244    /// The version-agnostic `python3.dll` import library is generated
245    /// by default unless the version-specific `pythonXY.dll` import
246    /// was requested via `version()`.
247    pub fn generate(&self, out_dir: &Path) -> Result<()> {
248        create_dir_all(out_dir)?;
249
250        let defpath = self.write_def_file(out_dir)?;
251
252        // Try to guess the `dlltool` executable name from the target triple.
253        let dlltool_command = DllToolCommand::find_for_target(&self.arch, &self.env)?;
254
255        // Get the import library file extension from the used `dlltool` flavor.
256        let implib_ext = dlltool_command.implib_file_ext();
257
258        let implib_file = self.implib_file_path(out_dir, implib_ext);
259
260        // Build the complete `dlltool` command with all required arguments.
261        let mut command = dlltool_command.build(&defpath, &implib_file);
262
263        // Run the selected `dlltool` executable to generate the import library.
264        let status = command.status().map_err(|e| {
265            let msg = format!("{:?} failed with {}", command, e);
266            Error::new(e.kind(), msg)
267        })?;
268
269        if status.success() {
270            Ok(())
271        } else {
272            let msg = format!("{:?} failed with {}", command, status);
273            Err(Error::new(ErrorKind::Other, msg))
274        }
275    }
276
277    /// Writes out the embedded Python library definitions file to `out_dir`.
278    ///
279    /// Returns the newly created `python3.def` or `pythonXY.def` file path.
280    fn write_def_file(&self, out_dir: &Path) -> Result<PathBuf> {
281        let (def_file, def_file_content) = match self.implementation {
282            PythonImplementation::CPython => match self.version {
283                None => ("python3.def", include_str!("python3.def")),
284                Some((3, 7)) => ("python37.def", include_str!("python37.def")),
285                Some((3, 8)) => ("python38.def", include_str!("python38.def")),
286                Some((3, 9)) => ("python39.def", include_str!("python39.def")),
287                Some((3, 10)) => ("python310.def", include_str!("python310.def")),
288                Some((3, 11)) => ("python311.def", include_str!("python311.def")),
289                Some((3, 12)) => ("python312.def", include_str!("python312.def")),
290                Some((3, 13)) => match self.abiflags.as_deref() {
291                    Some("t") => ("python313t.def", include_str!("python313t.def")),
292                    None => ("python313.def", include_str!("python313.def")),
293                    _ => return Err(Error::new(ErrorKind::Other, "Unsupported Python ABI flags")),
294                },
295                Some((3, 14)) => match self.abiflags.as_deref() {
296                    Some("t") => ("python314t.def", include_str!("python314t.def")),
297                    None => ("python314.def", include_str!("python314.def")),
298                    _ => return Err(Error::new(ErrorKind::Other, "Unsupported Python ABI flags")),
299                },
300                _ => return Err(Error::new(ErrorKind::Other, "Unsupported Python version")),
301            },
302            PythonImplementation::PyPy => match self.version {
303                Some((3, 7)) | Some((3, 8)) => ("libpypy3-c.def", include_str!("libpypy3-c.def")),
304                Some((3, 9)) => ("libpypy3.9-c.def", include_str!("libpypy3.9-c.def")),
305                Some((3, 10)) => ("libpypy3.10-c.def", include_str!("libpypy3.10-c.def")),
306                Some((3, 11)) => ("libpypy3.11-c.def", include_str!("libpypy3.11-c.def")),
307                _ => return Err(Error::new(ErrorKind::Other, "Unsupported PyPy version")),
308            },
309        };
310
311        let mut defpath = out_dir.to_owned();
312        defpath.push(def_file);
313
314        write(&defpath, def_file_content)?;
315
316        Ok(defpath)
317    }
318
319    /// Builds the generated import library file name.
320    ///
321    /// The output file extension is passed in `libext`.
322    ///
323    /// Returns the full import library file path under `out_dir`.
324    fn implib_file_path(&self, out_dir: &Path, libext: &str) -> PathBuf {
325        let abiflags = self.abiflags.as_deref().unwrap_or_default();
326        let libname = match self.version {
327            Some((major, minor)) => {
328                format!("python{}{}{}{}", major, minor, abiflags, libext)
329            }
330            None => format!("python3{}", libext),
331        };
332
333        let mut libpath = out_dir.to_owned();
334        libpath.push(libname);
335
336        libpath
337    }
338}
339
340/// Generates `python3.dll` import library directly from the embedded
341/// Python Stable ABI definitions data for the specified compile target.
342///
343/// The import library file named `python3.dll.a` or `python3.lib` is created
344/// in directory `out_dir`.
345///
346/// The compile target architecture name (as in `CARGO_CFG_TARGET_ARCH`)
347/// is passed in `arch`.
348///
349/// The compile target environment ABI name (as in `CARGO_CFG_TARGET_ENV`)
350/// is passed in `env`.
351pub fn generate_implib_for_target(out_dir: &Path, arch: &str, env: &str) -> Result<()> {
352    ImportLibraryGenerator::new(arch, env).generate(out_dir)
353}
354
355/// `dlltool` utility command builder
356///
357/// Supports Visual Studio `lib.exe`, MinGW, LLVM and Zig `dlltool` flavors.
358#[derive(Debug)]
359enum DllToolCommand {
360    /// MinGW `dlltool` program (with prefix)
361    Mingw { command: Command },
362    /// LLVM `llvm-dlltool` program (no prefix)
363    Llvm { command: Command, machine: String },
364    /// MSVC `lib.exe` program (no prefix)
365    LibExe { command: Command, machine: String },
366    /// `zig dlltool` wrapper (no prefix)
367    Zig { command: Command, machine: String },
368}
369
370impl DllToolCommand {
371    /// Attempts to find the best matching `dlltool` flavor for the target.
372    fn find_for_target(arch: &str, env: &str) -> Result<DllToolCommand> {
373        // LLVM tools use their own target architecture names...
374        let machine = match arch {
375            "x86_64" => "i386:x86-64",
376            "x86" => "i386",
377            "aarch64" => "arm64",
378            arch => arch,
379        }
380        .to_owned();
381
382        // If `zig cc` is used as the linker, `zig dlltool` is the best choice.
383        if let Some(command) = find_zig() {
384            return Ok(DllToolCommand::Zig { command, machine });
385        }
386
387        match env {
388            // 64-bit and 32-bit MinGW-w64 (aka `{x86_64,i686}-pc-windows-gnu`)
389            "gnu" => Ok(DllToolCommand::Mingw {
390                command: get_mingw_dlltool(arch)?,
391            }),
392
393            // MSVC ABI (multiarch)
394            "msvc" => {
395                if let Some(command) = find_lib_exe(arch) {
396                    // MSVC tools use their own target architecture names...
397                    let machine = match arch {
398                        "x86_64" => "X64",
399                        "x86" => "X86",
400                        "aarch64" => "ARM64",
401                        arch => arch,
402                    }
403                    .to_owned();
404
405                    Ok(DllToolCommand::LibExe { command, machine })
406                } else {
407                    let command = Command::new(DLLTOOL_MSVC);
408
409                    Ok(DllToolCommand::Llvm { command, machine })
410                }
411            }
412            _ => {
413                let msg = format!("Unsupported target env ABI '{}'", env);
414                Err(Error::new(ErrorKind::Other, msg))
415            }
416        }
417    }
418
419    /// Returns the import library file extension used by
420    /// this `dlltool` flavor.
421    fn implib_file_ext(&self) -> &'static str {
422        if let DllToolCommand::Mingw { .. } = self {
423            IMPLIB_EXT_GNU
424        } else {
425            IMPLIB_EXT_MSVC
426        }
427    }
428
429    /// Generates the complete `dlltool` executable invocation command.
430    fn build(self, defpath: &Path, libpath: &Path) -> Command {
431        match self {
432            Self::Mingw { mut command } => {
433                command
434                    .arg("--input-def")
435                    .arg(defpath)
436                    .arg("--output-lib")
437                    .arg(libpath);
438
439                command
440            }
441            Self::Llvm {
442                mut command,
443                machine,
444            } => {
445                command
446                    .arg("-m")
447                    .arg(machine)
448                    .arg("-d")
449                    .arg(defpath)
450                    .arg("-l")
451                    .arg(libpath);
452
453                command
454            }
455            Self::LibExe {
456                mut command,
457                machine,
458            } => {
459                command
460                    .arg(format!("/MACHINE:{}", machine))
461                    .arg(format!("/DEF:{}", defpath.display()))
462                    .arg(format!("/OUT:{}", libpath.display()));
463
464                command
465            }
466            Self::Zig {
467                mut command,
468                machine,
469            } => {
470                // Same as `llvm-dlltool`, but invoked as `zig dlltool`.
471                command
472                    .arg("dlltool")
473                    .arg("-m")
474                    .arg(machine)
475                    .arg("-d")
476                    .arg(defpath)
477                    .arg("-l")
478                    .arg(libpath);
479
480                command
481            }
482        }
483    }
484}
485
486/// Chooses the appropriate MinGW-w64 `dlltool` executable
487/// for the target architecture.
488///
489/// Examines the user-provided `PYO3_MINGW_DLLTOOL` environment variable first
490/// and falls back to the default MinGW-w64 arch prefixes.
491fn get_mingw_dlltool(arch: &str) -> Result<Command> {
492    if let Ok(user_dlltool) = env::var("PYO3_MINGW_DLLTOOL") {
493        Ok(Command::new(user_dlltool))
494    } else {
495        let prefix_dlltool = match arch {
496            // 64-bit MinGW-w64 (aka `x86_64-pc-windows-gnu`)
497            "x86_64" => Ok(DLLTOOL_GNU),
498            // 32-bit MinGW-w64 (aka `i686-pc-windows-gnu`)
499            "x86" => Ok(DLLTOOL_GNU_32),
500            // AArch64?
501            _ => {
502                let msg = format!("Unsupported MinGW target arch '{}'", arch);
503                Err(Error::new(ErrorKind::Other, msg))
504            }
505        }?;
506
507        Ok(Command::new(prefix_dlltool))
508    }
509}
510
511/// Finds the `zig` executable (when built by `maturin --zig`).
512///
513/// Examines the `ZIG_COMMAND` environment variable
514/// to find out if `zig cc` is being used as the linker.
515fn find_zig() -> Option<Command> {
516    // `ZIG_COMMAND` may contain simply `zig` or `/usr/bin/zig`,
517    // or a more complex construct like `python3 -m ziglang`.
518    let zig_command = env::var("ZIG_COMMAND").ok()?;
519
520    // Try to emulate `sh -c ${ZIG_COMMAND}`.
521    let mut zig_cmdlet = zig_command.split_ascii_whitespace();
522
523    // Extract the main program component (e.g. `zig` or `python3`).
524    let mut zig = Command::new(zig_cmdlet.next()?);
525
526    // Append the rest of the commandlet.
527    zig.args(zig_cmdlet);
528
529    Some(zig)
530}
531
532/// Finds Visual Studio `lib.exe` when running on Windows.
533#[cfg(windows)]
534fn find_lib_exe(arch: &str) -> Option<Command> {
535    let target = match arch {
536        "x86_64" => "x86_64-pc-windows-msvc",
537        "x86" => "i686-pc-windows-msvc",
538        "aarch64" => "aarch64-pc-windows-msvc",
539        _ => return None,
540    };
541
542    cc::windows_registry::find(target, LIB_MSVC)
543}
544
545#[cfg(not(windows))]
546fn find_lib_exe(_arch: &str) -> Option<Command> {
547    None
548}
549
550#[cfg(test)]
551mod tests {
552    use std::path::PathBuf;
553
554    use super::*;
555
556    #[cfg(unix)]
557    #[test]
558    fn generate() {
559        // FIXME: Use "target/<arch>" dirs for temporary files.
560        let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
561        dir.push("target");
562        dir.push("x86_64-pc-windows-gnu");
563        dir.push("python3-dll");
564
565        ImportLibraryGenerator::new("x86_64", "gnu")
566            .generate(&dir)
567            .unwrap();
568
569        for minor in 7..=14 {
570            ImportLibraryGenerator::new("x86_64", "gnu")
571                .version(Some((3, minor)))
572                .generate(&dir)
573                .unwrap();
574        }
575
576        // Free-threaded CPython v3.13+
577        for minor in 13..=14 {
578            ImportLibraryGenerator::new("x86_64", "gnu")
579                .version(Some((3, minor)))
580                .abiflags(Some("t"))
581                .generate(&dir)
582                .unwrap();
583        }
584
585        // PyPy
586        for minor in 7..=11 {
587            ImportLibraryGenerator::new("x86_64", "gnu")
588                .version(Some((3, minor)))
589                .implementation(PythonImplementation::PyPy)
590                .generate(&dir)
591                .unwrap();
592        }
593    }
594
595    #[cfg(unix)]
596    #[test]
597    fn generate_gnu32() {
598        let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
599        dir.push("target");
600        dir.push("i686-pc-windows-gnu");
601        dir.push("python3-dll");
602
603        generate_implib_for_target(&dir, "x86", "gnu").unwrap();
604    }
605
606    #[test]
607    fn generate_msvc() {
608        let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
609        dir.push("target");
610        dir.push("x86_64-pc-windows-msvc");
611        dir.push("python3-dll");
612
613        ImportLibraryGenerator::new("x86_64", "msvc")
614            .generate(&dir)
615            .unwrap();
616
617        for minor in 7..=14 {
618            ImportLibraryGenerator::new("x86_64", "msvc")
619                .version(Some((3, minor)))
620                .generate(&dir)
621                .unwrap();
622        }
623
624        // Free-threaded CPython v3.13+
625        for minor in 13..=14 {
626            ImportLibraryGenerator::new("x86_64", "msvc")
627                .version(Some((3, minor)))
628                .abiflags(Some("t"))
629                .generate(&dir)
630                .unwrap();
631        }
632
633        // PyPy
634        for minor in 7..=11 {
635            ImportLibraryGenerator::new("x86_64", "msvc")
636                .version(Some((3, minor)))
637                .implementation(PythonImplementation::PyPy)
638                .generate(&dir)
639                .unwrap();
640        }
641    }
642
643    #[test]
644    fn generate_msvc32() {
645        let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
646        dir.push("target");
647        dir.push("i686-pc-windows-msvc");
648        dir.push("python3-dll");
649
650        generate_implib_for_target(&dir, "x86", "msvc").unwrap();
651    }
652
653    #[test]
654    fn generate_msvc_arm64() {
655        let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
656        dir.push("target");
657        dir.push("aarch64-pc-windows-msvc");
658        dir.push("python3-dll");
659
660        ImportLibraryGenerator::new("aarch64", "msvc")
661            .generate(&dir)
662            .unwrap();
663
664        for minor in 7..=14 {
665            ImportLibraryGenerator::new("aarch64", "msvc")
666                .version(Some((3, minor)))
667                .generate(&dir)
668                .unwrap();
669        }
670
671        // Free-threaded CPython v3.13+
672        for minor in 13..=14 {
673            let mut generator = ImportLibraryGenerator::new("aarch64", "msvc");
674            generator.version(Some((3, minor))).abiflags(Some("t"));
675            let implib_file_path = generator.implib_file_path(&dir, IMPLIB_EXT_MSVC);
676            let implib_file_stem = implib_file_path.file_stem().unwrap().to_str().unwrap();
677            assert!(implib_file_stem.ends_with("t"));
678
679            generator.generate(&dir).unwrap();
680        }
681
682        // PyPy
683        for minor in 7..=11 {
684            ImportLibraryGenerator::new("aarch64", "msvc")
685                .version(Some((3, minor)))
686                .implementation(PythonImplementation::PyPy)
687                .generate(&dir)
688                .unwrap();
689        }
690    }
691}