diff --git a/src/lib.rs b/src/lib.rs index 0034a1b..4944af2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,22 +75,17 @@ #![deny(missing_docs)] #![allow(clippy::needless_doctest_main)] +use std::ffi::OsStr; use std::fs::{create_dir_all, write}; use std::io::{Error, ErrorKind, Result}; use std::path::Path; use std::process::Command; -/// Module-Definition file name for `python3.dll` -const DEF_FILE: &str = "python3.def"; +/// Import library file extension for the GNU environment ABI (MinGW-w64) +const IMPLIB_EXT_GNU: &str = ".dll.a"; -/// Module-Definition file content for `python3.dll` -const DEF_FILE_CONTENT: &[u8] = include_bytes!("python3.def"); - -/// Canonical `python3.dll` import library file name for the GNU environment ABI (MinGW-w64) -const IMPLIB_FILE_GNU: &str = "python3.dll.a"; - -/// Canonical `python3.dll` import library file name for the MSVC environment ABI -const IMPLIB_FILE_MSVC: &str = "python3.lib"; +/// Import library file extension for the MSVC environment ABI +const IMPLIB_EXT_MSVC: &str = ".lib"; /// Canonical MinGW-w64 `dlltool` program name const DLLTOOL_GNU: &str = "x86_64-w64-mingw32-dlltool"; @@ -104,6 +99,169 @@ const DLLTOOL_MSVC: &str = "llvm-dlltool"; /// Canonical `lib` program name for the MSVC environment ABI (MSVC lib.exe) const LIB_MSVC: &str = "lib.exe"; +/// Windows import library generator for Python +/// +/// Generates `python3.dll` or `pythonXY.dll` import library directly from the +/// embedded Python ABI definitions data for the specified compile target. +#[derive(Debug, Clone)] +pub struct ImportLibraryGenerator { + /// The compile target architecture name (as in `CARGO_CFG_TARGET_ARCH`) + arch: String, + // The compile target environment ABI name (as in `CARGO_CFG_TARGET_ENV`) + env: String, + /// Python major and minor version + version: Option<(u8, u8)>, +} + +impl ImportLibraryGenerator { + /// Creates a new import library generator for the specified compile target + pub fn new(arch: &str, env: &str) -> Self { + Self { + arch: arch.to_string(), + env: env.to_string(), + version: None, + } + } + + /// Set python major and minor version + pub fn version(&mut self, version: Option<(u8, u8)>) -> &mut Self { + self.version = version; + self + } + + /// Generates the import library in `out_dir` + pub fn generate(&self, out_dir: &Path) -> Result<()> { + create_dir_all(out_dir)?; + + let mut defpath = out_dir.to_owned(); + let (def_file, def_file_content) = match self.version { + None => ("python3.def", include_str!("python3.def")), + Some((3, 7)) => ("python37.def", include_str!("python37.def")), + Some((3, 8)) => ("python38.def", include_str!("python38.def")), + Some((3, 9)) => ("python39.def", include_str!("python39.def")), + Some((3, 10)) => ("python310.def", include_str!("python310.def")), + Some((3, 11)) => ("python311.def", include_str!("python311.def")), + _ => return Err(Error::new(ErrorKind::Other, "Unsupported Python version")), + }; + defpath.push(def_file); + + write(&defpath, def_file_content)?; + + // Try to guess the `dlltool` executable name from the target triple. + let mut command = match (self.arch.as_str(), self.env.as_str()) { + // 64-bit MinGW-w64 (aka x86_64-pc-windows-gnu) + ("x86_64", "gnu") => self.build_dlltool_command( + DLLTOOL_GNU, + &def_file.replace(".def", IMPLIB_EXT_GNU), + &defpath, + out_dir, + ), + // 32-bit MinGW-w64 (aka i686-pc-windows-gnu) + ("x86", "gnu") => self.build_dlltool_command( + DLLTOOL_GNU_32, + &def_file.replace(".def", IMPLIB_EXT_GNU), + &defpath, + out_dir, + ), + // MSVC ABI (multiarch) + (_, "msvc") => { + let implib_file = def_file.replace(".def", IMPLIB_EXT_MSVC); + if let Some(command) = find_lib_exe(&self.arch) { + self.build_dlltool_command( + command.get_program(), + &implib_file, + &defpath, + out_dir, + ) + } else { + self.build_dlltool_command(DLLTOOL_MSVC, &implib_file, &defpath, out_dir) + } + } + _ => { + let msg = format!( + "Unsupported target arch '{}' or env ABI '{}'", + self.arch, self.env + ); + return Err(Error::new(ErrorKind::Other, msg)); + } + }; + + // Run the selected `dlltool` executable to generate the import library. + let status = command.status()?; + + if status.success() { + Ok(()) + } else { + let msg = format!("{:?} failed with {}", command, status); + Err(Error::new(ErrorKind::Other, msg)) + } + } + + /// Generates the complete `dlltool` executable invocation command. + /// + /// Supports Visual Studio `lib.exe`, LLVM and MinGW `dlltool` flavors. + fn build_dlltool_command( + &self, + dlltool: impl AsRef, + implib_file: &str, + defpath: &Path, + out_dir: &Path, + ) -> Command { + let dlltool = dlltool.as_ref(); + let mut libpath = out_dir.to_owned(); + let mut command = if self.env == "msvc" { + find_lib_exe(&self.arch).unwrap_or_else(|| Command::new(dlltool)) + } else { + Command::new(dlltool) + }; + + // Check whether we are using LLVM `dlltool` or MinGW `dlltool`. + if dlltool == DLLTOOL_MSVC { + libpath.push(implib_file); + + // LLVM tools use their own target architecture names... + let machine = match self.arch.as_str() { + "x86_64" => "i386:x86-64", + "x86" => "i386", + "aarch64" => "arm64", + arch => arch, + }; + + command + .arg("-m") + .arg(machine) + .arg("-d") + .arg(defpath) + .arg("-l") + .arg(libpath); + } else if Path::new(dlltool).file_name() == Some(LIB_MSVC.as_ref()) { + libpath.push(implib_file); + + // lib.exe use their own target architecure names... + let machine = match self.arch.as_str() { + "x86_64" => "X64", + "x86" => "X86", + "aarch64" => "ARM64", + arch => arch, + }; + command + .arg(format!("/MACHINE:{}", machine)) + .arg(format!("/DEF:{}", defpath.display())) + .arg(format!("/OUT:{}", libpath.display())); + } else { + libpath.push(implib_file); + + command + .arg("--input-def") + .arg(defpath) + .arg("--output-lib") + .arg(libpath); + } + + command + } +} + /// Generates `python3.dll` import library directly from the embedded /// Python Stable ABI definitions data for the specified compile target. /// @@ -116,42 +274,7 @@ const LIB_MSVC: &str = "lib.exe"; /// The compile target environment ABI name (as in `CARGO_CFG_TARGET_ENV`) /// is passed in `env`. pub fn generate_implib_for_target(out_dir: &Path, arch: &str, env: &str) -> Result<()> { - create_dir_all(out_dir)?; - - let mut defpath = out_dir.to_owned(); - defpath.push(DEF_FILE); - - write(&defpath, DEF_FILE_CONTENT)?; - - // Try to guess the `dlltool` executable name from the target triple. - let (command, dlltool) = match (arch, env) { - // 64-bit MinGW-w64 (aka x86_64-pc-windows-gnu) - ("x86_64", "gnu") => (Command::new(DLLTOOL_GNU), DLLTOOL_GNU), - // 32-bit MinGW-w64 (aka i686-pc-windows-gnu) - ("x86", "gnu") => (Command::new(DLLTOOL_GNU_32), DLLTOOL_GNU_32), - // MSVC ABI (multiarch) - (_, "msvc") => { - if let Some(command) = find_lib_exe(arch) { - (command, LIB_MSVC) - } else { - (Command::new(DLLTOOL_MSVC), DLLTOOL_MSVC) - } - } - _ => { - let msg = format!("Unsupported target arch '{arch}' or env ABI '{env}'"); - return Err(Error::new(ErrorKind::Other, msg)); - } - }; - - // Run the selected `dlltool` executable to generate the import library. - let status = build_dlltool_command(command, dlltool, arch, &defpath, out_dir).status()?; - - if status.success() { - Ok(()) - } else { - let msg = format!("{dlltool} failed with {status}"); - Err(Error::new(ErrorKind::Other, msg)) - } + ImportLibraryGenerator::new(arch, env).generate(out_dir) } /// Find Visual Studio lib.exe on Windows @@ -171,64 +294,6 @@ fn find_lib_exe(_arch: &str) -> Option { None } -/// Generates the complete `dlltool` executable invocation command. -/// -/// Supports Visual Studio `lib.exe`, LLVM and MinGW `dlltool` flavors. -fn build_dlltool_command( - mut command: Command, - dlltool: &str, - arch: &str, - defpath: &Path, - out_dir: &Path, -) -> Command { - let mut libpath = out_dir.to_owned(); - - // Check whether we are using LLVM `dlltool` or MinGW `dlltool`. - if dlltool == DLLTOOL_MSVC { - libpath.push(IMPLIB_FILE_MSVC); - - // LLVM tools use their own target architecture names... - let machine = match arch { - "x86_64" => "i386:x86-64", - "x86" => "i386", - "aarch64" => "arm64", - _ => arch, - }; - - command - .arg("-m") - .arg(machine) - .arg("-d") - .arg(defpath) - .arg("-l") - .arg(libpath); - } else if dlltool == LIB_MSVC { - libpath.push(IMPLIB_FILE_MSVC); - - // lib.exe use their own target architecure names... - let machine = match arch { - "x86_64" => "X64", - "x86" => "X86", - "aarch64" => "ARM64", - _ => arch, - }; - command - .arg(format!("/MACHINE:{}", machine)) - .arg(format!("/DEF:{}", defpath.display())) - .arg(format!("/OUT:{}", libpath.display())); - } else { - libpath.push(IMPLIB_FILE_GNU); - - command - .arg("--input-def") - .arg(defpath) - .arg("--output-lib") - .arg(libpath); - } - - command -} - #[cfg(test)] mod tests { use std::path::PathBuf; @@ -244,7 +309,12 @@ mod tests { dir.push("x86_64-pc-windows-gnu"); dir.push("python3-dll"); - generate_implib_for_target(&dir, "x86_64", "gnu").unwrap(); + for minor in 7..=11 { + ImportLibraryGenerator::new("x86_64", "gnu") + .version(Some((3, minor))) + .generate(&dir) + .unwrap(); + } } #[cfg(unix)] @@ -266,6 +336,12 @@ mod tests { dir.push("python3-dll"); generate_implib_for_target(&dir, "x86_64", "msvc").unwrap(); + for minor in 7..=11 { + ImportLibraryGenerator::new("x86_64", "msvc") + .version(Some((3, minor))) + .generate(&dir) + .unwrap(); + } } #[test]