Spaces:
Running
Running
Download collector/scripts/pack_lite.py from Stouching-Robotics/DataFlowing: direct link, hf CLI and curl.
- Browser
- Download file 24.4 kB
-
https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/collector/scripts/pack_lite.py
- Command line
-
hf download hf://spaces/Stouching-Robotics/DataFlowing/collector/scripts/pack_lite.py
-
curl -L -o pack_lite.py https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/collector/scripts/pack_lite.py
24.4 kB
| """极简采集版发布包收集脚本 —— 把 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() | |