feat(builder): declare multi-arch builds with ARCHES
Project architecture lists belong in makefile.builder, not .env.builder. deb/push-deb fan out when ARCH is omitted; Docker platforms derive from ARCHES. Legacy makefiles without ARCHES keep the old single-arch behavior.
This commit is contained in:
@@ -38,8 +38,9 @@ pouch add builder -g # 全局
|
||||
不改用户已有的 `Makefile` 或 `.env`。
|
||||
|
||||
1. `makefile.builder` 目标:`help/build/clean/version` + 条件 `deb/docker/push*`,
|
||||
变量 `ARCH/VERSION/DIST_DIR/PROJECT_NAME`。
|
||||
变量 `ARCHES/ARCH/VERSION/DIST_DIR/PROJECT_NAME`。
|
||||
`VERSION` 通过 `include <builder>/scripts/version.mk` 从 Git 推导。
|
||||
多架构清单是 `ARCHES`,见下方「多架构」。
|
||||
2. 运行 `python3 -I -S <builder>/scripts/check.py . --ready` 直到构建项 PASS。
|
||||
缺发布键只挡住上传,不挡住构建。
|
||||
3. 在项目根 `.env.builder` 配置发布环境变量(不要写进 `.env`):
|
||||
@@ -54,6 +55,51 @@ DOCKER_REGISTRY=registry.example.com
|
||||
4. 日常发布就是两条命令:
|
||||
`make -f makefile.builder deb && make -f makefile.builder push-deb`、
|
||||
`make -f makefile.builder push-docker`。
|
||||
写了 `ARCHES=amd64 arm64` 时,这两条会打出并上传两个 DEB;镜像一次推多平台。
|
||||
|
||||
## 多架构
|
||||
|
||||
项目要发哪些 CPU 架构,写在**已提交的** `makefile.builder` 里,不要写进
|
||||
`.env.builder`(那是密钥,换机器会丢)。
|
||||
|
||||
```makefile
|
||||
ARCHES ?= amd64 arm64
|
||||
```
|
||||
|
||||
`ARCH` 是「这一次」:默认等于清单第一项。不传 `ARCH=` 时,`deb` / `push-deb`
|
||||
按清单逐个做完;`push-docker` 把清单映射成 `linux/amd64,linux/arm64`。
|
||||
只做其中一个:`make -f makefile.builder deb ARCH=arm64`。
|
||||
|
||||
合法值只有 `amd64` 和 `arm64`(空格分隔)。不要默认写成双架构:本机编不出
|
||||
另一边时,只留一项。
|
||||
|
||||
```bash
|
||||
make -s -f makefile.builder arches # 打印清单,例如 amd64 arm64
|
||||
make -f makefile.builder deb # 打出两个 .deb
|
||||
make -f makefile.builder push-deb # 两个都上传(需你授权发布)
|
||||
make -f makefile.builder push-docker # 一次 buildx 推两个平台
|
||||
```
|
||||
|
||||
产物:
|
||||
|
||||
```text
|
||||
dist/<name>_<version>_amd64.deb
|
||||
dist/<name>_<version>_arm64.deb
|
||||
```
|
||||
|
||||
`build` 配方必须用 `$(ARCH)`,否则两个包内容会一样。初始化时 Agent 按仓库里
|
||||
已有命令来写(Go 一般是 `GOOS=linux GOARCH=$(ARCH) go build …`),不另做一套
|
||||
运行时探测脚本。看不出交叉编法就不要写双架构。
|
||||
|
||||
`packaging/DEBIAN/control` 的 `Architecture:` 必须随 `$(ARCH)` 变,不要写死
|
||||
`amd64`。模板会先拷到 `dist/packaging-$(ARCH)` 再改这一行。
|
||||
|
||||
本机临时只推一个平台,可以在 `.env.builder` 设 `PLATFORMS=linux/amd64`,这是
|
||||
覆盖不是清单。项目清单始终以 `ARCHES` 为准。
|
||||
|
||||
存量 `makefile.builder` 没有 `ARCHES` 时行为与以前相同(默认 `amd64`,
|
||||
`ARCH=arm64` 仍合法)。要改成双架构:加上面那一行,确认 `build` 吃 `$(ARCH)`,
|
||||
再跑 `python3 -I -S <builder>/scripts/check.py . --ready`。
|
||||
|
||||
## 版本号
|
||||
|
||||
@@ -119,11 +165,12 @@ detached HEAD 用 `detached`,CI 可注入 `BUILD_BRANCH` / `CI_COMMIT_BRANCH`
|
||||
用 builder 构建当前版本的 DEB 和镜像,先不要上传。
|
||||
用 builder 把当前 commit 的产物发布到项目已配置的仓库。
|
||||
用 builder 发布多平台 linux/amd64,linux/arm64 镜像。
|
||||
用 builder 把当前版本的 amd64 和 arm64 DEB 打出来并发布。
|
||||
```
|
||||
|
||||
## Agent 会做什么
|
||||
|
||||
1. 初始化:探测 DEB / Docker 轨道,写出 `makefile.builder` 和 `.env.builder` 骨架,不改你已有的 `Makefile` 或 `.env`。
|
||||
1. 初始化:探测 DEB / Docker 轨道,写出 `makefile.builder` 和 `.env.builder` 骨架,按仓库证据填 `ARCHES`(不默认双架构),不改你已有的 `Makefile` 或 `.env`。
|
||||
2. 检查:只跑 `scripts/check.py --ready`,不改文件。
|
||||
3. 构建:走 `make -f makefile.builder`;只获构建授权时不走会自动上传的目标。
|
||||
4. 发布:先校验产物,再上传。缺发布键、未授权、或可能覆盖同版本时停下问你。
|
||||
|
||||
@@ -27,8 +27,8 @@ description: >-
|
||||
|
||||
1. 确认项目根。探测 `makefile.builder`、`.env.builder`、用户 Makefile(只当抄配方的证据,不改)、`Dockerfile`、`debian/`。`.env.builder` 只看键是否存在且非空,不读、不打印值。不要读取用户 `.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。
|
||||
4. 已有 `makefile.builder`:跑检查;按 FAIL 给出修补说明。不覆盖该文件,除非用户明确要求按契约改。不要调用 create-makefile(版本规则冲突)。
|
||||
3. 没有 `makefile.builder`:把 `<skill-dir>/templates/makefile.builder` 拷到项目根。按轨道删掉未使用的 deb/docker/push* 段,把 `build` 的 TODO 换成仓库里已有的真实编译命令(可从用户 Makefile 抄配方,但不要 `include` 或递归调用它),且配方必须使用 `$(ARCH)`。按 [contract.md](references/contract.md) 填 `ARCHES`:`debian/control` / CI matrix / 用户点名的子集;否则只写本机(`x86_64`→`amd64`,`aarch64`→`arm64`)。不要默认双架构;看不出交叉编法时只留一项并说明。`include` builder 的 `scripts/version.mk`。双产物把 `push` 改成 `push: push-deb push-docker`。不要改用户 Makefile。
|
||||
4. 已有 `makefile.builder`:跑检查;按 FAIL 给出修补说明。不覆盖 `build`。用户明确要求按契约改时再加 `ARCHES`,并确认配方里有 `$(ARCH)`。不要调用 create-makefile(版本规则冲突)。
|
||||
5. 没有 `.env.builder`:把 `<skill-dir>/templates/env.builder` 拷到项目根(注释键,不含值)。缺发布键时在报告里给出可粘贴示例,并把 `.env.builder` 加入 `.gitignore`,不要提交。
|
||||
6. 运行 `python3 -I -S <skill-dir>/scripts/check.py <project-dir> --ready`。结构校验通过且当前轨道能构建才称「完成」;只缺发布键是「部分完成」。契约 FAIL 或轨道工具缺失是「阻塞」。除非用户明确要求,不提交、不推送、不上传。
|
||||
|
||||
@@ -59,7 +59,7 @@ DOCKER_REGISTRY=registry.example.com
|
||||
|
||||
1. 跑 `check.py <project-dir>`;需要工具链与发布键时加 `--ready`;要实构 deb 时加 `--build`。契约 FAIL 或轨道工具缺失:停下,转入「初始化」,不要绕过校验,不要改用户 Makefile。只缺发布键:允许构建,禁止上传。未接契约的存量项目按 contract.md §6 发现已有入口,上传仍用 builder 脚本;成功交付一次后引导迁到 `makefile.builder`。
|
||||
2. 仅当用户明确要求发布、上传或提交时才上传。不猜测生产端点;不把 token 写入输出、文件、提交或回复;不用 `set -x` 跑含凭据的命令。可能覆盖同版本时先问。脏工作树默认拒绝发布;用户明确接受时设 `ALLOW_UNCOMMITTED=1` 并注明未提交修改。
|
||||
3. 构建:`make -f makefile.builder build ARCH=<amd64|arm64> VERSION=<version>`;DEB 再 `make -f makefile.builder deb ARCH=<...>`。版本按契约从 Git 祖先稳定 tag 推导,不要调用 manage-release,不要用全仓库最新 tag。仅获构建授权时不要走会自动上传的目标。脚本含 `rm -rf` 时先确认为受限构建目录。
|
||||
4. DEB 上传前:`verify_deb.sh <exact-package-path.deb>`。多个包时不凭文件时间猜测。镜像轨道由 `publish_docker.sh` 自带远端 inspect。
|
||||
5. 发布优先 `make -f makefile.builder push[-deb|-docker]`,或直接调 `upload_deb.sh` / `publish_docker.sh`。脚本缺环境变量时加载 `.env.builder`(shell 显式值优先),不读 `.env`。不把 token 当命令行参数;不把脚本复制进项目。多平台镜像只能走 `publish_docker.sh`,不能拆进 make。
|
||||
3. 构建:用户指定了架构才传 `ARCH=`;否则不要传,让 makefile 按 `ARCHES` 展开(`make -s -f makefile.builder arches`)。`make -f makefile.builder build` 始终单架构;DEB 用 `make -f makefile.builder deb`。版本按契约从 Git 祖先稳定 tag 推导,不要调用 manage-release,不要用全仓库最新 tag。仅获构建授权时不要走会自动上传的目标。脚本含 `rm -rf` 时先确认为受限构建目录。
|
||||
4. DEB 上传前:对每个包 `verify_deb.sh <exact-package-path.deb> <version> <arch>`。多个包时不凭文件时间猜测。镜像轨道由 `publish_docker.sh` 自带远端 inspect。
|
||||
5. 发布优先 `make -f makefile.builder push[-deb|-docker]`,或直接调 `upload_deb.sh` / `publish_docker.sh`。未指定 `ARCH=` 时 `push-deb` 上传清单里每一个 `.deb`。脚本缺环境变量时加载 `.env.builder`(shell 显式值优先),不读 `.env`。不把 token 当命令行参数;不把脚本复制进项目。多平台镜像只能走 `publish_docker.sh`,不能拆进 make;不要把项目架构清单写进 `.env.builder`。
|
||||
6. 发布成功不能只看「curl/push 已执行」。要有退出码、HTTP 成功或远端 digest;索引异步时报告「上传已接受,索引尚待更新」。最终回复给出包名/镜像引用、版本、架构、SHA-256 或 digest、源 commit、工作区状态和未完成项。
|
||||
|
||||
@@ -28,15 +28,16 @@ make -f makefile.builder <target>
|
||||
| `help` | 分组列出全部目标;首屏含当前版本 |
|
||||
| `version` | 输出一行版本号,适合脚本消费 |
|
||||
| `clean` | 只删除明确、受限的构建产物目录 |
|
||||
| `build` | 编译/打包主产物;尊重 `ARCH`;**不得内含任何上传动作** |
|
||||
| `build` | 编译/打包主产物;尊重 `ARCH`(单架构原语);**不得内含任何上传动作** |
|
||||
|
||||
### 条件目标
|
||||
|
||||
| 目标 | 适用 | 要求 |
|
||||
|------|------|------|
|
||||
| `deb` | 有 DEB 产物的项目 | 产出唯一 `$(DIST_DIR)/<name>_<version>_<arch>.deb`;只构建不上传 |
|
||||
| `deb` | 有 DEB 产物的项目 | 产出 `$(DIST_DIR)/<name>_<version>_<arch>.deb`;只构建不上传。未在命令行/环境指定 `ARCH=` 且 `ARCHES` 多于一项时,按清单逐个递归构建;指定了 `ARCH=` 则只打那一个 |
|
||||
| `docker` | 有镜像的项目 | 构建本地单平台镜像 `linux/$(ARCH)`;**禁止 `--push`、禁止多平台** |
|
||||
| `push-deb` | 同时有 DEB 和镜像的项目 | 仅调 builder 的 `upload_deb.sh` 上传 `dist/*.deb` |
|
||||
| `arches` | 可选(新模板必备) | 打印一行空格分隔的 `ARCHES`;无此目标视为存量 makefile,隐式单架构 |
|
||||
| `push-deb` | 同时有 DEB 和镜像的项目 | 仅调 builder 的 `upload_deb.sh`。未指定 `ARCH=` 时上传 `ARCHES` 中每一项对应的 `.deb`;指定了则只上传那一个 |
|
||||
| `push-docker` | 同时有 DEB 和镜像的项目 | 仅调 builder 的 `publish_docker.sh` |
|
||||
| `push` | 单一产物类型时必备;双产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 |
|
||||
|
||||
@@ -48,17 +49,27 @@ make -f makefile.builder <target>
|
||||
`push` 即可。
|
||||
3. `docker` 目标只能本地构建。多平台镜像无法拆成"make 构建 + 单独推送"
|
||||
(`buildx --push` 是一步),因此多平台发布只能走 `publish_docker.sh`。
|
||||
未指定 `ARCH=` / `PLATFORMS` 时,`publish_docker.sh` 把 `ARCHES` 映射为
|
||||
`linux/<arch>,…`。
|
||||
4. push 类目标必须是薄包装:解析脚本路径后委托,不内联 curl/token/端点。
|
||||
5. `build` 保持单架构,供 `deb` 循环调用。不要让 `build` 自己遍历 `ARCHES`。
|
||||
|
||||
## 2. 变量
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|------|------|------|
|
||||
| `ARCH` | `amd64` | 仅允许 `amd64` \| `arm64`,非法值必须 `$(error)` 报错并提示合法值 |
|
||||
| `ARCHES` | `amd64` | 本项目要发布的架构清单,空格分隔,子集只能是 `amd64` `arm64`。写在 `makefile.builder` 里并提交;不要放进 `.env.builder`。缺省或无 `arches` 目标时视为单架构 `amd64`。不要默认写成双架构 |
|
||||
| `ARCH` | `$(firstword $(ARCHES))` | 这一次构建的架构。必须落在 `ARCHES` 里;非法值必须 `$(error)` 并提示合法值。命令行/环境传入时只构建/上传这一项 |
|
||||
| `VERSION` | `` (空) | 为空时按「版本号」节从 Git 祖先稳定 tag 推导;禁止 `sort -V` 取全局最新 |
|
||||
| `DIST_DIR` | `dist` | DEB 产物目录 |
|
||||
| `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 的
|
||||
@@ -120,7 +131,9 @@ include $(HOME)/.pouch/skills/builder/scripts/version.mk
|
||||
| `DOCKER_REGISTRY` | 是 | registry 主机,无 scheme |
|
||||
| `DOCKER_REPOSITORY` | 否 | 默认取 git 仓库名 |
|
||||
| `IMAGE_TAG` | 否 | 默认由规范版本渲染:正式为 `X.Y.Z`;测试将 `~` 换成 `-`、`+g` 换成 `.g` |
|
||||
| `PLATFORMS` | 否 | 默认 `linux/amd64`;多平台如 `linux/amd64,linux/arm64` |
|
||||
| `PLATFORMS` | 否 | 本机/本轮覆盖。未设置时由 `ARCHES` 映射为 `linux/<arch>,…`;再没有则 `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 已设置的值优先。
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| Registry | 必须显式确定,例如 `registry.example.com` |
|
||||
| Repository | 必须包含项目约定的 namespace;缺省取 git 仓库名 |
|
||||
| Tag | 必须显式确定;优先使用版本号或 Git SHA |
|
||||
| Platform | 必须显式确定,例如 `linux/amd64` 或 `linux/amd64,linux/arm64` |
|
||||
| Platform | 用户这轮显式值,否则 `.env.builder` 的 `PLATFORMS`,否则 `makefile.builder` 的 `ARCHES` 映射为 `linux/<arch>,…`,再否则 `linux/amd64` |
|
||||
| Dockerfile | 默认 `Dockerfile`,不存在或项目另有约定时明确指定 |
|
||||
| Context | 默认当前项目根目录 |
|
||||
|
||||
|
||||
+117
-12
@@ -78,8 +78,9 @@ def run_make(
|
||||
|
||||
|
||||
def has_no_rule(result: subprocess.CompletedProcess[str]) -> bool:
|
||||
err = result.stderr.lower()
|
||||
return result.returncode != 0 and (
|
||||
"No rule to make target" in result.stderr or "no rule to make target" in result.stderr.lower()
|
||||
"no rule to make target" in err or "没有规则可制作目标" in result.stderr
|
||||
)
|
||||
|
||||
|
||||
@@ -99,6 +100,33 @@ def clean_make_output(result: subprocess.CompletedProcess[str]) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def parse_dry_echo(result: subprocess.CompletedProcess[str]) -> str:
|
||||
"""Recover `@echo …` payload from `make -n` output."""
|
||||
out_lines = [
|
||||
ln.lstrip()[5:]
|
||||
for ln in result.stdout.splitlines()
|
||||
if ln.lstrip().startswith("echo ")
|
||||
]
|
||||
return "\n".join(out_lines).strip()
|
||||
|
||||
|
||||
def read_arches(project: Path) -> list[str] | None:
|
||||
"""Return ARCHES if the `arches` target exists.
|
||||
|
||||
None means a legacy makefile without the target. An empty list means the
|
||||
target exists but printed nothing usable.
|
||||
"""
|
||||
result = run_make(project, "arches")
|
||||
if has_no_rule(result):
|
||||
return None
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
raw = parse_dry_echo(result)
|
||||
if not raw:
|
||||
return []
|
||||
return raw.split()
|
||||
|
||||
|
||||
def check_required_targets(report: Report, project: Path) -> dict[str, bool]:
|
||||
present: dict[str, bool] = {}
|
||||
lines = []
|
||||
@@ -112,25 +140,68 @@ def check_required_targets(report: Report, project: Path) -> dict[str, bool]:
|
||||
|
||||
|
||||
def check_arch_guard(report: Report, project: Path) -> None:
|
||||
title = "ARCH / ARCHES 守卫与缺省值"
|
||||
problems: list[str] = []
|
||||
bad = run_make(project, "build", "ARCH=loongarch")
|
||||
guard_ok = bad.returncode != 0 and ("amd64" in bad.stderr or "arm64" in bad.stderr)
|
||||
guard_ok = bad.returncode != 0 and (
|
||||
"amd64" in bad.stderr or "arm64" in bad.stderr or "ARCH" in bad.stderr
|
||||
)
|
||||
default_ok = run_make(project, "build").returncode == 0
|
||||
lines = [
|
||||
f"invalid ARCH rejected: {'yes' if guard_ok else 'NO'}",
|
||||
f"default ARCH works: {'yes' if default_ok else 'no'}",
|
||||
]
|
||||
hint = "" if guard_ok else "\n Hint: add `$(error ARCH must be amd64 or arm64)` guarded by an ifneq filter."
|
||||
if guard_ok and default_ok:
|
||||
report.add(PASS, 2, "ARCH 守卫与缺省值", "\n".join(lines + hint.splitlines()))
|
||||
if not guard_ok:
|
||||
problems.append(
|
||||
"Hint: reject unknown ARCH with `$(error)` naming allowed values "
|
||||
"(amd64/arm64, or the project's ARCHES)."
|
||||
)
|
||||
if not default_ok:
|
||||
problems.append("`make -n build` without ARCH= must succeed")
|
||||
|
||||
arches_make = run_make(project, "arches")
|
||||
if has_no_rule(arches_make):
|
||||
lines.append("arches target: absent (legacy; implicit ARCHES = default ARCH)")
|
||||
elif arches_make.returncode != 0:
|
||||
problems.append(
|
||||
f"`make -n arches` failed: {arches_make.stderr.strip() or 'unknown error'}"
|
||||
)
|
||||
else:
|
||||
report.add(FAIL, 2, "ARCH 守卫与缺省值", "\n".join(lines) + hint)
|
||||
raw = parse_dry_echo(arches_make)
|
||||
arches = raw.split()
|
||||
lines.append(f"ARCHES: {raw!r}")
|
||||
if not arches:
|
||||
problems.append("arches target printed an empty list")
|
||||
if "," in raw:
|
||||
problems.append("ARCHES must be space-separated, not comma-separated")
|
||||
unknown = [a for a in arches if a not in ARCH_VALUES]
|
||||
if unknown:
|
||||
problems.append(f"ARCHES entries must be amd64 or arm64: {unknown}")
|
||||
if len(arches) != len(set(arches)):
|
||||
problems.append("ARCHES must not contain duplicates")
|
||||
outsider = next((a for a in ARCH_VALUES if a not in arches), None)
|
||||
if outsider:
|
||||
extra = run_make(project, "build", f"ARCH={outsider}")
|
||||
extra_ok = extra.returncode != 0
|
||||
lines.append(
|
||||
f"ARCH={outsider} rejected (not in ARCHES): "
|
||||
f"{'yes' if extra_ok else 'NO'}"
|
||||
)
|
||||
if not extra_ok:
|
||||
problems.append(
|
||||
f"ARCH={outsider} must fail when it is not listed in ARCHES"
|
||||
)
|
||||
|
||||
if problems:
|
||||
report.add(FAIL, 2, title, "\n".join(lines + [""] + problems))
|
||||
else:
|
||||
report.add(PASS, 2, title, "\n".join(lines))
|
||||
|
||||
|
||||
def check_version_output(report: Report, project: Path) -> None:
|
||||
result = run_make(project, "version")
|
||||
# Dry run: the echoed `@echo <version>` line IS the would-be output.
|
||||
out_lines = [ln.lstrip()[5:] for ln in result.stdout.splitlines() if ln.lstrip().startswith("echo ")]
|
||||
out = "\n".join(out_lines).strip()
|
||||
out = parse_dry_echo(result)
|
||||
single = len(out.splitlines()) == 1 and out != ""
|
||||
no_v = single and not out.startswith("v")
|
||||
detail = f"stdout={out!r}"
|
||||
@@ -185,6 +256,24 @@ def check_deb_recipe(report: Report, project: Path, built_deb: Path | None) -> N
|
||||
problems.append(f"dpkg-deb --info failed on {built_deb.name}")
|
||||
else:
|
||||
problems.append("dpkg-deb unavailable; metadata not verified (--build)")
|
||||
arches = read_arches(project)
|
||||
if arches and len(arches) > 1 and dry.returncode == 0:
|
||||
blob = dry.stdout + dry.stderr
|
||||
missing = [
|
||||
a for a in arches if f"ARCH={a}" not in blob and f"_{a}.deb" not in blob
|
||||
]
|
||||
if missing:
|
||||
problems.append(
|
||||
"ARCHES has multiple values but `make -n deb` does not fan out to: "
|
||||
+ ", ".join(missing)
|
||||
)
|
||||
pinned = arches[-1]
|
||||
one = run_make(project, "deb", f"ARCH={pinned}")
|
||||
one_blob = one.stdout + one.stderr
|
||||
if f"_{pinned}.deb" not in one_blob and f"ARCH={pinned}" not in one_blob:
|
||||
problems.append(
|
||||
f"`make -n deb ARCH={pinned}` does not build that single arch"
|
||||
)
|
||||
if problems:
|
||||
report.add(FAIL, 5, "deb 目标产物形状与纯构建", "\n".join(problems))
|
||||
else:
|
||||
@@ -226,7 +315,9 @@ SCRIPT_RESOLVE_SNIPPETS = tuple(
|
||||
)
|
||||
|
||||
|
||||
def check_push_delegates(report: Report, project: Path, dual_artifact: bool) -> None:
|
||||
def check_push_delegates(
|
||||
report: Report, project: Path, dual_artifact: bool, deb_project: bool
|
||||
) -> None:
|
||||
targets = ("push-deb", "push-docker") if dual_artifact else ("push",)
|
||||
missing = []
|
||||
inline = []
|
||||
@@ -250,6 +341,20 @@ def check_push_delegates(report: Report, project: Path, dual_artifact: bool) ->
|
||||
if missing:
|
||||
problems.append("missing targets: " + ", ".join(missing))
|
||||
problems.extend(inline)
|
||||
if deb_project:
|
||||
push_deb_target = "push-deb" if dual_artifact else "push"
|
||||
if push_deb_target not in missing:
|
||||
arches = read_arches(project)
|
||||
if arches and len(arches) > 1:
|
||||
dry = run_make(project, push_deb_target)
|
||||
if not has_no_rule(dry):
|
||||
text = chr(10).join(clean_make_output(dry))
|
||||
absent = [a for a in arches if f"_{a}.deb" not in text]
|
||||
if absent:
|
||||
problems.append(
|
||||
f"ARCHES has multiple values but `make -n {push_deb_target}` "
|
||||
"does not upload: " + ", ".join(absent)
|
||||
)
|
||||
status = PASS if not problems else FAIL
|
||||
detail = "\n".join(problems) if problems else "thin wrappers: " + ", ".join(thin)
|
||||
report.add(status, 7, "push 仅委托 builder 脚本(薄包装)", detail)
|
||||
@@ -453,7 +558,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
skip_detail = f"(no {BUILDER_MAKEFILE})"
|
||||
for number, title, detail in (
|
||||
(2, "ARCH 守卫与缺省值", skip_detail),
|
||||
(2, "ARCH / ARCHES 守卫与缺省值", skip_detail),
|
||||
(3, "version 输出一行非空规范版本(无 v 前缀)", skip_detail),
|
||||
(4, "build 不含上传动作", skip_detail),
|
||||
(5, "deb 目标产物形状与纯构建", skip_detail),
|
||||
@@ -497,7 +602,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
check_version_output(report, project)
|
||||
check_build_has_no_upload(report, project)
|
||||
else:
|
||||
report.add(SKIP, 2, "ARCH 守卫与缺省值", "(build target missing)")
|
||||
report.add(SKIP, 2, "ARCH / ARCHES 守卫与缺省值", "(build target missing)")
|
||||
report.add(SKIP, 3, "version 输出一行非空规范版本(无 v 前缀)", "(version target missing)")
|
||||
report.add(SKIP, 4, "build 不含上传动作", "(build target missing)")
|
||||
|
||||
@@ -517,7 +622,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
report.add(SKIP, 6, "docker 目标为本地单平台构建", "(no Dockerfile)")
|
||||
|
||||
dual = deb_project and docker_project
|
||||
check_push_delegates(report, project, dual)
|
||||
check_push_delegates(report, project, dual, deb_project)
|
||||
check_secrets_and_tags(report, project)
|
||||
check_script_paths(report)
|
||||
if args.ready:
|
||||
|
||||
@@ -17,7 +17,8 @@ usage() {
|
||||
# DOCKER_REGISTRY Required (or --registry)
|
||||
# DOCKER_REPOSITORY Optional, default: git repository name (or --repository)
|
||||
# IMAGE_TAG Optional, default: version.sh --docker (or --tag)
|
||||
# PLATFORMS Optional, default: linux/amd64 (or --platform)
|
||||
# PLATFORMS Optional. Else makefile.builder ARCHES mapped to
|
||||
# linux/<arch>,… ; else linux/amd64 (or --platform)
|
||||
# DOCKER_DOCKERFILE Optional, default: Dockerfile (--file)
|
||||
# DOCKER_CONTEXT Optional, default: . (--context)
|
||||
# DOCKER_BUILDER Optional buildx builder name (--builder)
|
||||
@@ -53,7 +54,7 @@ fi
|
||||
registry=${DOCKER_REGISTRY:-}
|
||||
repository=${DOCKER_REPOSITORY:-$git_repo_name}
|
||||
tag=${IMAGE_TAG:-}
|
||||
platform=${PLATFORMS:-linux/amd64}
|
||||
platform=${PLATFORMS:-}
|
||||
dockerfile=${DOCKER_DOCKERFILE:-Dockerfile}
|
||||
build_context=${DOCKER_CONTEXT:-.}
|
||||
builder=${DOCKER_BUILDER:-}
|
||||
@@ -126,6 +127,29 @@ if [[ "$tag" == latest && ${ALLOW_LATEST:-0} != 1 && "$mode" == push ]]; then
|
||||
echo "Set ALLOW_LATEST=1 only when the user explicitly asked for 'latest'." >&2
|
||||
exit 3
|
||||
fi
|
||||
if [[ -z "$platform" ]]; then
|
||||
if [[ -n "$project_root" && -f "$project_root/makefile.builder" ]]; then
|
||||
arches=$(make -C "$project_root" -f makefile.builder -s --no-print-directory arches 2>/dev/null || true)
|
||||
mapped=
|
||||
for arch in $arches; do
|
||||
case "$arch" in
|
||||
amd64|arm64)
|
||||
if [[ -n "$mapped" ]]; then
|
||||
mapped="$mapped,linux/$arch"
|
||||
else
|
||||
mapped="linux/$arch"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: unsupported ARCHES entry from makefile.builder: $arch" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
platform=$mapped
|
||||
fi
|
||||
platform=${platform:-linux/amd64}
|
||||
fi
|
||||
if [[ "$mode" == load && "$platform" == *,* ]]; then
|
||||
echo "Error: --load cannot be combined with multiple platforms: $platform" >&2
|
||||
exit 2
|
||||
|
||||
@@ -10,4 +10,4 @@
|
||||
# DOCKER_REGISTRY=
|
||||
# DOCKER_REPOSITORY=
|
||||
# IMAGE_TAG=
|
||||
# PLATFORMS=
|
||||
# PLATFORMS= # optional override; project ARCHES lives in makefile.builder
|
||||
|
||||
@@ -2,13 +2,26 @@
|
||||
# 不要把这些目标写进用户的 Makefile / makefile。
|
||||
# 初始化时按项目轨道删掉未使用的 deb / docker 段。
|
||||
# 把 build 的 TODO 换成真实编译命令;禁止在 build / deb / docker 里上传。
|
||||
# build 必须尊重 $(ARCH)。要发双架构时把 arm64 写进 ARCHES,不要改 .env.builder。
|
||||
#
|
||||
# include 路径见 references/contract.md。测试或非标准安装可设 BUILDER_SKILL_DIR。
|
||||
|
||||
ARCH ?= amd64
|
||||
ifneq ($(filter $(ARCH),amd64 arm64),$(ARCH))
|
||||
$(error ARCH must be amd64 or arm64)
|
||||
ARCHES ?= amd64
|
||||
ARCH ?= $(firstword $(ARCHES))
|
||||
ifeq ($(strip $(ARCHES)),)
|
||||
$(error ARCHES must not be empty)
|
||||
endif
|
||||
ifneq ($(filter-out amd64 arm64,$(ARCHES)),)
|
||||
$(error ARCHES entries must be amd64 or arm64)
|
||||
endif
|
||||
ifneq ($(filter $(ARCH),$(ARCHES)),$(ARCH))
|
||||
$(error ARCH must be one of: $(ARCHES))
|
||||
endif
|
||||
|
||||
# Fan-out over ARCHES only when ARCH was not set on the CLI/environment.
|
||||
_ARCH_FROM_FILE := $(filter file default undefined,$(origin ARCH))
|
||||
_MULTI_ARCH := $(and $(_ARCH_FROM_FILE),$(filter-out 1,$(words $(ARCHES))))
|
||||
_PUSH_DEB_ARCHES := $(if $(_ARCH_FROM_FILE),$(ARCHES),$(ARCH))
|
||||
|
||||
PROJECT_NAME ?= $(notdir $(CURDIR))
|
||||
DIST_DIR ?= dist
|
||||
@@ -21,40 +34,62 @@ endif
|
||||
|
||||
BUILDER_SCRIPTS := $(or $(BUILDER_SKILL_DIR),$(HOME)/.pouch/skills/builder)/scripts
|
||||
|
||||
.PHONY: help version clean build deb docker push push-deb push-docker
|
||||
.PHONY: help version arches clean build deb docker push push-deb push-docker
|
||||
|
||||
help:
|
||||
@echo "$(PROJECT_NAME) $(VERSION)"
|
||||
@echo " ARCHES=$(ARCHES) ARCH=$(ARCH)"
|
||||
@echo " make -f makefile.builder build [ARCH=amd64|arm64] - 构建主产物 / build"
|
||||
@echo " make -f makefile.builder version - 打印规范版本 / canonical version"
|
||||
@echo " make -f makefile.builder arches - 打印架构清单 / print ARCHES"
|
||||
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
|
||||
@echo " make -f makefile.builder deb - 打 DEB(只构建不上传)"
|
||||
@echo " make -f makefile.builder deb - 打 DEB(按 ARCHES;只构建不上传)"
|
||||
@echo " make -f makefile.builder docker - 本地单平台镜像"
|
||||
@echo " make -f makefile.builder push / push-deb / push-docker"
|
||||
|
||||
version:
|
||||
@echo $(VERSION)
|
||||
|
||||
arches:
|
||||
@echo $(ARCHES)
|
||||
|
||||
clean:
|
||||
rm -rf $(DIST_DIR)
|
||||
|
||||
# 替换为项目真实构建命令。不得 curl/scp/docker push。
|
||||
# 替换为项目真实构建命令,且必须使用 $(ARCH)。不得 curl/scp/docker push。
|
||||
build:
|
||||
mkdir -p $(DIST_DIR)
|
||||
@echo "TODO: build $(PROJECT_NAME) for linux/$(ARCH)"
|
||||
|
||||
ifdef _MULTI_ARCH
|
||||
deb:
|
||||
$(foreach a,$(ARCHES),$(MAKE) -f makefile.builder --no-print-directory deb ARCH=$(a) &&) true
|
||||
else
|
||||
deb: build
|
||||
mkdir -p $(DIST_DIR)
|
||||
dpkg-deb --build packaging $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).deb
|
||||
rm -rf $(DIST_DIR)/packaging-$(ARCH)
|
||||
cp -a packaging $(DIST_DIR)/packaging-$(ARCH)
|
||||
sed -e 's/^Architecture:.*/Architecture: $(ARCH)/' \
|
||||
$(DIST_DIR)/packaging-$(ARCH)/DEBIAN/control \
|
||||
> $(DIST_DIR)/packaging-$(ARCH)/DEBIAN/control.stamped
|
||||
mv $(DIST_DIR)/packaging-$(ARCH)/DEBIAN/control.stamped \
|
||||
$(DIST_DIR)/packaging-$(ARCH)/DEBIAN/control
|
||||
dpkg-deb --build $(DIST_DIR)/packaging-$(ARCH) $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).deb
|
||||
endif
|
||||
|
||||
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
|
||||
$(BUILDER_SCRIPTS)/upload_deb.sh $(foreach a,$(_PUSH_DEB_ARCHES),$(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(a).deb)
|
||||
|
||||
ifdef _ARCH_FROM_FILE
|
||||
push-docker:
|
||||
$(BUILDER_SCRIPTS)/publish_docker.sh
|
||||
else
|
||||
push-docker:
|
||||
PLATFORMS=linux/$(ARCH) $(BUILDER_SCRIPTS)/publish_docker.sh
|
||||
endif
|
||||
|
||||
# 单产物项目只保留一条 push。双产物改为:push: push-deb push-docker
|
||||
push: push-deb
|
||||
|
||||
Reference in New Issue
Block a user