Add shared PetaLinux eMMC and HJX9009 TF production skill
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: petalinux-emmc
|
||||
description: Build, reuse, deploy, recover, and hardware-validate a PetaLinux production system whose kernel and ext4 rootfs reside on eMMC or removable SD/TF while Zynq boots FSBL and U-Boot from QSPI. Use for PR26-style two-stage production deployment; HJX9009 uses TF instead of nonexistent eMMC.
|
||||
---
|
||||
|
||||
# PetaLinux eMMC/TF 正式系统
|
||||
|
||||
目标是从已经可恢复的 QSPI 过渡 Linux 部署正式 FIT 内核与持久 ext4 根文件系统,完成真实断电启动和读写验收。名称中的 `emmc` 指 PR26 的第二阶段;**介质和控制器由板卡证据决定**,不能从 `/dev/mmcblk0` 推断是板载 eMMC。PL bit→bin 和应用程序发布属于另外的 Petalinux-pl/ps 任务,不得冒称完成。
|
||||
|
||||
## 板卡和输入
|
||||
|
||||
若用户尚未指定板卡,先让用户选择:`1. HJX9009 2. emv9.3 3. ipots 4. others`。HJX9009 必须阅读 [references/hjx9009-tf.md](references/hjx9009-tf.md);其 J1 TF 接 PS SD0、没有 eMMC。其余板卡目前没有通过本 skill 验证的正式系统 profile,阅读 [references/board-intake.md](references/board-intake.md),取得原理图/HDF 或等效硬件配置并查实存储、启动、恢复和电源细节后才可烧写;不能套用 HJX9009 的 MIO、分区或桥接。
|
||||
|
||||
阅读项目 AGENTS/CLAUDE adapter、全局/项目 bmem 和 PR26/Result 原文。将用户交付的当前 Vivado `.hdf`(2018.1)或相应版本 `.xsa`、bit、PS7 初始化、板卡修订、PetaLinux/Vivado/SDK 版本、可恢复镜像与串口/JTAG路径绑定。旧参考工程只择项审阅,不整树覆盖;历史同名 HDF、旧 IP 和密码不是当前输入。现场网络地址、TFTP 绑定和介质容量每次实测,不复制笔记。
|
||||
|
||||
## QSPI 前置门禁
|
||||
|
||||
首次安装前**实际证明**目标板已经从 QSPI 冷启动进入可恢复的过渡 Linux:记录 BootROM/U-Boot/QSPI FIT 来源、initramfs、MTD 布局、网络/TFTP、分区和校验工具、已知良好 QSPI 镜像及恢复链。若板上已是正式 TF/eMMC 系统,先证明 QSPI 救援 FIT 仍在且可通过受控回退路径启动,不能要求默认 `bootcmd` 重回 QSPI-only,也不能为了检查而无条件重写现有生产 bootenv。不能以 delivery 文件存在或上次成功代替当前门禁。若过渡/救援系统尚未部署或与当前 HDF/板卡不匹配,调用共享 `petalinux-qspi` skill 完成其构建/部署/验收,然后返回本流程;保留现有生产系统的可恢复配置,该调用不免除板卡输入和写前边界检查。若过渡系统可启动但目标介质尚未正常枚举,先修复/验证检测链,不能通过格式化解决未枚举问题。
|
||||
|
||||
## 构建和复用
|
||||
|
||||
采用独立正式工程。保持 `boot media = QSPI`、`BOOT.BIN = 同一硬件交付的 FSBL + U-Boot`,正式 `image.ub` 在经验证的 eMMC/TF BOOT 分区,根文件系统在第二分区 ext4;实际 FIT/启动路径必须与板卡检测条件相容。需要早期桥接 initramfs、软件 CD 旁路或根 UUID 时,把源文件、DT、U-Boot overlay、rootfs 配置和启动参数都算作构建输入,不得仅改环境后仍声称镜像相同。`BOOT.BIN` 不自动包含 PL bit 或 FIT。QSPI kernel 分区中的过渡 `image.ub.qspi` 保持可恢复。
|
||||
|
||||
先为当前输入生成 `build_inputs.json`,可用 sibling `petalinux-qspi/scripts/qspi_cache.py capture` 的通用指纹捕获接口;HJX9009 使用稳定的 [references/hjx9009-profile.json](references/hjx9009-profile.json) 作 profile 文件。指纹包含 HDF/bit、所有 config/recipe/DT/U-Boot/桥接源、工具版本、BOOT 组成与根文件系统方案;部署时变化的 IP/密码不进指纹。再用 [scripts/emmc_cache.py](scripts/emmc_cache.py) 检查当前指纹与交付的 `BOOT.BIN`、`image.ub`、`rootfs.tar.bz2`、`build_inputs.json`、`summary.json`、`MANIFEST.md` 和 `SHA256SUMS`。构建门禁 G0=输入、G1=HDF 导入、G2=配置、G3=构建打包;仅在检查返回 `reusable=true`、G0–G3 均 PASS、产物与当前硬件输入/启动方案一致时跳过重建。文件存在、mtime、旧 bit 相同或仅有一次启动日志均不足;旧交付缺可核验指纹/清单时补齐证据或重建。复用构建**不能**跳过当前介质和板级部署验收。
|
||||
|
||||
示例:`python <skill>/scripts/emmc_cache.py check --current <new-build_inputs.json> --delivery <production-delivery>`。`summary.json` 中 `build_gates.G0..G3.status`(或 `gates.G0..G3.status`)均须为 `PASS`。若目标板当前已有同哈希 FIT、同根 UUID/内容与同 BOOT,先执行 E4 验收;达到所有门禁可报告“当前部署已是最新”,不必重复分区或烧写。
|
||||
|
||||
## 顺序门禁
|
||||
|
||||
1. **E0 前置/只读:** QSPI 门禁;当前 HDF/板号;U-Boot/SDHCI/MTD;介质 CID、容量、分区、挂载;首尾与拟用地址范围的重复只读校验。TF/eMMC 容量可见不证明高地址可写;出现不稳定范围时缩小到经验证范围或换介质,绝不盲用标称容量。计算唯一分区计划、UUID、空间余量、恢复路径和本轮目标文件哈希。
|
||||
2. **E1 构建:** 从同一 HDF 生成/审阅 PS7、设备树和 FSBL;配置正式 kernel/SD 驱动、ext4 rootfs 归档与必要 initramfs;构建并解包审计。生成 FIT、rootfs 归档和仅含 FSBL+U-Boot 的 BOOT.BIN,记录 BIF、版本、SHA-256、启动命令与构建日志。不得直接运行旧 `emmc_auto_format.sh`/`auto_up_emmc.sh`:它们写死设备/IP、无严密失败门禁,可能清空分区和 QSPI bootenv。
|
||||
3. **E2 介质部署:** 用户的正式系统部署请求包含目标介质重建时,仍先核对 CID/容量/无其它介质、精确设备节点、挂载点与分区计划;写前停用自动挂载并可靠卸载。先把全部产物传输到安全暂存区、检查文件大小/哈希/可用空间,任何传输失败即停止,不允许落入格式化或 Flash 擦除。按已验证范围分区/格式化,回读分区表、fsck;写 FAT BOOT 的 `image.ub` 并读回哈希;将对应 `rootfs.tar.bz2` 解包到 ext4,检查 `/sbin/init`、权限、设备节点及关键文件,sync、卸载、fsck、只读回读。若最终归档与上卡内容有差异,明确记录,不得报告逐字节一致。
|
||||
4. **E3 QSPI 启动更新:** 仅在 E2 全通过、旧 QSPI 镜像与 JTAG 恢复可用时,更新必要的 FSBL/U-Boot 或持久 bootcmd。计算 boot/bootenv/kernel 的实际范围;不要清空 bootenv 或覆盖救援 FIT。QSPI 擦写后做**全长回读比较与 CRC/哈希**,环境回读确认;不能只看 `flashcp`/`sf write` 无报错。卡缺失或 FIT 失效时的 QSPI 救援分支必须单独冷启动实测。
|
||||
5. **E4 现场验收:** 至少两次完整断电冷启动,串口证明 `QSPI FSBL/U-Boot → 目标介质 FIT → 目标 ext4 /`;核对 FIT hash、CID、根 SOURCE/FSTYPE/UUID、实际网络、MTD、FPGA manager(仅存在不等于 PL 已部署)。做受控文件写入、sync、断电,再读回持久化;检查 ext4/mmc 错误和 fsck。再单独验证无卡/损坏 FIT 的 QSPI 救援路径。某项未完成记为 `PARTIAL`/`BLOCKED`,不称“全功能完成”。
|
||||
|
||||
所有 U-Boot/串口命令逐条核对退出状态、期望回显、实际 `$filesize` 和 CRC。不能让 TFTP 失败后旧 DDR 数据落入写入步骤。把自动状态/log 放在项目 `Notes/auto/`;manifest 记录门禁、证据路径、板卡/介质身份、输入与产物哈希、实际网络和遗留限制。按项目 bmem 协议导入用户 Notes、沉淀会话和可持久结论并同步;分别报告本地/远端/中心同步状态。
|
||||
@@ -0,0 +1,7 @@
|
||||
# 未验证板卡的正式存储系统输入
|
||||
|
||||
适用于 emv9.3、ipots、others。先取得板卡版本、原理图或等效板级配置,当前 HDF/XSA、PetaLinux/Vivado 版本,以及 QSPI 过渡系统的门禁结果。emv9.3 历史原理图指出有 eMMC,但仍须查实这块目标板的实际芯片/焊接和读写;ipots/others 无已验证 profile。未验证字段要标 `UNKNOWN`,不能从 HJX9009 借值。
|
||||
|
||||
必须确认:BootROM 启动源绑带/拨码及重置采样;QSPI 拓扑/容量/擦除尺寸/救援分区;正式存储芯片或卡座、电压/复位/供电、SD 控制器实例、MIO/总线宽度、CD/WP/电压切换;FSBL 初始化;U-Boot 先于 Linux 的介质读取能力;根分区标识;串口/JTAG/恢复途径;现网 TFTP 地址。拿到这些证据后写本板 profile,明确是否需要 CD 旁路、FIT 桥接或不同 BOOT 组成,再运行主门禁。
|
||||
|
||||
若目标不是 Zynq-7000 或启动链含 PMUFW/ATF/bit 等额外组件,不能沿用 HJX9009 的 `FSBL+U-Boot` 打包规则。若介质无法无损识别,不允许用 `/dev/mmcblk0` 名称或历史容量猜测目标并执行格式化。
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"board": "HJX9009",
|
||||
"soc": "XC7Z100FF900",
|
||||
"production_storage": "removable-microsd-j1",
|
||||
"storage_controller": "ps7-sd0",
|
||||
"storage_mio": {"clock": 40, "command": 41, "data": [42, 43, 44, 45], "detect": 13},
|
||||
"bootrom_media": "qspi-dual-cascade",
|
||||
"boot_composition": ["fsbl", "u-boot"],
|
||||
"production_kernel_location": "microsd-fat-boot",
|
||||
"production_rootfs": "microsd-ext4",
|
||||
"qspi_rescue_fit": true,
|
||||
"profile_revision": "pr26-tf-20260915"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# HJX9009:用 J1 TF 替代 PR26 的 eMMC
|
||||
|
||||
本 profile 依据项目 `Notes/PR26.md`、`Notes/Task-PR26-1.md`、`Notes/Result-PR26-1.md`、`Petalinux/delivery/pr26_tf_20260915/MANIFEST.md` 以及 `Notes/auto/pr26_qspi_20260915/tf_final_*` 实测记录。每次核对 PCB 版本与当前交付;下面的 CID、容量、IP、镜像字节数和 UUID 均为**历史证据,不是未来部署常量**。
|
||||
|
||||
## 硬件与启动
|
||||
|
||||
- XC7Z100FF900;BootROM 启动绑带 `MIO[5:4:3]=100` 选 QSPI。U5/U6 双级联 S25FL128S,历史 `sf probe` 为 `s25fl128s_64k`、32 MiB、128 KiB 擦除。先做现板探测再写任何偏移。
|
||||
- 板上无 eMMC。J1 microSD 经 U12 MAX13035E 接 SD0,MIO40=CLK、41=CMD、42..45=DAT0..3,MIO13=`SDIO_SDDET`;SD1 的 MIO46..53 已有以太网/UART/MDIO用途。DT 应启用 `sdhci0`、4 bit、≤50 MHz、`no-1-8-v`、`disable-wp`,禁用 `sdhci1`;不要声明 `non-removable`。
|
||||
- 历史 HDF/FSBL 把 `SD0_CD_SEL` 指向 EMIO56,与原理图不符。优先从新 HDF/FSBL修正,不能仅靠无效的 `cd-inverted` 属性。历史 U-Boot `sd0_cd_fix` 把 `0xF8000830` 写为 `0x000D0037`,但 MIO13 曾被复用为 QSPI1 IO3 输出,原生 Card Present 仍为 0。实际完成的正式系统先由 U-Boot 读取 TF FAT FIT,再在早期 Linux 桥接 initramfs 设置 SDHCI CDTEST、枚举 SD 卡、挂载 ext4 并切换 `/sbin/init`。**CDTEST 是这版硬件的受控兼容旁路,不证明卡检测硬件修复;若新版硬件/FSBL 可原生检测,优先删除旁路并做双冷启动验收。** 若需沿用旁路,桥接必须先验证 CID/容量/根分区 UUID,超时和身份不匹配时停止或转救援,不可静默挂错介质。
|
||||
- 历史正式 `bootcmd` 为:`run phyfix; if mmc dev 0 && fatload mmc 0:1 0x02000000 image.ub; then run sd0_cd_fix; bootm 0x02000000; else sf probe 0 0 0 && sf read 0x02000000 0x00520000 0x00ED8E98 && run sd0_cd_fix && bootm 0x02000000; fi`。此处 `0x00ED8E98` 是**旧救援 FIT 的实际字节数**,未来必须从当前镜像/布局重算并验证 DDR 安全范围;不得照抄。先证明 TF 分支,再证明无卡/坏 FIT 救援分支;2026-09-15 交付的后一分支未独立实测。
|
||||
|
||||
## 介质边界与验收的真实结果
|
||||
|
||||
- 历史卡 15 GiB,CID `05000c61736466672200000012018c00`。原 1 GiB FAT32 BOOT + 14 GiB ext4 在约 8 GiB 及以上访问出现重复读写/文件系统不一致;无法唯一归咎卡或板端。因此最后采用低地址 p1 LBA 2048..2099199(1 GiB FAT32),p2 LBA 2099200..14682111(6 GiB ext4),尾部故意空置。**不要把这个历史保守布局应用于新卡,也不要把旧卡扩回 14 GiB;新介质要独立做拟用范围的重复块测试、fsck 和回读。**
|
||||
- 历史 `pr26_tf_20260915` 已将新 FSBL/U-Boot 写 QSPI boot,558976 字节写后全长 `cmp.b`/CRC 通过;TF p1 放 `image.ub`,p2 为 ext4 正式根。三次断电启动均显示 FAT FIT 读取、FIT hash 校验、桥接挂载 `/dev/mmcblk0p2`、sysvinit/Dropbear/联网;第三次证实跨断电持久化文件回读。日志含非致命 BusyBox mount/switch_root 与 RTC 警告,不可描述为“零警告”。
|
||||
- `rootfs.tar.bz2` 最终归档晚于 p2 写入;历史对照有 23 个构建元数据/包数据库/版本或认证记录差异,因此不能把最终归档宣称为板上逐字节版本。新交付应让上卡内容与冻结归档同源并核对关键文件。
|
||||
- 旧 QSPI kernel 救援 FIT 保留在 `0x520000`;2026-09-15 的最终 BOOT 更新后,**无卡实物冷启动回退尚未独立验证**。后续完整 E4 必须补上该门禁。
|
||||
|
||||
网络历史实测为板 `192.168.1.50/24`、Windows TFTP `192.168.1.126`;Notion 的 `192.168.1.140`/`.66` 是例子。每次由 live interface、TFTP 文件及 SHA-256 重新确认。PL FPGA manager 注册不等于最新 bit 已转换/加载;不要混入此 skill 的正式系统完成状态。
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check whether a production PetaLinux build is reusable; never validates target media."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
REQUIRED = ("BOOT.BIN", "image.ub", "rootfs.tar.bz2", "build_inputs.json",
|
||||
"summary.json", "MANIFEST.md")
|
||||
|
||||
|
||||
def digest(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as stream:
|
||||
for part in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
h.update(part)
|
||||
return h.hexdigest().upper()
|
||||
|
||||
|
||||
def fingerprint(obj):
|
||||
data = dict(obj)
|
||||
data.pop("fingerprint_sha256", None)
|
||||
encoded = json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
return hashlib.sha256(encoded.encode("utf-8")).hexdigest().upper()
|
||||
|
||||
|
||||
def load_json(path):
|
||||
with open(path, "r", encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def checksum_entries(path):
|
||||
entries = {}
|
||||
with open(path, "r", encoding="utf-8") as stream:
|
||||
for lineno, raw in enumerate(stream, 1):
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(None, 1)
|
||||
if len(parts) != 2 or len(parts[0]) != 64 or any(c not in "0123456789abcdefABCDEF" for c in parts[0]):
|
||||
raise ValueError("invalid SHA256SUMS line {}".format(lineno))
|
||||
name = parts[1].lstrip("* ").replace("\\", "/")
|
||||
chunks = name.split("/")
|
||||
if not name or os.path.isabs(name) or ".." in chunks or name == "SHA256SUMS" or name in entries:
|
||||
raise ValueError("unsafe or duplicate SHA256SUMS entry: {}".format(name))
|
||||
entries[name] = parts[0].upper()
|
||||
return entries
|
||||
|
||||
|
||||
def check(current_path, delivery):
|
||||
root = os.path.abspath(delivery)
|
||||
errors = []
|
||||
for name in REQUIRED + ("SHA256SUMS",):
|
||||
if not os.path.isfile(os.path.join(root, name)):
|
||||
errors.append("missing artifact: " + name)
|
||||
if errors:
|
||||
return {"reusable": False, "delivery": root, "errors": errors}
|
||||
|
||||
try:
|
||||
current = load_json(current_path)
|
||||
stored = load_json(os.path.join(root, "build_inputs.json"))
|
||||
for label, obj in (("current", current), ("stored", stored)):
|
||||
if obj.get("schema_version") != 1 or fingerprint(obj) != obj.get("fingerprint_sha256"):
|
||||
errors.append(label + " fingerprint invalid")
|
||||
if current.get("fingerprint_sha256") != stored.get("fingerprint_sha256"):
|
||||
errors.append("build-defining inputs differ")
|
||||
except Exception as exc:
|
||||
errors.append("fingerprint check failed: " + str(exc))
|
||||
|
||||
try:
|
||||
sums = checksum_entries(os.path.join(root, "SHA256SUMS"))
|
||||
for name in REQUIRED:
|
||||
if name not in sums:
|
||||
errors.append("artifact absent from SHA256SUMS: " + name)
|
||||
elif digest(os.path.join(root, name)) != sums[name]:
|
||||
errors.append("checksum mismatch: " + name)
|
||||
for name, expected in sums.items():
|
||||
path = os.path.realpath(os.path.join(root, name))
|
||||
if os.path.commonpath((os.path.realpath(root), path)) != os.path.realpath(root) or not os.path.isfile(path) or digest(path) != expected:
|
||||
errors.append("invalid checksum entry: " + name)
|
||||
except Exception as exc:
|
||||
errors.append("checksum check failed: " + str(exc))
|
||||
|
||||
try:
|
||||
summary = load_json(os.path.join(root, "summary.json"))
|
||||
gates = summary.get("build_gates", summary.get("gates", {}))
|
||||
for gate in ("G0", "G1", "G2", "G3"):
|
||||
item = gates.get(gate, {})
|
||||
if item.get("status") != "PASS":
|
||||
errors.append("build gate not PASS: " + gate)
|
||||
except Exception as exc:
|
||||
errors.append("build gate check failed: " + str(exc))
|
||||
|
||||
try:
|
||||
with tarfile.open(os.path.join(root, "rootfs.tar.bz2"), "r:bz2") as archive:
|
||||
init = None
|
||||
for member in archive:
|
||||
name = member.name
|
||||
clean = posixpath.normpath(name)
|
||||
if name.startswith("/") or clean == ".." or clean.startswith("../"):
|
||||
errors.append("unsafe rootfs archive path: " + name)
|
||||
break
|
||||
if member.issym() or member.islnk():
|
||||
target = member.linkname
|
||||
resolved_link = posixpath.normpath(posixpath.join(posixpath.dirname(clean), target))
|
||||
if not target.startswith("/") and (resolved_link == ".." or resolved_link.startswith("../")):
|
||||
errors.append("unsafe rootfs archive link: " + name)
|
||||
break
|
||||
if clean.lstrip("./") == "sbin/init":
|
||||
init = member
|
||||
if init is None or not (init.isfile() or init.issym() or init.islnk()):
|
||||
errors.append("rootfs archive lacks sbin/init")
|
||||
except Exception as exc:
|
||||
errors.append("rootfs archive check failed: " + str(exc))
|
||||
|
||||
return {"reusable": not errors, "delivery": root, "errors": errors}
|
||||
|
||||
|
||||
def self_test():
|
||||
root = tempfile.mkdtemp(prefix="emmc-cache-test-")
|
||||
expected_parent = os.path.realpath(tempfile.gettempdir())
|
||||
try:
|
||||
profile = {"schema_version": 1, "files": {}, "values": {"version": "2018.1"}}
|
||||
profile["fingerprint_sha256"] = fingerprint(profile)
|
||||
current = os.path.join(root, "current.json")
|
||||
delivery = os.path.join(root, "delivery")
|
||||
os.mkdir(delivery)
|
||||
with open(current, "w", encoding="utf-8") as stream:
|
||||
json.dump(profile, stream)
|
||||
with open(os.path.join(delivery, "build_inputs.json"), "w", encoding="utf-8") as stream:
|
||||
json.dump(profile, stream)
|
||||
for name in ("BOOT.BIN", "image.ub", "MANIFEST.md"):
|
||||
with open(os.path.join(delivery, name), "wb") as stream:
|
||||
stream.write(name.encode("ascii"))
|
||||
with tarfile.open(os.path.join(delivery, "rootfs.tar.bz2"), "w:bz2") as archive:
|
||||
import io
|
||||
info = tarfile.TarInfo("./sbin/init")
|
||||
info.size = 4
|
||||
archive.addfile(info, io.BytesIO(b"init"))
|
||||
with open(os.path.join(delivery, "summary.json"), "w", encoding="utf-8") as stream:
|
||||
json.dump({"build_gates": {gate: {"status": "PASS"} for gate in ("G0", "G1", "G2", "G3")}}, stream)
|
||||
with open(os.path.join(delivery, "SHA256SUMS"), "w", encoding="utf-8") as stream:
|
||||
for name in REQUIRED:
|
||||
stream.write("{} {}\n".format(digest(os.path.join(delivery, name)), name))
|
||||
if not check(current, delivery)["reusable"]:
|
||||
raise AssertionError("valid delivery rejected")
|
||||
with open(os.path.join(delivery, "image.ub"), "ab") as stream:
|
||||
stream.write(b"corrupt")
|
||||
if check(current, delivery)["reusable"]:
|
||||
raise AssertionError("corrupt delivery accepted")
|
||||
print(json.dumps({"self_test": "PASS"}))
|
||||
return 0
|
||||
finally:
|
||||
resolved = os.path.realpath(root)
|
||||
if os.path.dirname(resolved) == expected_parent and os.path.basename(resolved).startswith("emmc-cache-test-"):
|
||||
shutil.rmtree(resolved)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
sub = parser.add_subparsers(dest="action", required=True)
|
||||
p = sub.add_parser("check")
|
||||
p.add_argument("--current", required=True)
|
||||
p.add_argument("--delivery", required=True)
|
||||
sub.add_parser("self-test")
|
||||
args = parser.parse_args()
|
||||
if args.action == "self-test":
|
||||
return self_test()
|
||||
result = check(args.current, args.delivery)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0 if result["reusable"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Exception as exc:
|
||||
print(json.dumps({"error": str(exc)}), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
Reference in New Issue
Block a user