Files

230 lines
6.8 KiB
Python
Raw Permalink Normal View History

2026-05-30 12:51:19 +08:00
#!/usr/bin/env python3
"""
GEO Tool — 跨平台打包构建脚本
用法:
python build_scripts/build.py # 打包 Python 可执行文件
python build_scripts/build.py --clean # 先清理旧构建再打包
python build_scripts/build.py tauri # 打包 Python 可执行文件 + 构建 Tauri 桌面应用
输出:
dist_python/geo_tool_app (macOS / Linux)
dist_python/geo_tool_app.exe (Windows)
src-tauri/target/release/GEO工具 (Tauri 桌面应用)
依赖:
pip install pyinstaller
cargo tauri build (for tauri 命令)
"""
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
# ── 项目根目录 ────────────────────────────────────────────────
PROJECT_ROOT = Path(__file__).resolve().parent.parent
SPEC_FILE = PROJECT_ROOT / "geo_tool_app.spec"
DIST_DIR = PROJECT_ROOT / "dist_python"
BUILD_DIR = PROJECT_ROOT / "build_python"
TAURI_DIR = PROJECT_ROOT / "src-tauri"
def check_prerequisites():
"""检查必要工具是否已安装"""
# 检查 PyInstaller
try:
import PyInstaller # noqa: F401
except ImportError:
print("正在安装 PyInstaller ...")
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "pyinstaller"],
cwd=str(PROJECT_ROOT),
)
# 检查项目依赖是否已安装
try:
import streamlit # noqa: F401
except ImportError:
print("正在安装项目依赖 ...")
# 优先用 uv,否则 pip install pyproject.toml
for installer in ["uv", "pip"]:
try:
subprocess.check_call(
[installer, "sync"] if installer == "uv" else [installer, "install", "."],
cwd=str(PROJECT_ROOT),
)
break
except (subprocess.CalledProcessError, FileNotFoundError):
continue
def clean_build():
"""清理之前的构建产物"""
for d in [DIST_DIR, BUILD_DIR]:
if d.exists():
print(f" 清理 {d} ...")
shutil.rmtree(d)
# 清理 spec 生成的临时目录
temp_spec_dir = PROJECT_ROOT / "__pycache__"
if temp_spec_dir.exists():
shutil.rmtree(temp_spec_dir)
def build_tauri():
"""先打包 Python 可执行文件,然后构建 Tauri 桌面应用"""
build()
print("\n正在构建 Tauri 桌面应用 ...\n")
result = subprocess.run(
["cargo", "tauri", "build"],
cwd=str(TAURI_DIR),
)
if result.returncode != 0:
print(f"\n[错误] Tauri 构建失败 (返回码: {result.returncode})")
sys.exit(1)
print("\n" + "=" * 60)
print(" Tauri 构建完成!")
release_dir = TAURI_DIR / "target" / "release"
if sys.platform == "darwin":
bundles = list(release_dir.glob("*.dmg")) + list((release_dir / "bundle" / "dmg").glob("*.dmg")) + list((release_dir / "bundle" / "macos").glob("*.app"))
elif sys.platform == "win32":
bundles = list(release_dir.glob("*.msi")) + list((release_dir / "bundle" / "msi").glob("*.msi"))
else:
bundles = list(release_dir.glob("GEO工具")) + list(release_dir.glob("*.AppImage"))
for b in bundles:
size_mb = b.stat().st_size / (1024 * 1024) if b.is_file() else sum(f.stat().st_size for f in b.rglob("*")) / (1024 * 1024)
print(f" 输出: {b} ({size_mb:.1f} MB)")
print("=" * 60)
def build():
"""执行 PyInstaller 打包"""
# 确保 run_bundle.py 存在
entry_point = PROJECT_ROOT / "run_bundle.py"
if not entry_point.exists():
print(f"[错误] 未找到入口文件: {entry_point}")
sys.exit(1)
# 检查关键资源是否存在(给出警告而非中断)
resources = [
("geo_tool.py", "file"),
("config.json", "file"),
("geo_data.db", "file"),
(".streamlit", "dir"),
("modules", "dir"),
]
for path, _kind in resources:
p = PROJECT_ROOT / path
if not p.exists():
print(f"[警告] 资源不存在: {p}")
# 构建命令
cmd = [
sys.executable,
"-m",
"PyInstaller",
str(SPEC_FILE),
"--distpath",
str(DIST_DIR),
"--workpath",
str(BUILD_DIR),
"--clean",
"--noconfirm",
]
print("=" * 60)
print(" GEO Tool — 打包构建")
print("=" * 60)
print(f" 平台: {sys.platform}")
print(f" Python: {sys.version}")
print(f" 入口: {entry_point}")
print(f" Spec: {SPEC_FILE}")
print(f" 输出目录: {DIST_DIR}")
print("=" * 60)
# 执行打包
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT))
if result.returncode != 0:
print(f"\n[错误] 打包失败 (返回码: {result.returncode})")
print(f" 请检查 {BUILD_DIR}/ 下的日志文件获取详细错误信息。")
sys.exit(1)
# 输出结果
print("\n" + "=" * 60)
print(" 打包完成!")
print("=" * 60)
# 查找产物
if sys.platform == "win32":
output = DIST_DIR / "geo_tool_app.exe"
elif sys.platform == "darwin":
output = DIST_DIR / "geo_tool_app"
else:
output = DIST_DIR / "geo_tool_app"
if output.exists():
size_mb = output.stat().st_size / (1024 * 1024)
print(f" 输出文件: {output}")
print(f" 文件大小: {size_mb:.1f} MB")
else:
# macOS 下可能生成 .app
app_bundle = DIST_DIR / "geo_tool_app.app"
if app_bundle.exists():
size_mb = sum(f.stat().st_size for f in app_bundle.rglob("*")) / (1024 * 1024)
print(f" 输出文件: {app_bundle}")
print(f" 文件大小: {size_mb:.1f} MB")
print(f" 运行方式: 双击 {output.name} 或终端执行 {output}")
print("=" * 60)
def main():
parser = argparse.ArgumentParser(
description="GEO Tool 跨平台打包脚本",
)
parser.add_argument(
"mode",
nargs="?",
default="python",
choices=["python", "tauri"],
help="打包模式: python (默认) 或 tauri (Python + 桌面壳)",
)
parser.add_argument(
"--clean",
action="store_true",
help="构建前清理旧的 dist 和 build 目录",
)
parser.add_argument(
"--skip-deps",
action="store_true",
help="跳过依赖检查(仅安装 PyInstaller",
)
args = parser.parse_args()
# 切换到项目根目录
os.chdir(str(PROJECT_ROOT))
# 清理
if args.clean:
print("清理旧的构建产物 ...")
clean_build()
# 检查环境
if not args.skip_deps:
check_prerequisites()
# 打包
if args.mode == "tauri":
build_tauri()
else:
build()
if __name__ == "__main__":
main()