feat: strengthen BSP and PSP validation and handoff
This commit is contained in:
1 parent
bcea35a91e
commit
1641fac631
39 files changed
+2695
-90
No files matched your search
@@ -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())
|
||||
Reference in new issue
Block a user