392 lines
12 KiB
Python
392 lines
12 KiB
Python
|
|
"""
|
|||
|
|
奶酪云工具箱 - 打包脚本
|
|||
|
|
支持 Windows (7/10/11) 32位/64位, macOS 打包
|
|||
|
|
|
|||
|
|
使用方法:
|
|||
|
|
python build_app.py # 打包当前平台
|
|||
|
|
python build_app.py --all # 尝试打包所有平台(需要对应环境)
|
|||
|
|
python build_app.py --platform win64 # 指定平台
|
|||
|
|
"""
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import shutil
|
|||
|
|
import platform
|
|||
|
|
import subprocess
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
# 项目配置
|
|||
|
|
APP_NAME = "奶酪云工具箱"
|
|||
|
|
APP_NAME_EN = "CheeseCloudTools"
|
|||
|
|
VERSION = "1.0.0"
|
|||
|
|
AUTHOR = "奶酪源码"
|
|||
|
|
DESCRIPTION = "多功能图片/PDF/Excel处理工具"
|
|||
|
|
|
|||
|
|
# 路径配置
|
|||
|
|
PROJECT_ROOT = Path(__file__).parent
|
|||
|
|
DIST_DIR = PROJECT_ROOT / "dist"
|
|||
|
|
BUILD_DIR = PROJECT_ROOT / "build"
|
|||
|
|
ICON_SOURCE = PROJECT_ROOT / "image" / "生成奶酪商城官方店介绍.png"
|
|||
|
|
ICON_DIR = PROJECT_ROOT / "build_icons"
|
|||
|
|
|
|||
|
|
# 需要包含的数据文件
|
|||
|
|
DATA_FILES = [
|
|||
|
|
("resources", "resources"),
|
|||
|
|
("image", "image"),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def create_icon_dir():
|
|||
|
|
"""创建图标目录"""
|
|||
|
|
ICON_DIR.mkdir(exist_ok=True)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def convert_png_to_ico(png_path: Path, ico_path: Path, sizes=None):
|
|||
|
|
"""将PNG转换为ICO格式(Windows图标)"""
|
|||
|
|
if sizes is None:
|
|||
|
|
sizes = [(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)]
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from PIL import Image
|
|||
|
|
|
|||
|
|
img = Image.open(png_path)
|
|||
|
|
|
|||
|
|
# 创建不同尺寸的图标
|
|||
|
|
icon_images = []
|
|||
|
|
for size in sizes:
|
|||
|
|
resized = img.copy()
|
|||
|
|
resized.thumbnail(size, Image.Resampling.LANCZOS)
|
|||
|
|
# 确保尺寸正确
|
|||
|
|
if resized.size != size:
|
|||
|
|
new_img = Image.new('RGBA', size, (0, 0, 0, 0))
|
|||
|
|
offset = ((size[0] - resized.size[0]) // 2,
|
|||
|
|
(size[1] - resized.size[1]) // 2)
|
|||
|
|
new_img.paste(resized, offset)
|
|||
|
|
resized = new_img
|
|||
|
|
icon_images.append(resized)
|
|||
|
|
|
|||
|
|
# 保存为ICO
|
|||
|
|
icon_images[0].save(
|
|||
|
|
ico_path,
|
|||
|
|
format='ICO',
|
|||
|
|
sizes=[(img.size[0], img.size[1]) for img in icon_images]
|
|||
|
|
)
|
|||
|
|
print(f"✅ 已创建 Windows 图标: {ico_path}")
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"❌ 创建 ICO 失败: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def convert_png_to_icns(png_path: Path, icns_path: Path):
|
|||
|
|
"""将PNG转换为ICNS格式(macOS图标)"""
|
|||
|
|
try:
|
|||
|
|
from PIL import Image
|
|||
|
|
|
|||
|
|
img = Image.open(png_path)
|
|||
|
|
|
|||
|
|
# macOS 需要特定尺寸
|
|||
|
|
sizes = [16, 32, 64, 128, 256, 512, 1024]
|
|||
|
|
|
|||
|
|
# 创建临时 iconset 目录
|
|||
|
|
iconset_dir = icns_path.parent / f"{icns_path.stem}.iconset"
|
|||
|
|
iconset_dir.mkdir(exist_ok=True)
|
|||
|
|
|
|||
|
|
for size in sizes:
|
|||
|
|
# 标准分辨率
|
|||
|
|
resized = img.copy()
|
|||
|
|
resized.thumbnail((size, size), Image.Resampling.LANCZOS)
|
|||
|
|
resized.save(iconset_dir / f"icon_{size}x{size}.png")
|
|||
|
|
|
|||
|
|
# 2x 分辨率 (Retina)
|
|||
|
|
if size <= 512:
|
|||
|
|
resized_2x = img.copy()
|
|||
|
|
resized_2x.thumbnail((size * 2, size * 2), Image.Resampling.LANCZOS)
|
|||
|
|
resized_2x.save(iconset_dir / f"icon_{size}x{size}@2x.png")
|
|||
|
|
|
|||
|
|
# 使用 iconutil 转换(仅macOS可用)
|
|||
|
|
if platform.system() == "Darwin":
|
|||
|
|
subprocess.run(["iconutil", "-c", "icns", str(iconset_dir), "-o", str(icns_path)])
|
|||
|
|
print(f"✅ 已创建 macOS 图标: {icns_path}")
|
|||
|
|
else:
|
|||
|
|
# 在非macOS上,复制PNG作为替代
|
|||
|
|
shutil.copy(png_path, icns_path.with_suffix('.png'))
|
|||
|
|
print(f"⚠️ 非macOS环境,已复制PNG: {icns_path.with_suffix('.png')}")
|
|||
|
|
|
|||
|
|
# 清理临时目录
|
|||
|
|
shutil.rmtree(iconset_dir, ignore_errors=True)
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"❌ 创建 ICNS 失败: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def prepare_icons():
|
|||
|
|
"""准备各平台图标"""
|
|||
|
|
create_icon_dir()
|
|||
|
|
|
|||
|
|
ico_path = ICON_DIR / "app.ico"
|
|||
|
|
icns_path = ICON_DIR / "app.icns"
|
|||
|
|
|
|||
|
|
if ICON_SOURCE.exists():
|
|||
|
|
convert_png_to_ico(ICON_SOURCE, ico_path)
|
|||
|
|
convert_png_to_icns(ICON_SOURCE, icns_path)
|
|||
|
|
else:
|
|||
|
|
print(f"⚠️ 图标源文件不存在: {ICON_SOURCE}")
|
|||
|
|
|
|||
|
|
return ico_path, icns_path
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_pyinstaller_args(target_platform: str, ico_path: Path, icns_path: Path):
|
|||
|
|
"""获取 PyInstaller 参数"""
|
|||
|
|
|
|||
|
|
# 基础参数
|
|||
|
|
args = [
|
|||
|
|
"pyinstaller",
|
|||
|
|
"--noconfirm",
|
|||
|
|
"--clean",
|
|||
|
|
"--name", f"{APP_NAME_EN}",
|
|||
|
|
"--windowed", # GUI应用,不显示控制台
|
|||
|
|
"--onedir", # 打包为目录(更稳定)
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# 添加数据文件
|
|||
|
|
for src, dst in DATA_FILES:
|
|||
|
|
src_path = PROJECT_ROOT / src
|
|||
|
|
if src_path.exists():
|
|||
|
|
args.extend(["--add-data", f"{src_path}{os.pathsep}{dst}"])
|
|||
|
|
|
|||
|
|
# 隐藏导入
|
|||
|
|
hidden_imports = [
|
|||
|
|
"PySide6.QtSvg",
|
|||
|
|
"PySide6.QtSvgWidgets",
|
|||
|
|
"PIL",
|
|||
|
|
"PIL.Image",
|
|||
|
|
"fitz",
|
|||
|
|
"pandas",
|
|||
|
|
"openpyxl",
|
|||
|
|
"matplotlib",
|
|||
|
|
"matplotlib.backends.backend_qtagg",
|
|||
|
|
]
|
|||
|
|
for hi in hidden_imports:
|
|||
|
|
args.extend(["--hidden-import", hi])
|
|||
|
|
|
|||
|
|
# 平台特定参数
|
|||
|
|
if target_platform.startswith("win"):
|
|||
|
|
if ico_path.exists():
|
|||
|
|
args.extend(["--icon", str(ico_path)])
|
|||
|
|
# Windows 版本信息
|
|||
|
|
args.extend([
|
|||
|
|
"--version-file", str(PROJECT_ROOT / "version_info.txt"),
|
|||
|
|
])
|
|||
|
|
elif target_platform.startswith("mac"):
|
|||
|
|
if icns_path.exists():
|
|||
|
|
args.extend(["--icon", str(icns_path)])
|
|||
|
|
# macOS bundle 标识符
|
|||
|
|
args.extend([
|
|||
|
|
"--osx-bundle-identifier", "com.naiyuanma.cheesetools",
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
# 输出目录
|
|||
|
|
output_dir = DIST_DIR / target_platform
|
|||
|
|
args.extend(["--distpath", str(output_dir)])
|
|||
|
|
args.extend(["--workpath", str(BUILD_DIR / target_platform)])
|
|||
|
|
args.extend(["--specpath", str(BUILD_DIR)])
|
|||
|
|
|
|||
|
|
# 主入口文件
|
|||
|
|
args.append(str(PROJECT_ROOT / "main.py"))
|
|||
|
|
|
|||
|
|
return args
|
|||
|
|
|
|||
|
|
|
|||
|
|
def create_version_info():
|
|||
|
|
"""创建 Windows 版本信息文件"""
|
|||
|
|
version_parts = VERSION.split(".")
|
|||
|
|
while len(version_parts) < 4:
|
|||
|
|
version_parts.append("0")
|
|||
|
|
|
|||
|
|
version_tuple = ", ".join(version_parts)
|
|||
|
|
|
|||
|
|
content = f'''# UTF-8
|
|||
|
|
VSVersionInfo(
|
|||
|
|
ffi=FixedFileInfo(
|
|||
|
|
filevers=({version_tuple}),
|
|||
|
|
prodvers=({version_tuple}),
|
|||
|
|
mask=0x3f,
|
|||
|
|
flags=0x0,
|
|||
|
|
OS=0x40004,
|
|||
|
|
fileType=0x1,
|
|||
|
|
subtype=0x0,
|
|||
|
|
date=(0, 0)
|
|||
|
|
),
|
|||
|
|
kids=[
|
|||
|
|
StringFileInfo(
|
|||
|
|
[
|
|||
|
|
StringTable(
|
|||
|
|
u'080404b0',
|
|||
|
|
[
|
|||
|
|
StringStruct(u'CompanyName', u'{AUTHOR}'),
|
|||
|
|
StringStruct(u'FileDescription', u'{DESCRIPTION}'),
|
|||
|
|
StringStruct(u'FileVersion', u'{VERSION}'),
|
|||
|
|
StringStruct(u'InternalName', u'{APP_NAME_EN}'),
|
|||
|
|
StringStruct(u'LegalCopyright', u'Copyright (C) 2024 {AUTHOR}'),
|
|||
|
|
StringStruct(u'OriginalFilename', u'{APP_NAME_EN}.exe'),
|
|||
|
|
StringStruct(u'ProductName', u'{APP_NAME}'),
|
|||
|
|
StringStruct(u'ProductVersion', u'{VERSION}'),
|
|||
|
|
]
|
|||
|
|
)
|
|||
|
|
]
|
|||
|
|
),
|
|||
|
|
VarFileInfo([VarStruct(u'Translation', [2052, 1200])])
|
|||
|
|
]
|
|||
|
|
)
|
|||
|
|
'''
|
|||
|
|
|
|||
|
|
version_file = PROJECT_ROOT / "version_info.txt"
|
|||
|
|
with open(version_file, "w", encoding="utf-8") as f:
|
|||
|
|
f.write(content)
|
|||
|
|
print(f"✅ 已创建版本信息文件: {version_file}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_for_platform(target_platform: str):
|
|||
|
|
"""为指定平台打包"""
|
|||
|
|
print(f"\n{'='*50}")
|
|||
|
|
print(f"🔨 开始打包: {target_platform}")
|
|||
|
|
print(f"{'='*50}")
|
|||
|
|
|
|||
|
|
# 准备图标
|
|||
|
|
ico_path, icns_path = prepare_icons()
|
|||
|
|
|
|||
|
|
# 创建版本信息(Windows)
|
|||
|
|
if target_platform.startswith("win"):
|
|||
|
|
create_version_info()
|
|||
|
|
|
|||
|
|
# 获取 PyInstaller 参数
|
|||
|
|
args = get_pyinstaller_args(target_platform, ico_path, icns_path)
|
|||
|
|
|
|||
|
|
print(f"📦 执行命令: {' '.join(args)}")
|
|||
|
|
|
|||
|
|
# 执行打包
|
|||
|
|
try:
|
|||
|
|
result = subprocess.run(args, check=True)
|
|||
|
|
print(f"\n✅ {target_platform} 打包成功!")
|
|||
|
|
print(f"📁 输出目录: {DIST_DIR / target_platform}")
|
|||
|
|
return True
|
|||
|
|
except subprocess.CalledProcessError as e:
|
|||
|
|
print(f"\n❌ {target_platform} 打包失败: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_current_platform():
|
|||
|
|
"""获取当前平台标识"""
|
|||
|
|
system = platform.system().lower()
|
|||
|
|
machine = platform.machine().lower()
|
|||
|
|
|
|||
|
|
if system == "windows":
|
|||
|
|
if machine in ["amd64", "x86_64"]:
|
|||
|
|
return "win64"
|
|||
|
|
else:
|
|||
|
|
return "win32"
|
|||
|
|
elif system == "darwin":
|
|||
|
|
if machine == "arm64":
|
|||
|
|
return "mac_arm64"
|
|||
|
|
else:
|
|||
|
|
return "mac_x64"
|
|||
|
|
elif system == "linux":
|
|||
|
|
if machine in ["amd64", "x86_64"]:
|
|||
|
|
return "linux64"
|
|||
|
|
else:
|
|||
|
|
return "linux32"
|
|||
|
|
|
|||
|
|
return "unknown"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def clean_build():
|
|||
|
|
"""清理构建目录"""
|
|||
|
|
print("🧹 清理构建目录...")
|
|||
|
|
if BUILD_DIR.exists():
|
|||
|
|
shutil.rmtree(BUILD_DIR)
|
|||
|
|
if ICON_DIR.exists():
|
|||
|
|
shutil.rmtree(ICON_DIR)
|
|||
|
|
print("✅ 清理完成")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""主函数"""
|
|||
|
|
import argparse
|
|||
|
|
|
|||
|
|
parser = argparse.ArgumentParser(description="奶酪云工具箱打包脚本")
|
|||
|
|
parser.add_argument("--platform", "-p",
|
|||
|
|
choices=["win32", "win64", "mac_x64", "mac_arm64", "current"],
|
|||
|
|
default="current",
|
|||
|
|
help="目标平台")
|
|||
|
|
parser.add_argument("--all", "-a", action="store_true",
|
|||
|
|
help="打包所有平台(需要对应环境)")
|
|||
|
|
parser.add_argument("--clean", "-c", action="store_true",
|
|||
|
|
help="清理构建目录")
|
|||
|
|
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
print(f"""
|
|||
|
|
╔══════════════════════════════════════════════╗
|
|||
|
|
║ 奶酪云工具箱 - 打包工具 ║
|
|||
|
|
║ 版本: {VERSION} ║
|
|||
|
|
╚══════════════════════════════════════════════╝
|
|||
|
|
""")
|
|||
|
|
|
|||
|
|
if args.clean:
|
|||
|
|
clean_build()
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 检查 PyInstaller
|
|||
|
|
try:
|
|||
|
|
import PyInstaller
|
|||
|
|
print(f"✅ PyInstaller 版本: {PyInstaller.__version__}")
|
|||
|
|
except ImportError:
|
|||
|
|
print("❌ 未安装 PyInstaller,正在安装...")
|
|||
|
|
subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"])
|
|||
|
|
|
|||
|
|
# 确定要打包的平台
|
|||
|
|
if args.all:
|
|||
|
|
# 打包当前系统支持的所有架构
|
|||
|
|
system = platform.system().lower()
|
|||
|
|
if system == "windows":
|
|||
|
|
# Windows 可以同时打包 32位和64位(如果有对应Python)
|
|||
|
|
platforms = ["win64"] # 默认打包64位
|
|||
|
|
print(f"📋 Windows 系统将打包: {platforms}")
|
|||
|
|
print(f"⚠️ 32位版本需要在32位Python环境中单独打包")
|
|||
|
|
elif system == "darwin":
|
|||
|
|
# macOS
|
|||
|
|
machine = platform.machine().lower()
|
|||
|
|
if machine == "arm64":
|
|||
|
|
platforms = ["mac_arm64"]
|
|||
|
|
else:
|
|||
|
|
platforms = ["mac_x64"]
|
|||
|
|
print(f"📋 macOS 系统将打包: {platforms}")
|
|||
|
|
else:
|
|||
|
|
platforms = [get_current_platform()]
|
|||
|
|
print(f"📋 当前平台: {platforms}")
|
|||
|
|
elif args.platform == "current":
|
|||
|
|
platforms = [get_current_platform()]
|
|||
|
|
else:
|
|||
|
|
platforms = [args.platform]
|
|||
|
|
|
|||
|
|
# 创建输出目录
|
|||
|
|
DIST_DIR.mkdir(exist_ok=True)
|
|||
|
|
BUILD_DIR.mkdir(exist_ok=True)
|
|||
|
|
|
|||
|
|
# 打包
|
|||
|
|
success_count = 0
|
|||
|
|
for plat in platforms:
|
|||
|
|
if build_for_platform(plat):
|
|||
|
|
success_count += 1
|
|||
|
|
|
|||
|
|
print(f"\n{'='*50}")
|
|||
|
|
print(f"📊 打包完成: {success_count}/{len(platforms)} 成功")
|
|||
|
|
print(f"📁 输出目录: {DIST_DIR}")
|
|||
|
|
print(f"{'='*50}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|
|||
|
|
|