diff --git a/skills/builder/README.md b/skills/builder/README.md index a3267bc..204fcbe 100644 --- a/skills/builder/README.md +++ b/skills/builder/README.md @@ -48,9 +48,9 @@ pouch add builder -g # 全局 多架构清单是 `ARCHES`,见下方「多架构」。 2. 运行 `python3 -I -S /scripts/check.py . --ready` 直到构建项 PASS。 缺发布键只挡住上传,不挡住构建。 -3. 在项目根 `.env.builder` 配置发布环境变量(不要写进 `.env`)。APK/IPA 可配置 - `COPY_SCRIPT=.pouch/builder/publish-apk.sh`,它接收产物路径作为位置参数;未配置时使用 - `COPY_DEST` 的本地目录或容器目录后备逻辑: +3. 在项目根 `.env.builder` 配置发布环境变量(不要写进 `.env`)。项目内 APK/IPA + 分发脚本的路径写在 `makefile.builder` 的 `COPY_SCRIPT`,脚本接收产物路径作为位置参数; + 未配置项目脚本时使用 `COPY_DEST` 的本地目录或容器目录后备逻辑: ```text DEB_SERVER_URL=https://deb.example.com @@ -59,7 +59,7 @@ DEB_TOKEN= # 只放 .env.builder 或密钥系统,不进 git DOCKER_REGISTRY=registry.example.com COPY_DEST=file-server:/srv COPY_AS=latest.apk -# 或:COPY_SCRIPT=.pouch/builder/publish-apk.sh +# COPY_SCRIPT= # 仅外部/临时脚本覆盖;项目内脚本写 makefile.builder ``` 4. 日常发布就是两条命令: diff --git a/skills/builder/SKILL.md b/skills/builder/SKILL.md index cf16578..d8267e6 100644 --- a/skills/builder/SKILL.md +++ b/skills/builder/SKILL.md @@ -15,6 +15,11 @@ description: >- submodule,submodule 是独立 builder 项目:它的 `makefile.builder`、`.env.builder` 和 `.pouch/builder/` 必须放在 submodule 内,父仓库只负责调用或固定 submodule commit。 +配置来源原则:项目要构建什么、发布哪些轨道、使用哪个已提交的分发脚本,放在 +`makefile.builder`;本机路径和工具链覆盖放在被忽略的 `.pouch/builder/local.mk`;凭据、 +发布目标和本轮别名放在被忽略的 `.env.builder`。`COPY_SCRIPT` 若指向项目内 +`.pouch/builder/` 的脚本,优先写入 `makefile.builder`,这样迁移项目时配置与脚本一起进入 Git。 + 开始时解析当前 `SKILL.md` 所在目录,记为 ``。优先 `git rev-parse --show-toplevel` 解析项目根。 @@ -52,7 +57,7 @@ DEB_TOKEN= DOCKER_REGISTRY=registry.example.com COPY_DEST=file-server:/srv 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` 并注明未提交修改。 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 `。多个包时不凭文件时间猜测。镜像轨道由 `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、工作区状态和未完成项。 diff --git a/skills/builder/references/contract.md b/skills/builder/references/contract.md index b3bbe98..b4d6f3a 100644 --- a/skills/builder/references/contract.md +++ b/skills/builder/references/contract.md @@ -67,6 +67,11 @@ make -f makefile.builder ## 2. 变量 +配置来源按变量的稳定性分层:`makefile.builder` 是项目构建与发布轨道的可迁移事实, +包括 `ARCHES`、产物命名、目标和项目内 `COPY_SCRIPT`;`.pouch/builder/local.mk` 是 +不提交的机器工具链覆盖;`.env.builder` 是不提交的凭据、发布端点、目标目录和本轮别名。 +命令行/环境变量可以覆盖前两者,但不应把项目契约依赖在本机环境上。 + | 变量 | 默认 | 说明 | |------|------|------| | `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_SCRIPT` | 与 `COPY_DEST` 至少配置一个 | 推荐 `.pouch/builder/` 下的项目相对路径,也可用绝对路径;必须是可执行文件,同时配置时优先于 `COPY_DEST` | +| `COPY_SCRIPT` | 与 `COPY_DEST` 至少配置一个 | 项目内 `.pouch/builder/` 脚本的路径优先写在 `makefile.builder` 并提交;也可用绝对路径或环境/.env.builder 覆盖;必须是可执行文件,同时配置时优先于 `COPY_DEST` | | `COPY_AS` | 否 | 第一份文件的额外 basename,逗号或空格分隔 | `copy_artifact.sh FILE [FILE ...]` 是 APK/IPA 等产物的统一分发入口。不读 `.env`,脏工作树默认拒绝, `ALLOW_UNCOMMITTED=1` 放行。 未配置 `COPY_SCRIPT` 时,入口把文件拷到 `COPY_DEST`,校验目标存在后打印 SHA-256。 -配置 `COPY_SCRIPT=.pouch/builder/publish-apk.sh` 时,入口按项目根解析、先校验每个文件存在,然后直接执行: +配置 `COPY_SCRIPT=.pouch/builder/publish-apk.sh` 时(项目内脚本通常由 `makefile.builder` 传入),入口按项目根解析、先校验每个文件存在,然后直接执行: ```text /absolute/path/to/publish-script FILE [FILE ...] diff --git a/skills/builder/scripts/check.py b/skills/builder/scripts/check.py index 163dafb..379efe8 100755 --- a/skills/builder/scripts/check.py +++ b/skills/builder/scripts/check.py @@ -492,13 +492,44 @@ def env_file_values(project: Path) -> dict[str, str]: return values -def key_present(key: str, env_keys: set[str]) -> bool: - return bool(os.environ.get(key)) or key in env_keys +MAKE_ASSIGNMENT = re.compile( + 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.""" - 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]: @@ -515,8 +546,13 @@ def resolve_copy_script(project: Path, script: str) -> tuple[Path | None, str | return resolved, None -def check_copy_script(report: Report, project: Path, env_values: dict[str, str]) -> None: - script = configured_value("COPY_SCRIPT", env_values) +def check_copy_script( + 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: return path, resolve_error = resolve_copy_script(project, script) @@ -579,6 +615,7 @@ def check_ready_env_keys( ) -> None: env_keys = env_file_keys(project) env_values = env_file_values(project) + make_values = makefile_values(project) lines = [] missing: list[str] = [] 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)") if docker_project: 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'}") if not found: missing.append(key) @@ -602,11 +639,13 @@ def check_ready_env_keys( lines.append("DOCKER_*: skipped (no docker track)") if copy_project: 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'}") - 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") - check_copy_script(report, project, env_values) + check_copy_script(report, project, env_values, make_values) else: lines.append("COPY_*: skipped (no copy track)") if missing: diff --git a/skills/builder/templates/env.builder b/skills/builder/templates/env.builder index dbfe8ee..ca01825 100644 --- a/skills/builder/templates/env.builder +++ b/skills/builder/templates/env.builder @@ -12,5 +12,5 @@ # IMAGE_TAG= # PLATFORMS= # optional override; project ARCHES lives in makefile.builder # 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