#!/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 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) 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}") 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": config_dir.name if config_dir else None, "system_form": "dual-os" if config_dir and detects_dual_os(config_dir) else "single-os", }, "bounded_paths": { "build_env": str(build_env), "config_directory": str(config_dir) if config_dir else None, "sync_script": str(sync_script), "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 为正整数") 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") return { "schema_version": 1, "kind": "build-preflight", "passed": not errors, "errors": errors, "warnings": warnings, "project": project_name, "project_path": str(project), "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())