sync: WIN10-I9-64G 2026-10-19

This commit is contained in:
Myfro9
2026-10-19 22:44:51 +08:00
parent 47faf8bbb7
commit 479769eff0
15 changed files with 1794 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
---
name: release_zynq7k
description: Package a hardware-verified Zynq-7000 (PetaLinux) product version into a release for colleagues — PetaLinux BSP, QSPI flash files and a whole-flash image, eMMC/SD flash files and deploy script, the final merged rootfs, PL bitstream, PS runtime files, app sources (PS and companion MCUs such as i.MX6), a README and SHA-256 manifest — under ./release/<name>_<version>/. Use when the user asks to make/build/prepare a release package, "release 包", "打包 release", "制作发布包", "交付给同事", hand a Zynq-7000 version to another team, or names a requirement doc (e.g. Notes/PRxx.md) listing release items for a Zynq-7000 board. Not for building or deploying the system itself (petalinux-qspi / petalinux-emmc / petalinux-ps / petalinux-pl do that).
---
# release_zynq7k:Zynq-7000 release 打包
把一个**已经在硬件上验证过**的版本,整理成同事拿到就能部署、能重建的 release 包。
release 只做收集、整理、合成、校验和说明,不重新设计,也不改变已验证的产物。
凡是没在硬件上验证过的东西(新脚本、新合成的文件),README 里必须如实写明。
## 0. 先读
- **项目约定**:项目 adapter(AGENTS.md / CLAUDE.md)、全局与本项目 bmem 记忆、用户指定的需求文档(如 `Notes/PRxx.md`)。
需求文档列出的条目就是 release 必须交付的条目;它明确排除的内容(例如 "除了 vivado 工程源码")不放进包里。
- **机器参数**:交叉编译 VM 的地址和账号、PetaLinux / Vivado 路径都从 `~\skills_env.md` 读取,不写死在任何地方。
plink/pscp 第一次连接时会报未知主机指纹,从报错里取出指纹,用 `-hostkey` 传入。
- **参考案例**:[references/emv93-dvbt2.md](references/emv93-dvbt2.md) 是第一次用本流程做的 release(EMV9.3 DVB-T2),
里面记录了版式、证据来源,以及这次踩过的坑。
## 1. 确定发布哪个版本(门禁 R0)
1. **定位版本**:在 git tag、bmem 恢复入口和交付目录里找到要发布的版本,列出每个产物的 SHA-256。
2. **核对产物来源**:每个产物都必须有证据表明它就是硬件验收时用的那份。
证据可以是:交付目录里的哈希、板上读回的哈希,或 VM `images/linux` 里与交付一致的哈希。
没有证据的文件不进 release;若它确实必需,就在 README 里标成"未验证"。
3. **源码 tag**:源码有 git 就用 tag。没有 git 先问用户;若用户同意,`git init` 后提交并打 tag。
新建仓库要做三件事:
- 设置 `core.autocrlf=false`,否则板上的 shell 脚本会被转成 CRLF;
- 在 `.gitignore` 里排除构建目录和大体积抓包;
- 排除 Windows 保留文件名(如 `COM3.log`),git 无法打开这类文件。
4. **板子不是必需的**:打包只用已验证的文件。不要为了打包去改板上的状态,也不要重启板子。
## 2. 目录版式
`./release/<NAME>_v<版本>/`(目录名与 git tag 一一对应):
| 目录 | 内容 |
|---|---|
| `01_petalinux_bsp/` | 每个 PetaLinux 工程一个 `.bsp`(正式系统 + QSPI 救援系统) |
| `02_qspi_flash/` | BOOT.BIN、QSPI 内核 FIT、整片镜像 `qspi_full_0x0.bin`、`uboot_scripts/` |
| `03_emmc_flash/` | `p1_boot_fat/`(FIT + PL bin)、`emmc_deploy.sh`、`EMMC_SHA256SUMS`、`rescue_scripts/`(原来验证过的脚本) |
| `04_rootfs/` | 最终根文件系统 tar.gz,以及生成它的 spec 和脚本 |
| `05_pl_bit/` | 板上实际加载的 `.bin`,同次构建的 `.bit` 和 `.hdf` / `.xsa` |
| `06_ps_runtime/` | 应用二进制、init 脚本、配置、内核模块、运行数据,按板上目录分子目录 |
| `07_ps_app_src/` | 用 `git archive <tag> <dir>` 导出的应用源码和 PetaLinux meta-user 层 |
| `08_<mcu>_app_src/`、`09_<mcu>_runtime/` | 其它处理器(如 i.MX6)的源码和部署件;没有就省略 |
| `10_pc_tools/` | 可选:PC 端验证工具 |
| `README.md` | 说明文档,从 [templates/README_template.md](templates/README_template.md) 开始写 |
| `00_SHA256SUMS` | 全部文件的校验和,`sha256sum -c` 格式 |
另在 `./release/` 下放 `<目录名>.tar.gz` 和对应的 `.sha256`。`.tar.gz` 不入 git(它只是目录的打包,可以随时重新生成),目录本身入 git。
## 3. 逐项制作
### BSP(R1)
- 在 VM 上执行 `petalinux-package --bsp -p <proj> --output <name>.bsp`。
- **先同步应用配方**:打包前确认 `project-spec/meta-user` 里各应用配方的 `files/src` 是本次发布的源码。
若不是,先把旧配方 tar 备份,再用发布源码替换,并去掉 CR。替换后读回配方里关键文件的 sha 核对。
- 每个归档解到**各自的空目录**,避免两个包混在同一个目录里。
- **HDF 一致性**:比较 BSP 中 `hw-description/ps7_init.c` 与发布 `.hdf` 里的 `ps7_init.c`。
- 相同:说明只有 PL 不同(PL 由 U-Boot 加载),README 写明"换 PL 不需要重建 Linux";
- 不同:必须告诉用户,由用户决定是否用发布的 HDF 重建。
- 检查 `tar tzf` 的结果,确认 BSP 里包含配方、DT 和 U-Boot 补丁,并且没有 `build/` 和 `images/`。
### QSPI(R2)
- **布局从证据取**:U-Boot 烧写脚本、`/proc/mtd`、flash 布局 json,不要猜。
- 生成整片镜像和布局 json:
```
python scripts/qspi_image.py qspi_full_0x0.bin 0x0:BOOT.BIN <KOFF>:image.ub.qspi --erase <ENVOFF>:<ENVLEN> --layout qspi_layout.json
```
每段的 CRC32 必须与证据记录的一致。
- 用 [templates/qspi_program.txt](templates/qspi_program.txt) 填好长度和 CRC,IP 留占位符;原来验证过的脚本一并附上。
### eMMC / SD(R3)
- **分区计划从证据取**:`fdisk -l`、`/proc/partitions`、`blkid`,记录扇区边界、文件系统、卷标和 FAT 容量。
- 从 [templates/emmc_deploy.sh](templates/emmc_deploy.sh) 改出本板脚本:只改顶部 profile 变量,其余照搬已验证脚本的步骤顺序。
- 写成 LF 换行,在 VM 上跑 `sh -n` 和 `busybox sh -n`;
- **不要用 `! cmd` 当检查**:`set -e` 不会因为它失败而退出,要写成显式的 `if`。
- 脚本没在板上整体跑过,就在 README 写"未上板实测,首次对照原脚本"。
- `EMMC_SHA256SUMS` 只写文件名,与 TFTP 下载到板上的文件对应。
### rootfs(R4)
- 写一个 spec json,交给 [scripts/merge_rootfs.py](scripts/merge_rootfs.py) 合成。
- spec 要还原已验证部署脚本的效果:基础 rootfs、覆盖包、替换的文件(带权限)、符号链接、目录。
- spec 的 `expect` 里写每个关键文件的已验证 sha。
- 运行 `merge_rootfs.py spec.json out.tar.gz --verify`,结果必须全部 OK。
- 用 tarfile 合成,不在 Windows 上解开再重新打包,否则会丢失属主、权限、符号链接和设备节点。
- 生成后在 VM 上跑一次 `tar tzf`,确认 GNU tar 能读,成员数与预期一致。
- **不放每板唯一的数据**:DPD/校准曲线、主机密钥、序列号、MAC 都不放。若 rootfs 自带这类文件,用 spec 的 `remove` 去掉,并在 README 写明新板如何生成。
### PL / PS 运行文件 / 源码(R5–R7)
- **PL**:发布板上实际加载的 `.bin`(或 bootgen 产物),再附同次构建的 `.bit` 和 `.hdf`。
- **PS 运行文件**:从交付目录或合成的 rootfs 里取,哈希与 rootfs 中的成员一致。
- **源码**:用 `git archive <tag> <dirs> | tar x`,只导出纳入版本控制的源码。
- 用 `release_check.py scan --src-dirs` 检查源码目录里没有 ELF、`.o` 或 `.ko`;
- 需求文档排除的部分(如 Vivado 工程)不导出。
### README(R8)
按模板写,必须包括:
- 版本清单(板上位置 + sha 前 16 位)、存储布局表;
- 从全新或出厂板开始的部署步骤,以及冷启动验收的期望值;
- 现场相关设置:每板唯一值和站点值,逐个写出所在文件;
- 从源码构建的方法、已知限制。
写完后自己对照脚本和包内实际内容核对每一步,例如 `install.sh` 是否已经做了某一步。别把旧板专用的步骤写成通用步骤。
## 4. 检查与收尾(R9)
1. **扫描**:
```
python scripts/release_check.py scan <dir> --env ~/skills_env.md --mac <测试板 MAC> ... --src-dirs 07_ps_app_src,08_xxx_app_src
```
- 退出码 2 表示找到了凭据值或 `user:pass@` URL,必须清除后重做,不得发布;
- 工具脚本里写死的口令,改成从环境变量读取,并在源头(仓库)修正后重新导出,不要只改 release 副本;
- 例外:厂商出厂默认账户(随固件发布,常常就是品牌名,比如网页里的品牌 URL)。确认属实后,用 `--allow <KEY>` 把它归为 acknowledged,并在 README 写明"出厂默认账户,现场建议修改";
- 工具状态文件(`*.upstate.json` 等)从源码目录里删掉;
- 测试板 MAC:发布用的配置文件要留空或改成占位符,源码和旧脚本里残留的要在 README 列出;
- 开发机路径、私钥(例如 mini_httpd 自带的示例 pem):逐项判断后写进 README。
2. **生成并核对校验和**:`release_check.py sums <dir>`,然后 `release_check.py verify <dir>` 必须 0 bad、0 not listed。
3. **打包**:`tar czf` 生成整包,再生成 `.sha256`。
4. **git**:提交 release 目录,打 tag `release-v<版本>`,`.gitignore` 加上 `release/*.tar.gz`。
5. **bmem**:用 `bmem add --prj` 记一条:
- release 位置、tag、整包 sha;
- 各目录的要点、VM 上做过的改动(备份位置)、未验证项;
- 然后执行 `bmem sync`。
6. **向用户汇报**:目录表、未验证项、需要同事注意的每板值和站点值、仓库与 bmem 状态。
有硬件异常(例如打包中途板子失联)要说明,但不要擅自重启板子。
## 脚本
| 脚本 | 用途 |
|---|---|
| `scripts/qspi_image.py` | 按偏移拼整片 QSPI 镜像(空隙 0xFF),检查重叠,输出每段的 CRC32 和布局 json |
| `scripts/merge_rootfs.py` | 按 spec 无损、可复现地合成 rootfs tar.gz;`--verify` 核对 expect 哈希和符号链接 |
| `scripts/release_check.py` | `sums` / `verify` 生成和核对 `00_SHA256SUMS`;`scan` 查凭据、MAC、开发机路径、源码目录里的二进制、保留文件名 |
| `templates/` | README 骨架、U-Boot QSPI 烧写脚本、eMMC 部署脚本模板 |
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "Zynq-7000 release"
short_description: "Package a hardware-verified Zynq-7000 PetaLinux version into a release"
default_prompt: "Use $release_zynq7k to package the current verified version into ./release per the requirement doc."
+84
View File
@@ -0,0 +1,84 @@
# Worked example: EMV9.3 DVB-T2 release v2026.10.15
First release built with this procedure (project prjID `emv93-dvbt2`; its bmem
entry records the location and the git tag `release-v2026.10.15`). Use it as a model.
Board values below belong to EMV9.3 only and must not be reused for another board.
## Platform
- Zynq xc7z100 (PL + PS) plus an i.MX6ULL board controller that has its own app and web UI.
- Toolchain: PetaLinux 2018.1, Vivado 2018.1.
- Two PetaLinux projects on the cross-build VM:
- `emv93_t2`: production; FIT on eMMC, PL bin loaded by U-Boot `plload`.
- `emv93_qspi`: rescue initramfs in QSPI.
- The VM host and credentials come from `~\skills_env.md` (`CROSS_VM_*`).
## Requirement doc
`Notes/PR02.md` listed nine items:
1. BSP
2. QSPI flash files
3. eMMC flash files
4. rootfs
5. PL bit on the rootfs
6. PS runtime
7. PS app source
8. i.MX6 app source
9. a README
The Vivado (PL) sources were explicitly excluded.
The i.MX6 runtime and the PC test tools were added as directories 09 and 10, because item 8 cannot be deployed without them.
## Layouts recorded from evidence
QSPI layout (from the proven U-Boot scripts `g4_stage1.txt` and `g4g5_write.txt`):
| Region | Offset | Content length | Erase length | CRC32 |
|---|---|---|---|---|
| BOOT.BIN | 0x0 | 0x89df0 | 0x500000 | 03cd9c7b |
| U-Boot env | 0x500000 | — | 0x20000, left blank | — |
| image.ub.qspi | 0x520000 | 0x9bbca0 | 0x1800000 | 0653b9e8 |
| Full image | 0x0 | 0xEDBCA0 | — | 183d24ec |
The full image was built with:
```
qspi_image.py qspi_full_0x0.bin 0x0:BOOT.BIN 0x520000:image.ub.qspi --erase 0x500000:0x20000
```
eMMC layout:
- Device `/dev/mmcblk1`, 7.3 GiB, the factory DOS partition table.
- The partition boundaries came from `/proc/partitions` (179,1 = 125016 KiB) and a `fdisk -l` line ("16 250047 250032 122.1M b W95 FAT32").
- p1: FAT32, sectors 16–250047. It holds only 120 MB: P-0059 recorded that with seven bit files on it, `cp` failed silently.
- p2: ext4, sector 250048 to the end.
## Pieces and where they came from
| Release dir | Source |
|---|---|
| 01 BSP | `petalinux-package --bsp -p <proj> --output <name>.bsp` on the VM, for both projects. Before packaging, the t2ps recipe's `files/src` was replaced with the released 1.8 sources; the old recipe was tarred as a backup first. |
| 02 / 03 / 05 | The verified delivery directory (`Notes/auto/delivery_t2_dpd_20261014`). The VM `images/linux` hashes were checked against it, and they matched. |
| 04 rootfs | Merged from the base `rootfs.tar.gz`, the T2 runtime overlay, the released binaries and configs, and the C23 symlinks. Each key member hash was checked against the board-verified files, and GNU tar on the VM listed all 1229 members. |
| 06 | Copied from the delivery dirs; the stream files were extracted from the overlay tarball. |
| 07 / 08 | `git archive <tag> ps` and `git archive <tag> imx6`, giving 325 files with no build output. |
## What the scan / review caught (and how it was handled)
- **i.MX6 MACs.**
- `network.conf` carried the test board's MAC. It was blanked in the release copy, with a comment.
- The source copy and an old deploy script held two other boards' MACs; these were listed in the README.
- Deriving the MAC from OTP does not work on that image, so the README says each board's MAC must be filled in.
- **TP receiver address.** `t2ps.conf` holds the receiving PC's MAC/IP for the TP IQ stream; this is documented as a site setting.
- **DPD curve.** It depends on the board and its loopback, so it was left out; a new board runs a full DPD instead.
- **Development-machine paths.** The PC tools contain dev-machine paths; this is documented under the known limits.
- **Credentials.** The product's own web account (`usr.conf`) is part of the firmware and stays. No infrastructure password appeared.
- **Hardware not used.** The board went silent during packaging. That was harmless, because nothing in the release was taken from the live board.
## Script mistakes made the first time (now handled by the skill's scripts)
- **Two archives unpacked into one directory.** The recipe tarball and the source tarball both unpacked into `t2ps/`, mixing their contents. Unpack each archive into its own empty directory.
- **Shell `set -e` ignores a negated command.** `! grep` does not stop the script; write the test as an explicit `if`.
- **Over-eager path normalisation.** `n.lstrip("./")` also strips the leading dot of names such as `./.ssh`. `merge_rootfs.py` removes only the `./` prefixes.
- **CRLF from Windows editors.** Shell scripts written on Windows can get CRLF line endings. Strip them, then run `sh -n` and `busybox sh -n` on the VM.
- **Archive bytes are not reproducible.** The gzip header mtime and the mtime of added files changed the archive hash on every run. `merge_rootfs.py` is now deterministic, so compare member hashes rather than archive bytes across tool versions.
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""merge_rootfs.py -- assemble a release root filesystem tarball from a JSON spec.
python merge_rootfs.py <spec.json> <out.tar.gz> [--verify]
Works on Windows or Linux without root: tar members are copied as TarInfo, so
owners, modes, symlinks, hard links and device nodes survive exactly (an
extract-and-retar on Windows, or as non-root on Linux, would lose them).
Later layers override earlier ones by path; the output is deterministic
(fixed mtime for added entries, gzip header mtime 0), so the same spec gives
the same archive bytes.
spec.json (paths relative to the spec file unless absolute):
{
"mtime": 1792022400, # added entries; optional
"layers": [ "rootfs.tar.gz", "overlay.tar.gz" ], # applied in order
"remove": [ "etc/dropbear/dropbear_rsa_host_key" ], # paths (and their subtrees)
"files": [ {"path": "usr/bin/app", "src": "bin/app", "mode": "0755"} ],
"links": [ {"path": "etc/app/current.cfg", "target": "/etc/app/a.cfg"} ],
"dirs": [ "aw/dpd_curve" ],
"expect": { "usr/bin/app": "<sha256 or a prefix>" } # checked with --verify
}
"""
import gzip, hashlib, io, json, os, sys, tarfile
def norm(n):
while n.startswith("./"):
n = n[2:]
n = n.strip("/")
return "./" + n if n not in ("", ".") else "./"
class Tree:
def __init__(self):
self.m, self.order = {}, []
def put(self, ti, data):
ti.name = norm(ti.name)
if ti.islnk():
ti.linkname = norm(ti.linkname)
if ti.name not in self.m:
self.order.append(ti.name)
self.m[ti.name] = (ti, data)
def remove(self, p):
p = norm(p)
for n in list(self.m):
if n == p or n.startswith(p + "/"):
del self.m[n]
self.order = [n for n in self.order if n in self.m]
def info(name, kind, mode, mtime):
ti = tarfile.TarInfo(name); ti.type = kind; ti.mode = mode; ti.mtime = mtime
ti.uid = ti.gid = 0; ti.uname = ti.gname = "root"
return ti
def main(argv):
if len(argv) < 3:
sys.exit(__doc__)
spec_path, out = argv[1], argv[2]
base = os.path.dirname(os.path.abspath(spec_path))
rel = lambda p: p if os.path.isabs(p) else os.path.join(base, p)
spec = json.load(open(spec_path))
mt = int(spec.get("mtime", 1700000000))
t = Tree()
for layer in spec.get("layers", []):
with tarfile.open(rel(layer), "r:*") as tf:
for ti in tf.getmembers():
t.put(ti, tf.extractfile(ti).read() if ti.isfile() else None)
for p in spec.get("remove", []):
t.remove(p)
for f in spec.get("files", []):
d = open(rel(f["src"]), "rb").read()
ti = info(f["path"], tarfile.REGTYPE, int(f.get("mode", "0644"), 8), mt); ti.size = len(d)
t.put(ti, d)
for l in spec.get("links", []):
ti = info(l["path"], tarfile.SYMTYPE, 0o777, mt); ti.linkname = l["target"]
t.put(ti, None)
for d in spec.get("dirs", []):
parts = norm(d)[2:].split("/")
for k in range(1, len(parts) + 1): # parents first, keep existing ones
p = norm("/".join(parts[:k]))
if p not in t.m:
t.put(info(p, tarfile.DIRTYPE, 0o755, mt), None)
raw = io.BytesIO()
with tarfile.open(fileobj=raw, mode="w", format=tarfile.GNU_FORMAT) as tf:
for n in t.order:
ti, data = t.m[n]
tf.addfile(ti, io.BytesIO(data) if data is not None else None)
with open(out, "wb") as fo, gzip.GzipFile(filename="", mode="wb", fileobj=fo, mtime=0) as gz:
gz.write(raw.getvalue())
n_dev = sum(1 for n in t.order if t.m[n][0].isdev())
print(f"{len(t.order)} members ({n_dev} device nodes) -> {out}")
print("sha256", hashlib.sha256(open(out, "rb").read()).hexdigest())
if "--verify" in argv:
bad = 0
for p, want in spec.get("expect", {}).items():
e = t.m.get(norm(p))
got = hashlib.sha256(e[1]).hexdigest() if e and e[1] is not None else None
ok = got is not None and got.startswith(want.lower())
bad += not ok
print(("OK " if ok else "BAD ") + (got or "missing")[:16], p)
for l in spec.get("links", []):
e = t.m.get(norm(l["path"]))
ok = e is not None and e[0].issym() and e[0].linkname == l["target"]
bad += not ok
print(("OK " if ok else "BAD ") + "link", l["path"], "->", l["target"])
if bad:
sys.exit(f"{bad} check(s) failed")
if __name__ == "__main__":
main(sys.argv)
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""qspi_image.py -- build a whole-flash QSPI image for a Zynq-7000 board.
python qspi_image.py <out.bin> <offset>:<file> [<offset>:<file> ...]
[--erase <offset>:<len> ...] [--size <bytes>] [--layout <out.json>]
Every part is placed at its offset; gaps (and --erase ranges, e.g. the U-Boot
environment sector that must stay blank so the default bootcmd is used) are
0xFF, exactly what a fresh `sf erase` leaves. The image ends at the last byte
of the last part (or --size). Overlapping parts are refused. Prints
offset / length / CRC32 of every part and of the image, the numbers to check
against `crc32` in U-Boot, and writes them to --layout as JSON.
Offsets and lengths accept 0x hex.
"""
import json, sys, zlib
def num(s): return int(s, 0)
def main(argv):
if len(argv) < 3:
sys.exit(__doc__)
out, rest = argv[1], argv[2:]
parts, erase, size, layout = [], [], None, None
i = 0
while i < len(rest):
a = rest[i]
if a == "--erase":
o, l = rest[i + 1].split(":"); erase.append((num(o), num(l))); i += 2
elif a == "--size":
size = num(rest[i + 1]); i += 2
elif a == "--layout":
layout = rest[i + 1]; i += 2
else:
o, f = a.split(":", 1); parts.append((num(o), f)); i += 1
blobs = []
for o, f in sorted(parts):
d = open(f, "rb").read()
blobs.append((o, f, d))
for (o1, f1, d1), (o2, f2, d2) in zip(blobs, blobs[1:]):
if o1 + len(d1) > o2:
sys.exit(f"overlap: {f1} 0x{o1:x}+0x{len(d1):x} runs into {f2} at 0x{o2:x}")
end = max(o + len(d) for o, _, d in blobs) # erased tails need no bytes: blank anyway
if size is not None:
if size < end:
sys.exit(f"--size 0x{size:x} smaller than the content end 0x{end:x}")
end = size
img = bytearray(b"\xff" * end)
for o, _, d in blobs:
img[o:o + len(d)] = d
for o, l in erase:
for o2, f2, d2 in blobs:
if o < o2 + len(d2) and o2 < o + l:
sys.exit(f"--erase 0x{o:x}:0x{l:x} overlaps {f2}")
open(out, "wb").write(img)
rep = {"image": out, "length": len(img), "crc32": "%08x" % zlib.crc32(img), "parts": [],
"erased": [{"offset": hex(o), "length": hex(l)} for o, l in erase]}
for o, f, d in blobs:
rep["parts"].append({"file": f, "offset": hex(o), "length": hex(len(d)),
"crc32": "%08x" % zlib.crc32(d)})
print(f"0x{o:08x} len 0x{len(d):x} crc32 {zlib.crc32(d):08x} {f}")
for o, l in erase:
print(f"0x{o:08x} len 0x{l:x} erased (0xFF)")
print(f"image {out}: len 0x{len(img):x} crc32 {rep['crc32']}")
if layout:
json.dump(rep, open(layout, "w"), indent=1)
if __name__ == "__main__":
main(sys.argv)
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""release_check.py -- checksum, verify and scan a release directory.
python release_check.py sums <release_dir> # (re)write 00_SHA256SUMS
python release_check.py verify <release_dir> # check every listed file
python release_check.py scan <release_dir> [--env ~/skills_env.md] [--mac AA:BB:..]...
[--src-dirs 07_ps_app_src,08_imx6_app_src] [--allow KEY]...
scan reports (never prints a secret value, only the file and the key name):
* values of *PASS* / *PASSWORD* / *TOKEN* / *SECRET* / *KEY* keys in --env found
in any text file, and "user:password@" URLs; --allow KEY moves that key's hits
to "acknowledged" (e.g. a vendor factory-default account that ships in the
firmware and is also the brand name) -- still listed, no failure;
* tool state files (*.upstate.json) inside --src-dirs;
* private keys (-----BEGIN ... PRIVATE KEY-----);
* the given board MACs (--mac, e.g. a test board's) and any other MAC literal;
* development-machine absolute paths (X:\\..., /home/<user>, /Users/<user>);
* ELF / object / archive files inside --src-dirs (build output in a source tree);
* Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) that break copies.
Exit status 0 = scan finished (read the report), 2 = a secret was found.
"""
import hashlib, os, re, sys
SUMS = "00_SHA256SUMS"
RESERVED = re.compile(r"^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$", re.I)
MAC = re.compile(r"\b[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}\b")
WINPATH = re.compile(r"\b[A-Za-z]:[\\/](?:[^\s\"'<>|]+)")
NIXHOME = re.compile(r"/(?:home|Users)/[A-Za-z0-9_.-]+")
URLCRED = re.compile(r"[a-z][a-z0-9+.-]*://[^/\s:@]+:[^/\s@]+@", re.I)
PRIVKEY = re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")
def files(root):
for d, _, fs in os.walk(root):
for f in fs:
yield os.path.join(d, f)
def relp(root, p):
return "./" + os.path.relpath(p, root).replace(os.sep, "/")
def sha(p):
h = hashlib.sha256()
with open(p, "rb") as f:
for b in iter(lambda: f.read(1 << 20), b""):
h.update(b)
return h.hexdigest()
def is_text(b):
return b"\0" not in b[:8192]
def cmd_sums(root):
lines = [f"{sha(p)} {relp(root, p)}" for p in sorted(files(root))
if os.path.basename(p) != SUMS]
open(os.path.join(root, SUMS), "w", newline="\n").write("\n".join(lines) + "\n")
print(f"{len(lines)} files -> {SUMS}")
def cmd_verify(root):
bad = n = 0
listed = set()
for line in open(os.path.join(root, SUMS)):
h, p = line.rstrip("\n").split(" ", 1)
listed.add(p); n += 1
fp = os.path.join(root, p[2:])
if not os.path.exists(fp) or sha(fp) != h:
bad += 1; print("BAD", p)
extra = [relp(root, p) for p in files(root)
if os.path.basename(p) != SUMS and relp(root, p) not in listed]
for p in extra:
print("NOT LISTED", p)
print(f"{n} listed, {bad} bad, {len(extra)} not listed")
sys.exit(1 if bad or extra else 0)
def env_secrets(path):
out = {}
if not path or not os.path.exists(path):
return out
for line in open(path, encoding="utf-8", errors="replace"):
m = re.match(r"\s*([A-Z0-9_]*(?:PASS|PASSWORD|TOKEN|SECRET|KEY)[A-Z0-9_]*)\s*=\s*(\S+)", line)
if m and len(m.group(2)) >= 4:
out[m.group(1)] = m.group(2)
return out
def cmd_scan(root, args):
env = os.path.expanduser(args[args.index("--env") + 1]) if "--env" in args else None
macs = [args[i + 1].lower() for i, a in enumerate(args) if a == "--mac"]
srcd = args[args.index("--src-dirs") + 1].split(",") if "--src-dirs" in args else []
allow = {args[i + 1] for i, a in enumerate(args) if a == "--allow"}
secrets = env_secrets(env)
found_secret = False
rep = {k: [] for k in ("secret", "acknowledged", "url_cred", "privkey", "board_mac", "mac", "dev_path",
"src_binary", "reserved")}
for p in files(root):
r = relp(root, p)
if RESERVED.match(os.path.basename(p)):
rep["reserved"].append(r)
b = open(p, "rb").read()
if any(r.startswith("./" + s.strip("/") + "/") for s in srcd):
if p.endswith(".upstate.json"):
rep["src_binary"].append(r + " (tool state file, remove)")
elif b[:4] == b"\x7fELF" or p.endswith((".o", ".a", ".so", ".ko")):
rep["src_binary"].append(r)
if not is_text(b):
continue
t = b.decode("utf-8", "replace")
for k, v in secrets.items():
if v in t:
if k in allow:
rep["acknowledged"].append(f"{r} (value of {k})")
else:
rep["secret"].append(f"{r} (value of {k})"); found_secret = True
if URLCRED.search(t):
rep["url_cred"].append(r); found_secret = True
if PRIVKEY.search(t):
rep["privkey"].append(r)
ms = {m.lower() for m in MAC.findall(t)}
for m in ms:
(rep["board_mac"] if m in macs else rep["mac"]).append(f"{r} {m}")
dp = sorted(set(WINPATH.findall(t)) | set(NIXHOME.findall(t)))
if dp:
rep["dev_path"].append(f"{r} ({len(dp)}: {dp[0][:60]}{' ...' if len(dp) > 1 else ''})")
titles = {"secret": "SECRET VALUES (must be removed)",
"acknowledged": "acknowledged values (--allow; say so in the README)",
"url_cred": "user:password@ URLs",
"privkey": "private keys (check: product key or leaked?)",
"board_mac": "given board MACs (must not ship)", "mac": "other MAC literals (review)",
"dev_path": "development-machine paths (review / document)",
"src_binary": "binaries inside source dirs", "reserved": "Windows reserved names"}
for k, title in titles.items():
print(f"== {title}: {len(rep[k])}")
for x in rep[k][:40]:
print(" ", x)
if len(rep[k]) > 40:
print(f" ... {len(rep[k]) - 40} more")
sys.exit(2 if found_secret else 0)
if __name__ == "__main__":
if len(sys.argv) < 3:
sys.exit(__doc__)
c, root = sys.argv[1], sys.argv[2]
{"sums": lambda: cmd_sums(root), "verify": lambda: cmd_verify(root),
"scan": lambda: cmd_scan(root, sys.argv[3:])}[c]()
@@ -0,0 +1,82 @@
# <产品/板卡> <功能> Release <版本>
<一句话:平台(Zynq-7000 型号 + 其它处理器)、本版本的功能要点。>
本包内容对应工程 <git 仓库/路径> 的 tag `<tag>`,<日期> 在 <哪块板> 上实测:<验收结论,逐项>。
每个文件的完整 SHA-256 见 `00_SHA256SUMS`,部署后请逐个读回核对。
<若有发布需求文档(如 Notes/PRxx.md),写明按哪一份制作,以及哪些内容按要求不在包内。>
## 1. 目录
| 目录 | 需求项 | 内容 |
|---|---|---|
| `01_petalinux_bsp/` | BSP | <正式系统 .bsp>、<救援系统 .bsp>,PetaLinux <版本> |
| `02_qspi_flash/` | QSPI 烧写文件 | BOOT.BIN、image.ub.qspi、`qspi_full_0x0.bin`、`uboot_scripts/` |
| `03_emmc_flash/` | eMMC/SD 烧写文件 | `p1_boot_fat/`、`emmc_deploy.sh`、`EMMC_SHA256SUMS`、原始已验证脚本 |
| `04_rootfs/` | rootfs | `<名>_rootfs.tar.gz`(最终合成),合成脚本与 spec |
| `05_pl_bit/` | PL bit | `<top>.bin`(板上加载的)、同次构建 `.bit` / `.hdf`(或 `.xsa`) |
| `06_ps_runtime/` | PS 运行文件 | 应用、初始化脚本、配置、内核模块、运行数据 |
| `07_ps_app_src/` | PS app 源码 | 应用源码 + PetaLinux meta-user 层 |
| `08_<mcu>_app_src/` | 其它处理器源码 | <可选> |
| `09_<mcu>_runtime/` | 部署所需 | <可选> |
| `10_pc_tools/` | 可选 | PC 端验证工具 |
| `README.md` | 说明 | 本文件 |
## 2. 版本清单
| 层 | 文件 | 板上位置 | sha256 前 16 位 |
|---|---|---|---|
| QSPI | BOOT.BIN(FSBL + U-Boot <配置>) | QSPI 0x0 | |
| QSPI | image.ub.qspi(救援) | QSPI <偏移> | |
| 启动分区 | image.ub | /image.ub | |
| 启动分区 | <top>.bin(PL) | / | |
| 根分区 | <rootfs>.tar.gz | / | |
| rootfs | <应用 / 配置,逐个列出> | | |
工具链:PetaLinux <版本>,Vivado <版本>,<交叉编译器>。
## 3. 存储布局
QSPI(`sf`):
| 偏移 | 长度 | 内容 |
|---|---|---|
| 0x000000 | | BOOT.BIN,CRC32 |
| <环境区> | | U-Boot 环境区,保持擦除(全 0xFF),使用默认 bootcmd |
| <内核区> | | image.ub.qspi,CRC32 |
- `qspi_full_0x0.bin`:上表按偏移拼好的整片镜像,空隙填 0xFF,从 0x0 写入;长度 <>,CRC32 <>。
- 默认 bootcmd = `<...>`;救援启动命令:`<...>`。
eMMC/SD(`<设备>`,<容量>,<分区表类型>):
| 分区 | 扇区 | 文件系统 | 内容 |
|---|---|---|---|
## 4. 部署步骤(全新 / 出厂状态的板子)
0. 准备:
- 先备份出厂内容(QSPI、eMMC、其它处理器);
- 串口参数;
- PC 上的 TFTP / HTTP 服务;
- 默认网络地址表。
1. QSPI:二选一,U-Boot 脚本或编程器写整片镜像。
2. eMMC:
- 进入救援 Linux,执行 `sh emmc_deploy.sh <PC_IP> [--partition]`。
- 若该脚本未在板上整体跑过,在此明确写出来。
3. <其它处理器> 部署。
4. 冷启动与验收:寄存器 / 日志 / 网页 / 业务功能,逐项写出期望值。
## 5. 现场相关设置(换环境必须改)
<每板唯一值(MAC、序列号、校准/DPD 曲线),以及站点值(IP、接收端 MAC/IP),逐个写出所在文件和处理方法。>
## 6. <本版本的主要功能说明>
## 7. 从源码构建
<BSP → petalinux-create -s / build / package --boot;应用单独编译与单元测试;rootfs 重新合成方法;BSP 的 hw-description 与发布 bit 的关系。>
## 8. 已知限制
<未验证项、只做了部分验证的项、测试环境的异常、工具里的开发机路径等。>
+74
View File
@@ -0,0 +1,74 @@
#!/bin/sh
# emmc_deploy.sh -- write a Zynq-7000 production system (FAT boot + ext4 root) to
# eMMC/SD from the QSPI rescue Linux. TEMPLATE from the release_zynq7k skill:
# fill the variables below from the board's evidence (fdisk -l / /proc/partitions,
# the project's proven deploy script), then test it once on the board and say so
# in the README until that happened.
#
# sh emmc_deploy.sh <PC_IP> [--partition]
#
# The PC serves, by TFTP: every file in BOOT_FILES, ROOTFS and SUMS.
set -e
# ---------------------------------------------------------------- board profile
DEV=/dev/mmcblk1 # verified device node (NOT inferred from the name)
P1_START=16 # DOS partition plan, sectors (only used with --partition)
P1_SIZE=250032
P1_TYPE=b # b = FAT32
P2_START=250048 # p2 runs to the end of the device
BOOT_FILES="image.ub system_top.bin" # copied to p1 (FAT); keep FAT capacity in mind
ROOTFS=rootfs.tar.gz # extracted to p2 (ext4)
SUMS=EMMC_SHA256SUMS # sha256sum -c format for BOOT_FILES + ROOTFS
CHECK_IN_ROOT="usr/bin/app etc/init.d/app-init" # read back after writing
# ------------------------------------------------------------------------------
PC=${1:?usage: emmc_deploy.sh <PC_IP> [--partition]}
W=/tmp/e2
if grep " / " /proc/mounts | grep -q "$(basename $DEV)"; then
echo "running from $DEV: boot the QSPI rescue system"; exit 1
fi
[ -b $DEV ] || { echo "$DEV missing"; exit 1; }
cat /sys/block/$(basename $DEV)/device/type /sys/block/$(basename $DEV)/device/name /sys/block/$(basename $DEV)/size
mkdir -p $W && cd $W
for f in $SUMS $BOOT_FILES $ROOTFS; do
rm -f $f; tftp -g -r $f $PC
done
sha256sum -c $SUMS # a failed transfer stops here, before any write
echo SHA_OK
[ -f /etc/udev/rules.d/automount.rules ] && mv /etc/udev/rules.d/automount.rules /tmp/automount.rules.off && udevadm control --reload-rules || true
umount ${DEV}p1 2>/dev/null || true
umount ${DEV}p2 2>/dev/null || true
if grep -q "$(basename $DEV)" /proc/mounts; then echo "$DEV still mounted"; exit 1; fi
if [ "$2" = "--partition" ]; then
printf 'label: dos\nstart=%s, size=%s, type=%s\nstart=%s, type=83\n' \
$P1_START $P1_SIZE $P1_TYPE $P2_START | sfdisk $DEV
sleep 1
fi
[ -b ${DEV}p1 ] && [ -b ${DEV}p2 ] || { echo "no p1/p2 on $DEV (use --partition)"; exit 1; }
mkfs.vfat -F 32 -n BOOT ${DEV}p1
mkfs.ext4 -F -q -L rootfs ${DEV}p2
mkdir -p /mnt/b /mnt/r
mount -t vfat ${DEV}p1 /mnt/b
mount -t ext4 ${DEV}p2 /mnt/r
for f in $BOOT_FILES; do cp $W/$f /mnt/b/$f; done
tar xzf $W/$ROOTFS -C /mnt/r
sync
cd /
umount /mnt/b
umount /mnt/r
sync
fsck.vfat -n ${DEV}p1 | tail -n 1
e2fsck -fn ${DEV}p2 | tail -n 1
echo 3 > /proc/sys/vm/drop_caches
mount -o ro -t vfat ${DEV}p1 /mnt/b
mount -o ro -t ext4 ${DEV}p2 /mnt/r
for f in $BOOT_FILES; do sha256sum /mnt/b/$f; done
for f in $CHECK_IN_ROOT; do sha256sum /mnt/r/$f; done
umount /mnt/b
umount /mnt/r
[ -f /tmp/automount.rules.off ] && mv /tmp/automount.rules.off /etc/udev/rules.d/automount.rules || true
echo "EMMC_DEPLOY_DONE -- compare the hashes above with 00_SHA256SUMS, then power-cycle"
+27
View File
@@ -0,0 +1,27 @@
# U-Boot commands: program the Zynq-7000 QSPI -- TEMPLATE from release_zynq7k.
# Fill the lengths, CRC32 and offsets from scripts/qspi_image.py --layout output
# and the board's flash layout; keep the IP addresses as placeholders.
# Paste one line at a time on the PS console after stopping autoboot, and check
# every crc32 / cmp.b result. A failed tftpboot must stop the sequence: never
# let stale DDR content be written.
# BOOT.BIN len <LEN_BOOT> crc32 <CRC_BOOT> -> QSPI 0x0
# (bootenv) <ENV_OFF>, <ENV_LEN> erased -> default bootcmd
# image.ub.qspi len <LEN_KERN> crc32 <CRC_KERN> -> QSPI <KERN_OFF>
setenv ipaddr <BOARD_IP>
setenv serverip <PC_IP>
tftpboot 0x10000000 BOOT.BIN
crc32 0x10000000 <LEN_BOOT>
tftpboot 0x11000000 image.ub.qspi
crc32 0x11000000 <LEN_KERN>
sf probe 0
sf erase 0x0 <BOOT_ERASE_LEN>
sf write 0x10000000 0x0 <LEN_BOOT>
sf read 0x12000000 0x0 <LEN_BOOT>
cmp.b 0x10000000 0x12000000 <LEN_BOOT>
crc32 0x12000000 <LEN_BOOT>
sf erase <ENV_OFF> <ENV_LEN>
sf erase <KERN_OFF> <KERN_ERASE_LEN>
sf write 0x11000000 <KERN_OFF> <LEN_KERN>
sf read 0x13000000 <KERN_OFF> <LEN_KERN>
cmp.b 0x11000000 0x13000000 <LEN_KERN>
crc32 0x13000000 <LEN_KERN>