diff --git a/.cursor/mcp.json b/.cursor/mcp.json index be488ea..262ea08 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "esp-pilot": { "type": "http", - "url": "https://mcp.esp-pilot.espressif.tools/mcp" + "url": "https://mcp.esp-pilot.espressif.com/mcp" } } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd546b6..c4f7f28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,10 +34,24 @@ jobs: target: ${{ matrix.target }} path: ci/test_app command: | - pip install -q esp-bmgr-assist + if [ -n "${{ matrix.component_manager }}" ]; then + python -m pip install -q --upgrade "${{ matrix.component_manager }}" + fi + python -m pip install -q --upgrade esp-bmgr-assist + python -m pip show esp-bmgr-assist esp-bmgr-py idf-component-manager || true python ../scripts/pin_bmgr_version.py "${{ matrix.bmgr }}" - idf.py bmgr -b "${{ matrix.board }}" - idf.py build + idf.py bmgr -l + if python ../scripts/check_bmgr_catalog.py --chip "${{ matrix.target }}"; then + idf.py bmgr -b "${{ matrix.board }}" + idf.py build + else + status=$? + if [ "${status}" -eq 10 ]; then + echo "::notice title=Unsupported board/IDF combination::${{ matrix.board }} (${{ matrix.target }}) is not supported by Board Manager ${{ matrix.bmgr }} with this ESP-IDF version." + exit 0 + fi + exit "${status}" + fi upload: needs: test @@ -55,7 +69,7 @@ jobs: fi python3 -m pip install -q idf-component-manager compote component upload \ - --namespace "${{ github.repository_owner }}" \ + --namespace "LiuCodee" \ --name "${{ github.event.repository.name }}" \ --allow-existing \ --dry-run diff --git a/1/2/example_board2/board_devices.yaml b/1/2/example_board2/board_devices.yaml new file mode 100644 index 0000000..3b34c9a --- /dev/null +++ b/1/2/example_board2/board_devices.yaml @@ -0,0 +1,9 @@ +devices: + - name: power_control + type: gpio_ctrl + version: default + config: + active_level: 1 + default_level: 0 + peripherals: + - gpio_name: gpio_power diff --git a/1/2/example_board2/board_info.yaml b/1/2/example_board2/board_info.yaml new file mode 100644 index 0000000..9891f4a --- /dev/null +++ b/1/2/example_board2/board_info.yaml @@ -0,0 +1,5 @@ +board: example_board2 +chip: esp32s31 +version: "1.0.0" +description: "Placeholder board. Replace this directory with a real board." +manufacturer: "CHANGE_ME" diff --git a/1/2/example_board2/board_peripherals.yaml b/1/2/example_board2/board_peripherals.yaml new file mode 100644 index 0000000..e86a3c3 --- /dev/null +++ b/1/2/example_board2/board_peripherals.yaml @@ -0,0 +1,8 @@ +peripherals: + - name: gpio_power + type: gpio + role: io + config: + pin: 4 + mode: GPIO_MODE_OUTPUT + default_level: 0 diff --git a/1/example_board1/board_devices.yaml b/1/example_board1/board_devices.yaml new file mode 100644 index 0000000..3b34c9a --- /dev/null +++ b/1/example_board1/board_devices.yaml @@ -0,0 +1,9 @@ +devices: + - name: power_control + type: gpio_ctrl + version: default + config: + active_level: 1 + default_level: 0 + peripherals: + - gpio_name: gpio_power diff --git a/1/example_board1/board_info.yaml b/1/example_board1/board_info.yaml new file mode 100644 index 0000000..7ce49c2 --- /dev/null +++ b/1/example_board1/board_info.yaml @@ -0,0 +1,5 @@ +board: example_board1 +chip: esp32 +version: "1.0.0" +description: "Placeholder board. Replace this directory with a real board." +manufacturer: "CHANGE_ME" diff --git a/1/example_board1/board_peripherals.yaml b/1/example_board1/board_peripherals.yaml new file mode 100644 index 0000000..e86a3c3 --- /dev/null +++ b/1/example_board1/board_peripherals.yaml @@ -0,0 +1,8 @@ +peripherals: + - name: gpio_power + type: gpio + role: io + config: + pin: 4 + mode: GPIO_MODE_OUTPUT + default_level: 0 diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md deleted file mode 100644 index 4fa3580..0000000 --- a/GETTING_STARTED.md +++ /dev/null @@ -1,90 +0,0 @@ -# 厂家上手 - -用本模板创建自己的板组件仓库后,按下面 5 步完成第一次发布。GitHub Actions 已经写好,不必改 workflow。 - -## 开始前 - -- 用**个人 GitHub 账号**创建仓库。组件注册库的默认 namespace 就是这个 GitHub 用户名。先不要建在 GitHub Organization 下,否则用户名和 namespace 对不上,上传会失败。 -- 新仓库名必须包含 `boards`,只使用小写字母、数字、下划线,例如 `acme_boards`。ESP Board Manager 只自动扫描名称里带 `boards` 的组件。 -- 开发板目录名、`board_info.yaml` 里的 `board` 字段必须一致,同样只允许小写字母、数字、下划线,不能用中划线。 - -## 第 1 步:用模板建仓库 - -在 GitHub 上打开本模板仓库,点击 **Use this template** → **Create a new repository**,仓库名填 `yourcompany_boards`。 - -克隆到本地: - -```bash -git clone https://github.com/YOUR_GITHUB_USERNAME/yourcompany_boards.git -cd yourcompany_boards -``` - -## 第 2 步:注册组件注册库并保存 Token - -1. 用**同一个** GitHub 账号打开 [components.espressif.com](https://components.espressif.com) 并登录。 -2. 右上角用户名 → **Tokens** → **Create**,复制 Token。 -3. 打开 GitHub 仓库 **Settings** → **Secrets and variables** → **Actions** → **New repository secret**。 -4. Name 填 `IDF_COMPONENT_API_TOKEN`,Secret 填刚才的 Token。 - -不要把 Token 写进仓库文件,也不要发到聊天软件。 - -## 第 3 步:换成自己的开发板 - -1. 删除或改名 `example_board/`。 -2. 对照原理图编写 `board_info.yaml`、`board_peripherals.yaml`、`board_devices.yaml`。可用 Cursor,本仓库已预置 MCP:`https://mcp.esp-pilot.espressif.tools/mcp`。引脚和地址必须对照原理图确认。 -3. 也可使用网页工具 。 -4. 把 `LICENSE` 末尾的 `CHANGE_ME` 改成公司名,更新 `README.md` / `README_CN.md` 里的开发板列表。 -5. 需要更高版本的 Board Manager 能力时,只改根目录 `idf_component.yml` 里的: - - ```yaml - espressif/esp_board_manager: - version: ">=0.7.0" - ``` - - 把 `0.7.0` 抬到实际需要的下限。CI 会自动读这个下限,并再测当前最新 Board Manager。不要去改 `.github/workflows/ci.yml`。 - -本地先验证: - -```bash -pip install esp-bmgr-assist -cd /path/to/your_idf_project -# 在工程 main/idf_component.yml 里用 override_path 指向本仓库,然后: -idf.py bmgr -l -idf.py bmgr -b -idf.py build -``` - -## 第 4 步:合入 main - -把改动推到 `main`(或先开 Pull Request,绿了再合)。 - -CI 会对仓库里每一块开发板,在 ESP-IDF `v5.5.4` 和 `latest` 上,分别用 Board Manager 下限版本和最新版本执行 `idf.py bmgr -b` 和编译。任一路失败都不能合入,包括 `latest`。 - -新仓库第一次合入 `main` 时,当前 `version`(模板是 `0.7.0`,与 Board Manager 对齐)还不存在,会发第一次。之后同一 `version` 再合入,upload 会跳过,不会覆盖。这是故意的:允许先合入未发布的改动。 - -## 第 5 步:发布组件 - -确认要给应用使用时: - -1. 更新 `CHANGELOG.md`。 -2. 把根目录 `idf_component.yml` 的 `version` 从 `0.7.0` 改成新版本(例如 `0.7.1`)。 -3. 提交并推到 `main`。 - -上传 job 发现该版本尚未存在,就会发到: - -`https://components.espressif.com/components//<仓库名>` - -同一 `version` 不能覆盖。发错只能再发一个新版本号。 - -应用侧添加: - -```bash -idf.py add-dependency "YOUR_GITHUB_USERNAME/YOUR_REPO_NAME" -``` - -## 不要改 - -- `.github/workflows/ci.yml` -- `CMakeLists.txt`(保持 `idf_component_register()`) -- 不要把 `esp_board_manager` 源码拷进本仓库 -- 不要把原理图、大二进制放进要上传的组件包(`.github/` 和 `ci/` 已排除) diff --git a/LICENSE b/LICENSE index 86d22d8..a641b09 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2026 CHANGE_ME + Copyright 2026 My_boards Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 2878912..bd75f11 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,54 @@ -# ESP Board Manager 厂家板组件模板 +# My_boards ESP Board Manager Board Pack [中文](README_CN.md) -This repository is an ESP Board Manager **board pack**: YAML board definitions that applications can download from the [ESP Component Registry](https://components.espressif.com). +This component provides ESP Board Manager YAML definitions for My_boards +development boards. Applications can install it from the +[ESP Component Registry](https://components.espressif.com) and select a board. -## Use in an application +> Template note: Replace `My_boards` and update the following table with +> the supported boards before publishing. -1. Install the helper once in your ESP-IDF Python environment: +## Supported Boards - ```bash - pip install esp-bmgr-assist - ``` +Run `python scripts/update_supported_boards_table.py` to update the board and +chip columns. Maintainers fill in the device capability columns. -2. Add this board pack to the project (replace the namespace and name with your GitHub user name and this repository name): + +| Board | Chip | Audio | SD Card | LCD | LCD Touch | Camera | Buttons | LED Strip | Knob | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `example_board` | ESP32-S3 | | | | | | | | | +| `example_board2` | ESP32-S31 | | | | | | | | | +| `example_board1` | ESP32 | | | | | | | | | + - ```bash - idf.py add-dependency "YOUR_GITHUB_USERNAME/YOUR_REPO_NAME" - ``` +## Use This Pack in an Application - The pack already depends on `espressif/esp_board_manager` (`>=0.7.0`). You do not need to add Board Manager again unless you want a tighter pin. +Install the helper once in the active ESP-IDF Python environment: -3. List and select a board: +```bash +python -m pip install --upgrade esp-bmgr-assist +``` - ```bash - idf.py bmgr -l - idf.py bmgr -b - idf.py build - ``` +Add the released component, then select a board: -## Supported boards +```bash +idf.py add-dependency "LiuCodee/demo_boards" +idf.py bmgr -l +idf.py bmgr -b +idf.py build +``` -| Board | Chip | Notes | -|---|---|---| -| `example_board` | ESP32-S3 | Placeholder. Replace it with a real board before publishing. | +The board pack already declares `espressif/esp_board_manager`; applications do +not need to add it again unless they need a tighter version constraint. -## Create or publish this pack +## Vendor Maintenance Guide -Vendors starting from this GitHub template should follow [GETTING_STARTED.md](GETTING_STARTED.md). +For creating a repository from this template, migrating an existing BSP, +configuring CI, and publishing the component, see +[VENDOR_GUIDE.md](VENDOR_GUIDE.md). + +## License + +This component uses Apache-2.0. Preserve and comply with existing copyright +notices and licenses when migrating a BSP or adding third-party content. diff --git a/README_CN.md b/README_CN.md index 9499b45..cb90fcd 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,39 +1,46 @@ -# ESP Board Manager 厂家板组件模板 +# My_boards ESP Board Manager 板组件 [English](README.md) -本仓库是一份 ESP Board Manager **板组件**:用 YAML 描述开发板,应用可以从 [ESP 组件注册库](https://components.espressif.com) 下载后选板。 +本组件提供 My_boards 开发板的 ESP Board Manager YAML 定义,可从 [ESP 组件注册库](https://components.espressif.com) 下载并在应用工程中选择。 -## 在应用工程中使用 +> 模板提示:发布前替换 `My_boards`,并将下表更新为实际支持的开发板。 -1. 在已激活的 ESP-IDF Python 环境中安装一次辅助工具: +## 支持的开发板 - ```bash - pip install esp-bmgr-assist - ``` +运行 `python scripts/update_supported_boards_table.py` 可更新板名和芯片列;设备能力列由维护者填写。 -2. 将本板组件加入工程(把命名空间和组件名换成您的 GitHub 用户名和本仓库名): + +| 开发板名称 | 芯片 | 音频 | SD 卡 | LCD | LCD 触摸 | 摄像头 | 按键 | LED 灯带 | 旋钮 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `example_board` | ESP32-S3 | | | | | | | | | +| `example_board2` | ESP32-S31 | | | | | | | | | +| `example_board1` | ESP32 | | | | | | | | | + - ```bash - idf.py add-dependency "YOUR_GITHUB_USERNAME/YOUR_REPO_NAME" - ``` +## 在应用工程中使用本组件 - 本组件已经依赖 `espressif/esp_board_manager`(`>=0.7.0`)。除非您要收紧版本,否则不必再单独添加 Board Manager。 +在已激活的 ESP-IDF Python 环境中安装一次辅助工具: -3. 列出并选择开发板: +```bash +python -m pip install --upgrade esp-bmgr-assist +``` - ```bash - idf.py bmgr -l - idf.py bmgr -b - idf.py build - ``` +添加已发布的组件后,选择开发板: -## 支持的开发板 +```bash +idf.py add-dependency "LiuCodee/demo_boards" +idf.py bmgr -l +idf.py bmgr -b +idf.py build +``` + +本板组件已声明 `espressif/esp_board_manager` 依赖。除非应用需要更严格的版本限制,否则不必再次添加。 + +## 厂家维护指南 -| 开发板 | 芯片 | 说明 | -|---|---|---| -| `example_board` | ESP32-S3 | 占位示例。正式发布前请替换为真实开发板。 | +从本模板创建仓库、迁移现有 BSP、配置 CI 和发布组件,请参见 [VENDOR_GUIDE_CN.md](VENDOR_GUIDE_CN.md)。 -## 创建或发布本组件 +## 许可证 -从本 GitHub 模板新建仓库的厂家,请按 [GETTING_STARTED.md](GETTING_STARTED.md) 操作。 +本组件采用 Apache-2.0。迁移 BSP 或引入第三方内容时,请保留并遵守其原有版权声明和许可证。 diff --git a/VENDOR_GUIDE.md b/VENDOR_GUIDE.md new file mode 100644 index 0000000..27324f6 --- /dev/null +++ b/VENDOR_GUIDE.md @@ -0,0 +1,123 @@ +# ESP Board Manager Vendor Board Pack Guide + +[中文](VENDOR_GUIDE_CN.md) + +Use this guide to create a vendor board-pack repository from this GitHub +template, adapt boards, and configure CI. Maintain the public component +description in [README.md](README.md). + +## 1. Create and Configure the Repository + +1. Open the [template repository](https://github.com/LiuCodee/boards-template) + on GitHub and select **Use this template** → **Create a new repository**. + + ![Create a repository from the GitHub template](image.png) + +2. Choose the personal GitHub account or company organization that will own the + repository, name it, and choose **Public**. +3. Clone the new repository: + + ```bash + git clone git@github.com:YOUR_NAMESPACE/REPOSITORY_NAME.git + cd REPOSITORY_NAME + ``` + +4. Run `python scripts/initialize_board_pack.py`. The script asks in Chinese + and English for the vendor name, namespace, component name, description, + and copyright holder. Press Enter to accept a default value; the script + updates the README files, `idf_component.yml`, `LICENSE`, and CI. +5. Sign in to [ESP Component Registry](https://components.espressif.com) with + your GitHub account and create an API token with the `write:components` + scope. +6. In your GitHub repository, open **Settings** → **Secrets and variables** → + **Actions** → **New repository secret**. Create the + `IDF_COMPONENT_API_TOKEN` secret and set it to the token value. + +## 2. Adapt and Publish the Board + +First prepare the board materials: schematics, hardware feature descriptions, +and board initialization code from existing projects, if available. + +Configure [ESP Pilot MCP](https://mcp.espressif.com/#esp-pilot-mcp) in your AI +tool, then provide those materials to the AI to help create or migrate board +configurations. If its introduction page is unavailable, give the AI assistant +this direct URL: `https://mcp.esp-pilot.espressif.com/mcp`. + +To create or adjust configurations manually, refer to the +[Create a Board Guide](https://docs.espressif.com/projects/esp-board-manager/en/latest/create-board/index.html) +and choose the appropriate method. + +After adding boards, run +`python scripts/update_supported_boards_table.py` to update the board tables in +both README files. The script does not modify existing table content; it only +appends the board name and chip for new boards at the end of the table. + +Submit board changes through a pull request. CI generates and builds every +supported board combination across the declared Board Manager lower bound, the +latest version, ESP-IDF `v5.5.4`, and `latest`. + +Before production publishing, check that: + +- `example_board/` has been removed and real boards have been added. +- Both README files list the vendor, component description, and supported boards. +- Every CI matrix entry has passed. + +When the checks pass, remove `--dry-run` from the upload command in +`.github/workflows/ci.yml`, then merge to `main` or push a `v*` tag to publish +the new version. + +## Notes + +### Component Namespace + +The first sign-in to ESP Component Registry with a GitHub account automatically +creates a default namespace matching that GitHub username. No separate request +is needed: + +```text +GitHub: user/demo_boards → Registry: user/demo_boards +``` + +To use a company name or another specified namespace, open the account menu in +ESP Component Registry and request it under **Permissions** → **Namespace +Requests**. After approval, create the API token with an account that has +permission to publish to that namespace. + +### Repository and Board Directories + +- Use a public repository name containing `boards`, with only lowercase letters, + numbers, and underscores, for example `acme_boards`. +- Board directories can be nested up to three levels below the repository root. + CI discovers and tests every board within this range: + + ```text + example_board/ + audio/esp32_s3_speaker/ + display/round/esp32_p4_screen/ + ``` + +- Each board directory name must match the `board` field in its + `board_info.yaml`. Directories at the fourth level or deeper are not + discovered. +- Retain the `esp_board_manager`, `board_manager`, and `boards` values in + `idf_component.yml`'s `tags` list so board-pack components can be discovered + automatically. You may add tags, but do not remove these three. + +### License + +This template uses Apache-2.0. For new content, replace `CHANGE_ME` in +`LICENSE` with the copyright holder, normally the legal company name. Preserve +and comply with existing notices and licenses when adding third-party content. + +### Release Version + +The component version must follow the [ESP-IDF Component Manager versioning +scheme](https://docs.espressif.com/projects/idf-component-manager/en/latest/reference/versioning.html). +The upload job in `.github/workflows/ci.yml` uses +`compote component upload --dry-run` by default to validate authentication and +packaging without creating a public registry version. `--allow-existing` +prevents the workflow from overwriting an existing version. + +The initialization script keeps `${{ github.repository_owner }}` in the upload +command by default. It replaces that argument with a fixed namespace only when +the entered namespace differs from the repository owner. diff --git a/VENDOR_GUIDE_CN.md b/VENDOR_GUIDE_CN.md new file mode 100644 index 0000000..ac40a68 --- /dev/null +++ b/VENDOR_GUIDE_CN.md @@ -0,0 +1,77 @@ +# ESP Board Manager 厂家板组件维护指南 + +[English](VENDOR_GUIDE.md) + +本指南用于从本 GitHub 模板创建厂家板组件仓库、适配板卡和配置 CI。发布后的组件说明请维护在 [README_CN.md](README_CN.md)。 + +## 1. 创建和配置仓库 + +1. 在 GitHub 打开[模板仓库](https://github.com/LiuCodee/boards-template),选择 **Use this template** → **Create a new repository**。 +![从 GitHub 模板创建仓库](image.png) +2. 选择个人 GitHub 账号或公司 Organization,填写仓库名,并选择 **Public**。 +3. 克隆新仓库: + + ```bash + git clone git@github.com:YOUR_NAMESPACE/REPOSITORY_NAME.git + cd REPOSITORY_NAME + ``` + +4. 运行 `python scripts/initialize_board_pack.py` 脚本,逐项输入厂商名称、namespace、组件名、组件描述和版权持有人,输入回车可采用默认值,运行结束自动更新 README、`idf_component.yml`、`LICENSE` 和 CI。 +5. 使用 GitHub 账号登录 [ESP 组件注册库](https://components.espressif.com),创建具有 `write:components` scope 的 API Token。 +6. 在 GitHub 仓库中打开 **Settings** → **Secrets and variables** → **Actions** → **New repository secret**,创建名为 `IDF_COMPONENT_API_TOKEN` 的 Secret 并填入刚才创建的 Token。 + +## 2. 适配板子并发布 + +先准备开发板资料:原理图、硬件功能说明,以及现有工程中的板级初始化代码(如有)。 + +建议优先在 AI 工具中配置 [ESP Pilot MCP](https://mcp.espressif.com/#esp-pilot-mcp),并将上述资料提供给 AI,辅助创建或迁移板卡配置。如果 MCP 介绍页无法访问,可直接将 `https://mcp.esp-pilot.espressif.com/mcp` 提供给 AI 助手进行配置。 + +如需手动调整或创建配置,请参考 [创建开发板指南](https://docs.espressif.com/projects/esp-board-manager/zh_CN/latest/create-board/index.html) 选择合适的方法。 + +添加开发板后,运行 `python scripts/update_supported_boards_table.py` 更新两份 README 的开发板表。脚本不会修改已有表格内容,只会将新开发板的板名和芯片追加到表格末尾。 + +通过 Pull Request 提交开发板改动。CI 会对每块开发板在声明的 Board Manager 最低支持版本、最新版本以及 ESP-IDF `v5.5.4`、`latest` 的组合中进行兼容性检查,并生成和编译所有受支持的组合。 + +正式发布前,请检查以下内容: + +- 删除 `example_board/`,并添加真实开发板。 +- 更新 `idf_component.yml` 、 `README.md` 和 `README_CN.md` 中的厂商名称、组件说明和开发板列表等信息。 +- 所有 CI 矩阵项通过。 + +确认无误后,从 `.github/workflows/ci.yml` 的 upload 命令中删除 `--dry-run`,再合入 `main` 或推送 `v*` tag 发布新版本。 + +## 注意事项 + +### 组件命名空间 + +首次使用 GitHub 账号登录 ESP 组件注册库时,系统会自动创建与 GitHub 用户名相同的默认 namespace,无需另行申请: + +```text +GitHub:user/demo_boards → 组件注册库:user/demo_boards +``` + +如果需要使用公司名称或其他指定 namespace,请在 ESP 组件注册库右上角账号菜单的 **Permissions** → **Namespace Requests** 中申请。申请通过后,使用具有该 namespace 发布权限的账号创建 Token。 + +### 仓库和开发板目录 + +- 仓库名必须包含 `boards`,并且只使用小写字母、数字和下划线,例如 `acme_boards`。 +- 开发板目录相对仓库根目录最多支持三层嵌套。CI 会扫描并测试这三层范围内的所有开发板: + + ```text + example_board/ + audio/esp32_s3_speaker/ + display/round/esp32_p4_screen/ + ``` + +- 每个开发板目录名必须与 `board_info.yaml` 中的 `board` 字段一致。第四层及更深层的目录不会被发现。 +- 保留 `idf_component.yml` 中的 `esp_board_manager`、`board_manager` 和 `boards` 三个 `tags` 值,以便后续自动发现板卡组件。可以新增标签,但不要删除这三个标签。 + +### 许可证 + +本模板采用 Apache-2.0。对新建内容,将 `LICENSE` 中的 `CHANGE_ME` 替换为版权持有人,通常为公司法定名称。引入第三方内容时,保留并遵守原有版权声明和许可证。 + +### 发布版本 + +组件版本号须遵循 [ESP-IDF Component Manager 版本规则](https://docs.espressif.com/projects/idf-component-manager/en/latest/reference/versioning.html)。`.github/workflows/ci.yml` 的 upload job 默认使用 `compote component upload --dry-run` 验证认证和打包,不会创建公开版本。`--allow-existing` 会阻止 workflow 覆盖已存在的版本。 + +初始化脚本默认保留 upload 命令中的 `${{ github.repository_owner }}`。仅当输入的 namespace 与仓库所有者不同时,脚本才会将该参数改为固定的自定义 namespace。 diff --git a/ci/scripts/check_bmgr_catalog.py b/ci/scripts/check_bmgr_catalog.py new file mode 100644 index 0000000..70050e5 --- /dev/null +++ b/ci/scripts/check_bmgr_catalog.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Check whether a Board Manager catalog supports a chip for the active IDF.""" + +from __future__ import annotations + +import argparse +import os +import re +import sys +from pathlib import Path +from typing import Callable, Tuple + + +EXIT_UNSUPPORTED = 10 +IDF_VERSION_RE = re.compile(r"^\s*set\s*\(\s*IDF_VERSION_([A-Z]{5})\s+(\d+)") + + +def _normalize_chip(chip: str) -> str: + return chip.strip().lower().replace("-", "") + + +def _load_provider(bmgr_root: Path, idf_version: str): + catalog_dir = bmgr_root / "private_inc" / "soc_capability_catalog" + if not catalog_dir.is_dir(): + raise FileNotFoundError(f"missing Board Manager catalog: {catalog_dir}") + sys.path.insert(0, str(bmgr_root)) + from generators.utils.soc_capabilities import SocCapabilityProvider + + return SocCapabilityProvider.load_for_idf_version(catalog_dir, idf_version) + + +def check_compatibility( + bmgr_root: Path, + idf_version: str, + chip: str, + provider_loader: Callable[[Path, str], object] = _load_provider, +) -> Tuple[bool, str]: + """Return whether the selected BMGR catalog profile contains ``chip``.""" + provider = provider_loader(bmgr_root, idf_version) + profile = str(provider.selected_profile_id) + try: + provider.chip(_normalize_chip(chip)) + except KeyError: + return False, profile + return True, profile + + +def _idf_version(idf_path: Path) -> str: + version_file = idf_path / "tools" / "cmake" / "version.cmake" + parts = {} + with version_file.open(encoding="utf-8") as file: + for line in file: + match = IDF_VERSION_RE.match(line) + if match: + parts[match.group(1)] = match.group(2) + try: + return "{MAJOR}.{MINOR}.{PATCH}".format(**parts) + except KeyError as error: + raise RuntimeError(f"could not read ESP-IDF version from {version_file}") from error + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Check active ESP-IDF chip support in the resolved BMGR catalog." + ) + parser.add_argument("--chip", required=True) + parser.add_argument( + "--bmgr-root", + type=Path, + default=Path("managed_components/espressif__esp_board_manager"), + ) + parser.add_argument("--idf-path", type=Path, default=os.environ.get("IDF_PATH")) + args = parser.parse_args() + + if args.idf_path is None: + print("IDF_PATH is not set", file=sys.stderr) + return 2 + try: + idf_version = _idf_version(args.idf_path) + supported, profile = check_compatibility( + args.bmgr_root, idf_version, args.chip + ) + except Exception as error: + print(f"failed to inspect Board Manager catalog: {error}", file=sys.stderr) + return 2 + + if supported: + print( + f"Board Manager catalog profile {profile} supports " + f"{_normalize_chip(args.chip)}." + ) + return 0 + + print( + f"Board Manager catalog profile {profile} does not support " + f"{_normalize_chip(args.chip)} for ESP-IDF {idf_version}." + ) + return EXIT_UNSUPPORTED + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ci/scripts/discover_matrix.py b/ci/scripts/discover_matrix.py index b638375..e1d8090 100644 --- a/ci/scripts/discover_matrix.py +++ b/ci/scripts/discover_matrix.py @@ -15,10 +15,18 @@ REPO_ROOT = Path(__file__).resolve().parents[2] IDF_VERSIONS = ["v5.5.4", "latest"] +IDF_COMPONENT_MANAGER_SPECS = { + # idf-component-manager 2.4.8 in the v5.5.4 image mishandles BMGR's + # Kconfig-based dependency conditions. Keep the workaround in the IDF 5.x + # series; ESP-IDF latest provides its own compatible Component Manager. + "v5.5.4": "idf-component-manager>=2.5.0,<3.0.0", + "latest": "", +} BMGR_DEP_KEYS = ( "espressif/esp_board_manager", "esp_board_manager", ) +MAX_BOARD_DIRECTORY_DEPTH = 3 REGISTRY_VERSIONS_URL = ( "https://components.espressif.com/api/v1/components/espressif/esp_board_manager/versions" ) @@ -101,8 +109,12 @@ def _key(value: str): def _discover_boards() -> list[dict[str, str]]: boards = [] - for info_path in sorted(REPO_ROOT.glob("*/board_info.yaml")): - if info_path.parent.name.startswith("."): + for info_path in sorted(REPO_ROOT.rglob("board_info.yaml")): + board_dir = info_path.parent.relative_to(REPO_ROOT) + if ( + len(board_dir.parts) > MAX_BOARD_DIRECTORY_DEPTH + or any(part.startswith(".") for part in board_dir.parts) + ): continue with info_path.open(encoding="utf-8") as fh: info = yaml.safe_load(fh) or {} @@ -147,6 +159,7 @@ def main() -> int: "target": board["target"], "idf": idf, "bmgr": bmgr, + "component_manager": IDF_COMPONENT_MANAGER_SPECS[idf], } ) matrix = {"include": include} diff --git a/ci/scripts/pin_bmgr_version.py b/ci/scripts/pin_bmgr_version.py index 26dad56..0136c62 100644 --- a/ci/scripts/pin_bmgr_version.py +++ b/ci/scripts/pin_bmgr_version.py @@ -15,7 +15,7 @@ def main() -> int: print("usage: pin_bmgr_version.py ", file=sys.stderr) return 2 requested = sys.argv[1].strip() - version = "*" if requested == "*" else f"=={requested}" + version = '"*"' if requested == "*" else f'"=={requested}"' text = MANIFEST.read_text(encoding="utf-8") updated, count = re.subn( r"(espressif/esp_board_manager:[\s\S]*?version:\s*)([^\n]+)", diff --git a/ci/scripts/test_check_bmgr_catalog.py b/ci/scripts/test_check_bmgr_catalog.py new file mode 100644 index 0000000..0ea8d84 --- /dev/null +++ b/ci/scripts/test_check_bmgr_catalog.py @@ -0,0 +1,61 @@ +import importlib.util +import unittest +from pathlib import Path + + +SCRIPT_PATH = Path(__file__).with_name("check_bmgr_catalog.py") +SPEC = importlib.util.spec_from_file_location("check_bmgr_catalog", SCRIPT_PATH) +check_bmgr_catalog = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +SPEC.loader.exec_module(check_bmgr_catalog) + + +class FakeProvider: + selected_profile_id = "5.5" + + def __init__(self, supported_chips): + self.supported_chips = supported_chips + + def chip(self, chip): + if chip not in self.supported_chips: + raise KeyError(chip) + + +class CheckBmgrCatalogTest(unittest.TestCase): + def test_accepts_chip_present_in_selected_catalog(self): + provider = FakeProvider({"esp32s3"}) + + supported, profile = check_bmgr_catalog.check_compatibility( + Path("/unused"), + "5.5.4", + "ESP32-S3", + provider_loader=lambda _root, _version: provider, + ) + + self.assertTrue(supported) + self.assertEqual(profile, "5.5") + + def test_reports_chip_missing_from_selected_catalog_as_unsupported(self): + provider = FakeProvider({"esp32s3"}) + + supported, profile = check_bmgr_catalog.check_compatibility( + Path("/unused"), + "5.5.4", + "esp32s31", + provider_loader=lambda _root, _version: provider, + ) + + self.assertFalse(supported) + self.assertEqual(profile, "5.5") + + def test_does_not_classify_catalog_load_errors_as_unsupported(self): + def broken_loader(_root, _version): + raise RuntimeError("catalog is corrupt") + + with self.assertRaisesRegex(RuntimeError, "catalog is corrupt"): + check_bmgr_catalog.check_compatibility( + Path("/unused"), + "5.5.4", + "esp32s31", + provider_loader=broken_loader, + ) diff --git a/ci/scripts/test_discover_matrix.py b/ci/scripts/test_discover_matrix.py new file mode 100644 index 0000000..7ef5a1a --- /dev/null +++ b/ci/scripts/test_discover_matrix.py @@ -0,0 +1,47 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_PATH = Path(__file__).with_name("discover_matrix.py") +SPEC = importlib.util.spec_from_file_location("discover_matrix", SCRIPT_PATH) +discover_matrix = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +SPEC.loader.exec_module(discover_matrix) + + +class DiscoverBoardsTest(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.repo_root = Path(self.temp_dir.name) + self.original_repo_root = discover_matrix.REPO_ROOT + discover_matrix.REPO_ROOT = self.repo_root + + def tearDown(self): + discover_matrix.REPO_ROOT = self.original_repo_root + self.temp_dir.cleanup() + + def _add_board(self, relative_dir: str, name: str): + board_dir = self.repo_root / relative_dir + board_dir.mkdir(parents=True) + (board_dir / "board_info.yaml").write_text( + f"board: {name}\nchip: esp32s3\n", encoding="utf-8" + ) + + def test_discovers_board_directories_up_to_three_levels_deep(self): + self._add_board("root_board", "root_board") + self._add_board("audio/speaker_board", "speaker_board") + self._add_board("display/round/screen_board", "screen_board") + self._add_board("archived/old/unused/ignored_board", "ignored_board") + + boards = discover_matrix._discover_boards() + + self.assertEqual( + boards, + [ + {"board": "speaker_board", "target": "esp32s3"}, + {"board": "screen_board", "target": "esp32s3"}, + {"board": "root_board", "target": "esp32s3"}, + ], + ) diff --git a/ci/test_app/main/idf_component.yml b/ci/test_app/main/idf_component.yml index 8a1f8e8..c84cee3 100644 --- a/ci/test_app/main/idf_component.yml +++ b/ci/test_app/main/idf_component.yml @@ -2,6 +2,6 @@ dependencies: espressif/esp_board_manager: version: ">=0.7.0" require: public - local/vendor_boards: - override_path: ../.. + demo_boards: + override_path: ../../../ version: "*" diff --git a/docs/HOW_TO_GITHUB.md b/docs/HOW_TO_GITHUB.md deleted file mode 100644 index 9490b01..0000000 --- a/docs/HOW_TO_GITHUB.md +++ /dev/null @@ -1,94 +0,0 @@ -# 把本模板推到 GitHub(给乐鑫维护者) - -这份说明只给要**发布模板仓库**的人看。厂家请看仓库根目录的 `GETTING_STARTED.md`。 - -下面默认您还没有自己建过 GitHub 仓库。不需要先建空文件夹:模板文件已经在本地写好。 - -## 几个名字 - -| 名字 | 含义 | -|---|---| -| Git | 电脑上的版本记录工具。本目录执行 `git init` 之后,才有提交历史。 | -| GitHub | 存放 Git 仓库的网站。浏览器打开 github.com。 | -| 仓库(repository) | 一组文件加历史。本地一份,GitHub 上一份,用 `git push` 同步。 | -| Template repository | 一种特殊仓库。别人打开后可以点 **Use this template**,复制出属于自己的新仓库,而不必 Fork。 | - -厂家之后的操作是:打开这个模板 → Use this template → 得到 `某公司_boards`。您现在要做的,只是把本地这份文件变成 GitHub 上的模板仓库。 - -## 第 1 步:GitHub 账号 - -1. 打开 ,用公司邮箱注册,或使用已有账号登录。 -2. 建议最终把模板放在乐鑫组织下,例如 `espressif/esp-vendor-boards-template`。第一次练习也可以先建在您的个人账号下,确认流程后再转到组织。 - -## 第 2 步:本机已有文件 - -模板路径: - -```text -/home/liujinhong/esp/esp-vendor-boards-template -``` - -若该目录还不是 Git 仓库,在终端执行: - -```bash -cd /home/liujinhong/esp/esp-vendor-boards-template -git init -git add . -git status -``` - -此时还不要 `git commit`,除非您准备好提交说明。需要提交时可以说一声,由助手按仓库规范提交。 - -## 第 3 步:在 GitHub 上新建空仓库 - -1. 登录后打开 。 -2. Repository name 填 `esp-vendor-boards-template`(可改,建议带 `template` 以免和厂家的 `*_boards` 混淆)。 -3. Public。 -4. **不要**勾选 Add a README、Add .gitignore、Choose a license。本地已经有这些文件,GitHub 再生成会冲突。 -5. 点击 **Create repository**。 - -页面会给出一个空仓库地址,例如: - -```text -https://github.com/YOUR_USER/esp-vendor-boards-template.git -``` - -## 第 4 步:把本地文件推上去 - -把 `YOUR_USER` 换成第 1 步的 GitHub 用户名或组织名: - -```bash -cd /home/liujinhong/esp/esp-vendor-boards-template -git remote add origin https://github.com/YOUR_USER/esp-vendor-boards-template.git -git branch -M main -git push -u origin main -``` - -第一次 `git push` 会要求登录 GitHub。浏览器授权或 Personal Access Token 都可以。公司账号若开了 SSO,按提示授权。 - -推送成功后,刷新 GitHub 仓库页面,应能看到 `GETTING_STARTED.md`、`example_board/`、`.github/workflows/ci.yml`。 - -## 第 5 步:标记为 Template - -1. 打开该仓库的 **Settings**。 -2. 在 General 页顶部找到 **Template repository**,勾选。 -3. 保存后,仓库页的绿色 Code 按钮旁边会出现 **Use this template**。 - -发给厂家的就是这个仓库链接。厂家只点 Use this template,不必 Fork,也不必改 GitHub Actions 文件。 - -## 第 6 步:自己先走一遍厂家流程(建议) - -用另一个 GitHub 账号(或同一账号下另一个仓库名,例如 `demo_boards`)点 Use this template,按 `GETTING_STARTED.md` 配 Token、改一块板、合入 `main`。确认: - -- 第一次合入 `main`(模板自带 `version: 0.7.0`)后,组件出现在 `https://components.espressif.com/components/<用户名>/demo_boards`。 -- 之后没 bump `version` 时,upload 成功但注册库版本不变。 -- bump 成 `0.7.1` 后再推 `main`,注册库会出现新版本。 - -本模板声明 Board Manager `>=0.7.0`。若注册库上还没有 0.7.0,CI 下限格会失败,需要等 0.7 发布,或临时把下限改成当前已发布版本做联调。 - -## 常见卡住的地方 - -- **push 被拒**:本地还没有 commit。先 `git add` 和 `git commit`,再 `git push`。 -- **Use this template 按钮没有**:Settings 里未勾选 Template repository。 -- **厂家上传 401**:Secret 名称必须是 `IDF_COMPONENT_API_TOKEN`,Token 必须来自同一 GitHub 账号登录的组件注册库。 -- **厂家上传成功但 `idf.py bmgr -l` 看不到板**:仓库名不含 `boards`,或板目录超过扫描深度(板目录必须直接放在仓库根下)。 diff --git a/example_board/board_devices.yaml b/example_board/board_devices.yaml index aec257d..3b34c9a 100644 --- a/example_board/board_devices.yaml +++ b/example_board/board_devices.yaml @@ -6,4 +6,4 @@ devices: active_level: 1 default_level: 0 peripherals: - - name: gpio_power + - gpio_name: gpio_power diff --git a/idf_component.yml b/idf_component.yml index 9132998..5e5f09d 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,15 +1,15 @@ version: "0.7.0" -description: Board definitions for ESP Board Manager +description: "ESP Board Manager board definitions for My_boards development boards" license: Apache-2.0 tags: - - board_manager - esp_board_manager + - board_manager - boards dependencies: espressif/esp_board_manager: - version: ">=0.7.0" + version: ">=0.7.2" require: public files: @@ -18,4 +18,7 @@ files: - ".github" - ".cursor" - "ci" - - "docs" + - "image.png" + - "scripts" + - "VENDOR_GUIDE.md" + - "VENDOR_GUIDE_CN.md" diff --git a/image.png b/image.png new file mode 100644 index 0000000..50c2171 Binary files /dev/null and b/image.png differ diff --git a/scripts/initialize_board_pack.py b/scripts/initialize_board_pack.py new file mode 100644 index 0000000..5536486 --- /dev/null +++ b/scripts/initialize_board_pack.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Interactively initialize board-pack metadata and README files.""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path +from typing import Callable, NamedTuple + +from update_supported_boards_table import update_readme + + +class BoardPackConfig(NamedTuple): + vendor_name: str + namespace: str + use_repository_owner: bool + component_name: str + description: str + copyright_holder: str + + +def _git_remote_owner(repo_root: Path) -> str | None: + result = subprocess.run( + ["git", "remote", "get-url", "origin"], + cwd=repo_root, + capture_output=True, + text=True, + check=False, + ) + if result.returncode: + return None + match = re.search(r"github\.com[:/]([^/]+)/", result.stdout.strip()) + return match.group(1) if match else None + + +def _prompt( + input_fn: Callable[[str], str], chinese: str, english: str, default: str +) -> str: + value = input_fn(f"{chinese} / {english} [{default}]: ").strip() + return value or default + + +def collect_config( + repo_root: Path, + input_fn: Callable[[str], str] = input, + default_namespace: str | None = None, +) -> BoardPackConfig: + vendor_name = _prompt(input_fn, "厂商名称", "Vendor name", "YOUR_VENDOR_NAME") + namespace_default = ( + default_namespace or _git_remote_owner(repo_root) or "YOUR_NAMESPACE" + ) + namespace_input = input_fn( + f"组件命名空间 / Component namespace [{namespace_default}]: " + ).strip() + namespace = namespace_input or namespace_default + use_repository_owner = not namespace_input or namespace == namespace_default + component_name = _prompt( + input_fn, "组件名", "Component name", repo_root.name + ) + description = _prompt( + input_fn, + "组件描述", + "Component description", + f"ESP Board Manager board definitions for {vendor_name} development boards", + ) + copyright_holder = _prompt( + input_fn, "版权持有人", "Copyright holder", vendor_name + ) + return BoardPackConfig( + vendor_name, + namespace, + use_repository_owner, + component_name, + description, + copyright_holder, + ) + + +def _replace_readme_tokens(readme_path: Path, config: BoardPackConfig) -> None: + content = readme_path.read_text(encoding="utf-8") + content = content.replace("YOUR_VENDOR_NAME", config.vendor_name) + content = content.replace("YOUR_NAMESPACE", config.namespace) + content = content.replace("YOUR_COMPONENT_NAME", config.component_name) + readme_path.write_text(content, encoding="utf-8") + + +def _update_manifest(repo_root: Path, config: BoardPackConfig) -> None: + manifest_path = repo_root / "idf_component.yml" + content = manifest_path.read_text(encoding="utf-8") + description = config.description.replace("\\", "\\\\").replace('"', '\\"') + updated, replacements = re.subn( + r"^description:\s*.*$", + f'description: "{description}"', + content, + count=1, + flags=re.MULTILINE, + ) + if replacements != 1: + raise SystemExit(f"missing description field in {manifest_path}") + manifest_path.write_text(updated, encoding="utf-8") + + +def _update_license(repo_root: Path, config: BoardPackConfig) -> None: + license_path = repo_root / "LICENSE" + content = license_path.read_text(encoding="utf-8") + updated, replacements = re.subn( + r"^(\s*Copyright\s+\d{4}\s+).*$", + lambda match: f"{match.group(1)}{config.copyright_holder}", + content, + count=1, + flags=re.MULTILINE, + ) + if replacements != 1: + raise SystemExit(f"missing copyright notice in {license_path}") + license_path.write_text(updated, encoding="utf-8") + + +def _update_workflow(repo_root: Path, config: BoardPackConfig) -> None: + workflow_path = repo_root / ".github" / "workflows" / "ci.yml" + content = workflow_path.read_text(encoding="utf-8") + workflow_namespace = ( + "${{ github.repository_owner }}" + if config.use_repository_owner + else config.namespace + ) + updated, namespace_replacements = re.subn( + r'^(\s*--namespace\s+)"[^"]*"(.*)$', + lambda match: f'{match.group(1)}"{workflow_namespace}"{match.group(2)}', + content, + count=1, + flags=re.MULTILINE, + ) + if namespace_replacements != 1: + raise SystemExit(f"missing upload --namespace argument in {workflow_path}") + component_name = ( + "${{ github.event.repository.name }}" + if config.component_name == repo_root.name + else config.component_name + ) + updated, name_replacements = re.subn( + r'^(\s*--name\s+)"[^"]*"(.*)$', + lambda match: f'{match.group(1)}"{component_name}"{match.group(2)}', + updated, + count=1, + flags=re.MULTILINE, + ) + if name_replacements != 1: + raise SystemExit(f"missing upload --name argument in {workflow_path}") + workflow_path.write_text(updated, encoding="utf-8") + + +def apply_config(repo_root: Path, config: BoardPackConfig) -> None: + _replace_readme_tokens(repo_root / "README.md", config) + _replace_readme_tokens(repo_root / "README_CN.md", config) + _update_manifest(repo_root, config) + _update_license(repo_root, config) + _update_workflow(repo_root, config) + update_readme(repo_root / "README.md", repo_root, "en") + update_readme(repo_root / "README_CN.md", repo_root, "cn") + + +def main() -> int: + repo_root = Path(__file__).resolve().parent.parent + config = collect_config(repo_root) + apply_config(repo_root, config) + print( + "已更新 README、组件清单、许可证和 CI。" + " / Updated the README files, manifest, license, and CI." + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_initialize_board_pack.py b/scripts/test_initialize_board_pack.py new file mode 100644 index 0000000..34adf02 --- /dev/null +++ b/scripts/test_initialize_board_pack.py @@ -0,0 +1,127 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_PATH = Path(__file__).with_name("initialize_board_pack.py") +SPEC = importlib.util.spec_from_file_location("initialize_board_pack", SCRIPT_PATH) +initialize_board_pack = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +SPEC.loader.exec_module(initialize_board_pack) + + +class InitializeBoardPackTest(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.repo_root = Path(self.temp_dir.name) / "demo_boards" + self.repo_root.mkdir() + board_dir = self.repo_root / "example_board" + board_dir.mkdir() + (board_dir / "board_info.yaml").write_text( + "board: example_board\nchip: esp32s3\n", encoding="utf-8" + ) + self._write_template_files() + + def tearDown(self): + self.temp_dir.cleanup() + + def _write_template_files(self): + for filename in ("README.md", "README_CN.md"): + (self.repo_root / filename).write_text( + "\n".join( + [ + "# YOUR_VENDOR_NAME", + "YOUR_VENDOR_NAME: YOUR_NAMESPACE/YOUR_COMPONENT_NAME", + "", + "", + "", + ] + ), + encoding="utf-8", + ) + (self.repo_root / "idf_component.yml").write_text( + 'version: "0.7.0"\ndescription: "Board definitions for ESP Board Manager"\n', + encoding="utf-8", + ) + (self.repo_root / "LICENSE").write_text( + "Copyright 2026 CHANGE_ME\n", encoding="utf-8" + ) + workflow = self.repo_root / ".github" / "workflows" + workflow.mkdir(parents=True) + (workflow / "ci.yml").write_text( + '\n'.join( + [ + '--namespace "${{ github.repository_owner }}"', + '--name "${{ github.event.repository.name }}"', + '', + ] + ), + encoding="utf-8", + ) + + def test_collects_bilingual_prompts_and_applies_entered_metadata(self): + answers = iter(["Acme", "", "acme_board_pack", "", ""]) + prompts = [] + + def input_fn(prompt): + prompts.append(prompt) + return next(answers) + + config = initialize_board_pack.collect_config( + self.repo_root, input_fn=input_fn, default_namespace="acme" + ) + initialize_board_pack.apply_config(self.repo_root, config) + + self.assertEqual(config.vendor_name, "Acme") + self.assertEqual(config.namespace, "acme") + self.assertEqual(config.component_name, "acme_board_pack") + self.assertEqual( + config.description, + "ESP Board Manager board definitions for Acme development boards", + ) + self.assertEqual(config.copyright_holder, "Acme") + self.assertIn("厂商名称 / Vendor name", prompts[0]) + self.assertIn("组件命名空间 / Component namespace", prompts[1]) + self.assertIn("组件名 / Component name", prompts[2]) + self.assertIn( + "Acme: acme/acme_board_pack", + (self.repo_root / "README.md").read_text(encoding="utf-8"), + ) + self.assertIn( + 'description: "ESP Board Manager board definitions for Acme development boards"', + (self.repo_root / "idf_component.yml").read_text(encoding="utf-8"), + ) + self.assertIn( + "Copyright 2026 Acme", + (self.repo_root / "LICENSE").read_text(encoding="utf-8"), + ) + self.assertIn( + '--name "acme_board_pack"', + (self.repo_root / ".github" / "workflows" / "ci.yml").read_text( + encoding="utf-8" + ), + ) + self.assertIn( + '--namespace "${{ github.repository_owner }}"', + (self.repo_root / ".github" / "workflows" / "ci.yml").read_text( + encoding="utf-8" + ), + ) + + def test_uses_a_fixed_namespace_when_it_differs_from_repository_owner(self): + answers = iter(["Acme", "vendor_namespace", "", "", ""]) + + config = initialize_board_pack.collect_config( + self.repo_root, + input_fn=lambda _prompt: next(answers), + default_namespace="acme", + ) + initialize_board_pack.apply_config(self.repo_root, config) + + self.assertIn( + '--namespace "vendor_namespace"', + (self.repo_root / ".github" / "workflows" / "ci.yml").read_text( + encoding="utf-8" + ), + ) diff --git a/scripts/test_update_supported_boards_table.py b/scripts/test_update_supported_boards_table.py new file mode 100644 index 0000000..455e6e2 --- /dev/null +++ b/scripts/test_update_supported_boards_table.py @@ -0,0 +1,103 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_PATH = Path(__file__).with_name("update_supported_boards_table.py") +SPEC = importlib.util.spec_from_file_location("update_supported_boards_table", SCRIPT_PATH) +update_supported_boards_table = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +SPEC.loader.exec_module(update_supported_boards_table) + + +class UpdateSupportedBoardsTableTest(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.repo_root = Path(self.temp_dir.name) + + def tearDown(self): + self.temp_dir.cleanup() + + def _add_board(self, relative_dir: str, board: str, chip: str): + board_dir = self.repo_root / relative_dir + board_dir.mkdir(parents=True) + (board_dir / "board_info.yaml").write_text( + f"board: {board}\nchip: {chip}\n", encoding="utf-8" + ) + + def test_updates_board_and_chip_columns_and_preserves_manual_details(self): + self._add_board("audio/speaker_board", "speaker_board", "esp32s3") + self._add_board("display/round/screen_board", "screen_board", "esp32p4") + readme = self.repo_root / "README.md" + readme.write_text( + "\n".join( + [ + "# Boards", + "", + "| Board | Chip | Audio | SD Card | LCD | LCD Touch | Camera | Buttons | LED Strip | Knob |", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |", + "| `speaker_board` | ESP32-S3 | ES8311 | | | | | | | |", + "", + "", + ] + ), + encoding="utf-8", + ) + + update_supported_boards_table.update_readme(readme, self.repo_root, "en") + + self.assertEqual( + readme.read_text(encoding="utf-8"), + "\n".join( + [ + "# Boards", + "", + "| Board | Chip | Audio | SD Card | LCD | LCD Touch | Camera | Buttons | LED Strip | Knob |", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |", + "| `speaker_board` | ESP32-S3 | ES8311 | | | | | | | |", + "| `screen_board` | ESP32-P4 | | | | | | | | |", + "", + "", + ] + ), + ) + + def test_preserves_existing_rows_and_appends_new_boards(self): + self._add_board("existing_board", "existing_board", "esp32s3") + self._add_board("1/new_board", "new_board", "esp32c6") + readme = self.repo_root / "README.md" + readme.write_text( + "\n".join( + [ + "# Boards", + "", + "| Board | Chip | Audio | SD Card | LCD | LCD Touch | Camera | Buttons | LED Strip | Knob |", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |", + "| `existing_board` | ESP32 | ES8311 | | | | | | | |", + "| `manual_board` | ESP32-H2 | | | | | | | | |", + "", + "", + ] + ), + encoding="utf-8", + ) + + update_supported_boards_table.update_readme(readme, self.repo_root, "en") + + self.assertEqual( + readme.read_text(encoding="utf-8"), + "\n".join( + [ + "# Boards", + "", + "| Board | Chip | Audio | SD Card | LCD | LCD Touch | Camera | Buttons | LED Strip | Knob |", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |", + "| `existing_board` | ESP32 | ES8311 | | | | | | | |", + "| `manual_board` | ESP32-H2 | | | | | | | | |", + "| `new_board` | ESP32-C6 | | | | | | | | |", + "", + "", + ] + ), + ) diff --git a/scripts/update_supported_boards_table.py b/scripts/update_supported_boards_table.py new file mode 100644 index 0000000..01a840a --- /dev/null +++ b/scripts/update_supported_boards_table.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Update the supported-board tables in the repository README files.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import yaml + + +BEGIN_MARKER = "" +END_MARKER = "" +MAX_BOARD_DIRECTORY_DEPTH = 3 +TABLE_HEADERS = { + "cn": [ + "开发板名称", + "芯片", + "音频", + "SD 卡", + "LCD", + "LCD 触摸", + "摄像头", + "按键", + "LED 灯带", + "旋钮", + ], + "en": [ + "Board", + "Chip", + "Audio", + "SD Card", + "LCD", + "LCD Touch", + "Camera", + "Buttons", + "LED Strip", + "Knob", + ], +} + + +def _format_chip(chip: str) -> str: + normalized = chip.lower().replace("-", "") + if normalized == "esp32": + return "ESP32" + if normalized.startswith("esp32") and len(normalized) > len("esp32"): + return f"ESP32-{normalized[len('esp32'):].upper()}" + return chip.upper() + + +def _discover_boards(repo_root: Path) -> list[tuple[str, str]]: + boards = [] + for info_path in sorted(repo_root.rglob("board_info.yaml")): + board_dir = info_path.parent.relative_to(repo_root) + if ( + len(board_dir.parts) > MAX_BOARD_DIRECTORY_DEPTH + or any(part.startswith(".") for part in board_dir.parts) + ): + continue + with info_path.open(encoding="utf-8") as file: + info = yaml.safe_load(file) or {} + if not isinstance(info, dict): + raise SystemExit(f"invalid board definition: {info_path}") + board = str(info.get("board") or info_path.parent.name) + chip = str(info.get("chip") or "") + if not chip: + raise SystemExit(f"{info_path} is missing chip") + if board != info_path.parent.name: + raise SystemExit( + f"board name {board!r} must match directory {info_path.parent.name!r}" + ) + boards.append((board, _format_chip(chip))) + if not boards: + raise SystemExit("no board_info.yaml found; add at least one board directory") + return boards + + +def _table_cells(line: str) -> list[str]: + return [cell.strip() for cell in line.strip().strip("|").split("|")] + + +def _manual_details( + table: str, column_count: int +) -> tuple[dict[str, list[str]], list[str]]: + details = {} + board_order = [] + for line in table.splitlines(): + if not line.lstrip().startswith("|"): + continue + cells = _table_cells(line) + if len(cells) < 2 or all(set(cell) <= {"-", ":", " "} for cell in cells): + continue + board = cells[0].strip("`") + if not board or board.lower() in {"board", "开发板名称"}: + continue + details[board] = (cells[2:] + [""] * column_count)[:column_count] + board_order.append(board) + return details, board_order + + +def _render_table( + boards: list[tuple[str, str]], language: str, existing_details: dict[str, list[str]] +) -> str: + headers = TABLE_HEADERS[language] + lines = [ + "| " + " | ".join(headers) + " |", + "| " + " | ".join(["---"] * len(headers)) + " |", + ] + blank_details = [""] * (len(headers) - 2) + for board, chip in boards: + details = existing_details.get(board, blank_details) + row = "| " + " | ".join([f"`{board}`", chip, *details]) + " |" + while " | |" in row: + row = row.replace(" | |", " | |") + lines.append(row) + return "\n".join(lines) + + +def update_readme(readme_path: Path, repo_root: Path, language: str) -> None: + content = readme_path.read_text(encoding="utf-8") + start = content.find(BEGIN_MARKER) + end = content.find(END_MARKER, start + len(BEGIN_MARKER)) + if start == -1 or end == -1: + raise SystemExit( + f"{readme_path} must contain {BEGIN_MARKER} and {END_MARKER} markers" + ) + table_start = start + len(BEGIN_MARKER) + _, existing_order = _manual_details( + content[table_start:end], len(TABLE_HEADERS[language]) - 2 + ) + discovered_boards = _discover_boards(repo_root) + existing_names = set(existing_order) + new_boards = [ + board_entry + for board_entry in discovered_boards + if board_entry[0] not in existing_names + ] + if not new_boards: + return + new_rows = _render_table(new_boards, language, {}).splitlines()[2:] + updated = content[:end] + "\n".join(new_rows) + "\n" + content[end:] + readme_path.write_text(updated, encoding="utf-8") + + +def main() -> int: + repo_root = Path(__file__).resolve().parent.parent + update_readme(repo_root / "README.md", repo_root, "en") + update_readme(repo_root / "README_CN.md", repo_root, "cn") + return 0 + + +if __name__ == "__main__": + sys.exit(main())