211 lines
11 KiB
Python
211 lines
11 KiB
Python
#!/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())
|