264 lines
8.8 KiB
Python
264 lines
8.8 KiB
Python
"""打出可分发的压缩包。
|
|
|
|
在仓库根目录运行:
|
|
|
|
python scripts/package.py
|
|
|
|
Windows 会编译界面和桌面程序,并拷贝本地已有的 Python 缓存、adb、ffmpeg 和 WebView2 离线安装包。
|
|
macOS、Linux 以后可以在对应函数里补上自己的运行时,其余步骤可以复用。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import gzip
|
|
import json
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import urllib.request
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DIST = ROOT / "dist"
|
|
CACHE = DIST / "cache"
|
|
STAGE_NAME = "RaptDrama-Download-Tools"
|
|
|
|
PYTHON_VERSION = "3.13.15"
|
|
PYTHON_EMBED_URL = (
|
|
f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip"
|
|
)
|
|
GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
|
|
FFMPEG_URL = "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.1.1/ffmpeg-win32-x64.gz"
|
|
WEBVIEW_INSTALLER = "MicrosoftEdgeWebView2RuntimeInstallerX64.exe"
|
|
|
|
|
|
def log(message: str) -> None:
|
|
print(message, flush=True)
|
|
|
|
|
|
def run(command: list[str], cwd: Path) -> None:
|
|
log("+ " + " ".join(command))
|
|
subprocess.run(command, cwd=cwd, check=True)
|
|
|
|
|
|
def download(url: str, dest: Path) -> None:
|
|
if dest.is_file() and dest.stat().st_size > 0:
|
|
log(f"已有缓存 {dest.name}")
|
|
return
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
temporary = dest.with_suffix(dest.suffix + ".part")
|
|
log(f"下载 {url}")
|
|
request = urllib.request.Request(url, headers={"User-Agent": "RaptDrama"})
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=120) as response, temporary.open("wb") as handle:
|
|
total = int(response.headers.get("Content-Length") or 0)
|
|
done = 0
|
|
while True:
|
|
chunk = response.read(1024 * 1024)
|
|
if not chunk:
|
|
break
|
|
handle.write(chunk)
|
|
done += len(chunk)
|
|
if total:
|
|
log(f" {dest.name} {done * 100 // total}%")
|
|
except Exception:
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
raise
|
|
temporary.replace(dest)
|
|
|
|
|
|
def copy_tree(source: Path, dest: Path, ignore: shutil.IgnorePattern) -> None:
|
|
if dest.exists():
|
|
shutil.rmtree(dest)
|
|
shutil.copytree(source, dest, ignore=ignore)
|
|
|
|
|
|
def build_ui() -> None:
|
|
npm = "npm.cmd" if sys.platform == "win32" else "npm"
|
|
run([npm, "run", "build"], ROOT / "ui")
|
|
if not (DIST / "ui" / "index.html").is_file():
|
|
raise SystemExit("界面没有编译到 dist/ui")
|
|
|
|
|
|
def build_desktop() -> Path:
|
|
run(["cargo", "build", "--release"], ROOT / "src-tauri")
|
|
meta = subprocess.check_output(
|
|
["cargo", "metadata", "--format-version", "1", "--no-deps"],
|
|
cwd=ROOT / "src-tauri",
|
|
text=True,
|
|
)
|
|
target = Path(json.loads(meta)["target_directory"]) / "release"
|
|
name = "raptdrama.exe" if sys.platform == "win32" else "raptdrama"
|
|
exe = target / name
|
|
if not exe.is_file():
|
|
raise SystemExit(f"没有找到桌面程序 {exe}")
|
|
return exe
|
|
|
|
|
|
def bundle_python(stage: Path) -> None:
|
|
if sys.platform != "win32":
|
|
raise SystemExit("当前只内置了 Windows 版 Python,其他平台的运行时还没接上")
|
|
archive = CACHE / f"python-{PYTHON_VERSION}-embed-amd64.zip"
|
|
download(PYTHON_EMBED_URL, archive)
|
|
target = stage / "runtime" / "python"
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
with zipfile.ZipFile(archive) as packed:
|
|
packed.extractall(target)
|
|
pth = next(target.glob("python*._pth"))
|
|
lines = []
|
|
saw_site = False
|
|
for line in pth.read_text(encoding="utf-8").splitlines():
|
|
if line.strip() == "#import site":
|
|
lines.append("import site")
|
|
saw_site = True
|
|
else:
|
|
lines.append(line)
|
|
if line.strip() == "import site":
|
|
saw_site = True
|
|
if not any(item.strip().replace("\\", "/") == "Lib/site-packages" for item in lines):
|
|
lines.append("Lib/site-packages")
|
|
if not saw_site:
|
|
lines.append("import site")
|
|
pth.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
(target / "Lib" / "site-packages").mkdir(parents=True, exist_ok=True)
|
|
get_pip = CACHE / "get-pip.py"
|
|
download(GET_PIP_URL, get_pip)
|
|
python = target / "python.exe"
|
|
run([str(python), str(get_pip)], target)
|
|
run([str(python), "-m", "pip", "install", "-r", str(ROOT / "server" / "requirements.txt")], target)
|
|
|
|
|
|
def bundle_adb(stage: Path) -> None:
|
|
if sys.platform != "win32":
|
|
raise SystemExit("当前只内置了 Windows 版 adb,其他平台的文件还没放进 vendor")
|
|
source = ROOT / "vendor" / "platform-tools"
|
|
dest = stage / "tools" / "platform-tools"
|
|
if dest.exists():
|
|
shutil.rmtree(dest)
|
|
required = ["adb.exe", "AdbWinApi.dll", "AdbWinUsbApi.dll"]
|
|
missing = [name for name in required if not (source / name).is_file()]
|
|
if missing:
|
|
raise SystemExit("vendor/platform-tools 里没有 " + "、".join(missing))
|
|
shutil.copytree(source, dest)
|
|
|
|
|
|
def bundle_ffmpeg(stage: Path) -> None:
|
|
if sys.platform != "win32":
|
|
raise SystemExit("当前只内置了 Windows 版 ffmpeg,其他平台的下载地址还没接上")
|
|
dest_dir = stage / "tools" / "ffmpeg"
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
dest = dest_dir / "ffmpeg.exe"
|
|
existing = ROOT / "tools" / "ffmpeg" / "ffmpeg.exe"
|
|
if existing.is_file():
|
|
shutil.copy2(existing, dest)
|
|
return
|
|
archive = CACHE / "ffmpeg-win32-x64.gz"
|
|
download(FFMPEG_URL, archive)
|
|
with gzip.open(archive, "rb") as packed:
|
|
dest.write_bytes(packed.read())
|
|
|
|
|
|
def bundle_webview(stage: Path) -> None:
|
|
if sys.platform != "win32":
|
|
return
|
|
source = ROOT / "vendor" / "webview2" / WEBVIEW_INSTALLER
|
|
if not source.is_file():
|
|
raise SystemExit(f"vendor/webview2 里没有 {WEBVIEW_INSTALLER}")
|
|
dest = stage / "vendor" / "webview2"
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source, dest / source.name)
|
|
|
|
|
|
def copy_server(stage: Path) -> None:
|
|
copy_tree(
|
|
ROOT / "server",
|
|
stage / "server",
|
|
shutil.ignore_patterns(".venv", "__pycache__", "*.pyc", "*.pyo"),
|
|
)
|
|
|
|
|
|
def hide_server_source(stage: Path) -> None:
|
|
server = stage / "server"
|
|
python = stage / "runtime" / "python" / "python.exe"
|
|
if not python.is_file():
|
|
raise SystemExit("还没有内置 Python,不能编译服务端")
|
|
run(
|
|
[
|
|
str(python),
|
|
"-m",
|
|
"compileall",
|
|
"-b",
|
|
"-q",
|
|
"-s",
|
|
str(server),
|
|
"-p",
|
|
"server",
|
|
str(server),
|
|
],
|
|
server,
|
|
)
|
|
for path in server.rglob("*.py"):
|
|
path.unlink()
|
|
for path in list(server.rglob("__pycache__")):
|
|
if path.is_dir():
|
|
shutil.rmtree(path)
|
|
if any(server.rglob("*.py")):
|
|
raise SystemExit("服务端源码没有从分发包里去掉")
|
|
if not any(server.rglob("*.pyc")):
|
|
raise SystemExit("服务端没有编译出字节码")
|
|
|
|
|
|
def make_zip(stage: Path) -> Path:
|
|
suffix = "windows-x64" if sys.platform == "win32" else platform.system().lower()
|
|
archive = DIST / f"{STAGE_NAME}-{suffix}.zip"
|
|
temporary = archive.with_name(archive.stem + ".writing.zip")
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
log(f"压缩 {archive}")
|
|
with zipfile.ZipFile(temporary, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as packed:
|
|
for path in stage.rglob("*"):
|
|
if not path.is_file() or path.name.lower() == "readme.md":
|
|
continue
|
|
packed.write(path, Path(STAGE_NAME) / path.relative_to(stage))
|
|
try:
|
|
if archive.exists():
|
|
archive.unlink()
|
|
temporary.replace(archive)
|
|
except PermissionError:
|
|
held = archive.with_name(archive.stem + "-new.zip")
|
|
if held.exists():
|
|
held.unlink()
|
|
temporary.replace(held)
|
|
raise SystemExit(f"{archive.name} 正在被占用,新的压缩包写在 {held}")
|
|
return archive
|
|
|
|
|
|
def main() -> None:
|
|
if sys.platform != "win32":
|
|
raise SystemExit("目前只完成了 Windows 打包。脚本按平台拆开了,macOS 和 Linux 可以以后补运行时。")
|
|
CACHE.mkdir(parents=True, exist_ok=True)
|
|
build_ui()
|
|
exe = build_desktop()
|
|
stage = DIST / STAGE_NAME
|
|
if stage.exists():
|
|
shutil.rmtree(stage)
|
|
stage.mkdir(parents=True)
|
|
shutil.copy2(exe, stage / exe.name)
|
|
copy_server(stage)
|
|
ui_dest = stage / "dist" / "ui"
|
|
shutil.copytree(DIST / "ui", ui_dest)
|
|
bundle_python(stage)
|
|
hide_server_source(stage)
|
|
bundle_adb(stage)
|
|
bundle_ffmpeg(stage)
|
|
bundle_webview(stage)
|
|
archive = make_zip(stage)
|
|
log(f"完成 {archive} ({archive.stat().st_size / 1024 / 1024:.1f} MB)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|