Files
firmware-lowlevel-development/skills/firmware-lowlevel-bsp-development/scripts/bsp/bsp_preflight.py
T
tangfahong ea4ff92835 chore(release): 1.0.1
明确 BSP 的 RES 开发范围和 SDK 构建写入边界;调整 CV184X 强制补丁预检并补齐构建交接测试。
2026-10-09 18:05:07 +08:00

281 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Produce bounded BSP project and build preflight reports."""
from __future__ import annotations
import argparse
import json
import os
import re
import shlex
import sys
from pathlib import Path
import project_guard
VERSION_RE = re.compile(r"^\d{8}_[1-9]\d*$")
ASSIGNMENT_RE = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
def parse_assignments(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
for line in path.read_text(encoding="utf-8").splitlines():
match = ASSIGNMENT_RE.match(line)
if not match:
continue
value = match.group(2).strip()
if value and value[0:1] in {'"', "'"} and value[-1:] == value[0]:
value = value[1:-1]
values[match.group(1)] = value
return values
def config_directories(project: Path) -> list[Path]:
configs = project / "configs"
if not configs.is_dir():
return []
return sorted(
path
for path in configs.iterdir()
if path.is_dir() and not path.is_symlink() and not path.name.startswith(".")
)
def detects_dual_os(config_dir: Path) -> bool:
for path in sorted(config_dir.glob("*_defconfig")):
if re.search(r"^CONFIG_DUAL_OS=y\s*$", path.read_text(encoding="utf-8"), re.MULTILINE):
return True
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] = []
root = project_guard.normalize_root(root)
reference = project_guard.direct_child(root, reference_name, "reference-project", True)
target = project_guard.direct_child(root, target_name, "target-project", False)
if target.exists() or target.is_symlink():
errors.append(f"目标项目已存在: {target}")
build_env = reference / "buildEnvConfig.sh"
assignments: dict[str, str] = {}
if not build_env.is_file():
errors.append(f"缺少 buildEnvConfig.sh: {build_env}")
else:
assignments = parse_assignments(build_env)
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
if imgs_target != project_guard.expected_imgs_target(reference_name):
warnings.append("参考项目 imgs 不是约定的项目专属软链接")
return {
"schema_version": 1,
"kind": "project-preflight",
"passed": not errors,
"errors": errors,
"warnings": warnings,
"emxres_root": str(root),
"reference_project": reference_name,
"reference_path": str(reference),
"target_project": target_name,
"target_path": str(target),
"facts": {
"chip": assignments.get("EP_CHIP"),
"sdk": assignments.get("EP_SDK_VER"),
"architecture": assignments.get("EP_ARCH"),
"flash": assignments.get("EP_FLASH_TYPE"),
"nand_flash_id": assignments.get("NAND_FLASH_ID"),
"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": 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),
},
}
def build_preflight(project_name: str, version: str, root: Path, output_root: Path) -> dict[str, object]:
errors: list[str] = []
warnings: list[str] = []
root = project_guard.normalize_root(root)
project = project_guard.direct_child(root, project_name, "project", True)
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}")
elif not os.access(completion_script, os.R_OK):
errors.append(f"统一构建入口不可读: {completion_script}")
imgs = project / "imgs"
expected_imgs = project_guard.expected_imgs_target(project_name)
actual_imgs = os.readlink(imgs) if imgs.is_symlink() else None
if actual_imgs != expected_imgs:
errors.append(f"项目 imgs 必须指向 {expected_imgs},实际为 {actual_imgs!r}")
output_root = output_root.expanduser().resolve(strict=False)
output_directory = output_root / project_name / "imgs" / version
if output_directory.exists() or output_directory.is_symlink():
errors.append(f"构建输出目录已存在,禁止覆盖: {output_directory}")
command = (
f"cd {shlex.quote(str(root))} && source tools/script_completion.sh && "
f"EMX2_BuildSDKTool {project_name} {version} all"
)
warnings.append("统一构建可能同步配置到项目外 SDK,并按工具实现执行 git fetch origin")
is_cv184 = assignments.get("EP_CHIP", "").upper().startswith("CV184")
if is_cv184:
build_tool = root / "tools" / "BuildCvitek184XSDKTool.sh"
if not build_tool.is_file() or build_tool.is_symlink():
errors.append(f"CV184X 统一构建脚本缺失或不是普通文件: {build_tool}")
else:
script_lines = (
line.split("#", 1)[0]
for line in build_tool.read_text(encoding="utf-8").splitlines()
)
if any("--applypatch" in line and "--force" in line for line in script_lines):
errors.append(
f"统一构建脚本可能自动执行 --applypatch --force,禁止启动本次构建: {build_tool};"
"请由脚本所有者将该分支改为失败即退出"
)
warnings.append("CV184X 补丁完整性由统一构建脚本处理;Skill 不单独检查补丁环境")
return {
"schema_version": 1,
"kind": "build-preflight",
"passed": not errors,
"errors": errors,
"warnings": warnings,
"project": project_name,
"project_path": str(project),
"config_layout": layout["layout"],
"board_build_type": layout["board_build_type"],
"requires_patch_precheck": False,
"patch_check_owner": "native-build-script" if is_cv184 else None,
"build_version": version,
"working_directory": str(root),
"completion_script": str(completion_script),
"command": command,
"output_directory": str(output_directory),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--emxres-root", type=Path, default=project_guard.production_root())
subparsers = parser.add_subparsers(dest="command", required=True)
project_parser = subparsers.add_parser("project")
project_parser.add_argument("--reference-project", required=True)
project_parser.add_argument("--target-project", required=True)
build_parser = subparsers.add_parser("build")
build_parser.add_argument("--project", required=True)
build_parser.add_argument("--version", required=True)
build_parser.add_argument(
"--output-root", type=Path, default=Path("/epbuild/EMX2.0/EmxRes")
)
args = parser.parse_args()
try:
if args.command == "project":
result = project_preflight(
args.reference_project, args.target_project, args.emxres_root
)
else:
result = build_preflight(
args.project, args.version, args.emxres_root, args.output_root
)
except (project_guard.GuardError, OSError, UnicodeDecodeError) as exc:
result = {"passed": False, "errors": [str(exc)]}
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result.get("passed") is True else 2
if __name__ == "__main__":
raise SystemExit(main())