230 lines
6.8 KiB
Python
230 lines
6.8 KiB
Python
#!/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()
|