fix(builder): keep project script config in makefile

This commit is contained in:
2026-10-12 12:52:15 +08:00
parent 544cbcca32
commit 51f7c2e60c
5 changed files with 68 additions and 19 deletions
+4 -4
View File
@@ -48,9 +48,9 @@ pouch add builder -g # 全局
多架构清单是 `ARCHES`,见下方「多架构」。 多架构清单是 `ARCHES`,见下方「多架构」。
2. 运行 `python3 -I -S <builder>/scripts/check.py . --ready` 直到构建项 PASS。 2. 运行 `python3 -I -S <builder>/scripts/check.py . --ready` 直到构建项 PASS。
缺发布键只挡住上传,不挡住构建。 缺发布键只挡住上传,不挡住构建。
3. 在项目根 `.env.builder` 配置发布环境变量(不要写进 `.env`)。APK/IPA 可配置 3. 在项目根 `.env.builder` 配置发布环境变量(不要写进 `.env`)。项目内 APK/IPA
`COPY_SCRIPT=.pouch/builder/publish-apk.sh`,它接收产物路径作为位置参数;未配置时使用 分发脚本的路径写在 `makefile.builder` 的 `COPY_SCRIPT`,脚本接收产物路径作为位置参数;
`COPY_DEST` 的本地目录或容器目录后备逻辑: 未配置项目脚本时使用 `COPY_DEST` 的本地目录或容器目录后备逻辑:
```text ```text
DEB_SERVER_URL=https://deb.example.com DEB_SERVER_URL=https://deb.example.com
@@ -59,7 +59,7 @@ DEB_TOKEN=<token> # 只放 .env.builder 或密钥系统,不进 git
DOCKER_REGISTRY=registry.example.com DOCKER_REGISTRY=registry.example.com
COPY_DEST=file-server:/srv COPY_DEST=file-server:/srv
COPY_AS=latest.apk COPY_AS=latest.apk
# 或:COPY_SCRIPT=.pouch/builder/publish-apk.sh # COPY_SCRIPT= # 仅外部/临时脚本覆盖;项目内脚本写 makefile.builder
``` ```
4. 日常发布就是两条命令: 4. 日常发布就是两条命令:
+7 -2
View File
@@ -15,6 +15,11 @@ description: >-
submodule,submodule 是独立 builder 项目:它的 `makefile.builder`、`.env.builder` 和 submodule,submodule 是独立 builder 项目:它的 `makefile.builder`、`.env.builder` 和
`.pouch/builder/` 必须放在 submodule 内,父仓库只负责调用或固定 submodule commit。 `.pouch/builder/` 必须放在 submodule 内,父仓库只负责调用或固定 submodule commit。
配置来源原则:项目要构建什么、发布哪些轨道、使用哪个已提交的分发脚本,放在
`makefile.builder`;本机路径和工具链覆盖放在被忽略的 `.pouch/builder/local.mk`;凭据、
发布目标和本轮别名放在被忽略的 `.env.builder`。`COPY_SCRIPT` 若指向项目内
`.pouch/builder/` 的脚本,优先写入 `makefile.builder`,这样迁移项目时配置与脚本一起进入 Git。
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先 开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
`git rev-parse --show-toplevel` 解析项目根。 `git rev-parse --show-toplevel` 解析项目根。
@@ -52,7 +57,7 @@ DEB_TOKEN=
DOCKER_REGISTRY=registry.example.com DOCKER_REGISTRY=registry.example.com
COPY_DEST=file-server:/srv COPY_DEST=file-server:/srv
COPY_AS=latest.apk COPY_AS=latest.apk
# 或:COPY_SCRIPT=.pouch/builder/publish-apk.sh # 外部/临时脚本才放这里;项目内脚本优先在 makefile.builder 设置 COPY_SCRIPT
``` ```
## 检查 ## 检查
@@ -67,5 +72,5 @@ COPY_AS=latest.apk
2. 仅当用户明确要求发布、上传或提交时才上传。不猜测生产端点;不把 token 写入输出、文件、提交或回复;不用 `set -x` 跑含凭据的命令。可能覆盖同版本时先问。脏工作树默认拒绝发布;用户明确接受时设 `ALLOW_UNCOMMITTED=1` 并注明未提交修改。 2. 仅当用户明确要求发布、上传或提交时才上传。不猜测生产端点;不把 token 写入输出、文件、提交或回复;不用 `set -x` 跑含凭据的命令。可能覆盖同版本时先问。脏工作树默认拒绝发布;用户明确接受时设 `ALLOW_UNCOMMITTED=1` 并注明未提交修改。
3. 构建:用户指定了架构才传 `ARCH=`;否则不要传,让 makefile 按 `ARCHES` 展开(`make -s -f makefile.builder arches`)。`make -f makefile.builder build` 始终单架构;DEB 用 `make -f makefile.builder deb`。版本按契约从 Git 祖先稳定 tag 推导,不要调用 releaser,不要用全仓库最新 tag。仅获构建授权时不要走会自动上传的目标。脚本含 `rm -rf` 时先确认为受限构建目录。 3. 构建:用户指定了架构才传 `ARCH=`;否则不要传,让 makefile 按 `ARCHES` 展开(`make -s -f makefile.builder arches`)。`make -f makefile.builder build` 始终单架构;DEB 用 `make -f makefile.builder deb`。版本按契约从 Git 祖先稳定 tag 推导,不要调用 releaser,不要用全仓库最新 tag。仅获构建授权时不要走会自动上传的目标。脚本含 `rm -rf` 时先确认为受限构建目录。
4. DEB 上传前:对每个包 `verify_deb.sh <exact-package-path.deb> <version> <arch>`。多个包时不凭文件时间猜测。镜像轨道由 `publish_docker.sh` 自带远端 inspect。 4. DEB 上传前:对每个包 `verify_deb.sh <exact-package-path.deb> <version> <arch>`。多个包时不凭文件时间猜测。镜像轨道由 `publish_docker.sh` 自带远端 inspect。
5. 发布优先 `make -f makefile.builder push[-deb|-docker|-copy]`,或直接调 `upload_deb.sh` / `publish_docker.sh` / `copy_artifact.sh`。未指定 `ARCH=` 时 `push-deb` 上传清单里每一个 `.deb`。拷贝轨道至少配置 `COPY_DEST`(本地目录或 `container:/dir`)或 `COPY_SCRIPT`(推荐 `.pouch/builder/` 下的可执行脚本;同时配置时后者优先)。自定义脚本按 `FILE [FILE ...]` 接收产物路径,自行完成重命名、上传、远端校验和通知;相对路径按项目根解析,`BUILDER_PROJECT_ROOT` 提供项目根。`COPY_AS` 只由内置 `COPY_DEST` 路径处理。脚本缺环境变量时加载 `.env.builder`(shell 显式值优先),不读 `.env`。不要把 token 当命令行参数;脚本可随项目放在 `.pouch/builder/` 下。多平台镜像只能走 `publish_docker.sh`,不能拆进 make;不要把项目架构清单写进 `.env.builder`。`build` 里禁止 `docker cp`。 5. 发布优先 `make -f makefile.builder push[-deb|-docker|-copy]`,或直接调 `upload_deb.sh` / `publish_docker.sh` / `copy_artifact.sh`。未指定 `ARCH=` 时 `push-deb` 上传清单里每一个 `.deb`。拷贝轨道至少配置 `COPY_DEST`(本地目录或 `container:/dir`)或 `COPY_SCRIPT`(项目内脚本优先在 `makefile.builder` 设置;同时配置时命令行/环境值优先)。自定义脚本按 `FILE [FILE ...]` 接收产物路径,自行完成重命名、上传、远端校验和通知;相对路径按项目根解析,`BUILDER_PROJECT_ROOT` 提供项目根。`COPY_AS` 只由内置 `COPY_DEST` 路径处理。脚本缺环境变量时加载 `.env.builder`(shell 显式值优先),不读 `.env`。不要把 token 当命令行参数;脚本可随项目放在 `.pouch/builder/` 下。多平台镜像只能走 `publish_docker.sh`,不能拆进 make;不要把项目架构清单写进 `.env.builder`。`build` 里禁止 `docker cp`。
6. 发布成功不能只看「curl/push 已执行」。要有退出码、HTTP 成功或远端 digest;索引异步时报告「上传已接受,索引尚待更新」。最终回复给出包名/镜像引用、版本、架构、SHA-256 或 digest、源 commit、工作区状态和未完成项。 6. 发布成功不能只看「curl/push 已执行」。要有退出码、HTTP 成功或远端 digest;索引异步时报告「上传已接受,索引尚待更新」。最终回复给出包名/镜像引用、版本、架构、SHA-256 或 digest、源 commit、工作区状态和未完成项。
+7 -2
View File
@@ -67,6 +67,11 @@ make -f makefile.builder <target>
## 2. 变量 ## 2. 变量
配置来源按变量的稳定性分层:`makefile.builder` 是项目构建与发布轨道的可迁移事实,
包括 `ARCHES`、产物命名、目标和项目内 `COPY_SCRIPT`;`.pouch/builder/local.mk` 是
不提交的机器工具链覆盖;`.env.builder` 是不提交的凭据、发布端点、目标目录和本轮别名。
命令行/环境变量可以覆盖前两者,但不应把项目契约依赖在本机环境上。
| 变量 | 默认 | 说明 | | 变量 | 默认 | 说明 |
|------|------|------| |------|------|------|
| `ARCHES` | `amd64` | 本项目要发布的架构清单,空格分隔,子集只能是 `amd64` `arm64`。写在 `makefile.builder` 里并提交;不要放进 `.env.builder`。缺省或无 `arches` 目标时视为单架构 `amd64`。不要默认写成双架构 | | `ARCHES` | `amd64` | 本项目要发布的架构清单,空格分隔,子集只能是 `amd64` `arm64`。写在 `makefile.builder` 里并提交;不要放进 `.env.builder`。缺省或无 `arches` 目标时视为单架构 `amd64`。不要默认写成双架构 |
@@ -145,14 +150,14 @@ include $(HOME)/.pouch/skills/builder/scripts/version.mk
| 变量 | 必填 | 说明 | | 变量 | 必填 | 说明 |
|---|---|---| |---|---|---|
| `COPY_DEST` | 与 `COPY_SCRIPT` 至少配置一个 | 内置后备目标:本地目录,或 Docker 目标 `container:/absolute/dir`(`docker cp`) | | `COPY_DEST` | 与 `COPY_SCRIPT` 至少配置一个 | 内置后备目标:本地目录,或 Docker 目标 `container:/absolute/dir`(`docker cp`) |
| `COPY_SCRIPT` | 与 `COPY_DEST` 至少配置一个 | 推荐 `.pouch/builder/` 下的项目相对路径,也可用绝对路径;必须是可执行文件,同时配置时优先于 `COPY_DEST` | | `COPY_SCRIPT` | 与 `COPY_DEST` 至少配置一个 | 项目内 `.pouch/builder/` 脚本的路径优先写在 `makefile.builder` 并提交;也可用绝对路径或环境/.env.builder 覆盖;必须是可执行文件,同时配置时优先于 `COPY_DEST` |
| `COPY_AS` | 否 | 第一份文件的额外 basename,逗号或空格分隔 | | `COPY_AS` | 否 | 第一份文件的额外 basename,逗号或空格分隔 |
`copy_artifact.sh FILE [FILE ...]` 是 APK/IPA 等产物的统一分发入口。不读 `.env`,脏工作树默认拒绝, `copy_artifact.sh FILE [FILE ...]` 是 APK/IPA 等产物的统一分发入口。不读 `.env`,脏工作树默认拒绝,
`ALLOW_UNCOMMITTED=1` 放行。 `ALLOW_UNCOMMITTED=1` 放行。
未配置 `COPY_SCRIPT` 时,入口把文件拷到 `COPY_DEST`,校验目标存在后打印 SHA-256。 未配置 `COPY_SCRIPT` 时,入口把文件拷到 `COPY_DEST`,校验目标存在后打印 SHA-256。
配置 `COPY_SCRIPT=.pouch/builder/publish-apk.sh` 时,入口按项目根解析、先校验每个文件存在,然后直接执行: 配置 `COPY_SCRIPT=.pouch/builder/publish-apk.sh` 时(项目内脚本通常由 `makefile.builder` 传入),入口按项目根解析、先校验每个文件存在,然后直接执行:
```text ```text
/absolute/path/to/publish-script FILE [FILE ...] /absolute/path/to/publish-script FILE [FILE ...]
+49 -10
View File
@@ -492,13 +492,44 @@ def env_file_values(project: Path) -> dict[str, str]:
return values return values
def key_present(key: str, env_keys: set[str]) -> bool: MAKE_ASSIGNMENT = re.compile(
return bool(os.environ.get(key)) or key in env_keys r"^([A-Za-z_][A-Za-z0-9_]*)\s*(?:\?=|:=|=)\s*(.*?)\s*$"
)
def configured_value(key: str, env_values: dict[str, str]) -> str: def makefile_values(project: Path) -> dict[str, str]:
"""Return nonempty top-level make variable values for contract checks."""
path = project / BUILDER_MAKEFILE
values: dict[str, str] = {}
if not path.is_file():
return values
try:
text = path.read_text(encoding="utf-8")
except OSError:
return values
for raw in text.splitlines():
match = MAKE_ASSIGNMENT.match(raw)
if not match:
continue
value = re.split(r"\s+#", match.group(2), maxsplit=1)[0].strip()
if value:
values[match.group(1)] = value
return values
def key_present(
key: str, env_keys: set[str], make_values: dict[str, str] | None = None
) -> bool:
return bool(os.environ.get(key)) or key in env_keys or bool((make_values or {}).get(key))
def configured_value(
key: str,
env_values: dict[str, str],
make_values: dict[str, str] | None = None,
) -> str:
"""Read a configured value without ever printing it.""" """Read a configured value without ever printing it."""
return os.environ.get(key, "") or env_values.get(key, "") return os.environ.get(key, "") or env_values.get(key, "") or (make_values or {}).get(key, "")
def resolve_copy_script(project: Path, script: str) -> tuple[Path | None, str | None]: def resolve_copy_script(project: Path, script: str) -> tuple[Path | None, str | None]:
@@ -515,8 +546,13 @@ def resolve_copy_script(project: Path, script: str) -> tuple[Path | None, str |
return resolved, None return resolved, None
def check_copy_script(report: Report, project: Path, env_values: dict[str, str]) -> None: def check_copy_script(
script = configured_value("COPY_SCRIPT", env_values) report: Report,
project: Path,
env_values: dict[str, str],
make_values: dict[str, str],
) -> None:
script = configured_value("COPY_SCRIPT", env_values, make_values)
if not script: if not script:
return return
path, resolve_error = resolve_copy_script(project, script) path, resolve_error = resolve_copy_script(project, script)
@@ -579,6 +615,7 @@ def check_ready_env_keys(
) -> None: ) -> None:
env_keys = env_file_keys(project) env_keys = env_file_keys(project)
env_values = env_file_values(project) env_values = env_file_values(project)
make_values = makefile_values(project)
lines = [] lines = []
missing: list[str] = [] missing: list[str] = []
if not deb_project and not docker_project and not copy_project: if not deb_project and not docker_project and not copy_project:
@@ -594,7 +631,7 @@ def check_ready_env_keys(
lines.append("DEB_*: skipped (no deb track)") lines.append("DEB_*: skipped (no deb track)")
if docker_project: if docker_project:
for key in DOCKER_ENV_KEYS: for key in DOCKER_ENV_KEYS:
found = key_present(key, env_keys) found = key_present(key, env_keys, make_values)
lines.append(f"{key}: {'present' if found else 'MISSING'}") lines.append(f"{key}: {'present' if found else 'MISSING'}")
if not found: if not found:
missing.append(key) missing.append(key)
@@ -602,11 +639,13 @@ def check_ready_env_keys(
lines.append("DOCKER_*: skipped (no docker track)") lines.append("DOCKER_*: skipped (no docker track)")
if copy_project: if copy_project:
for key in COPY_ENV_KEYS: for key in COPY_ENV_KEYS:
found = key_present(key, env_keys) found = key_present(key, env_keys, make_values)
lines.append(f"{key}: {'present' if found else 'MISSING'}") lines.append(f"{key}: {'present' if found else 'MISSING'}")
if not any(key_present(key, env_keys) for key in COPY_ENV_KEYS): if "COPY_SCRIPT" in make_values and "COPY_SCRIPT" not in env_keys and not os.environ.get("COPY_SCRIPT"):
lines[-1] += " (makefile.builder)"
if not any(key_present(key, env_keys, make_values) for key in COPY_ENV_KEYS):
missing.append("COPY_DEST or COPY_SCRIPT") missing.append("COPY_DEST or COPY_SCRIPT")
check_copy_script(report, project, env_values) check_copy_script(report, project, env_values, make_values)
else: else:
lines.append("COPY_*: skipped (no copy track)") lines.append("COPY_*: skipped (no copy track)")
if missing: if missing:
+1 -1
View File
@@ -12,5 +12,5 @@
# IMAGE_TAG= # IMAGE_TAG=
# PLATFORMS= # optional override; project ARCHES lives in makefile.builder # PLATFORMS= # optional override; project ARCHES lives in makefile.builder
# COPY_DEST= # fallback: local dir, or docker dest container:/absolute/dir # COPY_DEST= # fallback: local dir, or docker dest container:/absolute/dir
# COPY_SCRIPT=.pouch/builder/publish-apk.sh # project-local executable; overrides COPY_DEST # COPY_SCRIPT= # external/temporary executable override; project-local paths belong in makefile.builder
# COPY_AS= # optional extra basenames for the first file # COPY_AS= # optional extra basenames for the first file