Spaces:
Running
Running
File size: 24,366 Bytes
d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 42af1ef 2535d68 d646dd7 2535d68 330cf83 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 330cf83 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 0901daa d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 d646dd7 2535d68 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 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 | """极简采集版发布包收集脚本 —— 把 lite 运行时闭包收集到一个目录。
只收集 start_lite.bat 真正需要的最小集合(约 1.6 MB 代码)与夹爪原生
载荷(Linux 包,约 440 MB),不拷贝 venv/venv_lite/data/wheels
(--with-wheels 除外)、tools 下的 demo/其它测试、
stouch_glove_toolkit-*/ 等与 lite 无关的内容。
代码文件以仓库根为源;start_lite.bat/.sh 与 使用说明_lite.md 是分发壳。
2026-09-16 起三个分发壳全部住在**仓库根**(与 main_lite.py、
requirements-lite.txt 同级 —— 这几个齐了根目录本身就是可直接运行的 lite
部署目录,根上已有 venv_lite/),由 _dist_shell_source 优先从那里取,
复制进包目录。
--force 只重建代码部分(main_lite/requirements-lite/config/core/ui/tools),
venv_lite/、wheels/、data/、分发壳与 core/gripper/native(470MB 载荷)
原样保留 —— lite_package 可同时当"发布模板"和"本机测试部署目录"用
(在包内跑过 start_lite.sh 装的环境不会被冲掉)。
收集完成后自动做导入自检:在目标目录内 `import main_lite`,缺任何被闭包
引用的模块都会当场报错(与 start_lite.bat 的错误 E 同口径);Linux 包再
断言夹爪闭包(`core.gripper.bridge`)与 `gripper_resources_available()`。
两个目标平台收的**夹爪 Python 代码是同一份**(core/gripper/**、core/
gripper_codec.py、core/s80m_manager.py),差别只在平台绑定件:Windows 包
不收 core/gripper/native/(ELF 原生栈)与 core/gripper/sightac_sdk/(pyarmor
.so 与 libSonixCamera.so 也是 ELF),并硬断言包内零 ELF。缺这些时
gripper_resources_available() 为假 → 扫描不到夹爪(设备组框还在、里面是
空的,属预期降级)。于是
「Windows 包」≡「Linux 包去掉载荷」,后者在 Linux 开发机上可直接造出来验证。
闭包清单与 tools/tests/lite_import_guard_test.py 的 WHITELIST 一致,
外加 egodata_writer/d435_camera 实际 import 的 core.calibration。
用法:
python scripts/pack_lite.py 收集到 lite_package/(本机平台)
python scripts/pack_lite.py --target windows 收集 Windows 包(代码同源,不含平台绑定件)
python scripts/pack_lite.py --out D:/lite 指定目录
python scripts/pack_lite.py --with-wheels 连同 wheels/ 离线依赖包一起
python scripts/pack_lite.py --force 重建代码部分(保留环境与载荷)
"""
import argparse
import os
import shutil
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 分发壳的兜底目录(三个壳 2026-09-16 都搬到仓库根了,这里只作回落)
DIST_SOURCE = os.path.join(ROOT, "lite_package")
# 代码文件(仓库根为源,收集时拷入包目录)
CODE_FILES = [
"main_lite.py",
"requirements-lite.txt",
]
# 分发壳文件(仓库根优先,找不到再回落 lite_package/;均原样复制)
DIST_FILES = [
"start_lite.bat",
"start_lite.sh",
"使用说明_lite.md",
]
def _dist_shell_source(rel: str):
"""分发壳的源路径:仓库根优先,其次 lite_package/;都没有返回 None。
三个壳 2026-09-16 都搬到了仓库根(与 main_lite.py 同级,使根目录直接
可运行);lite_package/ 只作回落,服务老布局的目录。
"""
for base in (ROOT, DIST_SOURCE):
path = os.path.join(base, rel)
if os.path.isfile(path):
return path
return None
# 夹爪载荷:Linux 包整目录收集,Windows 包只收代码(见 _PLATFORM_BOUND)。
GRIPPER_REL = "core/gripper"
GRIPPER_PAYLOAD_REL = "core/gripper/native" # --force 时原样保留
# 与平台绑定的原生件:Windows 包一律不收。
# native/ —— 整套 ELF x86-64 原生栈(460MB)
# sightac_sdk/ —— pyarmor 运行时 .so 与 libSonixCamera.so 同样是 ELF
# 缺它们时 paths.gripper_resources_available() 为假 → 扫描不到夹爪(设备组
# 框照建、里面空着),
# 且 core/gripper/*.py 在 Windows 上因顶层 `import fcntl` 本就导入不了
# (双重保险)。**Python 代码两个包都收**:一份源码两个包,差异只在载荷,
# 于是「Windows 包」= 「Linux 包去掉载荷」,这种形态在 Linux 开发机上可以
# 直接造出来验证(mv 走 native/ 即可),不必真去 Windows 上撞。
_PLATFORM_BOUND_NAMES = ("native", "sightac_sdk")
# 载荷里可以剪掉的(≈33MB):备份二进制、静态库、缓存。
# ★ 绝不能粗剪:native/ORB-SLAM 必须留着 —— 桥接二进制的 RUNPATH 里有
# $ORIGIN/../../../ORB-SLAM/Thirdparty/{DBoW2,g2o}/lib(实测 readelf),
# 剪掉会让 libDBoW2/libg2o 在客户机上找不到;ORBvoc.txt(139MB)与
# FaysSense_VI_Kit_Release/ 同理全是运行时资产。
_PAYLOAD_SKIP_SUFFIX = (".pyc", ".a")
_PAYLOAD_SKIP_INFIX = (".pre_", ".bak_") # 现场备份:xxx.pre_rebuild_2026...
# 构建源,不是运行时资产:两个包都不收(和 camera_service_src 同理)。
# orb_slam_src/ 是 2026-09-17 从 online/ 搬进 core/ 的 ORB-SLAM 真源,只在开发机
# 上 build.sh 用;运行时一切路径都由 core/gripper/paths.py 指到 native/。
# ★ 必须排除而不是「反正没 ELF」:它下面有 build/、dist/fays_opencv48/bin/、
# ORB-SLAM/{lib,Thirdparty/*/lib}/ 这些树内构建产物(全是 ELF),收进 Windows
# 包会直接撞上下面的「零 ELF」硬断言 sys.exit(1)。
_PAYLOAD_SKIP_NAMES = ("__pycache__", "camera_service_src", "orb_slam_src")
def _gripper_ignore_factory(src_root: str, windows: bool,
keep_existing_native: bool):
"""core/gripper 整目录收集的排除表(见上方注释)。
windows: 连平台绑定件一起排(Windows 包)
keep_existing_native: 目标包已有 native/(--force 重打包)时整棵跳过 ——
copytree 遇到已存在的同名软链会直接抛 File exists(symlinks=True 下
的 os.symlink 不覆盖),实测第二次打包必炸。
"""
def _ignore(dir_path: str, names: list):
ignored = set()
for name in names:
if name in _PAYLOAD_SKIP_NAMES:
ignored.add(name)
elif name.endswith(_PAYLOAD_SKIP_SUFFIX) or any(
s in name for s in _PAYLOAD_SKIP_INFIX):
ignored.add(name)
if os.path.abspath(dir_path) == os.path.abspath(src_root):
if windows:
ignored.update(_PLATFORM_BOUND_NAMES)
if keep_existing_native and not windows:
ignored.add("native")
return ignored
return _ignore
# 整目录拷贝(core.glove_usb 是包,内部有 __init__.py 与互引模块)。
# core.gripper 的排除表按目标平台现算(见 _gripper_ignore_factory),
# 且必须 symlinks=True —— 载荷里有 76 个相对软链,展开会让体积 468MB→800MB。
WHOLE_DIRS = ["core/glove_usb", GRIPPER_REL]
# 单文件拷贝:目录 → 文件列表(只收 lite 闭包,主程序 UI/重依赖模块不带)
MODULE_FILES = {
"ui": ["__init__.py", "lite_window.py"],
"config": ["__init__.py", "settings.py", "i18n.py"],
"core": [
"__init__.py",
"pipeline.py", "egodata_writer.py", "encoder_probe.py",
"depth_codec.py", "d435_manager.py", "stereo_depth.py",
"d435_camera.py", "camera.py", "device_detector.py",
"ble_engine.py", "usb_glove_engine.py", "uploader.py",
"api_client.py", "database.py", "helpers.py",
# egodata_writer.py:41 / d435_camera.py:447 实际 import
"calibration.py",
# 夹爪闭包(2026-09-16 并入 lite):力矩阵编码的家(主程序
# ui.main_window 只是 re-export —— 契约不能存两份)
"gripper_codec.py",
# core/gripper/bridge.py:39 顶层 import 它的双目丢帧窗口常量
"s80m_manager.py",
# core/pipeline.py:19 顶层 import 的帧空洞看门狗(录制侧与
# tools/audit_frame_gaps.py 审计脚本共用同一份口径)
"frame_gap.py",
# core/pipeline.py / core/egodata_writer.py 顶层 import 的收尾
# 分段计时(v1.3.11 L0-1;漏了它 lite 一 import 就 ModuleNotFoundError)
"timing.py",
# main_lite.py 顶层 import 的启动环境守卫(用错解释器时给出启动
# 指引再退出;零第三方依赖,必须在包里)
"startup_guard.py",
],
}
# 自检测试(客户机可选跑:无硬件验证导入隔离与采集链)
SELFTEST_FILES = [
"tools/tests/lite_import_guard_test.py",
"tools/tests/lite_smoke_test.py",
"tools/tests/lite_gripper_smoke_test.py",
]
# 随包工具:只有 Linux 包带 —— 它要驱动 native/ 里的厂商二进制,
# 在 Windows 上无处落脚。夹爪的「重新读取出厂标定」在 lite 里**不做 UI**,
# 命令行就是唯一入口,所以这个工具必须在包里,否则那条路在纯 lite 机器上
# 走不通(它顶层只 import stdlib + core.gripper.*,venv_lite 的依赖足够)。
TOOL_FILES_LINUX = [
"tools/import_gripper_calibration.py",
]
def _force_remove(path: str, keep: str) -> None:
"""删除 path;但 keep 那棵树(连同其本身)一个字节都不动。"""
path, keep = os.path.abspath(path), os.path.abspath(keep)
if path == keep:
return # 保留整棵树(夹爪载荷)
if keep.startswith(path + os.sep):
if os.path.isdir(path): # path 是 keep 的祖先:只删其它子项
for name in os.listdir(path):
_force_remove(os.path.join(path, name), keep)
return
if os.path.isdir(path):
shutil.rmtree(path, ignore_errors=True)
elif os.path.isfile(path):
os.remove(path)
def _tree_size(path: str) -> int:
"""目录的字节数(不跟随软链 —— 与分发后的实际落盘一致)。"""
total = 0
for dir_path, _, files in os.walk(path):
for name in files:
full = os.path.join(dir_path, name)
if not os.path.islink(full):
try:
total += os.path.getsize(full)
except OSError:
pass
return total
def _count_symlinks(path: str) -> int:
count = 0
for dir_path, _, files in os.walk(path):
for name in files:
if os.path.islink(os.path.join(dir_path, name)):
count += 1
return count
def _count_py(path: str) -> int:
"""**.py 个数(不跟随软链、不计 __pycache__)—— Windows 包「同一份源码」
的量化证据:与 Linux 包只差 sightac_sdk/ 里那 7 个包壳文件。"""
count = 0
for dir_path, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if d != "__pycache__"]
count += sum(1 for n in files
if n.endswith(".py")
and not os.path.islink(os.path.join(dir_path, n)))
return count
def _find_elf(root: str) -> list:
"""root 下所有 ELF 文件(前 4 字节 \\x7fELF)的相对路径。
Windows 包的硬不变量就是「一个 ELF 都没有」—— native/ 与 sightac_sdk/
里的 .so 都是 Linux 二进制,带上去了在客户机上只会以各种奇怪方式失败。
"""
found = []
for dir_path, _, files in os.walk(root):
for name in files:
full = os.path.join(dir_path, name)
if os.path.islink(full):
continue
try:
with open(full, "rb") as fh:
if fh.read(4) == b"\x7fELF":
found.append(os.path.relpath(full, root))
except OSError:
continue
return sorted(found)
def _load_paths_module():
"""独立加载 core/gripper/paths.py —— 故意**不经** core.gripper/__init__。
两个原因:① Windows 上 `import core.gripper` 会因顶层 `import fcntl`
直接失败,而打 Windows 包时本脚本仍要用它核对资源清单;
② __init__ 有导入期猴补丁,不必让打包进程沾上。
paths.py 顶层只 import os,独立加载是安全的。
"""
import importlib.util
path = os.path.join(ROOT, "core", "gripper", "paths.py")
spec = importlib.util.spec_from_file_location("_pack_lite_paths", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _assert_shell_matches_paths() -> None:
"""start_lite.sh 的 [错误 B] 资源清单必须与 paths.required_resources() 一致。
部署脚本那时还没装 venv、不能 import,只能把 7 条路径抄成 shell 字面量;
这个断言把「改了 paths.py 却忘了改脚本」(→ 客户机上缺资源却照常启动、
夹爪静默不可见)变成**打包当场失败**。
"""
shell_path = _dist_shell_source("start_lite.sh")
if shell_path is None:
print("[警告] 找不到 start_lite.sh,跳过 [错误 B] 清单核对")
return
text = open(shell_path, encoding="utf-8").read()
# 脚本里路径写的是变量($GRIPPER_NATIVE 等,为的是认 KSQ_* 环境变量);
# 未设环境变量时它们就取默认值,先把默认值代回去再比字面量
normalized = (text
.replace("$GRIPPER_NATIVE", "core/gripper/native")
.replace("$GRIPPER_SIGHTAC", "core/gripper/sightac_sdk"))
required = _load_paths_module().required_resources()
missing = [os.path.relpath(p, ROOT).replace(os.sep, "/")
for p, _ in required.values()
if os.path.relpath(p, ROOT).replace(os.sep, "/") not in normalized]
n_checks = text.count(" check_res ")
if missing or n_checks != len(required):
print(f"[失败] {os.path.basename(shell_path)} 的 [错误 B] 清单与 "
f"core/gripper/paths.py:required_resources() 不一致:")
for rel in missing:
print(f" 脚本里没有: {rel}")
print(f" check_res 条数 {n_checks} ≠ 资源项数 {len(required)}")
sys.exit(1)
print(f"[核对] start_lite.sh 的 [错误 B] 清单与 paths.py 一致({n_checks} 项)")
def _verify_payload(out_dir: str, target: str) -> None:
"""载荷完整性硬断言。
Linux 包:软链没被展开(R3)+ 7 项必需资源逐项就位。
Windows 包:代码照收、平台绑定件一个不留(含「零 ELF」这条硬不变量)——
于是「Windows 包」在语义上就是「Linux 包去掉载荷」,而这种形态在
Linux 开发机上可以直接造出来验证(mv 走 native/ 即可),不必真去
Windows 上撞。
"""
_assert_shell_matches_paths()
gr = os.path.join(out_dir, GRIPPER_REL)
if target != "linux":
for name in _PLATFORM_BOUND_NAMES:
if os.path.exists(os.path.join(gr, name)):
print(f"[失败] Windows 包不该含 {GRIPPER_REL}/{name}/")
sys.exit(1)
elf = _find_elf(gr)
if elf:
print(f"[失败] Windows 包含 ELF 文件 {len(elf)} 个:{elf[:5]}")
sys.exit(1)
absent = [f"core/gripper/{f}"
for f in ("__init__.py", "bridge.py", "paths.py",
"fays_runtime.py", "fays_single.py")
if not os.path.isfile(os.path.join(gr, f))]
if not os.path.isfile(os.path.join(out_dir, "core",
"gripper_codec.py")):
absent.append("core/gripper_codec.py")
for name in ("s80m_manager.py", "frame_gap.py"):
if not os.path.isfile(os.path.join(out_dir, "core", name)):
absent.append(f"core/{name}")
if absent:
print(f"[失败] Windows 包缺夹爪代码(两个包都收 .py): {absent}")
sys.exit(1)
print(f"[载荷] Windows 包:夹爪代码 {_count_py(gr)} 个 .py、零 ELF、"
f"无 {' / '.join(_PLATFORM_BOUND_NAMES)} —— "
f"那边的夹爪组框在、里面空着,属预期降级")
return
src = os.path.join(ROOT, GRIPPER_PAYLOAD_REL)
dst = os.path.join(out_dir, GRIPPER_PAYLOAD_REL)
if not os.path.isdir(dst):
print(f"[失败] Linux 包缺夹爪载荷 {GRIPPER_PAYLOAD_REL}/")
sys.exit(1)
n_src, n_dst = _count_symlinks(src), _count_symlinks(dst)
if n_src != n_dst:
print(f"[失败] 载荷软链被展开:源 {n_src} 个 → 包内 {n_dst} 个"
f"(copytree 必须传 symlinks=True)")
sys.exit(1)
size_mb = _tree_size(dst) / 1e6
size_src_mb = _tree_size(src) / 1e6
if size_mb > size_src_mb + 20:
print(f"[失败] 载荷体积异常:源 {size_src_mb:.0f}MB → 包内 {size_mb:.0f}MB")
sys.exit(1)
env = dict(os.environ, PYTHONPATH=out_dir, QT_QPA_PLATFORM="offscreen")
probe = ("import core.gripper.bridge, core.s80m_manager\n"
"from core.gripper import paths\n"
"missing = [k for k, (p, x) in paths.required_resources().items()\n"
" if not os.path.exists(p)\n"
" or (x and not os.access(p, os.X_OK))]\n"
"assert not missing, missing\n"
"assert paths.gripper_resources_available()\n"
"print('OK')")
proc = subprocess.run(
[sys.executable, "-c", "import os\n" + probe], cwd=out_dir, env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc.returncode != 0:
print("[失败] 包内夹爪资源自检未通过:")
print(proc.stderr.decode("utf-8", "replace"))
sys.exit(1)
print(f"[载荷] {GRIPPER_PAYLOAD_REL}/ {size_mb:.0f}MB、"
f"软链 {n_dst} 个原样保留、7 项必需资源全部可执行")
def collect(out_dir: str, with_wheels: bool, force: bool,
target: str) -> None:
out_dir = os.path.abspath(out_dir)
if force:
# 只重建代码路径;venv_lite/wheels/data 与分发壳原样保留;
# core/gripper/native(470MB)也不动 —— 它不由本脚本生成,
# 白删白拷一轮纯浪费(Windows 包目录里本来就没有它)
keep = os.path.join(out_dir, GRIPPER_PAYLOAD_REL)
for rel in (CODE_FILES + ["config", "core", "ui", "tools"]):
_force_remove(os.path.join(out_dir, rel), keep)
elif os.path.isfile(os.path.join(out_dir, "main_lite.py")):
print(f"[跳过] 目标目录已存在: {out_dir}")
print(" --force 只重建代码部分,venv_lite/wheels/data 与载荷会保留")
sys.exit(0)
copied = []
for rel in CODE_FILES + SELFTEST_FILES + (
TOOL_FILES_LINUX if target == "linux" else []):
src = os.path.join(ROOT, rel)
if not os.path.isfile(src):
print(f"[警告] 缺文件: {rel}")
continue
dst = os.path.join(out_dir, rel)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
copied.append(rel)
for rel in DIST_FILES:
src = _dist_shell_source(rel)
if src is None:
print(f"[警告] 缺分发文件: {rel}(应在仓库根或 lite_package/ 下)")
continue
dst = os.path.join(out_dir, rel)
if os.path.abspath(src) == os.path.abspath(dst):
copied.append(rel) # 源即目标(在 lite_package/ 内收集)
continue
shutil.copy2(src, dst)
copied.append(rel)
for rel in WHOLE_DIRS:
src = os.path.join(ROOT, rel)
dst = os.path.join(out_dir, rel)
if rel == GRIPPER_REL:
ignore = _gripper_ignore_factory(
src, windows=(target != "linux"),
# 目标包已有载荷(--force 重打包):native/ 整棵跳过,
# 只刷新 Python 代码 —— 也避开已存在软链的 File exists
keep_existing_native=os.path.isdir(
os.path.join(dst, "native")))
else:
ignore = shutil.ignore_patterns("__pycache__")
# symlinks=True 是硬要求(载荷里 76 个相对软链,展开 +330MB)
shutil.copytree(src, dst, symlinks=True, ignore=ignore,
dirs_exist_ok=True)
copied.append(rel + "/")
for sub, files in MODULE_FILES.items():
for name in files:
rel = os.path.join(sub, name)
src = os.path.join(ROOT, rel)
if not os.path.isfile(src):
print(f"[警告] 缺文件: {rel}")
continue
dst = os.path.join(out_dir, rel)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
copied.append(rel)
if with_wheels:
src = os.path.join(ROOT, "wheels")
if os.path.isdir(src):
shutil.copytree(src, os.path.join(out_dir, "wheels"))
copied.append("wheels/")
else:
print("[提示] --with-wheels 但仓库没有 wheels/ 目录,跳过")
# 自检:在目标目录内 import main_lite(与 start_lite.bat 错误 E 同口径)
env = dict(os.environ, PYTHONPATH=out_dir,
QT_QPA_PLATFORM=os.environ.get("QT_QPA_PLATFORM", "offscreen"))
proc = subprocess.run(
[sys.executable, "-c", "import main_lite"],
cwd=out_dir, env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc.returncode != 0:
print("[失败] 目标目录内 import main_lite 自检未通过:")
# 入口守卫(core/startup_guard.py)是往 **stdout** 打印指引再退出的,
# 只看 stderr 会把「缺哪个依赖」整个吞掉,只留一句无信息的失败。
# 自检必须用装了依赖的解释器跑(venv/bin/python),否则必然失败。
print(proc.stderr.decode("utf-8", "replace")
or proc.stdout.decode("utf-8", "replace")
or f"(无输出,退出码 {proc.returncode})")
sys.exit(1)
_verify_payload(out_dir, target)
total = sum(os.path.getsize(os.path.join(out_dir, r))
for r in copied if os.path.isfile(os.path.join(out_dir, r)))
if target == "linux":
payload = _tree_size(os.path.join(out_dir, GRIPPER_REL)) / 1e6
hint = (f" 客户机落盘 = 本目录 + 夹爪载荷 {payload:.0f}MB"
f" + venv_lite(自动安装, ~750MB) ≈ {payload / 1000 + 0.75:.2f}GB"
+ (" + wheels/" if with_wheels else ""))
else:
hint = (" 客户机落盘 = 本目录 + venv_lite(自动安装, ~750MB) ≈ 0.75GB"
"(Windows 包不含夹爪载荷 native/ 与 sightac_sdk/,"
"那边的夹爪组框在、里面空着)"
+ (" + wheels/" if with_wheels else ""))
print(f"[完成] 收集 {len(copied)} 个文件到 {out_dir}/,"
f"代码共 {total / 1024:.0f} KB,import main_lite 自检通过({target})")
print(hint)
def main() -> None:
parser = argparse.ArgumentParser(description="收集极简采集版发布包")
parser.add_argument("--out", default=os.path.join(ROOT, "lite_package"),
help="输出目录(默认仓库根 lite_package/)")
parser.add_argument("--target", default=None,
choices=("linux", "windows"),
help="目标平台(默认本机)。windows 包不含平台绑定件"
"(native/sightac_sdk),夹爪代码两个包同源")
parser.add_argument("--with-wheels", action="store_true",
help="连同 wheels/ 离线依赖包一起收集")
parser.add_argument("--force", action="store_true",
help="重建代码部分(保留 venv_lite/wheels/data/载荷)")
args = parser.parse_args()
target = args.target or ("windows" if os.name == "nt" else "linux")
collect(os.path.abspath(args.out), args.with_wheels, args.force, target)
if __name__ == "__main__":
main()
|