use super::repair::{AuditResult, AuditedArtifact, GraftedLib, WheelRepairer};
use crate::compile::BuildArtifact;
use anyhow::{Context, Result, bail};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub struct WindowsRepairer {
pub is_pypy: bool,
}
impl WheelRepairer for WindowsRepairer {
fn audit(&self, artifact: &BuildArtifact, ld_paths: Vec<PathBuf>) -> Result<AuditResult> {
let external_libs = find_external_libs(&artifact.path, ld_paths, self.is_pypy)
.context("Failed to find external libraries for Windows wheel")?;
Ok(AuditResult::new(super::Policy::default(), external_libs))
}
fn patch(
&self,
audited: &[AuditedArtifact],
grafted: &[GraftedLib],
_libs_dir: &Path,
_artifact_dir: &Path,
) -> Result<()> {
let mut replacements: Vec<(&str, &str)> = Vec::new();
for l in grafted {
if l.original_name != l.new_name {
replacements.push((l.original_name.as_str(), l.new_name.as_str()));
}
for alias in &l.aliases {
replacements.push((alias.as_str(), l.new_name.as_str()));
}
}
for aa in audited {
if !replacements.is_empty() {
super::pe_patch::replace_needed(&aa.artifact.path, &replacements).with_context(
|| {
format!(
"Failed to patch PE imports in {}",
aa.artifact.path.display()
)
},
)?;
}
}
for lib in grafted {
let lib_replacements: Vec<(&str, &str)> = lib
.needed
.iter()
.filter_map(|n| {
replacements
.iter()
.find(|(old, _)| old.eq_ignore_ascii_case(n))
.map(|(old, new)| (*old, *new))
})
.collect();
if !lib_replacements.is_empty() {
super::pe_patch::replace_needed(&lib.dest_path, &lib_replacements).with_context(
|| format!("Failed to patch PE imports in {}", lib.dest_path.display()),
)?;
} else {
super::pe_patch::clear_dependent_load_flags(&lib.dest_path)?;
}
}
Ok(())
}
fn init_py_patch(&self, libs_dir_name: &str, depth: usize) -> Option<String> {
let pardir_chain = "os.pardir, ".repeat(depth);
Some(format!(
"# start maturin patch\n\
def _maturin_dll_patch():\n\
\x20 import os\n\
\x20 import sys\n\
\x20 libs_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), {pardir_chain}\"{libs_dir_name}\"))\n\
\x20 if os.path.isdir(libs_dir):\n\
\x20 if sys.version_info >= (3, 8) and hasattr(os, 'add_dll_directory'):\n\
\x20 os.add_dll_directory(libs_dir)\n\
_maturin_dll_patch()\n\
del _maturin_dll_patch\n\
# end maturin patch\n"
))
}
}
fn is_api_set_dll(name: &str) -> bool {
name.starts_with("api-") || name.starts_with("ext-ms-")
}
fn is_python_dll(name: &str) -> bool {
if let Some(rest) = name.strip_prefix("python") {
let rest = rest.as_bytes();
let mut i = 0;
while i < rest.len() && rest[i].is_ascii_digit() {
i += 1;
}
if i == 0 {
return false;
}
if i < rest.len() && rest[i] == b't' {
i += 1;
}
if i + 1 < rest.len() && rest[i] == b'_' && rest[i + 1] == b'd' {
i += 2;
}
return rest[i..] == *b".dll";
}
if let Some(rest) = name.strip_prefix("libpypy") {
return rest.ends_with("-c.dll");
}
false
}
fn is_pypy_runtime_dll(name: &str) -> bool {
name.starts_with("libffi")
}
fn is_vc_runtime_dll(name: &str) -> bool {
name.starts_with("vcruntime")
|| name.starts_with("msvcp")
|| name.starts_with("msvcr")
|| name.starts_with("vccorlib")
|| name.starts_with("concrt")
|| name.starts_with("vcamp")
|| name.starts_with("vcomp")
|| name.starts_with("ucrtbase")
|| name.starts_with("mfc")
}
const KNOWN_SYSTEM_DLLS: &[&str] = &[
"kernel32.dll",
"kernelbase.dll",
"ntdll.dll",
"advapi32.dll",
"user32.dll",
"win32u.dll",
"gdi32.dll",
"gdi32full.dll",
"shell32.dll",
"ole32.dll",
"oleaut32.dll",
"rpcrt4.dll",
"msvcrt.dll",
"nsi.dll",
"ws2_32.dll",
"wsock32.dll",
"mswsock.dll",
"winhttp.dll",
"wininet.dll",
"iphlpapi.dll",
"dnsapi.dll",
"netapi32.dll",
"wldap32.dll",
"sechost.dll",
"sspicli.dll",
"secur32.dll",
"crypt32.dll",
"bcrypt.dll",
"bcryptprimitives.dll",
"ncrypt.dll",
"wintrust.dll",
"rsaenh.dll",
"dpapi.dll",
"cryptsp.dll",
"cryptbase.dll",
"shlwapi.dll",
"comctl32.dll",
"comdlg32.dll",
"imm32.dll",
"uxtheme.dll",
"shcore.dll",
"dwmapi.dll",
"msimg32.dll",
"userenv.dll",
"dbghelp.dll",
"psapi.dll",
"setupapi.dll",
"cfgmgr32.dll",
"version.dll",
"winmm.dll",
"powrprof.dll",
"cabinet.dll",
"msi.dll",
"imagehlp.dll",
"normaliz.dll",
"profapi.dll",
"wevtapi.dll",
"pdh.dll",
"avrt.dll",
"synchronization.dll",
"combase.dll",
"propsys.dll",
"devobj.dll",
"wtsapi32.dll",
"credui.dll",
"netutils.dll",
"samcli.dll",
"d3d9.dll",
"d3d10.dll",
"d3d11.dll",
"d3d12.dll",
"dxgi.dll",
"d2d1.dll",
"d3dcompiler_47.dll",
"dwrite.dll",
"opengl32.dll",
"glu32.dll",
"dxcore.dll",
"windowscodecs.dll",
"mfplat.dll",
"mf.dll",
"mfreadwrite.dll",
"mmdevapi.dll",
"hid.dll",
"winspool.drv",
"cldapi.dll",
"authz.dll",
];
fn is_in_windows_system_dir(realpath: &Path) -> bool {
let path_str = realpath.to_string_lossy().to_lowercase();
let path_normalized = path_str.replace('\\', "/");
let windir = std::env::var("WINDIR")
.or_else(|_| std::env::var("SystemRoot"))
.unwrap_or_default()
.to_lowercase()
.replace('\\', "/");
if !windir.is_empty() {
let system_subdirs = ["system32", "syswow64", "winsxs", "sysarm32"];
for subdir in system_subdirs {
if path_normalized.starts_with(&format!("{windir}/{subdir}/")) {
return true;
}
}
}
path_normalized.contains("/windows/system32/")
|| path_normalized.contains("/windows/syswow64/")
|| path_normalized.contains("/windows/winsxs/")
|| path_normalized.contains("/windows/sysarm32/")
}
fn is_system_dll(name: &str, realpath: Option<&Path>, is_pypy: bool) -> bool {
let lower = name.to_lowercase();
if is_api_set_dll(&lower) {
return true;
}
if is_python_dll(&lower) {
return true;
}
if is_pypy && is_pypy_runtime_dll(&lower) {
return true;
}
if is_vc_runtime_dll(&lower) {
return true;
}
if let Some(path) = realpath
&& is_in_windows_system_dir(path)
{
return true;
}
KNOWN_SYSTEM_DLLS.contains(&lower.as_str())
}
pub fn find_external_libs(
artifact: impl AsRef<Path>,
ld_paths: Vec<PathBuf>,
is_pypy: bool,
) -> Result<Vec<lddtree::Library>> {
let dep_analyzer = lddtree::DependencyAnalyzer::default().library_paths(ld_paths);
let deps = dep_analyzer.analyze(&artifact).with_context(|| {
format!(
"Failed to analyze dependencies for {}",
artifact.as_ref().display()
)
})?;
let mut ext_libs = Vec::new();
let mut seen: HashMap<String, PathBuf> = HashMap::new();
for (_name, lib) in deps.libraries {
if is_system_dll(&lib.name, lib.realpath.as_deref(), is_pypy) {
continue;
}
if !lib.found() {
continue;
}
let lower_name = lib.name.to_lowercase();
let realpath = lib.realpath.as_ref().expect("lib.found() was true");
if let Some(existing_path) = seen.get(&lower_name) {
if existing_path != realpath {
bail!(
"DLL basename collision: found two different DLLs named '{}'\n\
- {}\n\
- {}\n\
This is unsupported because PE import tables reference DLLs by \
basename only. Consider renaming one of the libraries or \
restructuring your dependencies.",
lib.name,
existing_path.display(),
realpath.display()
);
}
continue;
}
seen.insert(lower_name, realpath.clone());
ext_libs.push(lib);
}
Ok(ext_libs)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_api_set_dlls() {
assert!(is_api_set_dll("api-ms-win-crt-runtime-l1-1-0.dll"));
assert!(is_api_set_dll("ext-ms-win-ntuser-uicontext-ext-l1-1-0.dll"));
assert!(!is_api_set_dll("kernel32.dll"));
}
#[test]
fn test_python_dlls() {
assert!(is_python_dll("python3.dll"));
assert!(is_python_dll("python311.dll"));
assert!(is_python_dll("python312.dll"));
assert!(is_python_dll("python313t.dll"));
assert!(is_python_dll("python312_d.dll"));
assert!(is_python_dll("python313t_d.dll"));
assert!(is_python_dll("libpypy39-c.dll"));
assert!(!is_python_dll("pythoncom.dll"));
assert!(!is_python_dll("python3_bindings.dll"));
}
#[test]
fn test_vc_runtime_dlls() {
assert!(is_vc_runtime_dll("vcruntime140.dll"));
assert!(is_vc_runtime_dll("vcruntime140_1.dll"));
assert!(is_vc_runtime_dll("vcruntime140d.dll"));
assert!(is_vc_runtime_dll("msvcp140.dll"));
assert!(is_vc_runtime_dll("msvcp140_1.dll"));
assert!(is_vc_runtime_dll("concrt140.dll"));
assert!(is_vc_runtime_dll("vcomp140.dll"));
assert!(is_vc_runtime_dll("ucrtbase.dll"));
assert!(!is_vc_runtime_dll("mylib.dll"));
}
#[test]
fn test_pypy_runtime_dlls() {
assert!(is_pypy_runtime_dll("libffi-8.dll"));
assert!(is_pypy_runtime_dll("libffi.dll"));
assert!(!is_pypy_runtime_dll("libcrypto-3-x64.dll"));
}
#[test]
fn test_system_dll_by_name() {
assert!(is_system_dll("kernel32.dll", None, false));
assert!(is_system_dll("KERNEL32.DLL", None, false));
assert!(is_system_dll(
"api-ms-win-crt-runtime-l1-1-0.dll",
None,
false
));
assert!(is_system_dll("python311.dll", None, false));
assert!(is_system_dll("vcruntime140.dll", None, false));
assert!(!is_system_dll("libcrypto-3-x64.dll", None, false));
}
#[test]
fn test_system_dll_pypy_libffi() {
assert!(!is_system_dll("libffi-8.dll", None, false));
assert!(is_system_dll("libffi-8.dll", None, true));
}
#[test]
fn test_system_dll_by_path() {
let system32_path = PathBuf::from(r"C:\Windows\System32\obscure_system.dll");
assert!(is_system_dll(
"obscure_system.dll",
Some(&system32_path),
false
));
let syswow64_path = PathBuf::from(r"C:\Windows\SysWOW64\another.dll");
assert!(is_system_dll("another.dll", Some(&syswow64_path), false));
let user_path = PathBuf::from(r"C:\Users\me\libs\mylib.dll");
assert!(!is_system_dll("mylib.dll", Some(&user_path), false));
let temp_path = PathBuf::from(r"C:\Windows\Temp\mylib.dll");
assert!(!is_system_dll("mylib.dll", Some(&temp_path), false));
}
#[test]
fn test_init_py_patch_depth_1() {
let repairer = WindowsRepairer { is_pypy: false };
let patch = repairer.init_py_patch("mypackage.libs", 1).unwrap();
assert!(patch.contains("# start maturin patch"));
assert!(patch.contains("# end maturin patch"));
assert!(patch.contains("os.add_dll_directory(libs_dir)"));
assert!(patch.contains("hasattr(os, 'add_dll_directory')"));
assert!(patch.contains("sys.version_info >= (3, 8)"));
assert!(patch.contains(r#"os.pardir, "mypackage.libs""#));
}
#[test]
fn test_init_py_patch_depth_2() {
let repairer = WindowsRepairer { is_pypy: false };
let patch = repairer.init_py_patch("mypackage.libs", 2).unwrap();
assert!(patch.contains(r#"os.pardir, os.pardir, "mypackage.libs""#));
}
}