diff --git a/skills/builder/README.md b/skills/builder/README.md index 41730b4..a3267bc 100644 --- a/skills/builder/README.md +++ b/skills/builder/README.md @@ -38,13 +38,19 @@ pouch add builder -g # 全局 `templates/makefile.builder` 写出项目根 `makefile.builder`,并列出缺的发布配置。 不改用户已有的 `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*`, 变量 `ARCHES/ARCH/VERSION/DIST_DIR/PROJECT_NAME`。 `VERSION` 通过 `include /scripts/version.mk` 从 Git 推导。 多架构清单是 `ARCHES`,见下方「多架构」。 2. 运行 `python3 -I -S /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 DEB_SERVER_URL=https://deb.example.com @@ -53,6 +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 ``` 4. 日常发布就是两条命令: @@ -205,7 +212,7 @@ Docker 只有你点名镜像时才走。token 不会写进输出、提交或回 | `scripts/upload_deb.sh` | 上传 `.deb` 到 HTTP 包仓库(multipart package/token/repository_name) | | `scripts/publish_docker.sh` | buildx 构建 + 推送镜像,远端 digest 验证 | | `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.ios.builder` | iOS IPA + copy 轨道骨架 | diff --git a/skills/builder/SKILL.md b/skills/builder/SKILL.md index d0fa6b8..cf16578 100644 --- a/skills/builder/SKILL.md +++ b/skills/builder/SKILL.md @@ -10,8 +10,10 @@ description: >- # Builder:DEB / Docker / Android / iOS 构建发布 **make 管构建,skill 脚本管发布,本 SKILL.md 只留脚本做不了的决策。** -项目状态是根目录 `makefile.builder` 与发布用 `.env.builder`,不要创建 -`.pouch/builder/`,不要改用户的 `Makefile` / `makefile` / `.env`。 +项目状态是**当前 builder 项目根**的 `makefile.builder` 与 `.env.builder`;项目自定义分发脚本放在 +同一项目根的 `.pouch/builder/`,不要改用户的 `Makefile` / `makefile` / `.env`。如果父仓库包含 +submodule,submodule 是独立 builder 项目:它的 `makefile.builder`、`.env.builder` 和 +`.pouch/builder/` 必须放在 submodule 内,父仓库只负责调用或固定 submodule commit。 开始时解析当前 `SKILL.md` 所在目录,记为 ``。优先 `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 或仓库名。 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(版本规则冲突)。 @@ -50,6 +52,7 @@ DEB_TOKEN= DOCKER_REGISTRY=registry.example.com COPY_DEST=file-server:/srv 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` 并注明未提交修改。 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_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、工作区状态和未完成项。 diff --git a/skills/builder/references/contract.md b/skills/builder/references/contract.md index fe477da..b3bbe98 100644 --- a/skills/builder/references/contract.md +++ b/skills/builder/references/contract.md @@ -4,6 +4,10 @@ check.py,本文档跟随。所有接入项目按同一套 make 目标、产物形状和环境变量执行, builder 脚本只做发布,不做项目特定的构建逻辑。 +配置边界按 Git 项目根生效:`makefile.builder` 所在仓库就是 builder 项目根。 +如果父仓库通过 submodule 引入另一个项目,submodule 的构建、`.env.builder` 和 +`.pouch/builder/` 分发脚本属于 submodule;父仓库不得替它承载这些配置。 + 分工原则:**make 管构建(项目内、确定性),skill 脚本管发布(跨项目 SSOT), Agent 只保留授权判断和歧义处理。** @@ -41,7 +45,7 @@ make -f makefile.builder | `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-copy` | 同时有多种发布轨道的项目 | 仅调 builder 的 `copy_artifact.sh`;由它按配置选择内置拷贝或自定义脚本 | | `push` | 单一产物类型时必备;多产物项目为聚合 | 依序调用对应 push-* 或直接调脚本;是发布的唯一 make 入口 | 规则: @@ -49,7 +53,8 @@ make -f makefile.builder 1. 项目有 DEB 产物的判据:`makefile.builder` 配方引用 `dpkg-deb`/`debuild` 或产出 `.deb`。 有镜像的判据:项目根存在 `Dockerfile`。 Android 判据:配方产出 `.apk` / `.aab`。iOS 判据:配方产出 `.ipa`。 - 拷贝发布判据:配方调用 `copy_artifact.sh`。 + 拷贝发布判据:配方调用 `copy_artifact.sh`。`copy_artifact.sh` 是稳定入口,具体分发可由 + `COPY_SCRIPT` 接管。 2. 多于一种发布轨道必须拆 `push-deb` / `push-docker` / `push-copy`,`push` 依序聚合; 单轨道项目一个 `push` 即可。 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_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 轨道 diff --git a/skills/builder/scripts/check.py b/skills/builder/scripts/check.py index ae7477a..163dafb 100755 --- a/skills/builder/scripts/check.py +++ b/skills/builder/scripts/check.py @@ -442,7 +442,7 @@ def check_secrets_and_tags(report: Report, project: Path) -> None: DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY") 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_]*)=(.*)$") @@ -469,10 +469,72 @@ def env_file_keys(project: Path) -> set[str]: 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: 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( report: Report, *, deb_project: bool, docker_project: bool, copy_project: bool = False ) -> None: @@ -516,6 +578,7 @@ def check_ready_env_keys( copy_project: bool = False, ) -> None: env_keys = env_file_keys(project) + env_values = env_file_values(project) lines = [] missing: list[str] = [] 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: found = key_present(key, env_keys) lines.append(f"{key}: {'present' if found else 'MISSING'}") - if not found: - missing.append(key) + if not any(key_present(key, env_keys) for key in COPY_ENV_KEYS): + missing.append("COPY_DEST or COPY_SCRIPT") + check_copy_script(report, project, env_values) else: lines.append("COPY_*: skipped (no copy track)") 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( [ "", 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.", ] ) diff --git a/skills/builder/scripts/copy_artifact.sh b/skills/builder/scripts/copy_artifact.sh index ba8a5ec..d9eb907 100755 --- a/skills/builder/scripts/copy_artifact.sh +++ b/skills/builder/scripts/copy_artifact.sh @@ -6,12 +6,17 @@ usage() { Usage: COPY_DEST=/path/or/container:/dir \ 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 target `container:/absolute/dir` (docker cp). Does not read `.env`. +Copy built artifacts using COPY_SCRIPT when configured; otherwise copy to +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): - 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 (comma or space separated) ALLOW_UNCOMMITTED=1 Publish despite a dirty working tree @@ -58,9 +63,13 @@ while (($#)); do esac done -if [[ -z "$dest" || $# -eq 0 ]]; then - echo "Error: COPY_DEST and at least one file are required." >&2 - echo "Set them in the environment or the project root .env.builder." >&2 +if [[ -z "$dest" && -z "${COPY_SCRIPT:-}" || $# -eq 0 ]]; then + if [[ $# -eq 0 ]]; then + 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 exit 2 fi @@ -73,6 +82,63 @@ if [[ -n "$project_root" ]] && git -C "$project_root" rev-parse HEAD >/dev/null 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() { local d=$1 [[ "$d" != /* && "$d" != ./* && "$d" != ../* && "$d" != ~* ]] \ diff --git a/skills/builder/templates/env.builder b/skills/builder/templates/env.builder index 76e41a0..dbfe8ee 100644 --- a/skills/builder/templates/env.builder +++ b/skills/builder/templates/env.builder @@ -11,5 +11,6 @@ # DOCKER_REPOSITORY= # IMAGE_TAG= # 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 diff --git a/skills/builder/templates/makefile.android.builder b/skills/builder/templates/makefile.android.builder index c84894b..63cd745 100644 --- a/skills/builder/templates/makefile.android.builder +++ b/skills/builder/templates/makefile.android.builder @@ -1,5 +1,6 @@ # Builder 契约文件。放到项目根,用 `make -f makefile.builder `。 # Android 轨道:build 产出 dist/__.apk,push 走 copy_artifact.sh。 +# copy_artifact.sh 会按 COPY_SCRIPT 分发;未配置时回退到 COPY_DEST。 # ARCHES 是构建机 ISA;APK 可含多 ABI。不要把这些目标写进用户 Makefile。 ARCHES ?= amd64 @@ -35,7 +36,7 @@ help: @echo " make -f makefile.builder version - 打印规范版本" @echo " make -f makefile.builder arches - 打印架构清单" @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: @echo $(VERSION) diff --git a/skills/builder/templates/makefile.ios.builder b/skills/builder/templates/makefile.ios.builder index 6add0bb..64e7c62 100644 --- a/skills/builder/templates/makefile.ios.builder +++ b/skills/builder/templates/makefile.ios.builder @@ -1,5 +1,6 @@ # Builder 契约文件。放到项目根,用 `make -f makefile.builder `。 # iOS 轨道:build 产出 dist/__.ipa,push 走 copy_artifact.sh。 +# copy_artifact.sh 会按 COPY_SCRIPT 分发;未配置时回退到 COPY_DEST。 # ARCHES 是构建机 ISA。不要把这些目标写进用户 Makefile。 ARCHES ?= arm64 @@ -35,7 +36,7 @@ help: @echo " make -f makefile.builder version - 打印规范版本" @echo " make -f makefile.builder arches - 打印架构清单" @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: @echo $(VERSION)