399 lines
15 KiB
Python
399 lines
15 KiB
Python
import os
|
|
import sys
|
|
import shutil
|
|
import subprocess
|
|
import json
|
|
from typing import Dict
|
|
|
|
def check_and_install_packages(packages_with_versions: Dict[str, str]):
|
|
"""检查并安装指定版本的包"""
|
|
print("正在检查并安装必要的依赖包...")
|
|
|
|
for package, version in packages_with_versions.items():
|
|
try:
|
|
# 尝试导入包来检查是否已安装
|
|
__import__(package.replace('-', '_').replace('.', '_'))
|
|
print(f"✓ {package} 已安装")
|
|
except ImportError:
|
|
print(f"正在安装 {package}=={version}...")
|
|
try:
|
|
# 对于win11toast包特殊处理,忽略安装失败
|
|
if package == 'win11toast':
|
|
try:
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", f"{package}=={version}"],
|
|
check=False
|
|
)
|
|
print(f"✓ {package}=={version} 安装成功")
|
|
except:
|
|
print(f"! {package} 安装失败,但这不会影响程序核心功能,继续打包")
|
|
else:
|
|
# 其他包正常安装
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", f"{package}=={version}"],
|
|
check=True
|
|
)
|
|
print(f"✓ {package}=={version} 安装成功")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"! 无法安装 {package}=={version}: {str(e)}")
|
|
print(f" 尝试安装不指定版本的 {package}...")
|
|
try:
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", package],
|
|
check=True
|
|
)
|
|
print(f"✓ {package} 安装成功")
|
|
except subprocess.CalledProcessError as e2:
|
|
print(f"! 安装 {package} 失败: {str(e2)}")
|
|
if package == 'win11toast':
|
|
print(f" {package} 安装失败,但这不会影响程序核心功能")
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def create_notification_alternative():
|
|
"""创建替代win11toast的通知实现文件"""
|
|
print("正在创建通知替代实现...")
|
|
|
|
# 在dist目录下创建一个替代的通知模块
|
|
notification_dir = os.path.join('dist', 'win11toast')
|
|
if not os.path.exists(notification_dir):
|
|
os.makedirs(notification_dir)
|
|
|
|
# 创建__init__.py
|
|
with open(os.path.join(notification_dir, '__init__.py'), 'w', encoding='utf-8') as f:
|
|
f.write('''
|
|
# 替代win11toast的简易实现
|
|
import ctypes
|
|
import threading
|
|
import time
|
|
|
|
def toast(title, message, **kwargs):
|
|
"""显示一个Windows通知,使用MessageBox替代"""
|
|
def show():
|
|
ctypes.windll.user32.MessageBoxW(0, message, title, 0x40)
|
|
threading.Thread(target=show, daemon=True).start()
|
|
|
|
def notify(title, message, **kwargs):
|
|
"""显示一个Windows通知,使用MessageBox替代"""
|
|
toast(title, message, **kwargs)
|
|
''')
|
|
|
|
# 创建空的__pycache__目录以避免警告
|
|
pycache_dir = os.path.join(notification_dir, '__pycache__')
|
|
if not os.path.exists(pycache_dir):
|
|
os.makedirs(pycache_dir)
|
|
|
|
print("通知替代实现创建完成")
|
|
|
|
def get_installed_packages() -> Dict[str, str]:
|
|
"""获取当前已安装的包版本信息"""
|
|
result = {}
|
|
try:
|
|
output = subprocess.check_output([sys.executable, "-m", "pip", "freeze"]).decode('utf-8')
|
|
for line in output.split('\n'):
|
|
if '==' in line:
|
|
package, version = line.strip().split('==', 1)
|
|
result[package] = version
|
|
except Exception as e:
|
|
print(f"获取已安装包信息时出错: {str(e)}")
|
|
return result
|
|
|
|
def write_requirements_file(packages_with_versions: Dict[str, str]):
|
|
"""生成requirements.txt文件"""
|
|
print("正在生成requirements.txt文件...")
|
|
with open('requirements.txt', 'w', encoding='utf-8') as f:
|
|
for package, version in packages_with_versions.items():
|
|
f.write(f"{package}=={version}\n")
|
|
print("requirements.txt文件已生成")
|
|
|
|
def create_manifest_file():
|
|
"""创建应用程序清单文件,请求管理员权限"""
|
|
print("正在创建应用程序清单文件...")
|
|
manifest_content = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
<security>
|
|
<requestedPrivileges>
|
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
|
</requestedPrivileges>
|
|
</security>
|
|
</trustInfo>
|
|
</assembly>'''
|
|
|
|
with open('app.manifest', 'w', encoding='utf-8') as f:
|
|
f.write(manifest_content)
|
|
print("应用程序清单文件已创建")
|
|
|
|
def create_spec_file(sv_ttk_path: str):
|
|
"""创建PyInstaller spec文件"""
|
|
print("正在创建PyInstaller spec文件...")
|
|
|
|
# 这里列出所有需要的隐藏导入
|
|
hidden_imports = [
|
|
'sv_ttk',
|
|
'keyboard',
|
|
'mouse',
|
|
'win32gui',
|
|
'win32process',
|
|
'win32con',
|
|
'win32api',
|
|
'win32com.client',
|
|
'json',
|
|
'requests',
|
|
'math',
|
|
'ctypes',
|
|
'threading',
|
|
'time',
|
|
'webbrowser',
|
|
're',
|
|
'traceback',
|
|
'wmi',
|
|
'pythoncom',
|
|
'concurrent.futures',
|
|
'winreg',
|
|
'win11toast' # 总是包含win11toast,即使安装失败也不影响
|
|
]
|
|
|
|
# 创建spec文件内容
|
|
spec_content = f'''# -*- mode: python ; coding: utf-8 -*-
|
|
|
|
a = Analysis(
|
|
['chrome_manager.py'],
|
|
pathex=[],
|
|
binaries=[],
|
|
datas=[
|
|
('app.ico', '.'),
|
|
(r'{sv_ttk_path}', 'sv_ttk'),
|
|
('README.md', '.'),
|
|
('settings.json', '.') if os.path.exists('settings.json') else None,
|
|
],
|
|
hiddenimports={hidden_imports},
|
|
hookspath=[],
|
|
hooksconfig={{}},
|
|
runtime_hooks=[],
|
|
excludes=[],
|
|
noarchive=False,
|
|
)
|
|
|
|
# 移除None值
|
|
a.datas = [x for x in a.datas if x is not None]
|
|
|
|
pyz = PYZ(a.pure)
|
|
|
|
exe = EXE(
|
|
pyz,
|
|
a.scripts,
|
|
a.binaries,
|
|
a.zipfiles,
|
|
a.datas,
|
|
[('app.manifest', 'app.manifest', 'DATA')],
|
|
name='chrome_manager',
|
|
debug=False,
|
|
bootloader_ignore_signals=False,
|
|
strip=False,
|
|
upx=False, # 禁用UPX压缩,防止被杀毒软件误报
|
|
console=False,
|
|
icon=['app.ico'],
|
|
manifest="app.manifest",
|
|
uac_admin=True, # 添加UAC管理员请求
|
|
uac_uiaccess=False,
|
|
disable_windowed_traceback=False,
|
|
)
|
|
'''
|
|
|
|
with open('chrome_manager.spec', 'w', encoding='utf-8') as f:
|
|
f.write(spec_content)
|
|
print("PyInstaller spec文件已创建")
|
|
|
|
def find_sv_ttk_path():
|
|
try:
|
|
import sv_ttk
|
|
return os.path.dirname(sv_ttk.__file__)
|
|
except ImportError:
|
|
print("未找到sv_ttk模块,请先安装")
|
|
return None
|
|
|
|
def ensure_icon_exists():
|
|
if not os.path.exists('app.ico'):
|
|
print("警告: 未找到app.ico文件,将使用默认图标")
|
|
# 可以考虑生成一个简单的图标或从网络下载一个
|
|
try:
|
|
# 尝试从Windows系统中复制一个默认图标
|
|
shutil.copy(os.path.join(os.environ['SystemRoot'], 'System32', 'shell32.dll'), 'temp_icon.dll')
|
|
subprocess.run(['powershell', '-Command',
|
|
"(New-Object -ComObject Shell.Application).NameSpace(0).ParseName('temp_icon.dll').GetLink.GetIconLocation() | Out-File -FilePath 'app.ico'"],
|
|
check=True)
|
|
os.remove('temp_icon.dll')
|
|
except Exception as e:
|
|
print(f"生成默认图标失败: {str(e)}")
|
|
print("将使用PyInstaller默认图标")
|
|
|
|
def ensure_settings_exists():
|
|
"""确保settings.json文件存在"""
|
|
if not os.path.exists('settings.json'):
|
|
print("正在创建默认settings.json文件...")
|
|
default_settings = {
|
|
"shortcut_path": "",
|
|
"cache_dir": "",
|
|
"icon_dir": "",
|
|
"screen_selection": "",
|
|
"sync_shortcut": None,
|
|
"window_position": {"x": 100, "y": 100}
|
|
}
|
|
with open('settings.json', 'w', encoding='utf-8') as f:
|
|
json.dump(default_settings, f, ensure_ascii=False, indent=4)
|
|
print("默认settings.json文件已创建")
|
|
|
|
def modify_chrome_manager_for_win11toast():
|
|
"""修改chrome_manager.py中的通知相关代码,添加简单的try-except处理"""
|
|
print("检查chrome_manager.py是否需要修改通知实现...")
|
|
|
|
try:
|
|
with open('chrome_manager.py', 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# 如果已经有错误处理,则不需要修改
|
|
if "try:" in content and "from win11toast import notify, toast" in content and "except ImportError:" in content:
|
|
print("chrome_manager.py已包含通知错误处理")
|
|
return
|
|
|
|
# 查找win11toast导入行
|
|
if "from win11toast import notify, toast" in content:
|
|
# 替换成带错误处理的版本
|
|
original = "from win11toast import notify, toast"
|
|
replacement = '''# 添加通知错误处理
|
|
try:
|
|
from win11toast import notify, toast
|
|
except ImportError:
|
|
# 简单的空函数替代
|
|
def toast(title, message, **kwargs):
|
|
pass
|
|
def notify(title, message, **kwargs):
|
|
pass'''
|
|
|
|
modified_content = content.replace(original, replacement)
|
|
|
|
with open('chrome_manager.py', 'w', encoding='utf-8') as f:
|
|
f.write(modified_content)
|
|
|
|
print("成功添加通知错误处理到chrome_manager.py")
|
|
else:
|
|
print("未找到win11toast导入行,跳过修改")
|
|
except Exception as e:
|
|
print(f"修改chrome_manager.py失败: {str(e)}")
|
|
print("继续打包过程...")
|
|
|
|
def show_success_message():
|
|
print("\n")
|
|
print("─────────────────────────────────────────────────────")
|
|
print(" ")
|
|
print(" ✨ Chrome多窗口管理器 V2.0 打包成功 ✨ ")
|
|
print(" ")
|
|
print(" 📦 可执行文件已生成到dist文件夹 ")
|
|
print(" 🚀 双击chrome_manager.exe即可运行 ")
|
|
print(" 🔑 首次打开程序时间会稍长,请耐心等待 ")
|
|
print(" 🌐 使用中遇到问题,请查阅Github中的说明 ")
|
|
print(" ")
|
|
print("─────────────────────────────────────────────────────")
|
|
print("\n")
|
|
|
|
def show_failure_message(error_msg="未知错误"):
|
|
print("\n")
|
|
print("─────────────────────────────────────────────────────")
|
|
print(" ")
|
|
print(" ❌ Chrome多窗口管理器 V2.0 打包失败 ❌ ")
|
|
print(" ")
|
|
print(" ❗ 打包过程中出现错误 ")
|
|
print(f" 📋 错误信息: {error_msg[:35]}{'...' if len(error_msg) > 35 else ''}")
|
|
print(" 🔄 请查阅Github中的说明 ")
|
|
print(" 💡 然后尝试重新运行打包程序 ")
|
|
print(" ")
|
|
print("─────────────────────────────────────────────────────")
|
|
print("\n")
|
|
|
|
def build():
|
|
"""打包程序"""
|
|
print("\n===== 开始打包Chrome多窗口管理器 V2.0 =====\n")
|
|
|
|
# 修改chrome_manager.py添加简单的错误处理
|
|
modify_chrome_manager_for_win11toast()
|
|
|
|
# 需要的包和版本列表
|
|
required_packages = {
|
|
'pyinstaller': '6.12.0',
|
|
'sv-ttk': '2.6.0',
|
|
'keyboard': '0.13.5',
|
|
'mouse': '0.7.1',
|
|
'pywin32': '309',
|
|
'wmi': '1.5.1',
|
|
'requests': '2.32.3',
|
|
'pillow': '11.1.0',
|
|
'win11toast': '0.32', # 包含win11toast但允许安装失败
|
|
}
|
|
|
|
# 获取当前已安装的包
|
|
installed_packages = get_installed_packages()
|
|
|
|
# 更新为实际安装的版本
|
|
for package in required_packages:
|
|
if package in installed_packages:
|
|
required_packages[package] = installed_packages[package]
|
|
|
|
# 检查并安装必要的包
|
|
if not check_and_install_packages(required_packages):
|
|
print("安装必要的包失败,尝试继续打包...")
|
|
|
|
# 创建requirements.txt文件
|
|
write_requirements_file(required_packages)
|
|
|
|
# 确保其他必要文件存在
|
|
ensure_icon_exists()
|
|
ensure_settings_exists()
|
|
|
|
# 创建清单文件
|
|
create_manifest_file()
|
|
|
|
# 查找sv_ttk路径
|
|
sv_ttk_path = find_sv_ttk_path()
|
|
if not sv_ttk_path:
|
|
print("无法找到sv_ttk模块,打包终止")
|
|
show_failure_message("未找到sv_ttk模块")
|
|
return False
|
|
|
|
# 创建spec文件
|
|
create_spec_file(sv_ttk_path)
|
|
|
|
# 清理旧的构建文件
|
|
print("正在清理旧的构建文件...")
|
|
if os.path.exists('build'):
|
|
shutil.rmtree('build')
|
|
if os.path.exists('dist'):
|
|
shutil.rmtree('dist')
|
|
|
|
# 运行PyInstaller
|
|
print("\n正在打包程序...")
|
|
try:
|
|
# 当使用 .spec 文件时,不应在命令行传递 --clean 或 --noupx 等选项
|
|
# 这些选项应在 spec 文件内配置,或由脚本本身处理(如清理目录)
|
|
subprocess.run(['pyinstaller', 'chrome_manager.spec'], check=True)
|
|
print("\n打包完成!程序文件在dist文件夹中。")
|
|
|
|
# 复制额外文件到dist目录
|
|
if not os.path.exists(os.path.join('dist', 'settings.json')) and os.path.exists('settings.json'):
|
|
shutil.copy('settings.json', os.path.join('dist', 'settings.json'))
|
|
|
|
show_success_message()
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
error_msg = str(e)
|
|
show_failure_message(error_msg)
|
|
return False
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
success = build()
|
|
except Exception as e:
|
|
show_failure_message(str(e))
|
|
finally:
|
|
input("\n按回车键退出...") |