DataFlowing / collector /scripts /pack_wheels.py
stouchinglhh
Claude Code
collector: 更新采集工程至 v1.3.10(夹爪 RGB 帧空洞可见化 6/6 全部落地;普通版一键部署自带实时骨架解算与 D435)
330cf83
Raw History Blame Contribute Delete
10.5 kB
#!/usr/bin/env python3
"""生成 Windows 离线部署包(wheels/ 目录),供 start.bat 在无外网环境一键安装。
在任意**有网**的机器上运行(Linux 开发机即可,跨平台拉取 Windows 轮子),
把产出的 wheels/ 整个目录拷到客户项目根目录:
start.bat / start.sh 检测到 wheels\\*.whl 后自动改走纯离线安装。
跨平台解析用 uv(uv pip compile --python-platform windows,能正确按目标平台
评估环境标记,剔除 Linux-only 依赖如 bleak 的 dbus-fast);无 uv 时回退纯
pip download(此时建议直接在一台 Windows 机器上运行本脚本)。
用法:
python scripts/pack_wheels.py # 主程序必需依赖 + 手套工具包
python scripts/pack_wheels.py --lite # 仅极简采集版依赖(requirements-lite.txt,不带工具包)
python scripts/pack_wheels.py --extras # + mediapipe
python scripts/pack_wheels.py --torch # + CPU 版 torch(较大)
python scripts/pack_wheels.py --no-toolkit # 只打依赖轮子,不裁手套工具包
python scripts/pack_wheels.py --out /tmp/wheels # 自定义输出目录
产物:
<out>/*.whl Windows 3.12 依赖轮子(含依赖闭包)
<out>/python-3.12.10-amd64.exe Python 安装包(客户机器无 Python 时用)
<out>/toolkit/glove_toolkit.zip 手套工具包裁剪版(骨架解算用;见 pack_toolkit.py)
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
import urllib.request
from pathlib import Path
PY_VERSION = "3.12"
PY_INSTALLER = "python-3.12.10-amd64.exe"
PY_INSTALLER_URLS = [
f"https://mirrors.aliyun.com/python-release/windows/{PY_INSTALLER}",
f"https://registry.npmmirror.com/-/binary/python/3.12.10/{PY_INSTALLER}",
f"https://mirrors.huaweicloud.com/python/3.12.10/{PY_INSTALLER}",
]
ALIYUN = "https://mirrors.aliyun.com/pypi/simple/"
TUNA = "https://pypi.tuna.tsinghua.edu.cn/simple"
PYPI = "https://pypi.org/simple"
MIRRORS = [ALIYUN, TUNA, PYPI]
# pyrealsense2(D435/D405)已提升为 requirements.txt 的默认依赖,不再属于可选包
EXTRAS = ["mediapipe"]
TORCH_INDEXES = [
("https://mirrors.aliyun.com/pytorch-wheels/cpu/", ALIYUN),
("https://download.pytorch.org/whl/cpu", PYPI),
]
UV = shutil.which("uv")
def parse_requirements(path: Path) -> list[str]:
"""requirements.txt → 包规格列表(去注释、去空行)。"""
specs = []
for line in path.read_text(encoding="utf-8").splitlines():
line = line.split("#", 1)[0].strip()
if line:
specs.append(line)
return specs
def _run(cmd: list[str]) -> None:
subprocess.run(cmd, check=True)
def _pip_download(lock: Path, out: Path, mirror: str) -> None:
"""按锁定文件抓取 Windows 轮子(--no-deps:锁定已含全部闭包,无需再解析)。"""
_run([sys.executable, "-m", "pip", "download", "--no-deps",
"-r", str(lock), "-d", str(out),
"--platform", "win_amd64", "--python-version", PY_VERSION,
"--only-binary=:all:", "-i", mirror])
def download_closure(specs: list[str], out: Path,
index: str | None = None,
extra_index: str | None = None,
lock_name: str = "requirements-win.txt") -> Path:
"""uv compile(目标 Windows)→ pip download 抓轮子。无 uv 时直接 pip download。"""
lock = out / lock_name
if UV is None:
# 回退:在 Windows 机器上运行时 pip 会按本机平台评估标记,结果正确
req = out / ".pack_wheels_req.tmp"
req.write_text("\n".join(specs), encoding="utf-8")
_run([sys.executable, "-m", "pip", "download", "-d", str(out),
"-r", str(req), "--platform", "win_amd64",
"--python-version", PY_VERSION, "--only-binary=:all:",
"-i", index or MIRRORS[0]])
req.unlink()
return lock
cmd = [UV, "pip", "compile", "--python-platform", "windows",
"--python-version", PY_VERSION, "--output-file", str(lock)]
if index:
cmd += ["--index-url", index]
if extra_index:
cmd += ["--extra-index-url", extra_index]
req = out / ".pack_wheels_req.tmp"
req.write_text("\n".join(specs), encoding="utf-8")
cmd.append(str(req))
_run(cmd)
req.unlink()
# 按锁抓轮子(锁内索引头由 pip 原样生效;传同一镜像兜底非锁内包的查找)
_pip_download(lock, out, index or MIRRORS[0])
return lock
def download_python_installer(out: Path) -> None:
target = out / PY_INSTALLER
if target.exists() and target.stat().st_size > 10_000_000:
print(f" 已存在: {target.name},跳过下载")
return
last_err: Exception | None = None
for url in PY_INSTALLER_URLS:
try:
print(f" 下载 Python 安装包: {url}")
with urllib.request.urlopen(url, timeout=120) as r:
target.write_bytes(r.read())
print(f" 完成: {target.name} ({target.stat().st_size / 1e6:.1f} MB)")
return
except Exception as e: # noqa: BLE001 — 逐源回退
last_err = e
raise RuntimeError(f"Python 安装包下载失败: {last_err}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--lite", action="store_true",
help="仅打包极简采集版依赖(requirements-lite.txt)")
parser.add_argument("--extras", action="store_true",
help="额外打包 mediapipe")
parser.add_argument("--torch", action="store_true",
help="额外打包 CPU 版 torch(体积较大)")
parser.add_argument("--out", type=Path,
default=Path(__file__).resolve().parent.parent / "wheels",
help="输出目录(默认仓库根目录 wheels/)")
parser.add_argument("--no-python-installer", action="store_true",
help="跳过 Python 安装包下载")
parser.add_argument("--no-toolkit", action="store_true",
help="不裁手套工具包(骨架解算随包,仅 --lite 时才可省)")
args = parser.parse_args()
if args.lite and (args.extras or args.torch):
parser.error("--lite 与 --extras/--torch 互斥(极简版不装可选功能)")
out = args.out
out.mkdir(parents=True, exist_ok=True)
repo_root = Path(__file__).resolve().parent.parent
req_file = "requirements-lite.txt" if args.lite else "requirements.txt"
specs = parse_requirements(repo_root / req_file)
if not specs:
print(f"[错误] {req_file} 为空或不存在", file=sys.stderr)
return 1
if args.extras:
specs = specs + EXTRAS
print(f"[1/4] 解析 {len(specs)} 个包的 Windows {PY_VERSION} 依赖闭包 -> {out}/ ...")
if UV is None:
print(" [提示] 未检测到 uv,回退纯 pip 解析(建议在一台 Windows 机器上运行,"
"或 pip install uv 后重试)")
last_err: Exception | None = None
for mirror in MIRRORS:
try:
download_closure(specs, out, index=mirror)
break
except subprocess.CalledProcessError as e:
last_err = e
print(f" 源 {mirror} 失败,尝试下一个 ...")
else:
print(f"[错误] 依赖下载失败: {last_err}", file=sys.stderr)
if UV is None:
print(" 提示: 本机无 uv,且 pip 跨平台解析可能因 Linux-only 依赖失败;"
"请 pip install uv 后重试,或直接在一台 Windows 机器上运行本脚本。",
file=sys.stderr)
return 1
if args.torch:
print("[2/4] 解析 CPU 版 torch(Windows)...")
torch_done = False
for index, extra in TORCH_INDEXES:
try:
download_closure(["torch"], out, index=index, extra_index=extra,
lock_name="torch-win.txt")
torch_done = True
break
except subprocess.CalledProcessError as e:
last_err = e
print(f" 源 {index} 失败,尝试下一个 ...")
if not torch_done:
print(f"[错误] torch 下载失败: {last_err}", file=sys.stderr)
return 1
print("[2/4] torch 完成")
else:
print("[2/4] 跳过 torch(--torch 可打包 CPU 版)")
if not args.no_python_installer:
print("[3/4] 拉取 Python 安装包(客户机器无 Python 时 start.bat 会用)...")
try:
download_python_installer(out)
except RuntimeError as e:
print(f"[警告] {e};不影响 wheels 使用,手动放入同名文件即可",
file=sys.stderr)
else:
print("[3/4] 跳过 Python 安装包(--no-python-installer)")
# 手套工具包(骨架解算)——普通版随包,极简版不需要
if args.lite or args.no_toolkit:
print("[4/4] 跳过手套工具包"
+ ("(--lite 极简版不含骨架解算)" if args.lite else "(--no-toolkit)"))
else:
print("[4/4] 裁剪手套工具包(骨架解算随包)...")
rc = subprocess.run(
[sys.executable, str(repo_root / "scripts" / "pack_toolkit.py"),
"--out", str(out)]).returncode
if rc != 0:
# 不阻断 wheel 打包:工具包缺失只影响骨架,主程序照常
print("[警告] 工具包裁剪失败(骨架解算将不可用)。"
"单独重试: python scripts/pack_toolkit.py --out "
f"{out}", file=sys.stderr)
whls = sorted(out.glob("*.whl"))
total = sum(w.stat().st_size for w in whls) / 1e6
print()
print("=" * 60)
print(f"打包完成: {len(whls)} 个 wheel,共 {total:.0f} MB -> {out}/")
if not (args.lite or args.no_toolkit):
print(f" 手套工具包 -> {out}/toolkit/glove_toolkit.zip")
print("交付方式: 把整个 wheels/ 目录拷到客户项目根目录,")
print(" start.bat / start.sh 会自动改走纯离线安装,")
print(" 并把工具包展开到项目根(骨架解算开箱可用)。")
print("=" * 60)
return 0
if __name__ == "__main__":
sys.exit(main())