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()
|