187 lines
7.1 KiB
Python
187 lines
7.1 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 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())
|