feat: add skill init/check and isolate builder makefile

Give ack, builder, and deployer an explicit init/check mode that reports
missing project config instead of failing mid-work. Point builder at
makefile.builder so its contract targets do not collide with an existing
Makefile.
This commit is contained in:
2026-08-25 16:49:28 +08:00
parent e9d2b5fde6
commit 10d8800f07
21 changed files with 1129 additions and 95 deletions
+19 -9
View File
@@ -6,19 +6,25 @@
## 什么时候使用
- "用 builder 初始化这个项目"
- "帮我构建这个项目的 DEB / Docker 镜像"
- "把 1.2.3 发布到包仓库 / 镜像仓库"
- "检查这个项目的 Makefile 是否符合 builder 契约"
- "检查这个项目的 makefile.builder 是否符合 builder 契约"
- "看看项目现在的发布流程"
只构建不上传时明确说明即可;上传永远需要你显式授权。
## 项目接入契约
1. 用 create-makefile skill 生成或修正 Makefile(目标 `help/build/clean/version`
+ 条件 `deb/docker/push*`,变量 `ARCH/VERSION/DIST_DIR/PROJECT_NAME`
对新项目说「用 builder 初始化」。Agent 会探测轨道、按
`templates/makefile.builder` 写出项目根 `makefile.builder`,并列出缺的发布配置
不改用户已有的 `Makefile`
1. `makefile.builder` 目标:`help/build/clean/version` + 条件 `deb/docker/push*`
变量 `ARCH/VERSION/DIST_DIR/PROJECT_NAME`
`VERSION` 通过 `include <builder>/scripts/version.mk` 从 Git 推导。
2. 运行 `python3 -I -S <builder>/scripts/check.py .` 直到全部 PASS。
2. 运行 `python3 -I -S <builder>/scripts/check.py . --ready` 直到构建项 PASS。
缺发布键只挡住上传,不挡住构建。
3. 在项目根 `.env` 配置发布环境变量:
```text
@@ -28,7 +34,9 @@ DEB_TOKEN=<token> # 只放 .env 或密钥系统,不进 git
DOCKER_REGISTRY=registry.example.com
```
4. 日常发布就是两条命令:`make deb && make push-deb``make push-docker`
4. 日常发布就是两条命令:
`make -f makefile.builder deb && make -f makefile.builder push-deb`
`make -f makefile.builder push-docker`
## 版本号
@@ -40,7 +48,7 @@ DOCKER_REGISTRY=registry.example.com
| --- | --- |
| manage-release | 选定下一个正式 SemVer,打 annotated tag `vX.Y.Z` |
| builder | 读 HEAD:落在稳定 tag 上则打正式产物,否则打测试产物 |
| `make version` | 输出一行规范版本(无 `v` 前缀),DEB / Docker 都从它渲染 |
| `make -f makefile.builder version` | 输出一行规范版本(无 `v` 前缀),DEB / Docker 都从它渲染 |
**不要**用 `git tag \| sort -V \| tail -1` 取全仓库最大号,也不要把
`git describe --dirty` 的原始字符串写进 DEB 或镜像 tag。
@@ -83,7 +91,8 @@ detached HEAD 用 `detached`CI 可注入 `BUILD_BRANCH` / `CI_COMMIT_BRANCH`
## 使用示例
```text
用 builder 检查这个项目的 Makefile 是否符合契约
用 builder 初始化这个项目。
用 builder 检查这个项目的 makefile.builder 是否符合契约。
用 builder 构建当前版本的 DEB 和镜像,先不要上传。
用 builder 把当前 commit 的产物发布到项目已配置的仓库。
用 builder 发布多平台 linux/amd64,linux/arm64 镜像。
@@ -93,9 +102,10 @@ detached HEAD 用 `detached`CI 可注入 `BUILD_BRANCH` / `CI_COMMIT_BRANCH`
| 脚本 | 用途 |
|------|------|
| `scripts/check.py` | 校验项目 Makefile 是否符合契约(`--build` 实构核对产物) |
| `scripts/check.py` | 校验契约(`--ready` 含工具链与发布键名,`--build` 实构核对产物) |
| `templates/makefile.builder` | 初始化用的契约文件骨架,拷到项目根 |
| `scripts/version.sh` | 从 Git 祖先稳定 tag 推导规范版本 / Docker tag |
| `scripts/version.mk` | 项目 Makefile `include`,设置 `VERSION``IMAGE_TAG` |
| `scripts/version.mk` | `makefile.builder` `include`,设置 `VERSION``IMAGE_TAG` |
| `scripts/upload_deb.sh` | 上传 `.deb` 到 HTTP 包仓库(multipart package/token/repository_name |
| `scripts/publish_docker.sh` | buildx 构建 + 推送镜像,远端 digest 验证 |
| `scripts/verify_deb.sh` | 核对包元数据、内容与 SHA-256 |
+84 -18
View File
@@ -1,12 +1,13 @@
---
name: builder
description: >-
按统一契约构建并发布项目的 DEB 包与 Docker 镜像:先校验项目 Makefile 是否符合
builder 契约check.py),再 make 构建产物,经授权后用 skill 自带脚本上传并验证。
触发词:构建 deb、发布 deb、上传 deb、推送 apt 仓库、打 Debian 包、构建镜像
发布镜像、推送 Docker 镜像、make push、检查 Makefile 是否符合规范。仅分析打包
逻辑或只构建不上传时也可使用;不会在未获授权时执行任何上传。Docker 轨道保持
显式触发:用户点名(builder/publish docker)时才走镜像发布。
初始化或检查项目是否满足 DEB/Docker 构建发布契约,再按契约构建并发布:
先校验 makefile.buildercheck.py),再 make -f makefile.builder 构建产物,
经授权后用 skill 自带脚本上传并验证。触发词:初始化 builder、接入 builder
检查 makefile.builder、构建 deb、发布 deb、上传 deb、推送 apt 仓库、打 Debian
包、构建镜像、发布镜像、推送 Docker 镜像、make push。仅分析打包逻辑或只构建
不上传时也可使用;不会在未获授权时执行任何上传。Docker 轨道保持显式触发:
用户点名(builder/publish docker)时才走镜像发布。
---
# BuilderDEB / Docker 构建发布
@@ -14,28 +15,91 @@ description: >-
复用项目已有发布约定,安全地完成"校验 → 构建 → 检查 → 授权 → 上传 → 验证"。
分工原则:**make 管构建,skill 脚本管发布,本 SKILL.md 只留脚本做不了的决策。**
项目状态是根目录 `makefile.builder` 与发布用 `.env`,不要创建 `.pouch/builder/`
不要改用户的 `Makefile` / `makefile`
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
`git rev-parse --show-toplevel` 解析项目根。
## 选择模式
- 用户要求初始化、接入 builder,或新项目还没有 `makefile.builder`:执行“初始化”。
- 用户要求检查 builder 契约或发布配置是否齐全:执行“检查”。
- 用户要求构建、发布、上传:执行“工作流”。不要静默初始化。
## 何时使用
- 用户要求初始化或接入 builder。
- 用户要求构建、发布、上传 `.deb` 包或 Docker/OCI 镜像。
- 用户要求检查项目 Makefile 是否符合 builder 契约。
- 用户要求检查 `makefile.builder` 是否符合 builder 契约。
- 用户要求梳理或接通项目现有的 DEB/镜像发布流程。
不适用:本地安装/卸载 DEB;RPM/APK/语言包管理器;从零设计全新打包体系(先出方案);
普通编码与 Dockerfile 编辑。
## 初始化
1. 确认项目根。探测 `makefile.builder`、用户 `Makefile`/`makefile`(只当证据,
不改)、`Dockerfile``debian/`、语言清单、`.env` 键名(只看键是否存在,不读、
不打印值)。
2. 判定轨道:有 Dockerfile → docker;有 deb 信号或用户要打 deb → deb;都不清则问。
不要猜测 registry、token 或仓库名。
3. 没有 `makefile.builder`:把 `<skill-dir>/templates/makefile.builder` 拷到项目根。
按轨道删掉未使用的 deb/docker/push* 段,把 `build` 的 TODO 换成仓库里已有的
真实编译命令(可从用户 Makefile 抄配方,但不要 `include` 或递归调用它)。
`include` builder 的 `scripts/version.mk`。双产物把 `push` 改成
`push: push-deb push-docker`。不要改用户的 `Makefile` / `makefile`
4. 已有 `makefile.builder`:跑检查;按 FAIL 给出修补说明。不覆盖该文件,除非
用户明确要求按契约改。不要调用 create-makefile(其版本规则与本契约冲突)。
5. 不要创建 `.env`。缺发布键时在报告里给出可粘贴示例,并说明 `.env` 不进 Git。
6. 运行:
```bash
python3 -I -S <skill-dir>/scripts/check.py <project-dir> --ready
```
7. 按下面格式报告。结构校验通过且当前轨道能构建时才能称「完成」;只缺发布
键是「部分完成」(可构建,不可发布)。契约 FAIL 或轨道工具缺失是「阻塞」。
除非用户明确要求,不提交、不推送、不上传。
```text
## builder 初始化:完成 | 部分完成 | 阻塞
已具备: …
待配置: 路径 + 字段 + 可粘贴示例 + 缺了会挡住哪步
工具链: make / docker / dpkg-deb(缺则怎么装,不擅自安装)
下一步: 一句话
```
发布键示例(只示范键名):
```text
DEB_SERVER_URL=https://deb.example.com
DEB_REPOSITORY=main
DEB_TOKEN=
DOCKER_REGISTRY=registry.example.com
```
## 检查
只读。运行 `check.py <project-dir> --ready`,用同一报告格式,标题改为
`## builder 检查:…`。不写 `makefile.builder`、不改用户 Makefile、不创建 `.env`。
用户明确要求修复后再转入初始化。
## 工作流
### 0. 校验契约
```bash
python3 -I -S <skill-dir>/scripts/check.py <project-dir> # 静态检查
python3 -I -S <skill-dir>/scripts/check.py <project-dir> --ready # 含工具链与发布键
python3 -I -S <skill-dir>/scripts/check.py <project-dir> --build # 额外实构 deb 并核对产物
```
任一 FAIL:停下修复(引导用 create-makefile skill 补齐),不要绕过校验继续发布。
完整要求见 [contract.md](references/contract.md)。存量项目未接契约时走第 6 节
fallback;成功交付一次后引导用户迁移到契约。
契约 FAIL 或轨道工具缺失:停下,转入「初始化」按契约补齐 `makefile.builder`
不要绕过校验继续发布,不要改用户 Makefile。只缺发布键:允许构建,禁止上传。
完整要求见 [contract.md](references/contract.md)。存量项目未接契约时走
「存量项目 fallback」;成功交付一次后引导用户迁到 `makefile.builder`。
### 1. 确认发布边界
@@ -56,8 +120,8 @@ fallback;成功交付一次后引导用户迁移到契约。
### 2. 构建
```bash
make build ARCH=<amd64|arm64> VERSION=<version> # 主产物
make deb ARCH=<amd64|arm64> # DEB 项目
make -f makefile.builder build ARCH=<amd64|arm64> VERSION=<version> # 主产物
make -f makefile.builder deb ARCH=<amd64|arm64> # DEB 项目
```
版本缺省按契约「版本号」从 Git 祖先稳定 tag 推导(正式 = HEAD exact-match
@@ -78,7 +142,7 @@ verify_deb.sh 输出元数据、关键内容清单和 SHA-256。匹配到多个
### 4. 发布
优先 `make push[-deb|-docker]`(契约要求的薄包装);直接调用等价:
优先 `make -f makefile.builder push[-deb|-docker]`(契约要求的薄包装);直接调用等价:
```bash
DEB_SERVER_URL=… DEB_TOKEN=… DEB_REPOSITORY=… \
@@ -115,21 +179,23 @@ rg -n -i --hidden --glob '!.git' \
'build-deb|upload-deb|publish-deb|dpkg-deb|debuild|curl.*deb|\.deb\b|aptly|reprepro'
```
重点检查 Makefile、CI 配置、`debian/`、构建脚本和发布文档中的入口、变量传递方式、
端点与认证方式。优先复用已有构建入口;上传仍用 builder 脚本。交付后引导迁移到契约
create-makefile + check.py 通过为准)。
重点检查用户 Makefile、CI 配置、`debian/`、构建脚本和发布文档中的入口、变量传递方式、
端点与认证方式。优先复用已有构建入口;上传仍用 builder 脚本。交付后引导迁移到
`makefile.builder``templates/makefile.builder` + `check.py` 通过为准)。
## 修改 builder 自身时
- 上传/发布脚本是 SSOT:通用行为修改落在 `skills/builder/scripts/`,不同步复制到
业务项目。产物版本只通过 `scripts/version.sh` 推导,不要在项目 Makefile 内联
业务项目。产物版本只通过 `scripts/version.sh` 推导,不要在 `makefile.builder` 内联
`git describe` 或 `sort -V`。
- 契约变更先改 `scripts/check.py`,再同步 `references/contract.md`
- 契约变更先改 `scripts/check.py`,再同步 `references/contract.md` 与
`templates/makefile.builder`。
- 可用 `bash -n` 检查脚本语法;有 ShellCheck 时一并运行。
- 不通过真实生产上传测试脚本,除非用户明确授权并给出测试版本/仓库。
## 完成标准
- 初始化/检查:报告为完成、部分完成或阻塞;待配置项含文件、字段和示例。
- 仅分析:入口、调用链、配置来源和风险已被准确说明。
- 仅校验:check.py 结果逐条可解释,修复建议明确。
- 仅构建:产物已生成并通过 verify_deb.sh,未发生上传。
+23 -10
View File
@@ -7,6 +7,15 @@ builder 脚本只做发布,不做项目特定的构建逻辑。
分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT),
Agent 只保留授权判断和歧义处理。**
契约文件固定为项目根 `makefile.builder`,调用方式:
```bash
make -f makefile.builder <target>
```
不要把 builder 目标写进用户的 `Makefile``makefile``check.py` 只读
`makefile.builder`
## 1. Make 目标
### 必备目标(所有项目)
@@ -30,7 +39,7 @@ Agent 只保留授权判断和歧义处理。**
规则:
1. 项目有 DEB 产物的判据:Makefile 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`
1. 项目有 DEB 产物的判据:`makefile.builder` 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`
有镜像的判据:项目根存在 `Dockerfile`
2. 双产物项目必须拆 `push-deb`/`push-docker``push` 依序聚合两者;单产物项目一个
`push` 即可。
@@ -49,9 +58,9 @@ Agent 只保留授权判断和歧义处理。**
### 版本号
`make version` 输出一行规范版本(无 `v` 前缀)。DEB 的 `Version` 与文件名
直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh``make` 通过
`scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用
`make -f makefile.builder version` 输出一行规范版本(无 `v` 前缀)。DEB 的
`Version` 与文件名直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh`
`makefile.builder` 通过 `scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用
manage-release,不猜测下一个正式 SemVer。正式 tag 由 manage-release 事先打好。
`version.sh` 不执行 `git fetch`。
@@ -77,7 +86,7 @@ manage-release,不猜测下一个正式 SemVer。正式 tag 由 manage-release
`--dirty` 写入版本;脏树发布仍走既有门禁。显式 `VERSION=` / `IMAGE_TAG=`
可覆盖推导,但不得把非 exact-match 的 commit 标成正式 `X.Y.Z`。
项目 Makefile 不要内联 `git describe` 或 `sort -V`include 本 skill 的
`makefile.builder` 不要内联 `git describe` 或 `sort -V`include 本 skill 的
`scripts/version.mk`
```makefile
@@ -126,12 +135,16 @@ clone 到 `~/.pouch`。
## 5. 校验
`python3 -I -S <builder-scripts>/check.py <project-dir> [--build]` 对本项目逐条检查
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构 `make deb` 并核对
产物元数据(默认只静态检查配方)。校验失败时的修复路径:用 create-makefile skill
补齐或修正 Makefile,不要绕过校验器。
上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构
`make -f makefile.builder deb` 并核对产物元数据(默认只静态检查配方)。`--ready`
额外检查轨道工具链,以及 `.env` / 环境中的发布键名是否存在(不读取、不打印值;
缺键只挡住发布)。校验失败时的修复路径:按 `templates/makefile.builder` 补齐或
修正 `makefile.builder`,再跑 check.py,不要绕过校验器,不要改用户 Makefile,
不要用 create-makefile(版本推导与本契约冲突)。
## 6. 存量项目(legacy fallback
未接入契约的项目:builder 仍可按发现流程工作——从 `Makefile`、CI 配置、`debian/`
未接入契约的项目:builder 仍可按发现流程工作——从用户 `Makefile`、CI 配置、`debian/`
与发布文档中找已有构建/上传入口,优先复用;上传仍使用 builder 脚本。完成一次成功
交付后应引导用户用 create-makefile 把项目迁移到本契约,之后以 check.py 为准。
交付后应引导用户按 `templates/makefile.builder` 写入项目根 `makefile.builder`,之后以
check.py 为准。不把契约目标合并进用户 Makefile。
+1 -1
View File
@@ -15,7 +15,7 @@
1. 用户本次请求中明确给出的值。
2. 当前项目的 `.env``AGENTS.md`、发布文档。
3. Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
3. `makefile.builder`、用户 Makefile、CI 配置或现有构建脚本中一致且无歧义的配置。
4. 询问用户。
不要从其他项目、shell history 或无关的本地配置中猜测发布目标。
+186 -20
View File
@@ -1,14 +1,18 @@
#!/usr/bin/env python3
"""Executable form of the builder contract (references/contract.md).
Checks a project's Makefile against the contract by probing make itself with
dry runs (`make -n`) instead of parsing Makefile text: includes, conditionals,
and variable expansion are resolved by make, so behavior is what gets judged.
Checks a project's makefile.builder against the contract by probing make
itself with dry runs (`make -f makefile.builder -n`) instead of parsing
makefile text: includes, conditionals, and variable expansion are resolved
by make, so behavior is what gets judged. The default Makefile/makefile is
not read.
Usage:
python3 -I -S check.py <project-dir> [--build]
python3 -I -S check.py <project-dir> [--build] [--ready]
Exit codes: 0 = all PASS, 1 = at least one FAIL, 2 = usage/environment error.
`--ready` 额外检查轨道工具链和发布环境变量键名(只看键是否存在,永不打印值)。
无 makefile.builder 时普通模式退出 2`--ready` 输出结构化 FAIL 并继续工具链/发布键检查。
Change the contract here first, then mirror the change into contract.md.
"""
@@ -17,6 +21,7 @@ from __future__ import annotations
import argparse
import hashlib
import os
import re
import shutil
import subprocess
@@ -37,6 +42,7 @@ SECRET_PATTERNS = (
FLOATING_TAGS = (":latest", ":stable")
DEB_SHAPE = re.compile(r"^[^_\s]+_[^_\s]+_[^_\s]+\.deb$")
VALID_SCRIPT_NAMES = ("upload_deb.sh", "publish_docker.sh")
BUILDER_MAKEFILE = "makefile.builder"
PASS = "PASS"
FAIL = "FAIL"
@@ -58,10 +64,15 @@ class Report:
self.skips += 0 if self.skips else 1
def run_make(project: Path, *args: str, timeout: int = 60) -> subprocess.CompletedProcess[str]:
def run_make(
project: Path, *args: str, timeout: int = 60, dry_run: bool = True
) -> subprocess.CompletedProcess[str]:
cmd = ["make", "-C", str(project), "-f", BUILDER_MAKEFILE]
if dry_run:
cmd.append("-n")
cmd.extend(args)
return subprocess.run(
["make", "-C", str(project), "-n", *args],
capture_output=True, text=True, timeout=timeout, check=False,
cmd, capture_output=True, text=True, timeout=timeout, check=False
)
@@ -244,8 +255,7 @@ def check_push_delegates(report: Report, project: Path, dual_artifact: bool) ->
def check_secrets_and_tags(report: Report, project: Path) -> None:
makefile = project / "Makefile"
included_text = ""
makefile = project / BUILDER_MAKEFILE
problems = []
files = [makefile]
if makefile.exists():
@@ -267,9 +277,104 @@ def check_secrets_and_tags(report: Report, project: Path) -> None:
report.add(FAIL if problems else PASS, 8, "无内联机密、无隐式 latest/stable", "\n".join(problems) or "clean")
def check_script_paths(report: Report) -> None:
import os
DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY")
DOCKER_ENV_KEYS = ("DOCKER_REGISTRY",)
ENV_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=")
def env_file_keys(project: Path) -> set[str]:
"""Return key names defined in project `.env`. Never return or print values."""
path = project / ".env"
keys: set[str] = set()
if not path.is_file():
return keys
try:
text = path.read_text(encoding="utf-8")
except OSError:
return keys
for raw in text.splitlines():
stripped = raw.strip()
if not stripped or stripped.startswith("#"):
continue
match = ENV_KEY_LINE.match(stripped)
if match:
keys.add(match.group(1))
return keys
def key_present(key: str, env_keys: set[str]) -> bool:
return bool(os.environ.get(key)) or key in env_keys
def check_ready_toolchain(
report: Report, *, deb_project: bool, docker_project: bool
) -> None:
lines = [f"make: {'found' if shutil.which('make') else 'MISSING'}"]
problems = []
if docker_project:
docker_ok = shutil.which("docker") is not None
lines.append(f"docker: {'found' if docker_ok else 'MISSING (blocks docker track)'}")
if not docker_ok:
problems.append("install docker to build/publish images")
else:
lines.append("docker: skipped (no docker track)")
if deb_project:
dpkg_ok = shutil.which("dpkg-deb") is not None
lines.append(
f"dpkg-deb: {'found' if dpkg_ok else 'MISSING (blocks make deb / --build)'}"
)
if not dpkg_ok:
problems.append("install dpkg-dev (or equivalent) to build .deb packages")
else:
lines.append("dpkg-deb: skipped (no deb track)")
report.add(
FAIL if problems else PASS,
10,
"轨道工具链",
"\n".join(lines + ([""] + problems if problems else [])),
)
def check_ready_env_keys(
report: Report, project: Path, *, deb_project: bool, docker_project: bool
) -> None:
env_keys = env_file_keys(project)
lines = []
missing: list[str] = []
if not deb_project and not docker_project:
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "no deb/docker track")
return
if deb_project:
for key in DEB_ENV_KEYS:
found = key_present(key, env_keys)
lines.append(f"{key}: {'present' if found else 'MISSING'}")
if not found:
missing.append(key)
else:
lines.append("DEB_*: skipped (no deb track)")
if docker_project:
for key in DOCKER_ENV_KEYS:
found = key_present(key, env_keys)
lines.append(f"{key}: {'present' if found else 'MISSING'}")
if not found:
missing.append(key)
else:
lines.append("DOCKER_*: skipped (no docker track)")
if missing:
lines.extend(
[
"",
"blocks publish, not build. Put keys in the environment or project `.env`:",
*[f" {key}=" for key in missing],
"Do not commit `.env`. Never print values.",
]
)
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "\n".join(lines))
return
report.add(PASS, 11, "发布环境变量键名(不读取值)", "\n".join(lines))
def check_script_paths(report: Report) -> None:
candidates = []
env_dir = os.environ.get("BUILDER_SKILL_DIR")
if env_dir:
@@ -289,26 +394,80 @@ def check_script_paths(report: Report) -> None:
def build_project(project: Path) -> Path | None:
"""Run `make deb` for real and return the produced .deb, or None."""
result = subprocess.run(["make", "-C", str(project), "deb"], capture_output=True, text=True, timeout=1800, check=False)
"""Run `make -f makefile.builder deb` for real and return the produced .deb, or None."""
result = run_make(project, "deb", timeout=1800, dry_run=False)
if result.returncode != 0:
print(f"--build: `make deb` failed:\n{result.stderr[-2000:]}", file=sys.stderr)
print(
f"--build: `make -f {BUILDER_MAKEFILE} deb` failed:\n{result.stderr[-2000:]}",
file=sys.stderr,
)
return None
debs = sorted((p for p in (project / "dist").glob("*.deb") if p.is_file()), key=lambda p: p.stat().st_mtime, reverse=True)
return debs[0] if debs else None
MAKEFILE_HINT = (
f"Fix: copy <builder-skill>/templates/{BUILDER_MAKEFILE} to the project "
f"root as {BUILDER_MAKEFILE}. Do not put builder targets in Makefile or "
"makefile. Keep help/build/clean/version, include builder "
"scripts/version.mk, and enable deb/docker/push* for the tracks this "
"project actually uses. Then re-run check.py. Do not use create-makefile."
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("project", type=Path, help="project directory containing the Makefile")
parser.add_argument("--build", action="store_true", help="actually run `make deb` and verify the artifact")
parser.add_argument(
"project",
type=Path,
help="project directory containing makefile.builder",
)
parser.add_argument(
"--build",
action="store_true",
help="actually run `make -f makefile.builder deb` and verify the artifact",
)
parser.add_argument(
"--ready",
action="store_true",
help="also check toolchain and publish env key names (init/check mode)",
)
args = parser.parse_args(argv)
project = args.project.resolve()
makefile = project / "Makefile"
makefile = project / BUILDER_MAKEFILE
if not makefile.is_file():
print(f"Error: no Makefile in {project}", file=sys.stderr)
return 2
if not args.ready:
print(f"Error: no {BUILDER_MAKEFILE} in {project}", file=sys.stderr)
return 2
report = Report()
report.add(
FAIL,
1,
f"{BUILDER_MAKEFILE} 存在",
f"no {BUILDER_MAKEFILE} in {project}\n{MAKEFILE_HINT}",
)
skip_detail = f"(no {BUILDER_MAKEFILE})"
for number, title, detail in (
(2, "ARCH 守卫与缺省值", skip_detail),
(3, "version 输出一行非空规范版本(无 v 前缀)", skip_detail),
(4, "build 不含上传动作", skip_detail),
(5, "deb 目标产物形状与纯构建", skip_detail),
(6, "docker 目标为本地单平台构建", skip_detail),
(7, "push 仅委托 builder 脚本(薄包装)", skip_detail),
(8, "无内联机密、无隐式 latest/stable", skip_detail),
(9, "builder 脚本路径可达", skip_detail),
):
report.add(SKIP, number, title, detail)
docker_project = detect_docker_project(project)
deb_project = any(project.glob("debian/*"))
check_ready_toolchain(report, deb_project=deb_project, docker_project=docker_project)
check_ready_env_keys(
report, project, deb_project=deb_project, docker_project=docker_project
)
print()
print(f"RESULT: FAILED ({report.failures} check(s) failed)")
return 1
if shutil.which("make") is None:
print("Error: make is required.", file=sys.stderr)
return 2
@@ -340,7 +499,7 @@ def main(argv: list[str] | None = None) -> int:
if deb_project:
if args.build:
print("--build: running `make deb` ...")
print(f"--build: running `make -f {BUILDER_MAKEFILE} deb` ...")
built_deb = build_project(project)
if built_deb is None:
print("--build: no .deb produced; artifact checks degrade to recipe-only.", file=sys.stderr)
@@ -357,6 +516,13 @@ def main(argv: list[str] | None = None) -> int:
check_push_delegates(report, project, dual)
check_secrets_and_tags(report, project)
check_script_paths(report)
if args.ready:
check_ready_toolchain(
report, deb_project=deb_project, docker_project=docker_project
)
check_ready_env_keys(
report, project, deb_project=deb_project, docker_project=docker_project
)
total_fail = report.failures
print()
+1 -1
View File
@@ -1,4 +1,4 @@
# Include from a project Makefile. Sets VERSION (canonical, no leading v)
# Include from makefile.builder. Sets VERSION (canonical, no leading v)
# and IMAGE_TAG (Docker rendering) via builder version.sh.
#
# Command-line / environment VERSION= is passed as --version (official X.Y.Z
+60
View File
@@ -0,0 +1,60 @@
# Builder 契约文件。放到项目根,用 `make -f makefile.builder <target>`。
# 不要把这些目标写进用户的 Makefile / makefile。
# 初始化时按项目轨道删掉未使用的 deb / docker 段。
# 把 build 的 TODO 换成真实编译命令;禁止在 build / deb / docker 里上传。
#
# include 路径见 references/contract.md。测试或非标准安装可设 BUILDER_SKILL_DIR。
ARCH ?= amd64
ifneq ($(filter $(ARCH),amd64 arm64),$(ARCH))
$(error ARCH must be amd64 or arm64)
endif
PROJECT_NAME ?= $(notdir $(CURDIR))
DIST_DIR ?= dist
ifneq ($(BUILDER_SKILL_DIR),)
include $(BUILDER_SKILL_DIR)/scripts/version.mk
else
include $(HOME)/.pouch/skills/builder/scripts/version.mk
endif
BUILDER_SCRIPTS := $(or $(BUILDER_SKILL_DIR),$(HOME)/.pouch/skills/builder)/scripts
.PHONY: help version clean build deb docker push push-deb push-docker
help:
@echo "$(PROJECT_NAME) $(VERSION)"
@echo " make -f makefile.builder build [ARCH=amd64|arm64] - 构建主产物 / build"
@echo " make -f makefile.builder version - 打印规范版本 / canonical version"
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
@echo " make -f makefile.builder deb - 打 DEB(只构建不上传)"
@echo " make -f makefile.builder docker - 本地单平台镜像"
@echo " make -f makefile.builder push / push-deb / push-docker"
version:
@echo $(VERSION)
clean:
rm -rf $(DIST_DIR)
# 替换为项目真实构建命令。不得 curl/scp/docker push。
build:
mkdir -p $(DIST_DIR)
@echo "TODO: build $(PROJECT_NAME) for linux/$(ARCH)"
deb: build
mkdir -p $(DIST_DIR)
dpkg-deb --build packaging $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).deb
docker:
docker build --platform=linux/$(ARCH) -t $(PROJECT_NAME):$(IMAGE_TAG) .
push-deb:
$(BUILDER_SCRIPTS)/upload_deb.sh $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).deb
push-docker:
$(BUILDER_SCRIPTS)/publish_docker.sh
# 单产物项目只保留一条 push。双产物改为:push: push-deb push-docker
push: push-deb