267 lines
11 KiB
Python
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())
|