#!/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()