#![deny(missing_docs)]
#![allow(clippy::needless_doctest_main)]
#![allow(clippy::uninlined_format_args)]
use std::env;
use std::fs::{create_dir_all, write};
use std::io::{Error, ErrorKind, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
const IMPLIB_EXT_GNU: &str = ".dll.a";
const IMPLIB_EXT_MSVC: &str = ".lib";
const DLLTOOL_GNU: &str = "x86_64-w64-mingw32-dlltool";
const DLLTOOL_GNU_32: &str = "i686-w64-mingw32-dlltool";
const DLLTOOL_MSVC: &str = "llvm-dlltool";
#[cfg(windows)]
const LIB_MSVC: &str = "lib.exe";
#[derive(Debug, Clone, Copy)]
pub enum PythonImplementation {
CPython,
PyPy,
}
#[derive(Debug, Clone)]
pub struct ImportLibraryGenerator {
arch: String,
env: String,
version: Option<(u8, u8)>,
implementation: PythonImplementation,
abiflags: Option<String>,
}
impl ImportLibraryGenerator {
#[must_use]
pub fn new(arch: &str, env: &str) -> Self {
ImportLibraryGenerator {
arch: arch.to_string(),
env: env.to_string(),
version: None,
implementation: PythonImplementation::CPython,
abiflags: None,
}
}
pub fn version(&mut self, version: Option<(u8, u8)>) -> &mut Self {
self.version = version;
self
}
pub fn abiflags(&mut self, flags: Option<&str>) -> &mut Self {
self.abiflags = flags.map(ToOwned::to_owned);
self
}
pub fn implementation(&mut self, implementation: PythonImplementation) -> &mut Self {
self.implementation = implementation;
self
}
pub fn generate(&self, out_dir: &Path) -> Result<()> {
create_dir_all(out_dir)?;
let defpath = self.write_def_file(out_dir)?;
let dlltool_command = DllToolCommand::find_for_target(&self.arch, &self.env)?;
let implib_ext = dlltool_command.implib_file_ext();
let implib_file = self.implib_file_path(out_dir, implib_ext);
let mut command = dlltool_command.build(&defpath, &implib_file);
let status = command.status().map_err(|e| {
let msg = format!("{:?} failed with {}", command, e);
Error::new(e.kind(), msg)
})?;
if status.success() {
Ok(())
} else {
let msg = format!("{:?} failed with {}", command, status);
Err(Error::new(ErrorKind::Other, msg))
}
}
fn write_def_file(&self, out_dir: &Path) -> Result<PathBuf> {
let (def_file, def_file_content) = match self.implementation {
PythonImplementation::CPython => 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")),
Some((3, 12)) => ("python312.def", include_str!("python312.def")),
Some((3, 13)) => match self.abiflags.as_deref() {
Some("t") => ("python313t.def", include_str!("python313t.def")),
None => ("python313.def", include_str!("python313.def")),
_ => return Err(Error::new(ErrorKind::Other, "Unsupported Python ABI flags")),
},
Some((3, 14)) => match self.abiflags.as_deref() {
Some("t") => ("python314t.def", include_str!("python314t.def")),
None => ("python314.def", include_str!("python314.def")),
_ => return Err(Error::new(ErrorKind::Other, "Unsupported Python ABI flags")),
},
_ => return Err(Error::new(ErrorKind::Other, "Unsupported Python version")),
},
PythonImplementation::PyPy => match self.version {
Some((3, 7)) | Some((3, 8)) => ("libpypy3-c.def", include_str!("libpypy3-c.def")),
Some((3, 9)) => ("libpypy3.9-c.def", include_str!("libpypy3.9-c.def")),
Some((3, 10)) => ("libpypy3.10-c.def", include_str!("libpypy3.10-c.def")),
Some((3, 11)) => ("libpypy3.11-c.def", include_str!("libpypy3.11-c.def")),
_ => return Err(Error::new(ErrorKind::Other, "Unsupported PyPy version")),
},
};
let mut defpath = out_dir.to_owned();
defpath.push(def_file);
write(&defpath, def_file_content)?;
Ok(defpath)
}
fn implib_file_path(&self, out_dir: &Path, libext: &str) -> PathBuf {
let abiflags = self.abiflags.as_deref().unwrap_or_default();
let libname = match self.version {
Some((major, minor)) => {
format!("python{}{}{}{}", major, minor, abiflags, libext)
}
None => format!("python3{}", libext),
};
let mut libpath = out_dir.to_owned();
libpath.push(libname);
libpath
}
}
pub fn generate_implib_for_target(out_dir: &Path, arch: &str, env: &str) -> Result<()> {
ImportLibraryGenerator::new(arch, env).generate(out_dir)
}
#[derive(Debug)]
enum DllToolCommand {
Mingw { command: Command },
Llvm { command: Command, machine: String },
LibExe { command: Command, machine: String },
Zig { command: Command, machine: String },
}
impl DllToolCommand {
fn find_for_target(arch: &str, env: &str) -> Result<DllToolCommand> {
let machine = match arch {
"x86_64" => "i386:x86-64",
"x86" => "i386",
"aarch64" => "arm64",
arch => arch,
}
.to_owned();
if let Some(command) = find_zig() {
return Ok(DllToolCommand::Zig { command, machine });
}
match env {
"gnu" => Ok(DllToolCommand::Mingw {
command: get_mingw_dlltool(arch)?,
}),
"msvc" => {
if let Some(command) = find_lib_exe(arch) {
let machine = match arch {
"x86_64" => "X64",
"x86" => "X86",
"aarch64" => "ARM64",
arch => arch,
}
.to_owned();
Ok(DllToolCommand::LibExe { command, machine })
} else {
let command = Command::new(DLLTOOL_MSVC);
Ok(DllToolCommand::Llvm { command, machine })
}
}
_ => {
let msg = format!("Unsupported target env ABI '{}'", env);
Err(Error::new(ErrorKind::Other, msg))
}
}
}
fn implib_file_ext(&self) -> &'static str {
if let DllToolCommand::Mingw { .. } = self {
IMPLIB_EXT_GNU
} else {
IMPLIB_EXT_MSVC
}
}
fn build(self, defpath: &Path, libpath: &Path) -> Command {
match self {
Self::Mingw { mut command } => {
command
.arg("--input-def")
.arg(defpath)
.arg("--output-lib")
.arg(libpath);
command
}
Self::Llvm {
mut command,
machine,
} => {
command
.arg("-m")
.arg(machine)
.arg("-d")
.arg(defpath)
.arg("-l")
.arg(libpath);
command
}
Self::LibExe {
mut command,
machine,
} => {
command
.arg(format!("/MACHINE:{}", machine))
.arg(format!("/DEF:{}", defpath.display()))
.arg(format!("/OUT:{}", libpath.display()));
command
}
Self::Zig {
mut command,
machine,
} => {
command
.arg("dlltool")
.arg("-m")
.arg(machine)
.arg("-d")
.arg(defpath)
.arg("-l")
.arg(libpath);
command
}
}
}
}
fn get_mingw_dlltool(arch: &str) -> Result<Command> {
if let Ok(user_dlltool) = env::var("PYO3_MINGW_DLLTOOL") {
Ok(Command::new(user_dlltool))
} else {
let prefix_dlltool = match arch {
"x86_64" => Ok(DLLTOOL_GNU),
"x86" => Ok(DLLTOOL_GNU_32),
_ => {
let msg = format!("Unsupported MinGW target arch '{}'", arch);
Err(Error::new(ErrorKind::Other, msg))
}
}?;
Ok(Command::new(prefix_dlltool))
}
}
fn find_zig() -> Option<Command> {
let zig_command = env::var("ZIG_COMMAND").ok()?;
let mut zig_cmdlet = zig_command.split_ascii_whitespace();
let mut zig = Command::new(zig_cmdlet.next()?);
zig.args(zig_cmdlet);
Some(zig)
}
#[cfg(windows)]
fn find_lib_exe(arch: &str) -> Option<Command> {
let target = match arch {
"x86_64" => "x86_64-pc-windows-msvc",
"x86" => "i686-pc-windows-msvc",
"aarch64" => "aarch64-pc-windows-msvc",
_ => return None,
};
cc::windows_registry::find(target, LIB_MSVC)
}
#[cfg(not(windows))]
fn find_lib_exe(_arch: &str) -> Option<Command> {
None
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
#[cfg(unix)]
#[test]
fn generate() {
let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.push("target");
dir.push("x86_64-pc-windows-gnu");
dir.push("python3-dll");
ImportLibraryGenerator::new("x86_64", "gnu")
.generate(&dir)
.unwrap();
for minor in 7..=14 {
ImportLibraryGenerator::new("x86_64", "gnu")
.version(Some((3, minor)))
.generate(&dir)
.unwrap();
}
for minor in 13..=14 {
ImportLibraryGenerator::new("x86_64", "gnu")
.version(Some((3, minor)))
.abiflags(Some("t"))
.generate(&dir)
.unwrap();
}
for minor in 7..=11 {
ImportLibraryGenerator::new("x86_64", "gnu")
.version(Some((3, minor)))
.implementation(PythonImplementation::PyPy)
.generate(&dir)
.unwrap();
}
}
#[cfg(unix)]
#[test]
fn generate_gnu32() {
let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.push("target");
dir.push("i686-pc-windows-gnu");
dir.push("python3-dll");
generate_implib_for_target(&dir, "x86", "gnu").unwrap();
}
#[test]
fn generate_msvc() {
let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.push("target");
dir.push("x86_64-pc-windows-msvc");
dir.push("python3-dll");
ImportLibraryGenerator::new("x86_64", "msvc")
.generate(&dir)
.unwrap();
for minor in 7..=14 {
ImportLibraryGenerator::new("x86_64", "msvc")
.version(Some((3, minor)))
.generate(&dir)
.unwrap();
}
for minor in 13..=14 {
ImportLibraryGenerator::new("x86_64", "msvc")
.version(Some((3, minor)))
.abiflags(Some("t"))
.generate(&dir)
.unwrap();
}
for minor in 7..=11 {
ImportLibraryGenerator::new("x86_64", "msvc")
.version(Some((3, minor)))
.implementation(PythonImplementation::PyPy)
.generate(&dir)
.unwrap();
}
}
#[test]
fn generate_msvc32() {
let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.push("target");
dir.push("i686-pc-windows-msvc");
dir.push("python3-dll");
generate_implib_for_target(&dir, "x86", "msvc").unwrap();
}
#[test]
fn generate_msvc_arm64() {
let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.push("target");
dir.push("aarch64-pc-windows-msvc");
dir.push("python3-dll");
ImportLibraryGenerator::new("aarch64", "msvc")
.generate(&dir)
.unwrap();
for minor in 7..=14 {
ImportLibraryGenerator::new("aarch64", "msvc")
.version(Some((3, minor)))
.generate(&dir)
.unwrap();
}
for minor in 13..=14 {
let mut generator = ImportLibraryGenerator::new("aarch64", "msvc");
generator.version(Some((3, minor))).abiflags(Some("t"));
let implib_file_path = generator.implib_file_path(&dir, IMPLIB_EXT_MSVC);
let implib_file_stem = implib_file_path.file_stem().unwrap().to_str().unwrap();
assert!(implib_file_stem.ends_with("t"));
generator.generate(&dir).unwrap();
}
for minor in 7..=11 {
ImportLibraryGenerator::new("aarch64", "msvc")
.version(Some((3, minor)))
.implementation(PythonImplementation::PyPy)
.generate(&dir)
.unwrap();
}
}
}