ArticleReplaceBatch/Exeprotector/pyinstallertools.py
2025-08-07 10:55:41 +08:00

328 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
PyInstaller Helper Tool
用于调试和解决PyInstaller编译问题
"""
import os
import sys
import subprocess
import shutil
import tempfile
from pathlib import Path
class PyInstallerHelper:
def __init__(self):
self.temp_dir = None
def check_pyinstaller(self):
"""检查PyInstaller是否正确安装"""
try:
result = subprocess.run(['pyinstaller', '--version'],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
version = result.stdout.strip()
print(f"✓ PyInstaller已安装: {version}")
return True, version
else:
return False, "PyInstaller命令执行失败"
except FileNotFoundError:
return False, "PyInstaller未安装"
except Exception as e:
return False, f"检查PyInstaller时出错: {str(e)}"
def install_pyinstaller(self):
"""安装PyInstaller"""
try:
print("正在安装PyInstaller...")
result = subprocess.run([sys.executable, '-m', 'pip', 'install', 'pyinstaller'],
capture_output=True, text=True, timeout=60)
if result.returncode == 0:
print("✓ PyInstaller安装成功")
return True
else:
print(f"✗ PyInstaller安装失败: {result.stderr}")
return False
except Exception as e:
print(f"✗ 安装过程出错: {str(e)}")
return False
def create_spec_file(self, python_file, exe_name, output_dir):
"""创建PyInstaller规格文件"""
spec_content = f'''# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['{python_file}'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=['mysql.connector', 'cryptography', 'tkinter'],
hookspath=[],
hooksconfig={{}},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='{exe_name}',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
'''
spec_file_path = os.path.join(output_dir, f'{exe_name}.spec')
with open(spec_file_path, 'w', encoding='utf-8') as f:
f.write(spec_content)
return spec_file_path
def compile_with_spec(self, spec_file, output_dir):
"""使用spec文件编译"""
try:
original_cwd = os.getcwd()
os.chdir(output_dir)
try:
cmd = ['pyinstaller', '--clean', '--noconfirm', spec_file]
print(f"执行命令: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
return True, "编译成功"
else:
return False, f"编译失败:\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
finally:
os.chdir(original_cwd)
except subprocess.TimeoutExpired:
return False, "编译超时超过5分钟"
except Exception as e:
return False, f"编译过程出错: {str(e)}"
def compile_python_to_exe(self, python_file, output_exe=None, console=False):
"""将Python文件编译为EXE"""
try:
# 验证输入文件
if not os.path.exists(python_file):
return False, f"Python文件不存在: {python_file}"
# 检查PyInstaller
installed, msg = self.check_pyinstaller()
if not installed:
print(f"PyInstaller检查失败: {msg}")
if input("是否自动安装PyInstaller? (y/n): ").lower() == 'y':
if not self.install_pyinstaller():
return False, "PyInstaller安装失败"
else:
return False, "需要安装PyInstaller才能继续"
# 确定输出文件名
if output_exe is None:
output_exe = python_file.replace('.py', '.exe')
file_dir = os.path.dirname(os.path.abspath(python_file))
file_name = os.path.basename(python_file)
exe_name = os.path.basename(output_exe).replace('.exe', '')
print(f"编译文件: {python_file}")
print(f"输出目录: {file_dir}")
print(f"EXE名称: {exe_name}")
# 方法1: 直接编译
success = self.direct_compile(python_file, exe_name, file_dir, console)
if success:
return True, "编译成功"
# 方法2: 使用spec文件编译
print("直接编译失败尝试使用spec文件...")
spec_file = self.create_spec_file(file_name, exe_name, file_dir)
success, msg = self.compile_with_spec(spec_file, file_dir)
# 清理临时文件
self.cleanup_temp_files(file_dir, exe_name)
if success:
# 检查输出文件
expected_exe = os.path.join(file_dir, 'dist', f'{exe_name}.exe')
final_exe = os.path.join(file_dir, f'{exe_name}.exe')
if os.path.exists(expected_exe):
# 移动文件到最终位置
if os.path.exists(final_exe):
os.remove(final_exe)
shutil.move(expected_exe, final_exe)
# 清理dist目录
dist_dir = os.path.join(file_dir, 'dist')
if os.path.exists(dist_dir):
shutil.rmtree(dist_dir, ignore_errors=True)
return True, f"编译成功: {final_exe}"
else:
return False, f"编译完成但找不到输出文件: {expected_exe}"
else:
return False, msg
except Exception as e:
return False, f"编译过程出现异常: {str(e)}"
def direct_compile(self, python_file, exe_name, file_dir, console=False):
"""直接编译方法"""
try:
original_cwd = os.getcwd()
os.chdir(file_dir)
try:
cmd = [
'pyinstaller',
'--onefile',
'--clean',
'--noconfirm',
f'--name={exe_name}',
'--distpath=.',
]
if not console:
cmd.append('--noconsole')
# 添加隐式导入
hidden_imports = ['mysql.connector', 'cryptography', 'tkinter', 'tkinter.messagebox']
for imp in hidden_imports:
cmd.extend(['--hidden-import', imp])
cmd.append(os.path.basename(python_file))
print(f"执行直接编译: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
expected_exe = f'{exe_name}.exe'
if os.path.exists(expected_exe):
print(f"✓ 直接编译成功: {expected_exe}")
return True
print(f"直接编译失败: {result.stderr}")
return False
finally:
os.chdir(original_cwd)
except Exception as e:
print(f"直接编译出错: {str(e)}")
return False
def cleanup_temp_files(self, file_dir, exe_name):
"""清理临时文件"""
try:
# 清理build目录
build_dir = os.path.join(file_dir, 'build')
if os.path.exists(build_dir):
shutil.rmtree(build_dir, ignore_errors=True)
# 清理spec文件
spec_file = os.path.join(file_dir, f'{exe_name}.spec')
if os.path.exists(spec_file):
os.remove(spec_file)
print("✓ 临时文件清理完成")
except Exception as e:
print(f"清理临时文件时出错: {str(e)}")
def test_exe(self, exe_file):
"""测试生成的EXE文件"""
if not os.path.exists(exe_file):
return False, f"EXE文件不存在: {exe_file}"
try:
# 检查文件大小
size = os.path.getsize(exe_file)
if size < 1024: # 小于1KB
return False, f"EXE文件太小可能编译失败: {size} bytes"
print(f"✓ EXE文件大小: {size:,} bytes")
# 尝试运行(仅检查是否能启动,不等待结果)
try:
if os.name == 'nt': # Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
proc = subprocess.Popen([exe_file], startupinfo=startupinfo)
# 等待一秒看是否立即崩溃
import time
time.sleep(1)
if proc.poll() is None:
proc.terminate()
return True, "EXE文件可以正常启动"
else:
return False, f"EXE文件启动后立即退出退出码: {proc.returncode}"
else:
return True, "非Windows系统跳过运行测试"
except Exception as e:
return False, f"运行测试失败: {str(e)}"
except Exception as e:
return False, f"测试EXE文件时出错: {str(e)}"
def main():
"""主函数"""
if len(sys.argv) < 2:
print("用法: python pyinstaller_helper.py <python_file> [output_exe] [--console]")
print("示例: python pyinstaller_helper.py validator.py validator.exe")
return
python_file = sys.argv[1]
output_exe = sys.argv[2] if len(sys.argv) > 2 else None
console = '--console' in sys.argv
helper = PyInstallerHelper()
print("=" * 50)
print("PyInstaller编译助手")
print("=" * 50)
success, msg = helper.compile_python_to_exe(python_file, output_exe, console)
print("\n" + "=" * 50)
if success:
print(f"✓ 编译成功: {msg}")
# 测试生成的EXE
if output_exe and os.path.exists(output_exe):
print("\n测试EXE文件...")
test_success, test_msg = helper.test_exe(output_exe)
if test_success:
print(f"{test_msg}")
else:
print(f"{test_msg}")
else:
print(f"✗ 编译失败: {msg}")
print("=" * 50)
if __name__ == "__main__":
main()