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