# Builder 构建发布契约 v1 本契约是 builder skill 的规范本体。`scripts/check.py` 是它的可执行形态:改契约先改 check.py,本文档跟随。所有接入项目按同一套 make 目标、产物形状和环境变量执行, builder 脚本只做发布,不做项目特定的构建逻辑。 分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT), Agent 只保留授权判断和歧义处理。** 契约文件固定为项目根 `makefile.builder`,调用方式: ```bash make -f makefile.builder ``` 不要把 builder 目标写进用户的 `Makefile` 或 `makefile`。`check.py` 只读 `makefile.builder`。 发布凭据固定为项目根 `.env.builder`。不要把这些键写进用户的 `.env`。脚本不读取 `.env`。 ## 1. Make 目标 ### 必备目标(所有项目) | 目标 | 要求 | |------|------| | `help` | 分组列出全部目标;首屏含当前版本 | | `version` | 输出一行版本号,适合脚本消费 | | `clean` | 只删除明确、受限的构建产物目录 | | `build` | 编译/打包主产物;尊重 `ARCH`(单架构原语);**不得内含任何上传动作** | ### 条件目标 | 目标 | 适用 | 要求 | |------|------|------| | `deb` | 有 DEB 产物的项目 | 产出 `$(DIST_DIR)/__.deb`;只构建不上传。未在命令行/环境指定 `ARCH=` 且 `ARCHES` 多于一项时,按清单逐个递归构建;指定了 `ARCH=` 则只打那一个 | | `docker` | 有镜像的项目 | 构建本地单平台镜像 `linux/$(ARCH)`;**禁止 `--push`、禁止多平台** | | `android` | 有 Android APK/AAB 的项目 | 可与 `build` 相同。产出 `$(DIST_DIR)/__.apk`(或 `.aab`);只构建不上传。`ARCHES` 是构建机 ISA;fat APK 只留本机一项 | | `ios` | 有 iOS IPA 的项目 | 可与 `build` 相同。产出 `$(DIST_DIR)/__.ipa`;只构建不上传 | | `arches` | 可选(新模板必备) | 打印一行空格分隔的 `ARCHES`;无此目标视为存量 makefile,隐式单架构 | | `push-deb` | 同时有多种发布轨道的项目 | 仅调 builder 的 `upload_deb.sh`。未指定 `ARCH=` 时上传 `ARCHES` 中每一项对应的 `.deb`;指定了则只上传那一个 | | `push-docker` | 同时有多种发布轨道的项目 | 仅调 builder 的 `publish_docker.sh` | | `push-copy` | 同时有多种发布轨道的项目 | 仅调 builder 的 `copy_artifact.sh` | | `push` | 单一产物类型时必备;多产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 | 规则: 1. 项目有 DEB 产物的判据:`makefile.builder` 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`。 有镜像的判据:项目根存在 `Dockerfile`。 Android 判据:配方产出 `.apk` / `.aab`。iOS 判据:配方产出 `.ipa`。 拷贝发布判据:配方调用 `copy_artifact.sh`。 2. 多于一种发布轨道必须拆 `push-deb` / `push-docker` / `push-copy`,`push` 依序聚合; 单轨道项目一个 `push` 即可。 3. `docker` 目标只能本地构建。多平台镜像无法拆成"make 构建 + 单独推送" (`buildx --push` 是一步),因此多平台发布只能走 `publish_docker.sh`。 未指定 `ARCH=` / `PLATFORMS` 时,`publish_docker.sh` 把 `ARCHES` 映射为 `linux/,…`。 4. push 类目标必须是薄包装:解析脚本路径后委托,不内联 curl/token/端点/`docker cp`。 5. `build` 保持单架构,供 `deb` 循环调用。不要让 `build` 自己遍历 `ARCHES`。 `build` 禁止 `docker cp`、`copy_artifact.sh`、curl/scp。 ## 2. 变量 | 变量 | 默认 | 说明 | |------|------|------| | `ARCHES` | `amd64` | 本项目要发布的架构清单,空格分隔,子集只能是 `amd64` `arm64`。写在 `makefile.builder` 里并提交;不要放进 `.env.builder`。缺省或无 `arches` 目标时视为单架构 `amd64`。不要默认写成双架构 | | `ARCH` | `$(firstword $(ARCHES))` | 这一次构建的架构。必须落在 `ARCHES` 里;非法值必须 `$(error)` 并提示合法值。命令行/环境传入时只构建/上传这一项 | | `VERSION` | `` (空) | 为空时按「版本号」节从 Git 祖先稳定 tag 推导;禁止 `sort -V` 取全局最新 | | `DIST_DIR` | `dist` | 产物目录(deb / apk / ipa) | | `PROJECT_NAME` | git 仓库名 | 包名/镜像名主体 | `ARCHES` 是「这个项目发哪些架构」;`ARCH` 是「这一次编哪个」。宇宙集合仍是 `amd64` \| `arm64`,不开放其它 ISA。 `deb` 配方在打包前必须把 `packaging/DEBIAN/control`(或等价文件)的 `Architecture:` 写成当前 `$(ARCH)`,不要写死 `amd64`。推荐先拷到 `$(DIST_DIR)/packaging-$(ARCH)` 再改,避免弄脏源树。 `build` 配方必须使用 `$(ARCH)`(例如 `GOARCH=$(ARCH)`、`--platform=linux/$(ARCH)`)。配方若忽略 `ARCH`,双架构会打出两个文件名不同、内容却都是宿主机架构的包。Builder 不安装 qemu 或交叉编译器;看不出交叉编法时 `ARCHES` 只留本机一项。 ### 版本号 `make -f makefile.builder version` 输出一行规范版本(无 `v` 前缀)。DEB 的 `Version` 与文件名直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh` (`makefile.builder` 通过 `scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用 manage-release 选定下一正式 SemVer。测试号把祖先基线的 patch 机械 +1 作为排序键 (`v1.4.2` 之后 → `1.4.3~…`),不是在宣布下一正式号就是 1.4.3。正式 tag 由 manage-release 事先打好。`version.sh` 不执行 `git fetch`。 推导前 `git fetch --tags`(本地 linked worktree 共享 tags,不必再 fetch 才 能看见其它 worktree 新打的 tag)。基线是 **HEAD 祖先上最近的稳定 tag**, 不是全仓库 `sort -V` 的最大号。稳定 tag 仅 `v..`。 | 判定 | 规范版本 | DEB 文件 | Docker tag | | --- | --- | --- | --- | | HEAD exact-match 稳定 tag `v1.4.2` | `1.4.2` | `name_1.4.2_.deb` | `1.4.2` | | 其它 commit;祖先最近稳定 tag `v1.4.2` | `1.4.3~..+g` | `name_1.4.3~..+g_.deb` | `1.4.3-...g` | | 祖先中没有稳定 tag | `0.0.0~.+g` | 同上替换规范版本 | 同上映射 | 测试与正式进入同一 apt / Docker 仓库。测试 DEB 必须用 `~` 挂在基线的下一 patch 上:`1.4.2` < `1.4.3~…` < `1.4.3`。已装上一正式版的机器可以升到 bugfix 测试包;同号正式版仍盖住测试。不要写成 `1.4.2~…`(小于已发布的 `1.4.2`),不要写成 `1.4.2+` / `1.4.3-beta`(Debian 里 `-beta` 大于 `1.4.3`)。日期必须写在 `~` 之后。Docker tag 不得含 `~`、`/`、`:`,由规范 版本把 `~` → `-`、`+g` → `.g`。 ``:构建日 `YYYYMMDD`。已设 `SOURCE_DATE_EPOCH` 时用该时刻的 UTC 日期,否则用本机当前日期。正式 `X.Y.Z` 不加日期。 ``:`git rev-parse --abbrev-ref HEAD`,detached 时用 `BUILD_BRANCH` / `CI_COMMIT_BRANCH` / `GITHUB_REF_NAME`,再没有则 `detached`。清洗:小写;`/`、`_` → `-`;去掉非 `[a-z0-9-]`;压缩连续 `-`;过长截断(给 base、日期、距离、SHA 留位置;Docker tag 上限 128)。 `` 为基线 tag 到 HEAD 的 commit 数;`` 为 7 位短哈希。不要把 `--dirty` 写入版本;脏树发布仍走既有门禁。显式 `VERSION=` / `IMAGE_TAG=` 可覆盖推导,但不得把非 exact-match 的 commit 标成正式 `X.Y.Z`。 `makefile.builder` 不要内联 `git describe` 或 `sort -V`,include 本 skill 的 `scripts/version.mk`: ```makefile include $(HOME)/.pouch/skills/builder/scripts/version.mk ``` ## 3. 发布环境变量 ### DEB 轨道 | 变量 | 必填 | 说明 | |------|------|------| | `DEB_SERVER_URL` | 是 | 仓库服务地址 | | `DEB_TOKEN` | 是 | 认证令牌;只从环境读取,绝不进 argv/日志/git | | `DEB_REPOSITORY` | 是 | 目标仓库名 | | `DEB_UPLOAD_PATH` | 否 | 覆盖默认上传路径 `/api/v2/upload/package` | ### 拷贝轨道 | 变量 | 必填 | 说明 | |---|---|---| | `COPY_DEST` | 是 | 本地目录,或 Docker 目标 `container:/absolute/dir`(`docker cp`) | | `COPY_AS` | 否 | 第一份文件的额外 basename,逗号或空格分隔 | `copy_artifact.sh FILE [FILE ...]` 把已构建文件拷到 `COPY_DEST`,校验目标存在后打印 SHA-256。不读 `.env`。脏工作树默认拒绝,`ALLOW_UNCOMMITTED=1` 放行。 ### Docker 轨道 | 变量 | 必填 | 说明 | |------|------|------| | `DOCKER_REGISTRY` | 是 | registry 主机,无 scheme | | `DOCKER_REPOSITORY` | 否 | 默认取 git 仓库名 | | `IMAGE_TAG` | 否 | 默认由规范版本渲染:正式为 `X.Y.Z`;测试将 `~` 换成 `-`、`+g` 换成 `.g` | | `PLATFORMS` | 否 | 本机/本轮覆盖。未设置时由 `ARCHES` 映射为 `linux/,…`;再没有则 `linux/amd64`。不要把项目的架构清单只写在这里(`.env.builder` 不进 git) | `PLATFORMS` 优先级(高 → 低):用户这轮 `--platform` 或已 export 的 `PLATFORMS` → 命令行 `ARCH=` 映射的 `linux/$(ARCH)`(由 `push-docker` 传入)→ `.env.builder` 的 `PLATFORMS` → `makefile.builder` 的 `ARCHES` → `linux/amd64`。 配置来源优先级:shell 已显式设置的值 > 项目根 `.env.builder` > 失败并询问用户。 `.env.builder` 由 builder 脚本加载(不回显任何值);当前 shell 已设置的值优先。 不要读取或改写用户 `.env`。空值视为未配置。不要提交 `.env.builder`。 ### 工作区安全 脏工作树(有未提交修改)默认拒绝发布;`ALLOW_UNCOMMITTED=1` 显式放行并在汇报中 注明镜像/包包含哪些未提交修改。该门在 builder 脚本层实现,不在 make 层。 ## 4. 脚本解析顺序 push 目标与 `version.sh` 定位 builder 脚本时按以下顺序,命中即用,不做静默兜底: 1. `$BUILDER_SKILL_DIR/scripts/`(特殊安装位置) 2. `$HOME/.pouch/skills/builder/scripts/`(标准 clone 位) 两个位置都不可用时必须失败并提示:设置 `BUILDER_SKILL_DIR`,或把 pouch 仓库 clone 到 `~/.pouch`。 ## 5. 校验 `python3 -I -S /check.py [--build]` 对本项目逐条检查 上述要求,任一 FAIL 退出码非零,可直接挂 CI。`--build` 额外实构 `make -f makefile.builder deb` 并核对产物元数据(默认只静态检查配方)。`--ready` 额外检查轨道工具链,以及 `.env.builder` / 环境中的发布键名是否存在且非空(不读取、 不打印值;缺键只挡住发布)。校验失败时的修复路径:按 `templates/makefile.builder` 补齐或 修正 `makefile.builder`,再跑 check.py,不要绕过校验器,不要改用户 Makefile, 不要用 create-makefile(版本推导与本契约冲突)。 ## 6. 存量项目(legacy fallback) 未接入契约的项目:builder 仍可按发现流程工作——从用户 `Makefile`、CI 配置、`debian/` 与发布文档中找已有构建/上传入口,优先复用;上传仍使用 builder 脚本。从项目根查找, 不预设文件位置: ```bash rg -n -i --hidden --glob '!.git' \ 'build-deb|upload-deb|publish-deb|dpkg-deb|debuild|curl.*deb|\.deb\b|aptly|reprepro' ``` 完成一次成功交付后应引导用户按 `templates/makefile.builder` 写入项目根 `makefile.builder`,之后以 check.py 为准。不把契约目标合并进用户 Makefile。