#!/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())