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