Files
firmware-lowlevel-development/skills/firmware-lowlevel-bsp-development/scripts/bsp/bsp_preflight.py
T

267 lines
11 KiB
Python

#!/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")
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",
"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": requires_patch_precheck,
"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())