Files
nl-tools/build_app.py

392 lines
12 KiB
Python
Raw Permalink Normal View History

2025-12-11 09:47:00 +08:00
"""
奶酪云工具箱 - 打包脚本
支持 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()