Initial import

This commit is contained in:
2026-09-01 17:31:15 +08:00
commit 3ab37b7845
958 changed files with 231015 additions and 0 deletions
+622
View File
@@ -0,0 +1,622 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
###############################################################################
# @file package_release.py
# @brief 根据当前开发工程自动生成客户交付 Release 工程
# @author cyWu <1917507415@qq.com>
# @date 2026.08.04
# @version V1.0.0
# @history
# - V1.0.0, 2026.08.04, cyWu, 首次发布
# - V1.0.1, 2026.08.04, cyWu, 产物统一输出到 release/ 目录
# - V1.0.2, 2026.08.05, cyWu, 交付包命名改为 JBao_JL7016_SOC_SDK_Vx.x.x
#
# 使用方式(在项目根或任意目录):
# python tools/package_release.py
#
# 约束:
# - 仅使用 Python 标准库
# - 所有删除 / 过滤 / 复制只作用于 Release 副本
# - 绝不修改开发工程业务源码、Makefile、build_lib.bat、CodeBlocks 工程
###############################################################################
from __future__ import annotations
import fnmatch
import os
import re
import shutil
import sys
import zipfile
from datetime import datetime
from pathlib import Path
from typing import Dict, Iterable, List, Set
# =============================================================================
# 可配置区域(集中管理,扩展模块只改这里)
# =============================================================================
# 版本文件(相对项目根)
VERSION_FILE = "version.txt"
# Release 输出根目录(相对项目根,产物全部放这里)
RELEASE_OUTPUT_DIR = "release"
# Release 命名规则
RELEASE_DIR_PREFIX = "JBao_JL7016_SOC_SDK_V"
RELEASE_ZIP_SUFFIX = ".zip"
README_NAME = "JBao_JL7016_SOC_SDK_README.md"
# 工程展示名称(写入 README
PROJECT_DISPLAY_NAME = "AC701N Earphone SDK"
CHIP_DISPLAY_NAME = "AC7016CBR28"
# 复制时跳过的目录名(任意层级)
SKIP_DIR_NAMES: Set[str] = {
".git",
".vscode",
".idea",
"objs",
"obj",
"__pycache__",
RELEASE_OUTPUT_DIR, # 不把已有交付产物拷进新包
}
# 复制时跳过的根目录文件(仅项目根下,不进客户包)
SKIP_ROOT_FILES: Set[str] = {
".gitignore",
"version.txt",
}
# 复制时跳过的文件通配(任意层级,按文件名匹配)
SKIP_FILE_PATTERNS: List[str] = [
"*.o",
"*.d",
"*.dep",
"*.obj",
"*.bak",
"*.log",
"*.tmp",
"*.temp",
"*.swp",
"*.swo",
"*.orig",
"*.rej",
"*.pyc",
"*.layout",
"*.depend",
"Thumbs.db",
".DS_Store",
]
# 复制时跳过的路径前缀(相对项目根,防自复制嵌套)
SKIP_PATH_PREFIXES: List[str] = [
RELEASE_OUTPUT_DIR,
"Release_Project_V", # 兼容旧版曾放在项目根的产物
"JBao_JL7016_SOC_SDK_V", # 兼容曾放在项目根的新命名产物
]
# ---------------------------------------------------------------------------
# 需要封装交付的模块配置
# ---------------------------------------------------------------------------
# public_headers : 保留的公开头文件(仅文件名,相对模块根或子目录中同名匹配)
# remove_patterns : 删除的源码通配(递归)
# remove_internal_headers : True 时删除非 public_headers 的所有 .h/.hpp
# prune_empty_dirs : 清理后删除空目录
# remove_extra : 额外删除的相对路径(相对模块根)
# keep_extra : 额外强制保留的相对路径(文件或目录,相对模块根)
# ---------------------------------------------------------------------------
# 本产品工程 usr_le_code 已改为源码编译,交付时不再抽成 .a
MODULE_CONFIG: Dict[str, dict] = {}
# =============================================================================
# 路径管理
# =============================================================================
def get_project_root() -> Path:
"""
@brief 由脚本位置推导项目根目录(tools/ 的上一级)
@return 项目根 Path
"""
return Path(__file__).resolve().parent.parent
def read_version(project_root: Path) -> str:
"""
@brief 读取 version.txt
@param project_root 项目根
@return 版本号字符串,例如 "1.0.0"
"""
version_path = project_root / VERSION_FILE
if not version_path.is_file():
raise FileNotFoundError(f"version file not found: {version_path}")
version = version_path.read_text(encoding="utf-8").strip()
if not version:
raise ValueError(f"version file is empty: {version_path}")
# 简单校验:不允许路径分隔符,避免生成非法目录名
if any(ch in version for ch in ("/", "\\", "..")):
raise ValueError(f"invalid version string: {version!r}")
return version
def get_release_paths(project_root: Path, version: str) -> dict:
"""
@brief 统一生成 Release 相关路径(均位于 release/ 下)
@param project_root 项目根
@param version 版本号
@return 含 output_dir / dir / zip / readme 的字典
"""
dir_name = f"{RELEASE_DIR_PREFIX}{version}"
output_dir = project_root / RELEASE_OUTPUT_DIR
release_dir = output_dir / dir_name
release_zip = output_dir / f"{dir_name}{RELEASE_ZIP_SUFFIX}"
readme_path = release_dir / README_NAME
return {
"dir_name": dir_name,
"output_dir": output_dir,
"release_dir": release_dir,
"release_zip": release_zip,
"readme_path": readme_path,
}
# =============================================================================
# 复制过滤
# =============================================================================
def _match_any(name: str, patterns: Iterable[str]) -> bool:
"""
@brief 判断文件名是否匹配任一通配符
"""
return any(fnmatch.fnmatch(name, pat) for pat in patterns)
def should_skip_path(rel_path: Path) -> bool:
"""
@brief 判断相对项目根的路径在复制时是否应跳过
@param rel_path 相对路径
@return True=跳过
"""
parts = rel_path.parts
if not parts:
return False
# 跳过项目根下指定文件(如 .gitignore / version.txt
if len(parts) == 1 and parts[0] in SKIP_ROOT_FILES:
return True
# 跳过指定目录名(任意层级)
for part in parts:
if part in SKIP_DIR_NAMES:
return True
# 跳过 Release 产物路径(防嵌套)
rel_posix = rel_path.as_posix()
for prefix in SKIP_PATH_PREFIXES:
if rel_posix == prefix.rstrip("/") or rel_posix.startswith(prefix):
return True
# 跳过匹配的文件名
if _match_any(rel_path.name, SKIP_FILE_PATTERNS):
return True
# 跳过已有 zip 包(根目录下)
if len(parts) == 1 and rel_path.name.endswith(RELEASE_ZIP_SUFFIX):
if rel_path.name.startswith(RELEASE_DIR_PREFIX):
return True
return False
def copy_project(project_root: Path, release_dir: Path) -> int:
"""
@brief 过滤复制整工程到 Release 目录
@param project_root 开发工程根
@param release_dir 目标 Release 目录
@return 复制的文件数量
"""
copied = 0
for root, dirs, files in os.walk(project_root):
root_path = Path(root)
rel_root = root_path.relative_to(project_root)
# 就地过滤子目录,避免继续向下遍历
keep_dirs: List[str] = []
for d in dirs:
candidate = rel_root / d if str(rel_root) != "." else Path(d)
if should_skip_path(candidate):
continue
keep_dirs.append(d)
dirs[:] = keep_dirs
# 目标目录
dst_root = release_dir if str(rel_root) == "." else release_dir / rel_root
dst_root.mkdir(parents=True, exist_ok=True)
for name in files:
rel_file = rel_root / name if str(rel_root) != "." else Path(name)
if should_skip_path(rel_file):
continue
src = root_path / name
dst = dst_root / name
shutil.copy2(src, dst)
copied += 1
return copied
# =============================================================================
# 模块清理(仅作用于 Release 副本)
# =============================================================================
def _is_under_keep_extra(rel_posix: str, keep_extra: List[str]) -> bool:
"""
@brief 判断模块内相对路径是否属于 keep_extra 保护范围
"""
for keep in keep_extra:
keep_norm = keep.replace("\\", "/").rstrip("/")
if not keep_norm:
continue
# 目录保护:keep 以 / 结尾或配置为目录前缀
if rel_posix == keep_norm or rel_posix.startswith(keep_norm + "/"):
return True
# 兼容配置写成 "lib/" 的情况已在上方处理
if keep.endswith("/") and (rel_posix == keep_norm or rel_posix.startswith(keep_norm + "/")):
return True
return False
def _prune_empty_dirs(module_dir: Path) -> int:
"""
@brief 自底向上删除空目录
@return 删除的空目录数量
"""
removed = 0
# 深度优先:按路径长度降序
all_dirs = sorted(
(p for p in module_dir.rglob("*") if p.is_dir()),
key=lambda p: len(p.parts),
reverse=True,
)
for d in all_dirs:
try:
if not any(d.iterdir()):
d.rmdir()
removed += 1
except OSError:
pass
return removed
def clean_module(release_dir: Path, module_rel: str, cfg: dict) -> dict:
"""
@brief 按配置清理单个封装模块(仅 Release 内)
@param release_dir Release 根目录
@param module_rel 模块相对路径,如 apps/usr_le_code
@param cfg MODULE_CONFIG 中的单项配置
@return 统计信息字典
"""
module_dir = release_dir / module_rel
stats = {
"module": module_rel,
"removed_sources": 0,
"removed_headers": 0,
"removed_extra": 0,
"pruned_dirs": 0,
"missing": False,
}
if not module_dir.is_dir():
stats["missing"] = True
print(f"[WARN] module not found in Release: {module_rel}")
return stats
public_headers: Set[str] = set(cfg.get("public_headers", []))
remove_patterns: List[str] = list(cfg.get("remove_patterns", []))
remove_internal_headers: bool = bool(cfg.get("remove_internal_headers", True))
prune_empty_dirs: bool = bool(cfg.get("prune_empty_dirs", True))
remove_extra: List[str] = list(cfg.get("remove_extra", []))
keep_extra: List[str] = list(cfg.get("keep_extra", []))
# 1) 删除匹配 remove_patterns 的源文件
for path in list(module_dir.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(module_dir).as_posix()
if _is_under_keep_extra(rel, keep_extra):
continue
if _match_any(path.name, remove_patterns):
path.unlink()
stats["removed_sources"] += 1
# 2) 删除非公开头文件
if remove_internal_headers:
header_patterns = ["*.h", "*.hpp"]
for path in list(module_dir.rglob("*")):
if not path.is_file():
continue
if not _match_any(path.name, header_patterns):
continue
rel = path.relative_to(module_dir).as_posix()
if _is_under_keep_extra(rel, keep_extra):
continue
# 公开头文件按“文件名”匹配,便于配置只写文件名
if path.name in public_headers:
continue
path.unlink()
stats["removed_headers"] += 1
# 3) 删除额外指定文件/目录
for extra in remove_extra:
target = module_dir / extra
if not target.exists():
continue
if target.is_dir():
shutil.rmtree(target)
else:
target.unlink()
stats["removed_extra"] += 1
# 4) 清理空目录
if prune_empty_dirs:
stats["pruned_dirs"] = _prune_empty_dirs(module_dir)
return stats
def clean_all_modules(release_dir: Path) -> List[dict]:
"""
@brief 遍历 MODULE_CONFIG 清理所有模块
"""
results = []
for module_rel, cfg in MODULE_CONFIG.items():
print(f"[INFO] cleaning module: {module_rel}")
results.append(clean_module(release_dir, module_rel, cfg))
return results
def sanitize_release_makefile(release_dir: Path) -> None:
"""
@brief 调整 Release 副本中的 Makefile:仅链接预编译 libusr_le_code.a
开发工程 Makefile 不动;客户包不得再尝试用已删除源码重建库
本产品工程已改为源码直编,无 USR_LE_SRC_FILES 时直接跳过
@param release_dir Release 根目录
"""
makefile = release_dir / "Makefile"
if not makefile.is_file():
print("[WARN] Makefile not found in Release, skip sanitize")
return
text = makefile.read_text(encoding="utf-8")
if "USR_LE_SRC_FILES" not in text:
print("[INFO] Makefile already compiles usr_le_code from source, skip sanitize")
return
# 1) 去掉源码列表与 OBJS,仅保留预编译库路径
text2, n1 = re.subn(
r"# usr_le_code 静态库[^\n]*\n"
r"USR_LE_LIB := apps/usr_le_code/lib/libusr_le_code\.a\n"
r"USR_LE_SRC_FILES := \\\n"
r"(?:[ \t]+apps/usr_le_code/[^\n]+\n)+"
r"\n"
r"USR_LE_OBJS :=[^\n]+\n",
"# usr_le_code 预编译静态库(Release 不附带协议源码,禁止本地重建)\n"
"USR_LE_LIB := apps/usr_le_code/lib/libusr_le_code.a\n"
"\n",
text,
count=1,
)
if n1 == 0:
raise RuntimeError("sanitize Makefile failed: USR_LE_SRC_FILES block not found")
# 2) .PHONY 去掉 lib_usr_le
text2, n2 = re.subn(
r"\.PHONY:\s*all clean pre_build lib_usr_le\b",
".PHONY: all clean pre_build",
text2,
count=1,
)
if n2 == 0:
# 兼容顺序变化,尽量剥离 lib_usr_le
text2 = re.sub(r"\s+lib_usr_le\b", "", text2, count=1)
# 3) 删除 lib_usr_le 目标与 $(USR_LE_LIB): $(USR_LE_OBJS) 重建规则
# 保留 all / OUT_ELF 对 $(USR_LE_LIB) 的依赖:库文件已存在即可链接
text2, n3 = re.subn(
r"\n# 单独编译 usr_le_code 静态库:[^\n]*\n"
r"lib_usr_le:[^\n]*\n"
r"\n"
r"\$\(USR_LE_LIB\): \$\(USR_LE_OBJS\)\n"
r"(?:[ \t]+[^\n]+\n)+",
"\n",
text2,
count=1,
)
if n3 == 0:
raise RuntimeError("sanitize Makefile failed: lib_usr_le rebuild rule not found")
makefile.write_text(text2, encoding="utf-8")
print("[INFO] sanitized Release Makefile (prebuilt libusr_le_code.a only)")
# =============================================================================
# README / ZIP
# =============================================================================
def write_readme(readme_path: Path, version: str, module_stats: List[dict]) -> None:
"""
@brief 生成 JBao_JL7016_SOC_SDK_README.md
@param readme_path README 路径
@param version 版本号
@param module_stats 模块清理统计(用于目录说明)
"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines: List[str] = [
f"# {PROJECT_DISPLAY_NAME}",
"",
"## 基本信息",
"",
f"- 工程名称:{PROJECT_DISPLAY_NAME}",
f"- 版本号:V{version}",
f"- 生成时间:{now}",
f"- 适用芯片:{CHIP_DISPLAY_NAME}",
"",
"## 目录说明",
"",
"```",
f"{RELEASE_DIR_PREFIX}{version}/",
"├── apps/",
"│ ├── common/ # SDK 公共模块",
"│ ├── earphone/ # 耳机应用",
"│ ├── usr_jb_proto/ # 应用协议层",
"│ ├── usr_periph/ # 板级外设(RTC 等,源码开放)",
"│ └── usr_le_code/ # BLE 协议库(库文件 + 公开头文件)",
"├── cpu/ # 芯片相关代码与工具",
"├── include_lib/ # SDK 头文件",
"├── tools/ # 工程工具",
"├── Makefile",
"├── AC701N.cbp",
f"└── {README_NAME}",
"```",
"",
"### 封装模块公开接口",
"",
]
for module_rel, cfg in MODULE_CONFIG.items():
public_headers = cfg.get("public_headers", [])
lines.append(f"**{module_rel}**")
lines.append("")
lines.append(f"- 静态库:`{module_rel}/lib/`")
lines.append("- 公开头文件:")
for h in public_headers:
lines.append(f" - `{module_rel}/{h}`")
lines.append("")
lines.append("---")
lines.append("")
lines.append("*本文件由 tools/package_release.py 自动生成。*")
lines.append("")
readme_path.write_text("\n".join(lines), encoding="utf-8")
def make_zip(release_dir: Path, release_zip: Path) -> None:
"""
@brief 将 Release 目录压缩为 zip(与目录同级)
@param release_dir Release 目录
@param release_zip 目标 zip 路径
"""
if release_zip.exists():
release_zip.unlink()
with zipfile.ZipFile(release_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for path in release_dir.rglob("*"):
if path.is_file():
arcname = path.relative_to(release_dir.parent)
zf.write(path, arcname.as_posix())
# =============================================================================
# 主流程
# =============================================================================
def remove_old_release(release_dir: Path, release_zip: Path) -> None:
"""
@brief 删除已有同名 Release 目录与 zip
"""
if release_dir.exists():
print(f"[INFO] remove old release dir: {release_dir.name}")
shutil.rmtree(release_dir)
if release_zip.exists():
print(f"[INFO] remove old release zip: {release_zip.name}")
release_zip.unlink()
def main() -> int:
"""
@brief Release 打包主入口
@return 进程退出码,0=成功
"""
try:
project_root = get_project_root()
version = read_version(project_root)
paths = get_release_paths(project_root, version)
release_dir: Path = paths["release_dir"]
release_zip: Path = paths["release_zip"]
readme_path: Path = paths["readme_path"]
dir_name: str = paths["dir_name"]
print("=" * 60)
print(" AC701N Release Package Tool")
print("=" * 60)
output_dir: Path = paths["output_dir"]
print(f"[INFO] project root : {project_root}")
print(f"[INFO] version : {version}")
print(f"[INFO] output dir : {RELEASE_OUTPUT_DIR}/")
print(f"[INFO] release dir : {RELEASE_OUTPUT_DIR}/{dir_name}")
print()
# 确保 release/ 输出目录存在
output_dir.mkdir(parents=True, exist_ok=True)
# ① 删除旧产物
remove_old_release(release_dir, release_zip)
# ② 过滤复制
print("[INFO] copying project ...")
copied = copy_project(project_root, release_dir)
print(f"[INFO] copied files : {copied}")
# ③ 模块清理(仅 Release 副本)
print("[INFO] cleaning encapsulated modules ...")
module_stats = clean_all_modules(release_dir)
for st in module_stats:
if st["missing"]:
continue
print(
f" - {st['module']}: "
f"src={st['removed_sources']}, "
f"hdr={st['removed_headers']}, "
f"extra={st['removed_extra']}, "
f"empty_dirs={st['pruned_dirs']}"
)
# ④ Release Makefile:禁止用已删除源码重建库(开发工程 Makefile 不动)
print("[INFO] sanitizing Release Makefile ...")
sanitize_release_makefile(release_dir)
# ⑤ README
print("[INFO] writing README ...")
write_readme(readme_path, version, module_stats)
# ⑥ ZIP
print("[INFO] creating zip ...")
make_zip(release_dir, release_zip)
print()
print("=" * 60)
print("[OK] Release package generated successfully")
print(f" dir : {release_dir}")
print(f" zip : {release_zip}")
print("=" * 60)
return 0
except Exception as exc:
print(f"[FAIL] {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())