Spaces:
Running
Running
File size: 10,493 Bytes
d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 330cf83 d646dd7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | #!/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())
|