feat: strengthen BSP and PSP validation and handoff

This commit is contained in:
tangfahong committed 2026-09-24 11:52:22 +08:00
1 parent bcea35a91e
commit 1641fac631
39 files changed
+2695 -90

No files matched your search

@@ -16,6 +16,8 @@ description: "基于 EmxRes 参考项目创建和适配板级 BSP,处理 CV18
5. 涉及媒体内存或分区时,必须读取 [内存与分区规则](references/memory-and-partition.md)。
6. 涉及项目证据、SDK 同步、统一编译、imgs 路径或 PSP 交接时,必须读取 [构建、证据与交接规则](references/build-and-evidence.md)。
7. 只有从原理图提取或重新映射 Pinmux、需要审核表时,才读取 [Pinmux 证据工作流](references/pinmux.md);CSV 不是修改 BSP 的必需输入。
8. `EP_CHIP` 为 CV181X、且参考项目使用 `configs/kernel/` 与 `configs/uboot/` 时,读取 [CV181X 分目录与 SDK 只读核对](references/cv181x-sdk-readonly-review.md)。需要访问 SDK 时还必须有用户明确给出的精确 SDK 路径;默认不访问。
9. `EP_CHIP` 为 CV184X、且项目使用单板卡配置目录时,读取 [CV184X 板级配置与构建链](references/cv184x-sdk-readonly-review.md)。需要核对 SDK 当前状态时,必须由用户给出精确 SDK 根目录;默认不访问。
## 默认工作模式
@@ -23,6 +25,7 @@ description: "基于 EmxRes 参考项目创建和适配板级 BSP,处理 CV18
- 默认调查范围只包含用户明确给出的附件、当前 Skill 及内置资料、参考项目、目标/staging 项目和 `EmxRes/tools` 中的精确构建入口;禁止扫描整个用户主目录、整个 `EapilBsp` 或整个 SDK。
- 未指定的 GPIO、内存、分区、板卡版本和其他配置继承参考项目;禁止以“清理”“顺手升级”扩大差异。
- 默认要求参考与目标的精确芯片、SDK、Flash 和单/双系统形态兼容。目录名只能提供线索,必须读取实际配置。
- 按 `buildEnvConfig.sh` 的 `EP_CHIP` 识别配置布局:CV184X 沿用 `configs/<board_build_type>/` 与 `configs/EPSYNCSDKConfig.sh`;CV181X 分别核对 `configs/kernel/`、`configs/uboot/` 与两侧 defconfig。不得把两种布局合并或凭项目名猜测。
- 新项目以 `<芯片型号>-<项目型号>` 命名;双系统追加 `_dualos`。额外版本、日期、存储或优化后缀只在用户明确要求时使用。
- 开始持久化复制前,展示一次预检摘要、交付级别和完整 GPIO 实施计划并等待确认。确认覆盖本地复制、已列明差量、项目内文档、所选静态验证模式,以及任务需要构建产物时的标准全量编译;不覆盖设备、上传或发布。
- 使用 `scripts/bsp/project_guard.py` 执行确定性的复制、清单、差异白名单和最终原子写入;项目特定配置由 Agent 根据真实结构修改,不使用万能文本替换。
@@ -42,6 +45,7 @@ description: "基于 EmxRes 参考项目创建和适配板级 BSP,处理 CV18
- 原理图派生 GPIO 必须先写入并通过 `scripts/bsp/validate_gpio_plan.py`,再由用户确认整个实施表;`provisional` 或 `blocked` 行不能进入代码。
- 其他厂商芯片的媒体内存不自动修改;报告人工修改警告,但可继续独立的已确认工作。
- 任务需要构建产物时,`bsp_preflight.py build` 通过后自动执行标准全量编译:在固定 `EmxRes` 根目录加载 `tools/script_completion.sh`,再运行 `EMX2_BuildSDKTool <项目名> <构建版本> all`,不再单独请求确认。标准脚本内部的配置同步和 `git fetch origin` 视为该构建的一部分。直接运行 `ep_kernel.sh`、`ep_uboot.sh`、`EPSYNCSDKConfig.sh`,改用组件构建、覆盖已有输出或修改统一构建脚本不属于自动范围,出现这些需求时停止并请用户确认。
- 编译成功后必须核对唯一 `kernel.bin`、`rootfs.bin` 与实际生效分区表;每个镜像相对其所有对应分区中的最小容量必须至少保留 512 KiB(524288 字节),否则不能记录 `build_passed`。通过后持久化检查证据、精确 imgs 版本目录、产物 hash 和 PSP 交接 manifest,并用 `project_guard.py audit-final` 复核正式项目。
- CV184X 的标准构建有额外例外:`bsp_preflight.py build` 报告 `requires_patch_precheck=true` 时,`passed=true` 仍不足以自动构建。补丁环境检查失败会走 `--applypatch --force`,可能对多个 SDK 子仓库执行 `git reset --hard`。一次性构建确认不包括丢弃这些仓库的已有改动。构建前先按 CV184X 专页核对补丁门禁及相关仓库状态;无法证明不会进入强制重置分支时停止,明确列出风险和受影响路径,另请用户决定。不得用只读文件比对代替补丁环境检查通过的证据。
- 编译成功后先对本次精确 imgs 版本路径自动执行并复核 `a+rX`:路径上的目录和输出内目录对所有用户可读、可进入,普通文件对所有用户可读,原本可执行的文件才补齐所有用户执行权限;不得跟随软链接、扩大到其他构建版本或使用 `chmod -R 777`。权限处理失败时不能记录 `build_passed`。随后核对唯一 `kernel.bin`、`rootfs.bin` 与实际生效分区表;每个镜像相对其所有对应分区中的最小容量必须至少保留 512 KiB(524288 字节),否则不能记录 `build_passed`。全部通过后持久化权限与余量证据、精确 imgs 版本目录、产物 hash 和 PSP 交接 manifest,并用 `project_guard.py audit-final` 复核正式项目。
- 本地配置检查通过只能声明项目已创建或 `local validation passed`;未执行同步、编译和板上验证时必须如实标注。
- 完成时按 [记录规范](../firmware-lowlevel-development/references/records.md) 和 [交付契约](../firmware-lowlevel-development/references/deliverable-contract.md) 报告;不执行 `git add`、提交或远端推送。
@@ -57,7 +57,9 @@ python3 <BSP Skill>/scripts/bsp/bsp_preflight.py build \
--version <YYYYMMDD_N>
```
只有 `passed=true` 才能自动执行。执行前把工具返回的精确工作目录、完整命令、项目与 SDK 路径、构建范围、最终 imgs 版本路径、工作区状态、配置同步副作用,以及统一脚本中的远端 `git fetch origin` 写入记录,不再请求第二次确认。不得从 `EmxRes` 的父目录启动,也不得依赖登录 Shell 的偶然当前目录。
`passed=true` 只是构建的必要条件。执行前把工具返回的精确工作目录、完整命令、项目与 SDK 路径、构建范围、最终 imgs 版本路径、工作区状态、配置同步副作用,以及统一脚本中的远端 `git fetch origin` 写入记录。CV184X 还须按下文检查 `requires_patch_precheck`;不能把这一字段当成普通提示后继续自动构建。不得从 `EmxRes` 的父目录启动,也不得依赖登录 Shell 的偶然当前目录。
CV184X 还要执行 [CV184X 板级配置与构建链](cv184x-sdk-readonly-review.md)中的补丁门禁。其统一脚本在补丁检查失败后会尝试 `--applypatch --force`,可能重置多个 SDK 子仓库;这不属于上述自动构建授权。相关仓库存在未确认改动、补丁检查未通过或无法证明不会进入强制重置分支时,停止自动构建并向用户展示精确风险,不能静默运行或代替用户批准丢弃改动。CV181X 不套用这个补丁流程。
下列情况不自动处理:预检失败、统一脚本或路径与固定规则不符、输出版本已存在、参考或目标出现新的重叠改动、需要直接运行同步脚本、需要组件构建、需要改变构建参数、需要修改统一脚本,或者构建失败后需要清理和重试。此时停止并向用户说明异常、影响和可选处理方式。
@@ -75,6 +77,14 @@ python3 <BSP Skill>/scripts/bsp/bsp_preflight.py build \
构建前记录新项目内同步脚本权限;统一脚本临时 `chmod 777` 后,构建结束恢复已记录的原权限并写入报告。只恢复新项目内的已知文件,不改 SDK 或其他项目权限。
编译命令成功后,`record_build_handoff.py` 必须在生成交接文件前自动处理本次精确 imgs 版本路径:
- 从 `--output-root` 到本次 `<项目名>/imgs/<构建版本>` 的路径节点都补齐 `a+rx`,保证所有用户都能读取并进入目录;
- 本次版本目录内递归执行等价于 `a+rX` 的受控处理:目录补齐 `a+rx`,普通文件补齐 `a+r`,只有原本带执行位的文件才补齐 `a+x`;
- 不跟随软链接,不改软链接目标,不处理其他项目或其他构建版本,也不使用 `chmod -R 777`;
- 不使用 `sudo`。当前用户无法修改权限、路径节点不是普通目录、输出包含特殊文件或复核仍不满足要求时立即停止,不能生成 `build_passed`;
- 将处理前后模式、变更数量和复核结果写入 `validation/output_permissions_<构建版本>.json`,并同时写入 PSP 交接 manifest。
## 构建证据与状态
成功必须同时满足:
@@ -82,6 +92,7 @@ python3 <BSP Skill>/scripts/bsp/bsp_preflight.py build \
- 命令退出码为零;
- 日志到达正常完成阶段;
- 输出目录存在且含非空固件文件;
- 本次精确 imgs 版本路径已经通过 `a+rX` 权限处理与复核;
- 当前项目实际生效的分区表可唯一确定,或已显式指定 `configs/` 内精确文件;
- 最终 imgs 中唯一 `kernel.bin` 和 `rootfs.bin` 相对各自最小对应分区均保留至少 512 KiB(524288 字节);
- 文件名、大小和 SHA-256 清单写入 `validation/`;
@@ -113,12 +124,13 @@ python3 <BSP Skill>/scripts/bsp/project_guard.py audit-final \
- `validation/artifact_manifest_<构建版本>.sha256`;
- `validation/image_partition_margin_<构建版本>.json`;
- `validation/output_permissions_<构建版本>.json`;
- `handoff/psp_handoff.json`;
- `status.json`。
`psp_handoff.json` 遵循统一入口的 [BSP→PSP 交接契约](../../firmware-lowlevel-development/references/bsp-psp-handoff.md)。`audit-final` 必须在这些文档写入后执行;它使用 prepare 时冻结的参考清单,而不是假定参考项目从未变化。
成功时保存简明摘要、kernel/rootfs 分区余量证据、固件哈希清单和精确 imgs 版本路径;失败时保存完整日志、阶段和关键错误。后续同步或编译失败不删除已经通过静态门禁的正式项目,只更新状态。不得只记录 `imgs` 软链接、父目录或“latest”;交付和 PSP 依赖必须绑定具体构建版本目录。
成功时保存简明摘要、输出路径权限证据、kernel/rootfs 分区余量证据、固件哈希清单和精确 imgs 版本路径;失败时保存完整日志、阶段和关键错误。后续同步或编译失败不删除已经通过静态门禁的正式项目,只更新状态。不得只记录 `imgs` 软链接、父目录或“latest”;交付和 PSP 依赖必须绑定具体构建版本目录。
没有同步和编译时写:
@@ -130,4 +142,4 @@ python3 <BSP Skill>/scripts/bsp/project_guard.py audit-final \
## 最终回复
完成构建时必须显式输出:项目路径、精确 imgs 版本路径、kernel/rootfs 的镜像大小、最小对应分区大小和实际余量、余量证据路径、交接 manifest 路径、产物清单/hash 路径、`audit-final` 结果,以及硬件/负责人/发布状态。没有生成交接 manifest 时不能声称已完成 PSP 交接。
完成构建时必须显式输出:项目路径、精确 imgs 版本路径、`a+rX` 权限复核结果及证据路径、kernel/rootfs 的镜像大小、最小对应分区大小和实际余量、余量证据路径、交接 manifest 路径、产物清单/hash 路径、`audit-final` 结果,以及硬件/负责人/发布状态。没有生成交接 manifest 时不能声称已完成 PSP 交接。
@@ -0,0 +1,32 @@
# CV181X 分目录与 SDK 只读核对
这份说明只适用于项目 `buildEnvConfig.sh` 指向 Cvitek CV181X、SPI NAND,并且板级配置分在 `configs/kernel/`、`configs/uboot/` 的情况。CV184X 仍按原来的单板卡目录和 `EPSYNCSDKConfig.sh` 工作,不能套用本页的路径。
## 先确定改哪里
- `configs/uboot/<board_build_type>_defconfig` 确定板型;同目录的 `cvitek_<board_build_type>_defconfig` 和 `configs/kernel/` 中的对应 defconfig 必须成组存在。
- U-Boot 板级初始化、分区表在 `configs/uboot/`;Kernel defconfig、DTS/DTSI、`memmap.py` 在 `configs/kernel/`。先从需求和生效引用找真正的源文件,再改项目配置;不要直接改 SDK 公共默认文件。
- `CONFIG_ENABLE_FREERTOS=y` 只说明启用了 FreeRTOS 相关配置,不能单凭这一项认定交付形态是双系统。
- 参考项目 `CV1815JA_F36` 只能作为已知实现的例子,不代表所有 CV181X 产品的分区、内存、GPIO 或传感器配置。
## 需要看 SDK 时
用户必须先给出精确 SDK 根目录。执行只读检查:
```bash
python3 <BSP Skill>/scripts/bsp/inspect_cv181x_sdk.py \
--project <EmxRes 中的项目绝对路径> \
--sdk-root <用户明确指定的 SDK 根目录绝对路径>
```
脚本先核对 `EP_CHIP`、SDK 版本、分目录布局和已知同步脚本,再比较项目文件与 SDK 对应文件。报告中的 `sdk_copy_differs` 表示当前内容不一致,不说明哪一份正确;`shared_sdk_target` 表示 SDK 板型文件实际是指向公共默认文件的软链接,后续复制可能影响别的板型;`generated_config_differs` 表示 SDK `build/.config` 与项目板级配置不一致,也可能只是 SDK 上次用于别的项目。对这些提示先确认使用的是哪个 SDK 和哪个构建版本,不擅自同步。
尤其要核对统一构建脚本里 `defconfig` 与 `Ep_Uboot_Config_Func`、`Ep_Kernel_Config_Func` 的先后。若先生成 `.config` 再同步项目配置,本次顶层配置可能仍是旧值;脚本会提示顺序风险,但不会代替真实构建。`ep_kernel.sh`、`ep_uboot.sh` 末尾若固定 `exit 0`,也不能只凭返回码宣称所有文件复制成功,应逐项核对源与 SDK 目标。
## 形成方案时要核对的关联项
- 改内存:从项目 `memmap.py` 追到 SDK 当前目标和生成结果,说明 ION/CMA/RTOS 占用及总量;不要把 F36 的数值复制给别的板。
- 改分区:把项目 XML、SDK 当前表、U-Boot 分区参数、rootfs A/B 实际 MTD 编号和最终镜像大小逐项对齐。若脚本固定按 MTD4/7 操作,分区顺序变化可能导致升级写错位置;未经确认不能自动调整。
- 改驱动或传感器:确认 Kernel DTS、GPIO 初始化、相关模块构建开关(例如 `BUILD_SDK_DRIVER`)、镜像是否打包驱动,以及用户态是否还依赖原传感器。只改一处不能声称功能完成。
只读核对的结论上限是“发现项目与当前 SDK 的差异或风险”。未运行统一构建时不能写“构建通过”;未上板时不能写“硬件功能正常”。SDK 同步、构建、设备操作和发布仍遵守原有审批与证据规则。
@@ -0,0 +1,29 @@
# CV184X 板级配置与构建链
适用于 `EP_CHIP=CV184*` 且项目采用 `configs/<board_build_type>/` 的 EmxRes 项目。CV181X 的 `configs/kernel/`、`configs/uboot/` 是另一条流程,不能混用。先看项目 `buildEnvConfig.sh`、板级目录和 `configs/EPSYNCSDKConfig.sh`,再从 `EmxRes/tools/EMX2_BuildSDKTool` 确认实际走的是 `BuildCvitek184XSDKTool.sh`;不要凭项目名猜板型。
## 配置如何生效
专用工具按 `EP_CHIP` 和 `EP_FLASH_TYPE` 选定 `CV_BUILD_TYPE`。`EPSYNCSDKConfig.sh` 将项目单板卡目录用 `rsync -av` 写到 SDK `build/boards/cv184x/<CV_BUILD_TYPE>/`,然后工具执行 SDK `defconfig`、`clean_all` 和分项构建。先核对工具选出的 build type 与项目目录一致;若不一致,停止,不让同步脚本猜目标。同步脚本末尾固定 `exit 0`,单看脚本退出码不够,需比较每个映射文件及实际构建日志。
项目板卡目录至少应结合真实需求检查顶层 `<board>_defconfig`、Kernel/U-Boot defconfig、`dts_arm/<board>.dts`、`u-boot/cvi_board_init.c` 与分区 XML。顶层 defconfig 经 `build/common_functions.sh:defconfig` 生成 SDK `build/.config`;`build/envsetup_soc.sh` 根据该配置选实际生效的分区 XML。`CONFIG_DUAL_OS` 还影响 `build/Makefile` 选用哪组模块加载顺序。DTS 的 GPIO 使用要追到 `cv184x_base_arm.dtsi`、封装 DTSI、存储 DTSI、`cv184x_default_memmap.dtsi` 以及最终 DTB;只检查项目顶层 DTS 不足以证明引脚已释放。媒体内存还要看 `cvi_board_memmap.h` 的生成值、Kernel reserved-memory 和 RTOS 占用。
在当前专用工具里,`all` 会进入驱动分支;该分支直接调用 `build_osdrv`、`build_libsophon` 和 `pack_rootfs`,并没有依据项目 `BUILD_SDK_DRIVER=n` 跳过这些步骤。项目设置了这个变量也不能据此断言 SDK 驱动未构建。最终 rootfs 还会从 SDK 目录外的 `CV184X/rootfs/<Flash>/` 打包;排查镜像内容时需要把这条独立输入链单独核对。
## 用户给出 SDK 路径后的只读核对
```bash
python3 <BSP Skill>/scripts/bsp/inspect_cv184x_sdk.py \
--project <EmxRes 项目绝对路径> \
--sdk-root <用户明确指定的 SDK 根目录绝对路径>
```
检查器核对工具实际选择的板型、同步顺序、项目到 SDK 的文件映射、当前 `build/.config`、SDK 子仓库的已跟踪文件状态,并报告补丁强制重置风险。`sdk_copy_differs` 只说明当前磁盘内容不同;SDK 可能上次构建了别的项目,不能自动判断谁应覆盖谁。检查器不运行同步、补丁检查、构建或设备操作,也不证明最终镜像用的就是当前项目配置。
例如 `CV1841CP-LY-SM-YT70_dualos` 使用 `cv1841cp_wevb_0015a_spinand`。截至本次分析,项目与 SDK 当前的顶层 defconfig、DTS、分区 XML、U-Boot 初始化文件内容不同;项目分区的 APP/APP2 为 32768 KiB,SDK 当前表为 19456 KiB 并另有 DATA 分区。这是“当前 SDK 快照不可当作该项目已同步基线”的证据,不是要把 SDK 改成项目值的授权。其他 CV184X 项目不能套用这组容量。
## 编译前的补丁门禁
`BuildCvitek184XSDKTool.sh` 在同步配置之前先运行 `cvitek_cv184x_patch/scripts/repos --check-env`;若失败,会自动调用 `--applypatch --force`。该补丁工具在部分仓库有未提交的已跟踪改动或提交不匹配时,可能运行 `git reset --hard`。SDK 顶层本身不是一个 Git 仓库,而是多个子仓库;需要逐个确认补丁作用范围、相关仓库状态和 `--check-env` 的真实结果。只读文件比对不能代替补丁检查。若检查不通过或会进入强制重置分支,停止自动构建,说明哪些改动可能丢失,再请用户决定;不要自行运行 `--applypatch --force`。
`EmxRes/tools/BuildGitCheck.sh` 会执行远端 `git fetch origin`,但对非 Git 顶层目录的错误不一定可靠传播,因此它显示“检查通过”不能单独证明所有 SDK 子仓库干净或版本一致。构建成功之后仍要核对实际生效分区表、`kernel.bin`/`rootfs.bin` 余量,以及 `fip.bin`、`boot.bin`、`yoc.bin`、驱动和 rootfs 是否按需求出现。单凭配置同步、命令零退出码或某个文件存在,不能声称 BSP 已通过板上验证。
@@ -24,6 +24,12 @@
该模式不会因为禁止人工读取 SDK 而阻塞项目创建,但会降低 DTS 结论的验证等级。
## CV181X 定点 SDK 只读核对(可选)
仅当用户明确提供 CV181X SDK 的精确根目录时使用。按 [CV181X 分目录与 SDK 只读核对](cv181x-sdk-readonly-review.md)比较项目与 SDK 的已知文件映射、生成配置及软链接目标;只访问映射到的文件,不遍历或搜索整个 SDK。结论仅代表当前磁盘快照,不意味着同步、构建或板上验证通过。若同步脚本或布局不符合已知契约,停止检查并报告,不猜测映射。
CV184X 有独立的 [单目录 SDK 只读核对](cv184x-sdk-readonly-review.md)。同样需要用户明确给出 SDK 根目录;只读核对不会运行 `EPSYNCSDKConfig.sh`、补丁脚本或构建。
## 完整 DTS 审核模式
仅在用户授权读取精确 SDK 基线路径时使用:
@@ -28,8 +28,11 @@
1. 将 `~` 解析为执行任务的当前用户主目录,确认参考项目是 `EmxRes` 的直接子目录,目标也是新的直接子目录。
2. 检查目标正式目录和同名残留目录;正式目录已存在时停止,不覆盖。
3. 先运行 `scripts/bsp/bsp_preflight.py project`,再只读取报告定位的 `buildEnvConfig.sh`、配置同步脚本和实际配置目录,发现精确芯片、SDK、Flash、架构、单/双系统形态和板卡 build type。
CV184X 保持单板卡目录;CV181X 必须分别检查 `configs/kernel/` 和 `configs/uboot/`,并以 U-Boot 板级 defconfig 及两侧同名 `cvitek_` defconfig 确定 build type。`CONFIG_ENABLE_FREERTOS=y` 不能单独当作双系统证据。结构不匹配时停止,不自动转换。
4. GPIO 任务按 [内置 GPIO 资料目录](builtin-gpio-pinout.md)从 `buildEnvConfig.sh` 的 `EP_CHIP` 自动选表并查询全部目标信号;将结果保存为输入证据。目录名与 `EP_CHIP` 冲突时以后者为芯片配置事实并报告命名风险,原理图芯片与 `EP_CHIP` 冲突时停止。
5. 按 [调查范围与验证模式](scope-and-validation-modes.md)执行 GPIO/DTS 检查。默认 EmxRes 配置模式不人工搜索 SDK;完整 DTS 审核模式只读取精确解析出的 include 链。
若用户另行指定精确 CV181X SDK 根目录,可按 [CV181X 分目录与 SDK 只读核对](cv181x-sdk-readonly-review.md)执行文件映射核对;该检查不授予同步或编译权限。
CV184X 项目可按 [CV184X 板级配置与构建链](cv184x-sdk-readonly-review.md)核对单目录映射、真实 build type、生成配置和补丁副作用;用户未指定 SDK 路径时仅分析 EmxRes 和 tools。
6. 默认要求参考与目标使用相同精确芯片、SDK、Flash 和系统形态。不同芯片或布局转换不是少量适配,要求更接近的参考项目或新的工作包。
7. 只读检查 `EmxRes` Git 状态。参考项目自身包含已修改或未跟踪文件时,列出并要求确认复制当前磁盘状态;其他项目的改动只记录、不阻塞。
8. 扫描软链接。允许保留项目内相对链接;约定的 `imgs` 链接按第 6 节处理;其他指向 `EmxRes` 外部的链接要求确认。
@@ -139,4 +142,4 @@ python3 scripts/bsp/project_guard.py finalize \
任务已要求构建产物时,正式项目静态检查通过后自动进入标准全量编译。预检失败、输出版本冲突、统一脚本缺失、工作区出现重叠改动、命令返回失败或产物检查失败时停止,不自动覆盖、清理或重试;记录证据后请用户决定如何处理。
完整编译成功后还要按分区表验证唯一 `kernel.bin`、`rootfs.bin` 均至少保留 512 KiB 分区余量,再记录精确 imgs 版本目录和 BSP→PSP 交接 manifest;未执行或未通过该检查,以及仅有 `imgs` 软链接或“最新目录”,都不能作为下游基线。
完整编译成功后先把本次精确 imgs 版本路径按 `a+rX` 处理并复核,确保其他用户和组能够读取、进入该路径;再按分区表验证唯一 `kernel.bin`、`rootfs.bin` 均至少保留 512 KiB 分区余量,最后记录精确 imgs 版本目录和 BSP→PSP 交接 manifest。权限或余量检查未执行、未通过,或者只有 `imgs` 软链接或“最新目录”,都不能作为下游基线。
@@ -49,6 +49,64 @@ def detects_dual_os(config_dir: Path) -> bool:
return False
def resolve_board_layout(project: Path, chip: str | None = None) -> dict[str, object]:
"""Keep the existing CV184X layout; recognize CV181X split configuration."""
config_dirs = config_directories(project)
if chip and chip.upper().startswith("CV181"):
kernel = project / "configs" / "kernel"
uboot = project / "configs" / "uboot"
if {path.name for path in config_dirs} != {"kernel", "uboot"}:
return {
"board_build_type": None,
"system_form": None,
"config_directory": None,
"layout": "unknown",
"error": "CV181X 项目需要 configs/kernel 和 configs/uboot 两个配置目录",
}
candidates = sorted(
path for path in uboot.glob("*_defconfig")
if path.is_file() and not path.is_symlink() and not path.name.startswith("cvitek_")
)
if len(candidates) == 1:
build_type = candidates[0].name.removesuffix("_defconfig")
sdk_config = uboot / f"cvitek_{build_type}_defconfig"
kernel_config = kernel / f"cvitek_{build_type}_defconfig"
if (sdk_config.is_file() and not sdk_config.is_symlink()
and kernel_config.is_file() and not kernel_config.is_symlink()):
return {
"board_build_type": build_type,
"system_form": "dual-os" if detects_dual_os(uboot) else "single-os",
"config_directory": str(uboot),
"layout": "split-kernel-uboot",
"error": None,
}
return {
"board_build_type": None,
"system_form": None,
"config_directory": None,
"layout": "split-kernel-uboot",
"error": "CV181X kernel/uboot 结构未能唯一定位完整的板级 defconfig 组",
}
if len(config_dirs) == 1 and config_dirs[0].name not in {"kernel", "uboot"}:
config_dir = config_dirs[0]
return {
"board_build_type": config_dir.name,
"system_form": "dual-os" if detects_dual_os(config_dir) else "single-os",
"config_directory": str(config_dir),
"layout": "board-directory",
"error": None,
}
return {
"board_build_type": None,
"system_form": None,
"config_directory": None,
"layout": "unknown",
"error": f"无法识别配置布局:configs/ 下有 {[path.name for path in config_dirs]}",
}
def project_preflight(reference_name: str, target_name: str, root: Path) -> dict[str, object]:
errors: list[str] = []
warnings: list[str] = []
@@ -64,13 +122,19 @@ def project_preflight(reference_name: str, target_name: str, root: Path) -> dict
errors.append(f"缺少 buildEnvConfig.sh: {build_env}")
else:
assignments = parse_assignments(build_env)
config_dirs = config_directories(reference)
if len(config_dirs) != 1:
errors.append(f"参考项目必须唯一定位一个配置目录,实际为 {len(config_dirs)} 个")
config_dir = config_dirs[0] if len(config_dirs) == 1 else None
sync_script = reference / "configs" / "EPSYNCSDKConfig.sh"
if not sync_script.is_file():
warnings.append(f"未找到项目同步脚本: {sync_script}")
layout = resolve_board_layout(reference, assignments.get("EP_CHIP"))
if layout["error"]:
errors.append(str(layout["error"]))
if layout["layout"] == "split-kernel-uboot":
sync_scripts = [reference / "configs" / part / script for part, script in (
("kernel", "ep_kernel.sh"), ("uboot", "ep_uboot.sh"))]
sync_script = None
else:
sync_script = reference / "configs" / "EPSYNCSDKConfig.sh"
sync_scripts = [sync_script]
for path in sync_scripts:
if not path.is_file() or path.is_symlink():
warnings.append(f"未找到普通项目同步脚本: {path}")
imgs = reference / "imgs"
imgs_target = os.readlink(imgs) if imgs.is_symlink() else None
@@ -94,13 +158,15 @@ def project_preflight(reference_name: str, target_name: str, root: Path) -> dict
"architecture": assignments.get("EP_ARCH"),
"flash": assignments.get("EP_FLASH_TYPE"),
"nand_flash_id": assignments.get("NAND_FLASH_ID"),
"board_build_type": config_dir.name if config_dir else None,
"system_form": "dual-os" if config_dir and detects_dual_os(config_dir) else "single-os",
"board_build_type": layout["board_build_type"],
"system_form": layout["system_form"],
"config_layout": layout["layout"],
},
"bounded_paths": {
"build_env": str(build_env),
"config_directory": str(config_dir) if config_dir else None,
"sync_script": str(sync_script),
"config_directory": layout["config_directory"],
"sync_script": str(sync_script) if sync_script else None,
"sync_scripts": [str(path) for path in sync_scripts],
"reference_imgs_target": imgs_target,
"target_imgs_target": project_guard.expected_imgs_target(target_name),
},
@@ -115,6 +181,14 @@ def build_preflight(project_name: str, version: str, root: Path, output_root: Pa
if not VERSION_RE.fullmatch(version):
errors.append("构建版本必须为 YYYYMMDD_N,且 N 为正整数")
build_env = project / "buildEnvConfig.sh"
assignments = parse_assignments(build_env) if build_env.is_file() and not build_env.is_symlink() else {}
if not assignments.get("EP_CHIP"):
errors.append(f"构建前无法确认 EP_CHIP: {build_env}")
layout = resolve_board_layout(project, assignments.get("EP_CHIP"))
if layout["error"]:
errors.append(str(layout["error"]))
completion_script = root / "tools" / "script_completion.sh"
if not completion_script.is_file():
errors.append(f"统一构建入口不存在: {completion_script}")
@@ -137,6 +211,9 @@ def build_preflight(project_name: str, version: str, root: Path, output_root: Pa
f"EMX2_BuildSDKTool {project_name} {version} all"
)
warnings.append("统一构建可能同步配置到项目外 SDK,并按工具实现执行 git fetch origin")
requires_patch_precheck = assignments.get("EP_CHIP", "").upper().startswith("CV184")
if requires_patch_precheck:
warnings.append("CV184X 构建补丁检查失败可能触发 --applypatch --force;本预检未验证补丁环境,不能单凭 passed=true 自动构建")
return {
"schema_version": 1,
"kind": "build-preflight",
@@ -145,6 +222,9 @@ def build_preflight(project_name: str, version: str, root: Path, output_root: Pa
"warnings": warnings,
"project": project_name,
"project_path": str(project),
"config_layout": layout["layout"],
"board_build_type": layout["board_build_type"],
"requires_patch_precheck": requires_patch_precheck,
"build_version": version,
"working_directory": str(root),
"completion_script": str(completion_script),
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""Read-only, bounded comparison of a CV181X EmxRes project and an explicit SDK tree."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
import bsp_preflight
import project_guard
CONFIG_RE = re.compile(r"^(CONFIG_[A-Za-z0-9_]+)=(.+)$", re.MULTILINE)
CONFIG_PREFIXES = (
"CONFIG_CHIP_", "CONFIG_BOARD_", "CONFIG_DDR_CFG_", "CONFIG_TOOLCHAIN_",
"CONFIG_STORAGE_TYPE_", "CONFIG_NANDFLASH_",
)
CONFIG_KEYS = {"CONFIG_ARCH", "CONFIG_SDK_VER", "CONFIG_ENABLE_FREERTOS", "CONFIG_BOOT_IMAGE_SINGLE_DTB"}
def selected_config(path: Path) -> dict[str, str]:
if not path.is_file() or path.is_symlink():
return {}
return {
key: value for key, value in CONFIG_RE.findall(path.read_text(encoding="utf-8"))
if key in CONFIG_KEYS or key.startswith(CONFIG_PREFIXES)
}
def sync_scripts_match(project: Path) -> bool:
kernel = project / "configs/kernel/ep_kernel.sh"
uboot = project / "configs/uboot/ep_uboot.sh"
if any(not path.is_file() or path.is_symlink() for path in (kernel, uboot)):
return False
kernel_lines = {line.strip() for line in kernel.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")}
uboot_lines = {line.strip() for line in uboot.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")}
return all(token in kernel_lines for token in (
"EP_KERNEL_BUILD_TOP=build/boards/cv181x/$CV_BUILD_TYPE",
"EP_KERNEL_BASE_DTSI=build/boards/default/dts/cv181x",
"cp $EP_KERNEL_CONFIGS_PATH/memmap.py $EP_KERNEL_BUILD_TOP/memmap.py",
"cp $EP_KERNEL_CONFIGS_PATH/cvitek_$CV_BUILD_TYPE\"_defconfig\" $EP_KERNEL_BUILD_TOP/linux/cvitek_$CV_BUILD_TYPE\"_defconfig\"",
"cp $EP_KERNEL_CONFIGS_PATH/*.dts $EP_KERNEL_BUILD_TOP/dts_arm/",
"cp $EP_KERNEL_CONFIGS_PATH/*.dtsi $EP_KERNEL_BASE_DTSI/",
)) and all(token in uboot_lines for token in (
"EP_UBOOT_BUILD_TOP=build/boards/cv181x/$CV_BUILD_TYPE",
"cp $EP_UBOOT_CONFIGS_PATH/$CV_BUILD_TYPE\"_defconfig\" $EP_UBOOT_BUILD_TOP/",
"cp $EP_UBOOT_CONFIGS_PATH/cvitek_$CV_BUILD_TYPE\"_defconfig\" $EP_UBOOT_BUILD_TOP/u-boot/cvitek_$CV_BUILD_TYPE\"_defconfig\"",
"cp $EP_UBOOT_CONFIGS_PATH/partition_spinand.xml $EP_UBOOT_BUILD_TOP/partition/partition_spinand.xml",
"cp $EP_UBOOT_CONFIGS_PATH/cvi_board_init.c $EP_UBOOT_BUILD_TOP/u-boot/cvi_board_init.c",
))
def mapped_files(build_type: str, project: Path) -> list[tuple[Path, Path]]:
board = Path("build/boards/cv181x") / build_type
files = [
(Path("configs/uboot") / f"{build_type}_defconfig", board / f"{build_type}_defconfig"),
(Path("configs/uboot") / f"cvitek_{build_type}_defconfig", board / "u-boot" / f"cvitek_{build_type}_defconfig"),
(Path("configs/uboot/cvi_board_init.c"), board / "u-boot/cvi_board_init.c"),
(Path("configs/uboot/partition_spinand.xml"), board / "partition/partition_spinand.xml"),
(Path("configs/kernel/memmap.py"), board / "memmap.py"),
(Path("configs/kernel") / f"cvitek_{build_type}_defconfig", board / "linux" / f"cvitek_{build_type}_defconfig"),
]
for path in sorted((project / "configs/kernel").glob("*.dts")):
files.append((path.relative_to(project), board / "dts_arm" / path.name))
for path in sorted((project / "configs/kernel").glob("*.dtsi")):
files.append((path.relative_to(project), Path("build/boards/default/dts/cv181x") / path.name))
return files
def inspect(project: Path, sdk_root: Path) -> dict[str, object]:
if project.is_symlink() or not project.is_dir() or sdk_root.is_symlink() or not sdk_root.is_dir():
raise ValueError("项目与 SDK 必须是存在的普通目录,不能是软链接")
root = project_guard.normalize_root(project.parent)
project = project_guard.direct_child(root, project.name, "project", True)
sdk_root = sdk_root.resolve()
env_path = project / "buildEnvConfig.sh"
if not env_path.is_file() or env_path.is_symlink():
raise ValueError("项目缺少普通 buildEnvConfig.sh")
env = bsp_preflight.parse_assignments(env_path)
if not env.get("EP_CHIP", "").startswith("CV181"):
raise ValueError("本检查器仅适用于 EP_CHIP=CV181* 项目")
if env.get("EP_COMPANY") != "Cvitek" or env.get("EP_FLASH_TYPE") != "nand":
raise ValueError("本检查器目前只核对 Cvitek CV181X SPI NAND 构建链")
if not env.get("EP_SDK_VER") or sdk_root.name != env["EP_SDK_VER"]:
raise ValueError("指定 SDK 目录名与项目 EP_SDK_VER 不符")
layout = bsp_preflight.resolve_board_layout(project, env.get("EP_CHIP"))
if layout["error"] or layout["layout"] != "split-kernel-uboot":
raise ValueError(str(layout["error"] or "需要 kernel/uboot 分目录布局"))
if not sync_scripts_match(project):
raise ValueError("同步脚本与已知 CV181X 复制契约不符;不猜测 SDK 写入目标")
build_type = str(layout["board_build_type"])
alerts: list[dict[str, str]] = []
if not build_type.startswith(env["EP_CHIP"].lower() + "_") or "spinand" not in build_type:
alerts.append({"code": "board_identity", "message": "板级配置名与芯片或 NAND 类型不一致,需人工核对"})
mappings: list[dict[str, object]] = []
for source_rel, dest_rel in mapped_files(build_type, project):
source = project / source_rel
dest = sdk_root / dest_rel
resolved = dest.resolve(strict=False)
entry: dict[str, object] = {
"project_file": str(source),
"sdk_path": str(dest),
"sdk_real_path": str(resolved),
"sdk_is_symlink": dest.is_symlink(),
"project_exists": source.is_file() and not source.is_symlink(),
"sdk_exists": dest.is_file(),
"same_content": None,
}
if not resolved.is_relative_to(sdk_root):
entry["sdk_exists"] = False
alerts.append({"code": "sdk_link_escape", "message": f"SDK 目标软链接越出指定 SDK: {dest_rel}"})
elif source.is_file() and not source.is_symlink() and dest.is_file():
entry["same_content"] = project_guard.sha256_file(source) == project_guard.sha256_file(dest)
if entry["sdk_is_symlink"]:
alerts.append({"code": "shared_sdk_target", "message": f"复制可能改写公共目标: {resolved}"})
if entry["same_content"] is False:
alerts.append({"code": "sdk_copy_differs", "message": f"项目与 SDK 当前内容不同: {source_rel}"})
else:
alerts.append({"code": "mapping_missing", "message": f"项目或 SDK 文件缺失: {source_rel}"})
if entry["sdk_is_symlink"] and entry["same_content"] is None and resolved.is_relative_to(sdk_root):
alerts.append({"code": "shared_sdk_target", "message": f"复制可能改写公共目标: {resolved}"})
mappings.append(entry)
source_config = project / "configs/uboot" / f"{build_type}_defconfig"
expected = selected_config(source_config)
actual = selected_config(sdk_root / "build/.config")
differences = [
{"key": key, "project": value, "sdk_generated": actual.get(key)}
for key, value in sorted(expected.items()) if actual.get(key) != value
] if actual else []
if not actual:
alerts.append({"code": "generated_config_missing", "message": "SDK build/.config 不存在,无法核对生成配置"})
elif differences:
alerts.append({"code": "generated_config_differs", "message": "生成配置与项目板级配置存在差异;可能属于其他项目或未重新生成"})
tool = root / "tools/BuildCvitekSDKTool.sh"
order = "unknown"
if tool.is_file() and not tool.is_symlink():
content = tool.read_text(encoding="utf-8")
positions = [content.find(s) for s in ("defconfig $CV_BUILD_TYPE", "Ep_Uboot_Config_Func", "Ep_Kernel_Config_Func")]
if -1 not in positions:
order = "sync-after-defconfig" if positions[0] < positions[1] and positions[0] < positions[2] else "other"
if order == "sync-after-defconfig":
alerts.append({"code": "sync_after_defconfig", "message": "工具先生成 .config 再同步项目配置;顶层配置变更可能不在当次生效"})
return {
"schema_version": 1,
"kind": "cv181x-sdk-readonly-inspection",
"project": str(project),
"sdk_root": str(sdk_root),
"chip": env["EP_CHIP"],
"sdk_version": env.get("EP_SDK_VER"),
"board_build_type": build_type,
"build_order": order,
"mappings": mappings,
"generated_config_differences": differences,
"alerts": alerts,
"note": "只读磁盘快照;未执行同步、编译或板上验证。映射以已识别的复制脚本为前提。",
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--project", required=True, type=Path)
parser.add_argument("--sdk-root", required=True, type=Path)
args = parser.parse_args()
try:
result = inspect(args.project, args.sdk_root)
except (ValueError, OSError, UnicodeDecodeError, project_guard.GuardError) as exc:
print(json.dumps({"passed": False, "error": str(exc)}, ensure_ascii=False, indent=2))
return 2
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""Read-only CV184X project-to-SDK snapshot review; never run sync or build."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
from pathlib import Path
import bsp_preflight
import project_guard
CONFIG_RE = re.compile(r"^(CONFIG_[A-Za-z0-9_]+)=(.+)$", re.MULTILINE)
NOT_SET_RE = re.compile(r"^# (CONFIG_[A-Za-z0-9_]+) is not set$", re.MULTILINE)
TRACKED_KEYS = {"CONFIG_DUAL_OS", "CONFIG_ION_SIZE", "CONFIG_RTOS_ION_SIZE", "CONFIG_RTOS_SYS_SIZE",
"CONFIG_DRAM_SIZE", "CONFIG_ENABLE_RTT", "CONFIG_RTOS_BUILD_IN_FIP",
"CONFIG_ROOTFS_B", "CONFIG_ROOTFS_RECOVERY", "CONFIG_VENDOR_PARTITION", "CONFIG_SKIP_UBOOT"}
TRACKED_PREFIXES = ("CONFIG_CHIP_", "CONFIG_BOARD_", "CONFIG_STORAGE_TYPE_", "CONFIG_SENSOR_")
CHIP_LINE = re.compile(r'\$EP_CHIP\s*==\s*"(CV184[^"]+)"')
FLASH_LINE = re.compile(r'\$EP_FLASH_TYPE\s*==\s*"(nor|nand|emmc)"')
BOARD_LINE = re.compile(r'^\s*CV_BUILD_TYPE=([a-z0-9_]+)\s*$')
def selected_config(path: Path) -> dict[str, str]:
if not path.is_file() or path.is_symlink():
return {}
content = path.read_text(encoding="utf-8")
values = {key: "n" for key in NOT_SET_RE.findall(content)
if key in TRACKED_KEYS or key.startswith(TRACKED_PREFIXES)}
values.update({key: value for key, value in CONFIG_RE.findall(content)
if key in TRACKED_KEYS or key.startswith(TRACKED_PREFIXES)})
return values
def partition_name(config: dict[str, str], flash: str) -> str:
if config.get("CONFIG_SKIP_UBOOT") == "y":
variant = "fastboot_"
elif config.get("CONFIG_VENDOR_PARTITION") == "y":
variant = "v_"
elif config.get("CONFIG_ROOTFS_B") == "y" and config.get("CONFIG_ROOTFS_RECOVERY") == "y":
variant = "abr_"
elif config.get("CONFIG_ROOTFS_B") == "y":
variant = "ab_"
elif config.get("CONFIG_ROOTFS_RECOVERY") == "y":
variant = "ar_"
else:
variant = ""
return f"partition_{variant}{'spinand' if flash == 'nand' else 'spinor' if flash == 'nor' else flash}.xml"
def build_type_from_tool(tool: Path, chip: str, flash: str) -> tuple[str | None, str]:
if not tool.is_file() or tool.is_symlink():
return None, "unknown"
content = tool.read_text(encoding="utf-8")
found_chip: str | None = None
found_flash: str | None = None
build_type: str | None = None
for line in content.splitlines():
chip_match = CHIP_LINE.search(line)
if chip_match:
found_chip, found_flash = chip_match.group(1), None
flash_match = FLASH_LINE.search(line)
if flash_match:
found_flash = flash_match.group(1)
board_match = BOARD_LINE.match(line)
if board_match and found_chip == chip and found_flash == flash:
build_type = board_match.group(1)
if "export CV_BUILD_TYPE" in line:
break
sync_match = re.search(r"(?m)^[ \t]*Ep_Sync_SDK_Config_Func[ \t]*$", content)
defconfig_match = re.search(r"(?m)^[ \t]*defconfig[ \t]+\$CV_BUILD_TYPE[ \t]*$", content)
order = "unknown"
if sync_match and defconfig_match:
order = "sync-before-defconfig" if sync_match.start() < defconfig_match.start() else "sync-after-defconfig"
return build_type, order
def tracked_git_state(sdk_root: Path) -> list[dict[str, object]]:
result = []
for name in ("build", "cvitek_cv184x_patch", "linux_5.10", "u-boot-2021.10", "fsbl"):
path = sdk_root / name
if not (path / ".git").exists():
result.append({"repo": name, "status": "not-a-repository"})
continue
proc = subprocess.run(("git", "-C", str(path), "status", "--porcelain=v1", "--untracked-files=no"),
capture_output=True, text=True, check=False)
lines = [line for line in proc.stdout.splitlines() if line]
result.append({"repo": name, "status": "dirty" if lines else "clean" if proc.returncode == 0 else "unknown",
"tracked_change_count": len(lines), "sample": lines[:8]})
return result
def inspect(project: Path, sdk_root: Path) -> dict[str, object]:
if project.is_symlink() or not project.is_dir() or sdk_root.is_symlink() or not sdk_root.is_dir():
raise ValueError("项目与 SDK 必须是存在的普通目录,不能是软链接")
root = project_guard.normalize_root(project.parent)
project = project_guard.direct_child(root, project.name, "project", True)
sdk_root = sdk_root.resolve()
env_path = project / "buildEnvConfig.sh"
if not env_path.is_file() or env_path.is_symlink():
raise ValueError("项目缺少普通 buildEnvConfig.sh")
env = bsp_preflight.parse_assignments(env_path)
chip, flash = env.get("EP_CHIP", ""), env.get("EP_FLASH_TYPE", "")
if not chip.startswith("CV184") or env.get("EP_COMPANY") != "Cvitek":
raise ValueError("本检查器只适用于 Cvitek CV184X 项目")
if not env.get("EP_SDK_VER") or sdk_root.name != env["EP_SDK_VER"]:
raise ValueError("指定 SDK 目录名与项目 EP_SDK_VER 不符")
layout = bsp_preflight.resolve_board_layout(project, chip)
if layout["error"] or layout["layout"] != "board-directory":
raise ValueError(str(layout["error"] or "CV184X 需要单板卡配置目录"))
board = str(layout["board_build_type"])
sync = project / "configs/EPSYNCSDKConfig.sh"
if not sync.is_file() or sync.is_symlink():
raise ValueError("缺少普通 EPSYNCSDKConfig.sh")
active = {line.split("#", 1)[0].strip() for line in sync.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")}
if not {"EP_UBOOT_BUILD_TOP=build/boards/cv184x/$CV_BUILD_TYPE",
f"rsync -av $EP_CONFIGS_PATH/{board}/. $EP_UBOOT_BUILD_TOP/"} <= active:
raise ValueError("同步脚本不符合已知 CV184X 映射;不猜测 SDK 目标")
expected, order = build_type_from_tool(root / "tools/BuildCvitek184XSDKTool.sh", chip, flash)
alerts: list[dict[str, str]] = []
if expected != board:
alerts.append({"code": "build_type_mismatch", "message": f"统一脚本板型 {expected!r} 与项目目录 {board!r} 不一致"})
if order != "sync-before-defconfig":
alerts.append({"code": "build_order_unknown", "message": "无法确认先同步项目配置再生成 SDK .config"})
board_dir = project / "configs" / board
sdk_board = sdk_root / "build/boards/cv184x" / board
mappings: list[dict[str, object]] = []
for source in sorted(board_dir.rglob("*")):
if source.is_dir() and not source.is_symlink():
continue
rel = source.relative_to(board_dir)
dest = sdk_board / rel
resolved = dest.resolve(strict=False)
inside = resolved.is_relative_to(sdk_root)
entry: dict[str, object] = {"project_file": str(source), "sdk_path": str(dest),
"sdk_real_path": str(resolved), "sdk_is_symlink": dest.is_symlink(),
"same_content": None}
if source.is_symlink() or not source.is_file():
alerts.append({"code": "project_file_not_regular", "message": str(source)})
elif not inside:
alerts.append({"code": "sdk_link_escape", "message": str(dest)})
elif not dest.is_file():
alerts.append({"code": "mapping_missing", "message": str(dest)})
else:
entry["same_content"] = project_guard.sha256_file(source) == project_guard.sha256_file(dest)
if entry["same_content"] is False:
alerts.append({"code": "sdk_copy_differs", "message": str(rel)})
if dest.is_symlink() and inside:
alerts.append({"code": "shared_sdk_target", "message": str(resolved)})
mappings.append(entry)
source_cfg = selected_config(board_dir / f"{board}_defconfig")
generated_cfg = selected_config(sdk_root / "build/.config")
differences = [{"key": key, "project": value, "sdk_generated": generated_cfg.get(key, "n")}
for key, value in sorted(source_cfg.items())
if generated_cfg and generated_cfg.get(key, "n") != value]
if not generated_cfg:
alerts.append({"code": "generated_config_missing", "message": "SDK build/.config 缺失或不可读"})
elif differences:
alerts.append({"code": "generated_config_differs", "message": "当前 SDK .config 与项目配置不同,需确认它属于哪次构建"})
project_partition = partition_name(source_cfg, flash)
sdk_partition = partition_name(generated_cfg, flash) if generated_cfg else None
if not (board_dir / "partition" / project_partition).is_file():
alerts.append({"code": "project_partition_missing", "message": f"项目配置选择的分区表不存在: {project_partition}"})
if sdk_partition and not (sdk_board / "partition" / sdk_partition).is_file():
alerts.append({"code": "sdk_partition_missing", "message": f"当前 SDK 配置选择的分区表不存在: {sdk_partition}"})
git_state = tracked_git_state(sdk_root)
if any(item["status"] == "dirty" for item in git_state):
alerts.append({"code": "dirty_sdk_repositories", "message": "关键 SDK 子仓库存在未提交的已跟踪文件改动"})
patch_script = root / "tools/BuildCvitek184XSDKTool.sh"
patch_code = sdk_root / "cvitek_cv184x_patch/scripts/repos"
tool_text = patch_script.read_text(encoding="utf-8") if patch_script.is_file() else ""
patch_text = patch_code.read_text(encoding="utf-8") if patch_code.is_file() else ""
auto_reset = "--applypatch --force" in tool_text and '"git", "reset", "--hard"' in patch_text
if auto_reset:
alerts.append({"code": "patch_force_reset_possible", "message": "补丁检查失败时构建脚本可能强制重置 SDK 子仓库;需先确认补丁状态及影响"})
if env.get("BUILD_SDK_DRIVER") == "n" and "build_osdrv" in tool_text and "BUILD_SDK_DRIVER" not in tool_text:
alerts.append({"code": "driver_switch_ignored", "message": "项目设置 BUILD_SDK_DRIVER=n,但当前 CV184X 专用工具未使用此变量,需按实际构建分支核对驱动"})
return {"schema_version": 1, "kind": "cv184x-sdk-readonly-inspection",
"project": str(project), "sdk_root": str(sdk_root), "chip": chip, "flash": flash,
"board_build_type": board, "tool_build_type": expected, "build_order": order,
"mappings": mappings, "generated_config_differences": differences,
"project_partition_candidate": project_partition, "sdk_partition_candidate": sdk_partition,
"inspected_git_repositories": git_state, "alerts": alerts,
"note": "只读磁盘快照;未执行同步、补丁检查、编译或板上验证。Git 状态仅覆盖列出的关键子仓库。"}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--project", required=True, type=Path)
parser.add_argument("--sdk-root", required=True, type=Path)
args = parser.parse_args()
try:
result = inspect(args.project, args.sdk_root)
except (ValueError, OSError, UnicodeDecodeError, project_guard.GuardError) as exc:
print(json.dumps({"passed": False, "error": str(exc)}, ensure_ascii=False, indent=2))
return 2
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -8,6 +8,7 @@ import hashlib
import json
import os
import shlex
import stat
import subprocess
import tempfile
import xml.etree.ElementTree as ET
@@ -20,6 +21,9 @@ import project_guard
REQUIRED_IMAGE_MARGIN_BYTES = 512 * 1024
REQUIRED_PARTITION_IMAGES = ("kernel.bin", "rootfs.bin")
REQUIRED_DIRECTORY_ACCESS_BITS = 0o555
REQUIRED_FILE_READ_BITS = 0o444
ALL_EXECUTE_BITS = 0o111
def now_utc() -> str:
@@ -113,6 +117,119 @@ def artifact_inventory(output_directory: Path) -> tuple[list[dict[str, object]],
return artifacts, warnings
def ensure_output_permissions(
output_root: Path, output_directory: Path
) -> dict[str, object]:
"""Apply and verify a+rX on one build output path without following symlinks."""
output_root = output_root.expanduser().absolute()
if output_root.is_symlink():
raise project_guard.GuardError(f"构建输出根目录是软链接: {output_root}")
output_root = output_root.resolve(strict=True)
output_directory = output_directory.expanduser().absolute()
if not output_root.is_dir() or output_root.is_symlink():
raise project_guard.GuardError(f"构建输出根目录不是普通目录: {output_root}")
try:
relative_output = output_directory.relative_to(output_root)
except ValueError as exc:
raise project_guard.GuardError(
f"构建输出目录不在约定输出根目录内: {output_directory}"
) from exc
changes: list[dict[str, object]] = []
checked = {
"path_directories": 0,
"tree_directories": 0,
"regular_files": 0,
"symlinks_skipped": 0,
}
def apply_required_bits(path: Path, required_bits: int, kind: str) -> None:
if path.is_symlink():
raise project_guard.GuardError(f"权限检查拒绝软链接路径节点: {path}")
try:
before = stat.S_IMODE(path.stat().st_mode)
requested = before | required_bits
if requested != before:
os.chmod(path, requested)
after = stat.S_IMODE(path.stat().st_mode)
except OSError as exc:
raise project_guard.GuardError(f"无法设置构建输出权限 {path}: {exc}") from exc
if after & required_bits != required_bits:
raise project_guard.GuardError(
f"构建输出权限不满足 {kind} 要求: {path},"
f"实际 {after:04o},至少需要 {required_bits:04o}"
)
if after != before:
changes.append(
{
"path": str(path),
"kind": kind,
"before_mode": f"{before:04o}",
"after_mode": f"{after:04o}",
}
)
current = output_root
path_directories = [current]
for part in relative_output.parts:
current = current / part
path_directories.append(current)
for path in path_directories:
if not path.is_dir() or path.is_symlink():
raise project_guard.GuardError(f"构建输出路径节点不是普通目录: {path}")
apply_required_bits(path, REQUIRED_DIRECTORY_ACCESS_BITS, "path_directory")
checked["path_directories"] += 1
def walk_error(exc: OSError) -> None:
raise project_guard.GuardError(f"无法遍历构建输出目录: {exc}") from exc
for parent, directory_names, file_names in os.walk(
output_directory, followlinks=False, onerror=walk_error
):
parent_path = Path(parent)
for name in list(directory_names):
path = parent_path / name
if path.is_symlink():
directory_names.remove(name)
checked["symlinks_skipped"] += 1
continue
if not path.is_dir():
raise project_guard.GuardError(f"构建输出包含非普通目录: {path}")
apply_required_bits(path, REQUIRED_DIRECTORY_ACCESS_BITS, "tree_directory")
checked["tree_directories"] += 1
for name in file_names:
path = parent_path / name
if path.is_symlink():
checked["symlinks_skipped"] += 1
continue
if not path.is_file():
raise project_guard.GuardError(f"构建输出包含非普通文件: {path}")
before = stat.S_IMODE(path.stat().st_mode)
required = REQUIRED_FILE_READ_BITS
if before & ALL_EXECUTE_BITS:
required |= ALL_EXECUTE_BITS
apply_required_bits(path, required, "regular_file")
checked["regular_files"] += 1
return {
"schema_version": 1,
"passed": True,
"checked_at": now_utc(),
"policy": "a+rX",
"output_root": str(output_root),
"imgs_version_path": str(output_directory),
"checked": checked,
"changed_count": len(changes),
"changes": changes,
"notes": [
"目录和输出路径节点必须对所有用户可读、可进入。",
"普通文件必须对所有用户可读;只有原本可执行的文件才向所有用户补执行权限。",
"不跟随软链接,也不修改软链接目标。",
],
}
def resolve_partition_table(project: Path, requested: Path | None) -> Path:
configs = project / "configs"
if not configs.is_dir() or configs.is_symlink():
@@ -258,10 +375,14 @@ def record_handoff(
root = project_guard.normalize_root(root)
project = project_guard.direct_child(root, project_name, "project", True)
output_root = output_root.expanduser().resolve(strict=False)
output_root = output_root.expanduser().absolute()
if output_root.is_symlink():
raise project_guard.GuardError(f"构建输出根目录是软链接: {output_root}")
output_root = output_root.resolve(strict=False)
output_directory = output_root / project_name / "imgs" / build_version
if not output_directory.is_dir() or output_directory.is_symlink():
raise project_guard.GuardError(f"最终 imgs 版本目录不存在或不是普通目录: {output_directory}")
output_permission_check = ensure_output_permissions(output_root, output_directory)
artifacts, warnings = artifact_inventory(output_directory)
if not artifacts:
raise project_guard.GuardError("最终 imgs 版本目录没有非空普通文件")
@@ -272,13 +393,10 @@ def record_handoff(
build_env = project / "buildEnvConfig.sh"
assignments = bsp_preflight.parse_assignments(build_env) if build_env.is_file() else {}
config_dirs = bsp_preflight.config_directories(project)
config_dir = config_dirs[0] if len(config_dirs) == 1 else None
if len(config_dirs) != 1:
warnings.append(f"无法唯一确定 board build type:找到 {len(config_dirs)} 个配置目录")
system_form = (
"dual-os" if config_dir is not None and bsp_preflight.detects_dual_os(config_dir) else "single-os"
)
layout = bsp_preflight.resolve_board_layout(project, assignments.get("EP_CHIP"))
if layout["error"]:
raise project_guard.GuardError(str(layout["error"]))
system_form = layout["system_form"]
command = build_command or (
f"cd {shlex.quote(str(root))} && source tools/script_completion.sh && "
f"EMX2_BuildSDKTool {project_name} {build_version} all"
@@ -288,6 +406,7 @@ def record_handoff(
handoff_dir = project / "doc" / "bsp_adaptation" / "handoff"
hash_path = validation_dir / f"artifact_manifest_{build_version}.sha256"
margin_path = validation_dir / f"image_partition_margin_{build_version}.json"
permission_path = validation_dir / f"output_permissions_{build_version}.json"
hash_lines = "".join(
f"{item['sha256']} {item['relative_path']}\n" for item in artifacts
)
@@ -295,6 +414,9 @@ def record_handoff(
write_text_atomic(
margin_path, json.dumps(image_partition_check, ensure_ascii=False, indent=2) + "\n"
)
write_text_atomic(
permission_path, json.dumps(output_permission_check, ensure_ascii=False, indent=2) + "\n"
)
manifest: dict[str, object] = {
"schema_version": 1,
@@ -312,7 +434,7 @@ def record_handoff(
"sdk": assignments.get("EP_SDK_VER"),
"architecture": assignments.get("EP_ARCH"),
"flash": assignments.get("EP_FLASH_TYPE"),
"board_build_type": config_dir.name if config_dir else None,
"board_build_type": layout["board_build_type"],
"system_form": system_form,
},
"build": {
@@ -326,11 +448,13 @@ def record_handoff(
"imgs_version_path": str(output_directory),
"artifact_manifest_path": str(hash_path),
"image_partition_margin_path": str(margin_path),
"output_permissions_path": str(permission_path),
},
"artifacts": artifacts,
"validation": {
"local_build": "passed",
"image_partition_margin": image_partition_check,
"output_permissions": output_permission_check,
"hardware": "pending",
"owner_acceptance": "pending",
"release": "not-authorized",
@@ -342,7 +466,7 @@ def record_handoff(
"immutable_artifacts": True,
"verify_sha256_before_use": True,
"select_latest_directory": False,
"baseline_change_rule": "Any project, build version, artifact path/size/hash, partition table/hash, or image-margin change invalidates downstream PSP validation.",
"baseline_change_rule": "Any project, build version, artifact path/size/hash, partition table/hash, output-permission evidence, or image-margin change invalidates downstream PSP validation.",
},
}
handoff_path = handoff_dir / "psp_handoff.json"
@@ -358,6 +482,8 @@ def record_handoff(
"psp_handoff_path": str(handoff_path),
"image_partition_margin_path": str(margin_path),
"image_partition_margin": image_partition_check,
"output_permissions_path": str(permission_path),
"output_permissions": output_permission_check,
"hardware_validation": "pending",
"owner_acceptance": "pending",
"release": "not-authorized",
@@ -371,6 +497,8 @@ def record_handoff(
"artifact_manifest_path": str(hash_path),
"image_partition_margin_path": str(margin_path),
"image_partition_margin": image_partition_check,
"output_permissions_path": str(permission_path),
"output_permissions": output_permission_check,
"psp_handoff_path": str(handoff_path),
"status_path": str(status_path),
"warnings": warnings,
@@ -3,8 +3,11 @@
import hashlib
import json
import os
import stat
import tempfile
from pathlib import Path
from unittest.mock import patch
import bsp_preflight
import project_guard
@@ -72,6 +75,19 @@ def main() -> int:
kernel.write_bytes(b"k" * (512 * 1024))
rootfs = output / "rootfs.bin"
rootfs.write_bytes(b"r" * (256 * 1024))
helper_dir = output / "tools"
helper_dir.mkdir()
helper = helper_dir / "inspect.sh"
helper.write_text("#!/bin/sh\n", encoding="utf-8")
os.chmod(output_root, 0o700)
os.chmod(output_root / reference.name, 0o700)
os.chmod(output_root / reference.name / "imgs", 0o700)
os.chmod(output, 0o700)
os.chmod(helper_dir, 0o700)
os.chmod(boot, 0o600)
os.chmod(kernel, 0o640)
os.chmod(rootfs, 0o600)
os.chmod(helper, 0o700)
conflict = bsp_preflight.build_preflight(
reference.name, "20260902_1", root, output_root
)
@@ -95,6 +111,21 @@ def main() -> int:
assert recorded["passed"] is True, recorded
assert recorded["imgs_version_path"] == str(output.resolve())
assert recorded["image_partition_margin"]["passed"] is True
assert recorded["output_permissions"]["passed"] is True
assert recorded["output_permissions"]["policy"] == "a+rX"
assert recorded["output_permissions"]["changed_count"] >= 1
for directory in (
output_root,
output_root / reference.name,
output_root / reference.name / "imgs",
output,
helper_dir,
):
assert stat.S_IMODE(directory.stat().st_mode) & 0o555 == 0o555
for artifact in (boot, kernel, rootfs, helper):
assert stat.S_IMODE(artifact.stat().st_mode) & 0o444 == 0o444
assert stat.S_IMODE(boot.stat().st_mode) & 0o111 == 0
assert stat.S_IMODE(helper.stat().st_mode) & 0o111 == 0o111
handoff_path = Path(recorded["psp_handoff_path"])
handoff = json.loads(handoff_path.read_text(encoding="utf-8"))
assert handoff["handoff_type"] == "bsp-to-psp"
@@ -109,10 +140,24 @@ def main() -> int:
assert kernel_check["actual_margin_bytes"] == 512 * 1024
assert kernel_check["effective_partition_size_bytes"] == 1024 * 1024
assert Path(recorded["image_partition_margin_path"]).is_file()
assert Path(recorded["output_permissions_path"]).is_file()
assert handoff["validation"]["output_permissions"]["passed"] is True
assert handoff["build"]["output_permissions_path"] == recorded["output_permissions_path"]
assert handoff["consumer_contract"]["select_latest_directory"] is False
assert handoff["artifacts"][0]["sha256"] == hashlib.sha256(boot.read_bytes()).hexdigest()
assert handoff["validation"]["hardware"] == "pending"
linked_output_root = Path(tmp) / "linked-output"
linked_output_root.symlink_to(output_root, target_is_directory=True)
try:
record_build_handoff.record_handoff(
reference.name, "20260902_1", 0, True, root, linked_output_root, None, []
)
except project_guard.GuardError as exc:
assert "输出根目录是软链接" in str(exc)
else:
raise AssertionError("linked output root must not be granted permissions")
overflow_output = output_root / reference.name / "imgs" / "20260902_2"
overflow_output.mkdir(parents=True)
(overflow_output / "boot.bin").write_bytes(b"boot")
@@ -141,6 +186,41 @@ def main() -> int:
/ "image_partition_margin_20260902_2.json"
).exists()
denied_output = output_root / reference.name / "imgs" / "20260902_3"
denied_output.mkdir(parents=True)
denied_boot = denied_output / "boot.bin"
denied_boot.write_bytes(b"boot")
(denied_output / "kernel.bin").write_bytes(b"k" * (512 * 1024))
(denied_output / "rootfs.bin").write_bytes(b"r" * (256 * 1024))
os.chmod(denied_boot, 0o600)
with patch.object(
record_build_handoff.os,
"chmod",
side_effect=PermissionError(13, "permission denied"),
):
try:
record_build_handoff.record_handoff(
reference.name,
"20260902_3",
0,
True,
root,
output_root,
None,
[],
)
except project_guard.GuardError as exc:
assert "无法设置构建输出权限" in str(exc)
else:
raise AssertionError("permission failure must block BSP handoff")
assert not (
reference
/ "doc"
/ "bsp_adaptation"
/ "validation"
/ "output_permissions_20260902_3.json"
).exists()
original_partition = (
reference
/ "configs"
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Regression checks for CV181X split layout and opt-in read-only SDK mapping."""
from pathlib import Path
from tempfile import TemporaryDirectory
import bsp_preflight
import inspect_cv181x_sdk
def write(path: Path, value: str = "test\n") -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value, encoding="utf-8")
def main() -> int:
with TemporaryDirectory() as temporary:
root = Path(temporary) / "EmxRes"
root.mkdir()
board = "cv1815ja_wevb_0020a_spinand"
project = root / "CV1815JA-TEST"
project.mkdir()
write(project / "buildEnvConfig.sh", "EP_CHIP=CV1815JA\nEP_COMPANY=Cvitek\nEP_FLASH_TYPE=nand\nEP_SDK_VER=v4.1.6_beta\n")
write(project / "configs/uboot" / f"{board}_defconfig", "CONFIG_ENABLE_FREERTOS=y\nCONFIG_CHIP_CV1815JA=y\n")
write(project / "configs/uboot" / f"cvitek_{board}_defconfig")
write(project / "configs/kernel" / f"cvitek_{board}_defconfig")
write(project / "configs/uboot/cvi_board_init.c")
write(project / "configs/uboot/partition_spinand.xml")
write(project / "configs/kernel/memmap.py")
write(project / "configs/kernel" / f"{board}.dts")
write(project / "configs/kernel/cv181x_base.dtsi")
write(project / "configs/kernel/ep_kernel.sh", "\n".join((
"EP_KERNEL_BUILD_TOP=build/boards/cv181x/$CV_BUILD_TYPE",
"EP_KERNEL_BASE_DTSI=build/boards/default/dts/cv181x",
"cp $EP_KERNEL_CONFIGS_PATH/memmap.py $EP_KERNEL_BUILD_TOP/memmap.py",
'cp $EP_KERNEL_CONFIGS_PATH/cvitek_$CV_BUILD_TYPE"_defconfig" $EP_KERNEL_BUILD_TOP/linux/cvitek_$CV_BUILD_TYPE"_defconfig"',
"cp $EP_KERNEL_CONFIGS_PATH/*.dts $EP_KERNEL_BUILD_TOP/dts_arm/",
"cp $EP_KERNEL_CONFIGS_PATH/*.dtsi $EP_KERNEL_BASE_DTSI/",
)))
write(project / "configs/uboot/ep_uboot.sh", "\n".join((
"EP_UBOOT_BUILD_TOP=build/boards/cv181x/$CV_BUILD_TYPE",
'cp $EP_UBOOT_CONFIGS_PATH/$CV_BUILD_TYPE"_defconfig" $EP_UBOOT_BUILD_TOP/',
'cp $EP_UBOOT_CONFIGS_PATH/cvitek_$CV_BUILD_TYPE"_defconfig" $EP_UBOOT_BUILD_TOP/u-boot/cvitek_$CV_BUILD_TYPE"_defconfig"',
"cp $EP_UBOOT_CONFIGS_PATH/partition_spinand.xml $EP_UBOOT_BUILD_TOP/partition/partition_spinand.xml",
"cp $EP_UBOOT_CONFIGS_PATH/cvi_board_init.c $EP_UBOOT_BUILD_TOP/u-boot/cvi_board_init.c",
)))
write(root / "tools/BuildCvitekSDKTool.sh", "defconfig $CV_BUILD_TYPE\nEp_Uboot_Config_Func\nEp_Kernel_Config_Func\n")
layout = bsp_preflight.resolve_board_layout(project, "CV1815JA")
assert layout["layout"] == "split-kernel-uboot", layout
assert layout["board_build_type"] == board, layout
assert layout["system_form"] == "single-os", layout
report = bsp_preflight.project_preflight(project.name, "CV1815JA-NEW", root)
assert report["passed"] is True, report
assert report["facts"]["config_layout"] == "split-kernel-uboot", report
assert len(report["bounded_paths"]["sync_scripts"]) == 2, report
assert bsp_preflight.resolve_board_layout(project, "CV1841CP")["error"]
write(root / "tools/script_completion.sh")
project.joinpath("imgs").symlink_to(f"/epbuild/EMX2.0/EmxRes/{project.name}/imgs")
build_report = bsp_preflight.build_preflight(project.name, "20260923_1", root, Path(temporary) / "output")
assert build_report["passed"] is True, build_report
assert build_report["config_layout"] == "split-kernel-uboot", build_report
assert build_report["requires_patch_precheck"] is False, build_report
sdk = Path(temporary) / "v4.1.6_beta"
for source_rel, dest_rel in inspect_cv181x_sdk.mapped_files(board, project):
write(sdk / dest_rel, (project / source_rel).read_text(encoding="utf-8"))
shared = sdk / "build/boards/default/memmap/shared.py"
write(shared, "test\n")
memmap = sdk / "build/boards/cv181x" / board / "memmap.py"
memmap.unlink()
memmap.symlink_to(shared)
write(sdk / "build/.config", "CONFIG_CHIP_CV1815JA=n\n")
result = inspect_cv181x_sdk.inspect(project, sdk)
codes = {item["code"] for item in result["alerts"]}
assert {"shared_sdk_target", "generated_config_differs", "sync_after_defconfig"} <= codes, codes
assert len(result["mappings"]) == 8, result
try:
inspect_cv181x_sdk.inspect(project, Path(temporary))
except ValueError as exc:
assert "EP_SDK_VER" in str(exc)
else:
raise AssertionError("mismatched SDK root must fail")
(project / "configs/kernel" / f"cvitek_{board}_defconfig").unlink()
assert bsp_preflight.resolve_board_layout(project, "CV1815JA")["error"]
assert bsp_preflight.build_preflight(project.name, "20260923_2", root, Path(temporary) / "output")["passed"] is False
cv184 = root / "CV1841CP-TEST"
write(cv184 / "configs/cv1841cp_wevb_0015a_spinand/board_defconfig")
old = bsp_preflight.resolve_board_layout(cv184, "CV1841CP")
assert old["layout"] == "board-directory", old
assert old["board_build_type"] == "cv1841cp_wevb_0015a_spinand", old
assert bsp_preflight.resolve_board_layout(cv184, "CV1815JA")["error"]
write(cv184 / "buildEnvConfig.sh", "EP_CHIP=CV1841CP\n")
cv184.joinpath("imgs").symlink_to(f"/epbuild/EMX2.0/EmxRes/{cv184.name}/imgs")
cv184_build = bsp_preflight.build_preflight(cv184.name, "20260923_1", root, Path(temporary) / "output")
assert cv184_build["requires_patch_precheck"] is True, cv184_build
assert any("--applypatch --force" in item for item in cv184_build["warnings"]), cv184_build
print("CV181X layout and SDK read-only inspection tests passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Behavior tests for the bounded CV184X read-only SDK inspector."""
from pathlib import Path
from tempfile import TemporaryDirectory
import inspect_cv184x_sdk
def write(path: Path, value: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value, encoding="utf-8")
def main() -> int:
with TemporaryDirectory() as temporary:
root = Path(temporary) / "EmxRes"
board = "cv1841cp_wevb_0015a_spinand"
project = root / "CV1841CP-TEST"
sdk = Path(temporary) / "cv184x_v6.3.3"
write(project / "buildEnvConfig.sh", "EP_COMPANY=Cvitek\nEP_CHIP=CV1841CP\nEP_FLASH_TYPE=nand\nEP_SDK_VER=cv184x_v6.3.3\nBUILD_SDK_DRIVER=n\n")
write(project / "configs/EPSYNCSDKConfig.sh",
"EP_UBOOT_BUILD_TOP=build/boards/cv184x/$CV_BUILD_TYPE # known board\n"
f"rsync -av $EP_CONFIGS_PATH/{board}/. $EP_UBOOT_BUILD_TOP/\nexit 0\n")
source = project / "configs" / board
write(source / f"{board}_defconfig", "CONFIG_DUAL_OS=y\nCONFIG_ION_SIZE=0x600000\n")
write(source / "partition/partition_spinand.xml", "<physical_partition/>\n")
write(root / "tools/BuildCvitek184XSDKTool.sh",
'if [[ $EP_CHIP == "CV1841CP" ]];then\n'
' if [[ $EP_FLASH_TYPE == "nor" ]];then\n'
' CV_BUILD_TYPE=cv1841cp_wevb_0015a_spinor\n'
' elif [[ $EP_FLASH_TYPE == "nand" ]];then\n'
f' CV_BUILD_TYPE={board}\n'
' fi\nfi\nexport CV_BUILD_TYPE\n'
'Ep_Sync_SDK_Config_Func\ndefconfig $CV_BUILD_TYPE\n'
'./cvitek_cv184x_patch/scripts/repos --applypatch --force\nbuild_osdrv\n')
write(sdk / "cvitek_cv184x_patch/scripts/repos", 'run_cmd(["git", "reset", "--hard", expected_commit])\n')
write(sdk / "build/boards/cv184x" / board / f"{board}_defconfig", "CONFIG_DUAL_OS=n\n")
write(sdk / "build/boards/cv184x" / board / "partition/partition_spinand.xml", "<physical_partition/>\n")
write(sdk / "build/.config", "CONFIG_DUAL_OS=n\nCONFIG_ION_SIZE=0x600000\n")
result = inspect_cv184x_sdk.inspect(project, sdk)
assert result["tool_build_type"] == board, result
assert result["build_order"] == "sync-before-defconfig", result
assert len(result["mappings"]) == 2, result
assert result["project_partition_candidate"] == "partition_spinand.xml", result
codes = {item["code"] for item in result["alerts"]}
assert {"sdk_copy_differs", "generated_config_differs", "patch_force_reset_possible", "driver_switch_ignored"} <= codes, codes
assert "build_type_mismatch" not in codes, codes
assert inspect_cv184x_sdk.partition_name({"CONFIG_ROOTFS_B": "y"}, "nand") == "partition_ab_spinand.xml"
assert inspect_cv184x_sdk.partition_name({"CONFIG_ROOTFS_B": "y", "CONFIG_ROOTFS_RECOVERY": "y"}, "nand") == "partition_abr_spinand.xml"
write(root / "tools/BuildCvitek184XSDKTool.sh",
'if [[ $EP_CHIP == "CV1841CP" ]];then\n'
' if [[ $EP_FLASH_TYPE == "nand" ]];then\n'
' CV_BUILD_TYPE=cv1841cp_other_spinand\n'
' fi\nfi\nexport CV_BUILD_TYPE\n'
'defconfig $CV_BUILD_TYPE\nEp_Sync_SDK_Config_Func\n')
mismatch = inspect_cv184x_sdk.inspect(project, sdk)
codes = {item["code"] for item in mismatch["alerts"]}
assert {"build_type_mismatch", "build_order_unknown"} <= codes, codes
write(project / "configs/EPSYNCSDKConfig.sh", "rsync -av $EP_CONFIGS_PATH/unknown/. $EP_UBOOT_BUILD_TOP/\n")
try:
inspect_cv184x_sdk.inspect(project, sdk)
except ValueError as exc:
assert "同步脚本" in str(exc)
else:
raise AssertionError("unknown mapping must fail closed")
try:
inspect_cv184x_sdk.inspect(project, Path(temporary))
except ValueError as exc:
assert "EP_SDK_VER" in str(exc)
else:
raise AssertionError("wrong SDK root must fail")
print("CV184X read-only SDK inspection tests passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())