feat(builder): support project-local distribution scripts
This commit is contained in:
@@ -38,13 +38,19 @@ pouch add builder -g # 全局
|
|||||||
`templates/makefile.builder` 写出项目根 `makefile.builder`,并列出缺的发布配置。
|
`templates/makefile.builder` 写出项目根 `makefile.builder`,并列出缺的发布配置。
|
||||||
不改用户已有的 `Makefile` 或 `.env`。
|
不改用户已有的 `Makefile` 或 `.env`。
|
||||||
|
|
||||||
|
配置跟随 `makefile.builder` 所在的 Git 根。如果 APK/IPA 位于 Git submodule,
|
||||||
|
就在 submodule 内配置 `.env.builder` 和 `.pouch/builder/`;父仓库只固定 submodule commit
|
||||||
|
并负责上层编排。
|
||||||
|
|
||||||
1. `makefile.builder` 目标:`help/build/clean/version` + 条件 `deb/docker/push*`,
|
1. `makefile.builder` 目标:`help/build/clean/version` + 条件 `deb/docker/push*`,
|
||||||
变量 `ARCHES/ARCH/VERSION/DIST_DIR/PROJECT_NAME`。
|
变量 `ARCHES/ARCH/VERSION/DIST_DIR/PROJECT_NAME`。
|
||||||
`VERSION` 通过 `include <builder>/scripts/version.mk` 从 Git 推导。
|
`VERSION` 通过 `include <builder>/scripts/version.mk` 从 Git 推导。
|
||||||
多架构清单是 `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`):
|
3. 在项目根 `.env.builder` 配置发布环境变量(不要写进 `.env`)。APK/IPA 可配置
|
||||||
|
`COPY_SCRIPT=.pouch/builder/publish-apk.sh`,它接收产物路径作为位置参数;未配置时使用
|
||||||
|
`COPY_DEST` 的本地目录或容器目录后备逻辑:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
DEB_SERVER_URL=https://deb.example.com
|
DEB_SERVER_URL=https://deb.example.com
|
||||||
@@ -53,6 +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
|
||||||
```
|
```
|
||||||
|
|
||||||
4. 日常发布就是两条命令:
|
4. 日常发布就是两条命令:
|
||||||
@@ -205,7 +212,7 @@ Docker 只有你点名镜像时才走。token 不会写进输出、提交或回
|
|||||||
| `scripts/upload_deb.sh` | 上传 `.deb` 到 HTTP 包仓库(multipart package/token/repository_name) |
|
| `scripts/upload_deb.sh` | 上传 `.deb` 到 HTTP 包仓库(multipart package/token/repository_name) |
|
||||||
| `scripts/publish_docker.sh` | buildx 构建 + 推送镜像,远端 digest 验证 |
|
| `scripts/publish_docker.sh` | buildx 构建 + 推送镜像,远端 digest 验证 |
|
||||||
| `scripts/verify_deb.sh` | 核对包元数据、内容与 SHA-256 |
|
| `scripts/verify_deb.sh` | 核对包元数据、内容与 SHA-256 |
|
||||||
| `scripts/copy_artifact.sh` | 把产物拷到 `COPY_DEST`(本地目录或 `container:/dir`) |
|
| `scripts/copy_artifact.sh` | 按 `COPY_SCRIPT` 分发产物,未配置时拷到 `COPY_DEST` |
|
||||||
| `templates/makefile.android.builder` | Android APK + copy 轨道骨架 |
|
| `templates/makefile.android.builder` | Android APK + copy 轨道骨架 |
|
||||||
| `templates/makefile.ios.builder` | iOS IPA + copy 轨道骨架 |
|
| `templates/makefile.ios.builder` | iOS IPA + copy 轨道骨架 |
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ description: >-
|
|||||||
# Builder:DEB / Docker / Android / iOS 构建发布
|
# Builder:DEB / Docker / Android / iOS 构建发布
|
||||||
|
|
||||||
**make 管构建,skill 脚本管发布,本 SKILL.md 只留脚本做不了的决策。**
|
**make 管构建,skill 脚本管发布,本 SKILL.md 只留脚本做不了的决策。**
|
||||||
项目状态是根目录 `makefile.builder` 与发布用 `.env.builder`,不要创建
|
项目状态是**当前 builder 项目根**的 `makefile.builder` 与 `.env.builder`;项目自定义分发脚本放在
|
||||||
`.pouch/builder/`,不要改用户的 `Makefile` / `makefile` / `.env`。
|
同一项目根的 `.pouch/builder/`,不要改用户的 `Makefile` / `makefile` / `.env`。如果父仓库包含
|
||||||
|
submodule,submodule 是独立 builder 项目:它的 `makefile.builder`、`.env.builder` 和
|
||||||
|
`.pouch/builder/` 必须放在 submodule 内,父仓库只负责调用或固定 submodule commit。
|
||||||
|
|
||||||
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
|
开始时解析当前 `SKILL.md` 所在目录,记为 `<skill-dir>`。优先
|
||||||
`git rev-parse --show-toplevel` 解析项目根。
|
`git rev-parse --show-toplevel` 解析项目根。
|
||||||
@@ -26,7 +28,7 @@ description: >-
|
|||||||
|
|
||||||
## 初始化
|
## 初始化
|
||||||
|
|
||||||
1. 确认项目根。探测 `makefile.builder`、`.env.builder`、用户 Makefile(只当抄配方的证据,不改)、`Dockerfile`、`debian/`。`.env.builder` 只看键是否存在且非空,不读、不打印值。不要读取用户 `.env`。
|
1. 确认项目根。优先以包含目标 `makefile.builder` 的仓库根为准;若目标位于 submodule,则以 submodule 根为准,不向父仓库回溯配置。探测 `makefile.builder`、`.env.builder`、用户 Makefile(只当抄配方的证据,不改)、`Dockerfile`、`debian/`。`.env.builder` 只看键是否存在且非空,不读、不打印值。不要读取用户 `.env`。
|
||||||
2. 判定轨道:有 Dockerfile → docker;有 deb 信号或用户要打 deb → deb;`app.config.ts` / `assembleRelease` / `.apk` → android;`ios/` / `xcodebuild` / `.ipa` → ios;用户要拷到目录或容器 → copy。可多轨道。都不清则问。不要猜测 registry、token、COPY_DEST 或仓库名。
|
2. 判定轨道:有 Dockerfile → docker;有 deb 信号或用户要打 deb → deb;`app.config.ts` / `assembleRelease` / `.apk` → android;`ios/` / `xcodebuild` / `.ipa` → ios;用户要拷到目录或容器 → copy。可多轨道。都不清则问。不要猜测 registry、token、COPY_DEST 或仓库名。
|
||||||
3. 没有 `makefile.builder`:按轨道拷模板。deb/docker 用 `templates/makefile.builder`;仅 android 用 `templates/makefile.android.builder`;仅 ios 用 `templates/makefile.ios.builder`。按轨道删掉未使用的 deb/docker/push* 段,把 `build` 的 TODO 换成仓库里已有的真实编译命令(可从用户 Makefile 抄配方,但不要 `include` 或递归调用它),且配方必须使用 `$(ARCH)`。Android/iOS 的 `ARCHES` 是构建机 ISA(fat APK/IPA 仍只留本机一项)。按 [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 push-copy` 的子集。不要改用户 Makefile。
|
3. 没有 `makefile.builder`:按轨道拷模板。deb/docker 用 `templates/makefile.builder`;仅 android 用 `templates/makefile.android.builder`;仅 ios 用 `templates/makefile.ios.builder`。按轨道删掉未使用的 deb/docker/push* 段,把 `build` 的 TODO 换成仓库里已有的真实编译命令(可从用户 Makefile 抄配方,但不要 `include` 或递归调用它),且配方必须使用 `$(ARCH)`。Android/iOS 的 `ARCHES` 是构建机 ISA(fat APK/IPA 仍只留本机一项)。按 [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 push-copy` 的子集。不要改用户 Makefile。
|
||||||
4. 已有 `makefile.builder`:跑检查;按 FAIL 给出修补说明。不覆盖 `build`。用户明确要求按契约改时再加 `ARCHES`,并确认配方里有 `$(ARCH)`。不要调用 create-makefile(版本规则冲突)。
|
4. 已有 `makefile.builder`:跑检查;按 FAIL 给出修补说明。不覆盖 `build`。用户明确要求按契约改时再加 `ARCHES`,并确认配方里有 `$(ARCH)`。不要调用 create-makefile(版本规则冲突)。
|
||||||
@@ -50,6 +52,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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 检查
|
## 检查
|
||||||
@@ -64,5 +67,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_AS` 给第一份文件额外名字。脚本缺环境变量时加载 `.env.builder`(shell 显式值优先),不读 `.env`。不把 token 当命令行参数;不把脚本复制进项目。多平台镜像只能走 `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`(推荐 `.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`。
|
||||||
6. 发布成功不能只看「curl/push 已执行」。要有退出码、HTTP 成功或远端 digest;索引异步时报告「上传已接受,索引尚待更新」。最终回复给出包名/镜像引用、版本、架构、SHA-256 或 digest、源 commit、工作区状态和未完成项。
|
6. 发布成功不能只看「curl/push 已执行」。要有退出码、HTTP 成功或远端 digest;索引异步时报告「上传已接受,索引尚待更新」。最终回复给出包名/镜像引用、版本、架构、SHA-256 或 digest、源 commit、工作区状态和未完成项。
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
check.py,本文档跟随。所有接入项目按同一套 make 目标、产物形状和环境变量执行,
|
check.py,本文档跟随。所有接入项目按同一套 make 目标、产物形状和环境变量执行,
|
||||||
builder 脚本只做发布,不做项目特定的构建逻辑。
|
builder 脚本只做发布,不做项目特定的构建逻辑。
|
||||||
|
|
||||||
|
配置边界按 Git 项目根生效:`makefile.builder` 所在仓库就是 builder 项目根。
|
||||||
|
如果父仓库通过 submodule 引入另一个项目,submodule 的构建、`.env.builder` 和
|
||||||
|
`.pouch/builder/` 分发脚本属于 submodule;父仓库不得替它承载这些配置。
|
||||||
|
|
||||||
分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT),
|
分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT),
|
||||||
Agent 只保留授权判断和歧义处理。**
|
Agent 只保留授权判断和歧义处理。**
|
||||||
|
|
||||||
@@ -41,7 +45,7 @@ make -f makefile.builder <target>
|
|||||||
| `arches` | 可选(新模板必备) | 打印一行空格分隔的 `ARCHES`;无此目标视为存量 makefile,隐式单架构 |
|
| `arches` | 可选(新模板必备) | 打印一行空格分隔的 `ARCHES`;无此目标视为存量 makefile,隐式单架构 |
|
||||||
| `push-deb` | 同时有多种发布轨道的项目 | 仅调 builder 的 `upload_deb.sh`。未指定 `ARCH=` 时上传 `ARCHES` 中每一项对应的 `.deb`;指定了则只上传那一个 |
|
| `push-deb` | 同时有多种发布轨道的项目 | 仅调 builder 的 `upload_deb.sh`。未指定 `ARCH=` 时上传 `ARCHES` 中每一项对应的 `.deb`;指定了则只上传那一个 |
|
||||||
| `push-docker` | 同时有多种发布轨道的项目 | 仅调 builder 的 `publish_docker.sh` |
|
| `push-docker` | 同时有多种发布轨道的项目 | 仅调 builder 的 `publish_docker.sh` |
|
||||||
| `push-copy` | 同时有多种发布轨道的项目 | 仅调 builder 的 `copy_artifact.sh` |
|
| `push-copy` | 同时有多种发布轨道的项目 | 仅调 builder 的 `copy_artifact.sh`;由它按配置选择内置拷贝或自定义脚本 |
|
||||||
| `push` | 单一产物类型时必备;多产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 |
|
| `push` | 单一产物类型时必备;多产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 |
|
||||||
|
|
||||||
规则:
|
规则:
|
||||||
@@ -49,7 +53,8 @@ make -f makefile.builder <target>
|
|||||||
1. 项目有 DEB 产物的判据:`makefile.builder` 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`。
|
1. 项目有 DEB 产物的判据:`makefile.builder` 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`。
|
||||||
有镜像的判据:项目根存在 `Dockerfile`。
|
有镜像的判据:项目根存在 `Dockerfile`。
|
||||||
Android 判据:配方产出 `.apk` / `.aab`。iOS 判据:配方产出 `.ipa`。
|
Android 判据:配方产出 `.apk` / `.aab`。iOS 判据:配方产出 `.ipa`。
|
||||||
拷贝发布判据:配方调用 `copy_artifact.sh`。
|
拷贝发布判据:配方调用 `copy_artifact.sh`。`copy_artifact.sh` 是稳定入口,具体分发可由
|
||||||
|
`COPY_SCRIPT` 接管。
|
||||||
2. 多于一种发布轨道必须拆 `push-deb` / `push-docker` / `push-copy`,`push` 依序聚合;
|
2. 多于一种发布轨道必须拆 `push-deb` / `push-docker` / `push-copy`,`push` 依序聚合;
|
||||||
单轨道项目一个 `push` 即可。
|
单轨道项目一个 `push` 即可。
|
||||||
3. `docker` 目标只能本地构建。多平台镜像无法拆成"make 构建 + 单独推送"
|
3. `docker` 目标只能本地构建。多平台镜像无法拆成"make 构建 + 单独推送"
|
||||||
@@ -139,10 +144,25 @@ include $(HOME)/.pouch/skills/builder/scripts/version.mk
|
|||||||
|
|
||||||
| 变量 | 必填 | 说明 |
|
| 变量 | 必填 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `COPY_DEST` | 是 | 本地目录,或 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_AS` | 否 | 第一份文件的额外 basename,逗号或空格分隔 |
|
| `COPY_AS` | 否 | 第一份文件的额外 basename,逗号或空格分隔 |
|
||||||
|
|
||||||
`copy_artifact.sh FILE [FILE ...]` 把已构建文件拷到 `COPY_DEST`,校验目标存在后打印 SHA-256。不读 `.env`。脏工作树默认拒绝,`ALLOW_UNCOMMITTED=1` 放行。
|
`copy_artifact.sh FILE [FILE ...]` 是 APK/IPA 等产物的统一分发入口。不读 `.env`,脏工作树默认拒绝,
|
||||||
|
`ALLOW_UNCOMMITTED=1` 放行。
|
||||||
|
|
||||||
|
未配置 `COPY_SCRIPT` 时,入口把文件拷到 `COPY_DEST`,校验目标存在后打印 SHA-256。
|
||||||
|
配置 `COPY_SCRIPT=.pouch/builder/publish-apk.sh` 时,入口按项目根解析、先校验每个文件存在,然后直接执行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/absolute/path/to/publish-script FILE [FILE ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
相对路径只允许位于项目的 `.pouch/builder/` 下;绝对路径仍支持外部脚本。不经过 shell 字符串求值,
|
||||||
|
不把 token 放进参数。脚本通过位置参数接收原始产物绝对路径,自行负责重命名、上传、远端校验和通知;
|
||||||
|
当前项目根以 `BUILDER_PROJECT_ROOT` 环境变量提供。
|
||||||
|
脚本返回非零即视为分发失败;成功后入口只报告脚本成功和本地 SHA-256,不宣称远端已校验。
|
||||||
|
`--dry-run` 只打印将调用的脚本与文件,不执行自定义脚本。
|
||||||
|
|
||||||
### Docker 轨道
|
### Docker 轨道
|
||||||
|
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ def check_secrets_and_tags(report: Report, project: Path) -> None:
|
|||||||
|
|
||||||
DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY")
|
DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY")
|
||||||
DOCKER_ENV_KEYS = ("DOCKER_REGISTRY",)
|
DOCKER_ENV_KEYS = ("DOCKER_REGISTRY",)
|
||||||
COPY_ENV_KEYS = ("COPY_DEST",)
|
COPY_ENV_KEYS = ("COPY_DEST", "COPY_SCRIPT")
|
||||||
ENV_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
|
ENV_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
|
||||||
|
|
||||||
|
|
||||||
@@ -469,10 +469,72 @@ def env_file_keys(project: Path) -> set[str]:
|
|||||||
return keys
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def env_file_values(project: Path) -> dict[str, str]:
|
||||||
|
"""Return nonempty `.env.builder` values for internal validation only."""
|
||||||
|
path = project / BUILDER_ENV
|
||||||
|
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():
|
||||||
|
stripped = raw.strip()
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
match = ENV_KEY_LINE.match(stripped)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
value = match.group(2).strip().strip("'\"")
|
||||||
|
if value:
|
||||||
|
values[match.group(1)] = value
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
def key_present(key: str, env_keys: set[str]) -> bool:
|
def key_present(key: str, env_keys: set[str]) -> bool:
|
||||||
return bool(os.environ.get(key)) or key in env_keys
|
return bool(os.environ.get(key)) or key in env_keys
|
||||||
|
|
||||||
|
|
||||||
|
def configured_value(key: str, env_values: dict[str, str]) -> str:
|
||||||
|
"""Read a configured value without ever printing it."""
|
||||||
|
return os.environ.get(key, "") or env_values.get(key, "")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_copy_script(project: Path, script: str) -> tuple[Path | None, str | None]:
|
||||||
|
"""Resolve COPY_SCRIPT without allowing arbitrary project-relative paths."""
|
||||||
|
configured = Path(script).expanduser()
|
||||||
|
if configured.is_absolute():
|
||||||
|
return configured, None
|
||||||
|
try:
|
||||||
|
resolved = (project / configured).resolve()
|
||||||
|
allowed_root = (project / ".pouch" / "builder").resolve()
|
||||||
|
resolved.relative_to(allowed_root)
|
||||||
|
except ValueError:
|
||||||
|
return None, "relative COPY_SCRIPT must be under .pouch/builder/"
|
||||||
|
return resolved, None
|
||||||
|
|
||||||
|
|
||||||
|
def check_copy_script(report: Report, project: Path, env_values: dict[str, str]) -> None:
|
||||||
|
script = configured_value("COPY_SCRIPT", env_values)
|
||||||
|
if not script:
|
||||||
|
return
|
||||||
|
path, resolve_error = resolve_copy_script(project, script)
|
||||||
|
problems = []
|
||||||
|
if resolve_error:
|
||||||
|
problems.append(resolve_error)
|
||||||
|
elif path is not None and not path.is_file():
|
||||||
|
problems.append("COPY_SCRIPT does not point to a file")
|
||||||
|
elif path is not None and not os.access(path, os.X_OK):
|
||||||
|
problems.append("COPY_SCRIPT is not executable")
|
||||||
|
report.add(
|
||||||
|
FAIL if problems else PASS,
|
||||||
|
14,
|
||||||
|
"COPY_SCRIPT 可执行且路径明确",
|
||||||
|
"\n".join(problems) if problems else "configured custom copy script is executable",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_ready_toolchain(
|
def check_ready_toolchain(
|
||||||
report: Report, *, deb_project: bool, docker_project: bool, copy_project: bool = False
|
report: Report, *, deb_project: bool, docker_project: bool, copy_project: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -516,6 +578,7 @@ def check_ready_env_keys(
|
|||||||
copy_project: bool = False,
|
copy_project: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
env_keys = env_file_keys(project)
|
env_keys = env_file_keys(project)
|
||||||
|
env_values = env_file_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:
|
||||||
@@ -541,16 +604,24 @@ def check_ready_env_keys(
|
|||||||
for key in COPY_ENV_KEYS:
|
for key in COPY_ENV_KEYS:
|
||||||
found = key_present(key, env_keys)
|
found = key_present(key, env_keys)
|
||||||
lines.append(f"{key}: {'present' if found else 'MISSING'}")
|
lines.append(f"{key}: {'present' if found else 'MISSING'}")
|
||||||
if not found:
|
if not any(key_present(key, env_keys) for key in COPY_ENV_KEYS):
|
||||||
missing.append(key)
|
missing.append("COPY_DEST or COPY_SCRIPT")
|
||||||
|
check_copy_script(report, project, env_values)
|
||||||
else:
|
else:
|
||||||
lines.append("COPY_*: skipped (no copy track)")
|
lines.append("COPY_*: skipped (no copy track)")
|
||||||
if missing:
|
if missing:
|
||||||
|
if "COPY_DEST or COPY_SCRIPT" in missing:
|
||||||
|
config_hints = [
|
||||||
|
" COPY_DEST= # local/container destination, or",
|
||||||
|
" COPY_SCRIPT=.pouch/builder/publish-apk.sh # choose one",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
config_hints = [f" {key}=" for key in missing]
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
f"blocks publish, not build. Put keys in the environment or {BUILDER_ENV}:",
|
f"blocks publish, not build. Put keys in the environment or {BUILDER_ENV}:",
|
||||||
*[f" {key}=" for key in missing],
|
*config_hints,
|
||||||
f"Do not commit {BUILDER_ENV}. Do not put these keys in `.env`. Never print values.",
|
f"Do not commit {BUILDER_ENV}. Do not put these keys in `.env`. Never print values.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ usage() {
|
|||||||
Usage:
|
Usage:
|
||||||
COPY_DEST=/path/or/container:/dir \
|
COPY_DEST=/path/or/container:/dir \
|
||||||
copy_artifact.sh FILE [FILE ...]
|
copy_artifact.sh FILE [FILE ...]
|
||||||
|
COPY_SCRIPT=.pouch/builder/publish-apk.sh \
|
||||||
|
copy_artifact.sh FILE [FILE ...]
|
||||||
|
|
||||||
Copy built artifacts to COPY_DEST. Dest is a local directory, or a Docker
|
Copy built artifacts using COPY_SCRIPT when configured; otherwise copy to
|
||||||
copy target `container:/absolute/dir` (docker cp). Does not read `.env`.
|
COPY_DEST. COPY_DEST is a local directory or a Docker copy target
|
||||||
|
`container:/absolute/dir` (docker cp). Does not read `.env`.
|
||||||
|
|
||||||
Environment (project root .env.builder; shell values win):
|
Environment (project root .env.builder; shell values win):
|
||||||
COPY_DEST Required. Local dir or container:/dir
|
COPY_DEST Optional fallback. Local dir or container:/dir
|
||||||
|
COPY_SCRIPT Optional .pouch/builder/ relative path or absolute
|
||||||
|
executable. Receives FILE paths as positional args.
|
||||||
COPY_AS Optional. Extra basenames for the first file
|
COPY_AS Optional. Extra basenames for the first file
|
||||||
(comma or space separated)
|
(comma or space separated)
|
||||||
ALLOW_UNCOMMITTED=1 Publish despite a dirty working tree
|
ALLOW_UNCOMMITTED=1 Publish despite a dirty working tree
|
||||||
@@ -58,9 +63,13 @@ while (($#)); do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z "$dest" || $# -eq 0 ]]; then
|
if [[ -z "$dest" && -z "${COPY_SCRIPT:-}" || $# -eq 0 ]]; then
|
||||||
echo "Error: COPY_DEST and at least one file are required." >&2
|
if [[ $# -eq 0 ]]; then
|
||||||
echo "Set them in the environment or the project root .env.builder." >&2
|
echo "Error: at least one file is required." >&2
|
||||||
|
else
|
||||||
|
echo "Error: set COPY_DEST or COPY_SCRIPT." >&2
|
||||||
|
fi
|
||||||
|
echo "Set the value in the environment or the project root .env.builder." >&2
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
@@ -73,6 +82,63 @@ if [[ -n "$project_root" ]] && git -C "$project_root" rev-parse HEAD >/dev/null
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${COPY_SCRIPT:-}" ]]; then
|
||||||
|
copy_script=$COPY_SCRIPT
|
||||||
|
if [[ "$copy_script" != /* ]]; then
|
||||||
|
relative_script=${copy_script#./}
|
||||||
|
if [[ "$relative_script" != .pouch/builder/* || -z "$project_root" ]]; then
|
||||||
|
echo "Error: relative COPY_SCRIPT must be under .pouch/builder/." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
copy_script="$project_root/$relative_script"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$copy_script" ]]; then
|
||||||
|
echo "Error: COPY_SCRIPT is not a file: $copy_script" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ ! -x "$copy_script" ]]; then
|
||||||
|
echo "Error: COPY_SCRIPT is not executable: $copy_script" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
artifact_args=()
|
||||||
|
for src in "$@"; do
|
||||||
|
if [[ ! -f "$src" ]]; then
|
||||||
|
echo "Error: not a file: $src" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ "$src" == /* ]]; then
|
||||||
|
artifact_args+=("$src")
|
||||||
|
else
|
||||||
|
artifact_args+=("$PWD/$src")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if $dry_run; then
|
||||||
|
printf 'custom copy script:'
|
||||||
|
printf ' %q' "$copy_script" "${artifact_args[@]}"
|
||||||
|
printf '\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
export BUILDER_PROJECT_ROOT=${project_root:-}
|
||||||
|
if "$copy_script" "${artifact_args[@]}"; then
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
for src in "${artifact_args[@]}"; do
|
||||||
|
sha256sum -- "$src"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo "copy_artifact: custom script completed for $# file(s)"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
echo "Error: COPY_SCRIPT failed with exit code $status." >&2
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$dest" ]]; then
|
||||||
|
echo "Error: COPY_DEST is required when COPY_SCRIPT is not configured." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
is_docker_dest() {
|
is_docker_dest() {
|
||||||
local d=$1
|
local d=$1
|
||||||
[[ "$d" != /* && "$d" != ./* && "$d" != ../* && "$d" != ~* ]] \
|
[[ "$d" != /* && "$d" != ./* && "$d" != ../* && "$d" != ~* ]] \
|
||||||
|
|||||||
@@ -11,5 +11,6 @@
|
|||||||
# DOCKER_REPOSITORY=
|
# DOCKER_REPOSITORY=
|
||||||
# IMAGE_TAG=
|
# IMAGE_TAG=
|
||||||
# PLATFORMS= # optional override; project ARCHES lives in makefile.builder
|
# PLATFORMS= # optional override; project ARCHES lives in makefile.builder
|
||||||
# COPY_DEST= # 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_AS= # optional extra basenames for the first file
|
# COPY_AS= # optional extra basenames for the first file
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Builder 契约文件。放到项目根,用 `make -f makefile.builder <target>`。
|
# Builder 契约文件。放到项目根,用 `make -f makefile.builder <target>`。
|
||||||
# Android 轨道:build 产出 dist/<name>_<version>_<arch>.apk,push 走 copy_artifact.sh。
|
# Android 轨道:build 产出 dist/<name>_<version>_<arch>.apk,push 走 copy_artifact.sh。
|
||||||
|
# copy_artifact.sh 会按 COPY_SCRIPT 分发;未配置时回退到 COPY_DEST。
|
||||||
# ARCHES 是构建机 ISA;APK 可含多 ABI。不要把这些目标写进用户 Makefile。
|
# ARCHES 是构建机 ISA;APK 可含多 ABI。不要把这些目标写进用户 Makefile。
|
||||||
|
|
||||||
ARCHES ?= amd64
|
ARCHES ?= amd64
|
||||||
@@ -35,7 +36,7 @@ help:
|
|||||||
@echo " make -f makefile.builder version - 打印规范版本"
|
@echo " make -f makefile.builder version - 打印规范版本"
|
||||||
@echo " make -f makefile.builder arches - 打印架构清单"
|
@echo " make -f makefile.builder arches - 打印架构清单"
|
||||||
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
|
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
|
||||||
@echo " make -f makefile.builder push - copy_artifact.sh 拷到 COPY_DEST"
|
@echo " make -f makefile.builder push - copy_artifact.sh 分发 APK"
|
||||||
|
|
||||||
version:
|
version:
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Builder 契约文件。放到项目根,用 `make -f makefile.builder <target>`。
|
# Builder 契约文件。放到项目根,用 `make -f makefile.builder <target>`。
|
||||||
# iOS 轨道:build 产出 dist/<name>_<version>_<arch>.ipa,push 走 copy_artifact.sh。
|
# iOS 轨道:build 产出 dist/<name>_<version>_<arch>.ipa,push 走 copy_artifact.sh。
|
||||||
|
# copy_artifact.sh 会按 COPY_SCRIPT 分发;未配置时回退到 COPY_DEST。
|
||||||
# ARCHES 是构建机 ISA。不要把这些目标写进用户 Makefile。
|
# ARCHES 是构建机 ISA。不要把这些目标写进用户 Makefile。
|
||||||
|
|
||||||
ARCHES ?= arm64
|
ARCHES ?= arm64
|
||||||
@@ -35,7 +36,7 @@ help:
|
|||||||
@echo " make -f makefile.builder version - 打印规范版本"
|
@echo " make -f makefile.builder version - 打印规范版本"
|
||||||
@echo " make -f makefile.builder arches - 打印架构清单"
|
@echo " make -f makefile.builder arches - 打印架构清单"
|
||||||
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
|
@echo " make -f makefile.builder clean - 删除 $(DIST_DIR)"
|
||||||
@echo " make -f makefile.builder push - copy_artifact.sh 拷到 COPY_DEST"
|
@echo " make -f makefile.builder push - copy_artifact.sh 分发 IPA"
|
||||||
|
|
||||||
version:
|
version:
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|||||||
Reference in New Issue
Block a user