diff --git a/.gitattributes b/.gitattributes index c03b4dae..92353b6c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,6 @@ # PowerShell 脚本固定 CRLF(仅在 Windows runner 运行)。 *.ps1 text eol=crlf + +# upstream.lock 被 bash 按行解析(fetch-binaries.sh lock_get),CRLF 会让 URL/SHA 带 \r 尾巴。 +scripts/pg/upstream.lock text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ed6762c..150c08f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,3 +149,6 @@ jobs: git config --global user.email "ci@inkframe.test" git config --global user.name "InkFrame CI" bash test/scripts/release_tag_test.sh + # fetch-binaries.sh 回归测试(PKG-2A:upstream/bucket 双模式,curl/brew 全 PATH-stub 零网络)。 + - name: fetch-binaries.sh regression test + run: bash test/scripts/fetch_binaries_test.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb163129..25503e89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,8 +9,10 @@ # release 已存在则更新并追加资产,不覆盖 release-tag.sh 生成的 notes。 # # 「证书/存储就绪前」一切可跑: -# - PG 二进制 fetch:仅当 repo variable PG_ARTIFACT_BASE_URL 配置时执行;缺则 ::warning:: -# 并继续(产物不含嵌入式 PG,无法在干净机首启 PG,仅供构建/链接验证)。 +# - PG 二进制 fetch:默认 upstream 直拉(PKG-2A 方案 A——Windows=EDB 官方 zip 按 +# scripts/pg/upstream.lock SHA256 锁定;macOS=runner Homebrew postgresql@17 + +# make-relocatable),零配置即产出含嵌入式 PG 的安装物;repo variable +# PG_ARTIFACT_BASE_URL 配置时切对象存储(方案 B 覆盖)。 # - 签名/公证/MSIX 签名:脚本自检环境,缺凭据 → 打印 SKIPPED 退出 0,不阻断。 # - 原始构建产物(zip 的 .app / Release 目录)无条件上传,零密钥也有可下载工件。 name: release @@ -63,17 +65,17 @@ jobs: fi echo "BUILD_NUMBER=${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV" - name: cache PG binaries - if: ${{ vars.PG_ARTIFACT_BASE_URL != '' }} uses: actions/cache@v4 with: path: macos/Runner/Resources/pg/ - key: pg-${{ hashFiles('scripts/pg/pg-version.txt') }}-macos-arm64 + key: pg-${{ hashFiles('scripts/pg/pg-version.txt', 'scripts/pg/upstream.lock') }}-${{ vars.PG_ARTIFACT_BASE_URL != '' && 'bucket' || 'upstream' }}-macos-arm64 + # 不以 cache-hit 门控:缓存树若未过脚本校验会走重建路径,届时必须有 brew keg 兜底 + #(否则坏缓存 = 每次 re-run 恢复同一份坏树的死循环)。主版本从 pg-version.txt 推导。 + - name: install upstream PostgreSQL (Homebrew) + if: ${{ vars.PG_ARTIFACT_BASE_URL == '' }} + run: brew install "postgresql@$(cut -d. -f1 scripts/pg/pg-version.txt)" - name: fetch embedded PG binaries - if: ${{ vars.PG_ARTIFACT_BASE_URL != '' }} run: bash scripts/pg/fetch-binaries.sh - - name: warn — PG not bundled - if: ${{ vars.PG_ARTIFACT_BASE_URL == '' }} - run: echo "::warning title=PG not bundled::PG_ARTIFACT_BASE_URL 未配置,产物不含嵌入式 PostgreSQL,无法在干净机首启 PG(仅验证构建/链接)。配置 repo variable PG_ARTIFACT_BASE_URL 后重跑。" - name: flutter pub get run: flutter pub get - name: flutter analyze @@ -125,18 +127,13 @@ jobs: "BUILD_NAME=$ver" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "BUILD_NUMBER=$env:GITHUB_RUN_NUMBER" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: cache PG binaries - if: ${{ vars.PG_ARTIFACT_BASE_URL != '' }} uses: actions/cache@v4 with: path: windows/runner/resources/pg/ - key: pg-${{ hashFiles('scripts/pg/pg-version.txt') }}-windows-x64 + key: pg-${{ hashFiles('scripts/pg/pg-version.txt', 'scripts/pg/upstream.lock') }}-${{ vars.PG_ARTIFACT_BASE_URL != '' && 'bucket' || 'upstream' }}-windows-x64 - name: fetch embedded PG binaries - if: ${{ vars.PG_ARTIFACT_BASE_URL != '' }} shell: bash run: bash scripts/pg/fetch-binaries.sh - - name: warn — PG not bundled - if: ${{ vars.PG_ARTIFACT_BASE_URL == '' }} - run: echo "::warning title=PG not bundled::PG_ARTIFACT_BASE_URL 未配置,产物不含嵌入式 PostgreSQL(仅验证构建/链接)。" - name: flutter pub get run: flutter pub get - name: flutter analyze @@ -179,6 +176,13 @@ jobs: merge-multiple: true - name: list artifacts run: ls -R dist + - name: generate checksums (QG-6) + run: | + set -o pipefail + cd dist + find . -type f ! -name checksums.txt -print0 | sort -z \ + | xargs -0 sha256sum | sed 's|\./||' > checksums.txt + cat checksums.txt - name: attach artifacts to release uses: softprops/action-gh-release@v2 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47e5066d..6253bb2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -196,19 +196,24 @@ flutter test ### 方式 B:嵌入二进制(发布打包) ```bash +# 默认 upstream 直拉(零配置):Windows=EDB 官方 zip(scripts/pg/upstream.lock 锁 SHA256), +# macOS=本机 Homebrew postgresql@17 + make-relocatable(即方式 C 的自动化封装) +./scripts/pg/fetch-binaries.sh + +# 可选:对象存储覆盖(方案 B) export PG_ARTIFACT_BASE_URL=<对象存储 base URL> ./scripts/pg/fetch-binaries.sh ``` -拉取脚本会按 `scripts/pg/pg-version.txt`(当前 17.2)校验 SHA256,写入: -- macOS: `macos/Runner/Resources/pg//bin + /lib` -- Windows: `windows/runner/resources/pg//bin + /lib` +拉取脚本按 `scripts/pg/pg-version.txt`(当前 17.2;macOS upstream 因 brew 补丁位浮动只锁主版本)校验,写入: +- macOS: `macos/Runner/Resources/pg//bin + /lib + /share` +- Windows: `windows/runner/resources/pg//bin + /lib + /share` -未配置对象存储时脚本输出 `NOT_CONFIGURED`,不会误判成功。 +装配在 `.partial` 完成并通过校验(必需工具 + 版本)后才对换,失败零残留。 -### 方式 C:本机 Homebrew → 可重定位嵌入 PG(macOS 开发/本地出包,无需对象存储) +### 方式 C:本机 Homebrew → 可重定位嵌入 PG(macOS 手动路径,等价于方式 B 的 macOS 分支) -当对象存储尚未配置时,开发机可直接从本机 Homebrew `postgresql@17` 生成「可重定位」的 +开发机可直接从本机 Homebrew `postgresql@17` 生成「可重定位」的 嵌入式 PG(vendoring 全部依赖闭包 + 改写 install name 为 `@rpath` + ad-hoc 重签名), 落地到 `macos/Runner/Resources/pg/macos-arm64/`,供 `flutter build macos` 打进 `.app`: diff --git a/ROADMAP.md b/ROADMAP.md index fa1db77d..78140c42 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -34,7 +34,7 @@ - **M4 能力完整** — 分镜流水线(脚本解析/序列预览) · 导出转码归一 · 画廊复用(存角色/发送画布) · 自定义 Provider 设置页 UI · 角色一致性进阶(见 MASTERPLAN §2) - **M5 能上生产** — 数据安全(备份/导出导入仍为零;崩溃钩子 #160、PG SCRAM #172、应用内检查更新 #173 已落地) · 签名公证打包 · onboarding -- **发布工程** — release.yml 已首跑出双平台产物;待补:PG 分发源(干净机可装)、签名凭据(U1/U2) +- **发布工程** — release.yml 已首跑出双平台产物;PG 分发源已落(官方上游直拉,产物含嵌入式 PG);待补:签名凭据(U1/U2) ## 🙋 Help Wanted diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a27727eb..e5e7baa1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1189,15 +1189,16 @@ PG 二进制**不进代码仓库**。当前现状: - **CI 测试**:`.github/workflows/ci.yml` 用 `postgres:17-alpine` service container(`TEST_PG_URL` 注入),不打包二进制。 - **本地开发**:`PgBinaryLocator`(`lib/storage/pg_binary_locator.dart`)按约定目录发现本机 PG;找不到时 storage 集成测试 `markTestSkipped`。 -> **Planned**:Release 构建打包内嵌 PG 二进制的拉取脚本(`scripts/fetch-pg-binaries.sh` + `scripts/pg-version.txt`,从对象存储按平台拉取并校验版本)尚未编写——Release 流水线立项时一并实现。 +- **Release 构建**:`scripts/pg/fetch-binaries.sh`(版本锁 `scripts/pg/pg-version.txt`)置备嵌入式 PG——默认 upstream 直拉(Windows=EDB 官方 zip 按 `scripts/pg/upstream.lock` SHA256 锁定裁剪;macOS=Homebrew postgresql@17 + `make-relocatable-macos.sh`),设 `PG_ARTIFACT_BASE_URL` 时切对象存储。详见 `docs/BUILD-RELEASE.md` §3。 ### 14.4 构建步骤 -> **Planned**:Release 流水线(含 `fetch-pg-binaries.sh` / `sign-and-notarize.sh`)尚未搭建;当前 CI 只跑 PR 检查(analyze + test + coverage)。以下为 Release 蓝图: +> Release 流水线已落地(`.github/workflows/release.yml`,alpha.10 起真实跑通;签名步缺凭据自跳过)。 +> 完整流程与顺序约束见 `docs/BUILD-RELEASE.md` §4/§13,此处仅示意: ```bash # 完整 Release 构建流程 -scripts/fetch-pg-binaries.sh # 1. 拉取 PG 二进制 +bash scripts/pg/fetch-binaries.sh # 1. 置备 PG 二进制(默认 upstream 直拉) flutter pub get # 2. 依赖 flutter gen-l10n # 3. 生成 i18n dart run build_runner build # 4. 代码生成(freezed / riverpod) diff --git a/docs/BOARD.md b/docs/BOARD.md index 922612a2..4f197c1b 100644 --- a/docs/BOARD.md +++ b/docs/BOARD.md @@ -8,8 +8,9 @@ > ROAD-TO-BETA)视为**归档快照**,不再更新;状态以本表为准。 > > 状态图例:✅ 完成 · 🔵 进行中 · ⬜ 未开始 · 🅿️ 已延后(附因) -> 最近更新:2026-07-14 · 最新发布:**v0.1.0-alpha.10**(release.yml 首跑,双平台 unsigned 产物; -> 待 PG 分发源 PKG-2 + 签名 U1/U2 方为干净机可装) +> 最近更新:2026-07-18 · 最新发布:**v0.1.0-alpha.10**(release.yml 首跑,双平台 unsigned 产物; +> PG 分发源已随 PKG-2A 落地——下个 tag 起产物含嵌入式 PG(mac 侧待 release CI 首跑验证); +> 待签名 U1/U2 方为干净机免绕行可装) ## M1 —「能用起来」✅ 完成(已随 PR #133 合入 main) @@ -97,6 +98,7 @@ | GAP-7 Inspector 测试欠账收口(预设点选应用+成本文案精确断言)+ ON-5 五屏空态 golden(Studio empty/error、Canvas empty、Gallery empty、Settings;ubuntu 铸线)——第 10 条(ON-3/4/GAP-7/ON-5)收官;**余 GAP-1 整卡、GAP-3 余量未清**(评审 P1 纠偏:勿宣「全部收官」) | #198 | | GAP-3 余量收口(24 站点审计:方向读错并入横幅链;raw toString 上屏收敛——设置页探测诊断行为有意例外;base style 编辑器读错中止防空覆盖(评审 P2-3);InkErrorBanner.onRetry 死参数删除;**InkAsyncSlot 判 YAGNI**——列表槽位 LB-06 已全收口) | #199 | | **GAP-1 设置页 Custom Provider 编辑 UI(上线前必做最后一卡)**:CustomProviderStore 写侧(raw 保真+损坏拒写+原子写);校验抽 core 纯函数双端共用;列表+表单+删除确认+重启生效常驻条;API Keys custom:* 行 displayName 顺带修——**随本卡合入,上线前必做 1-10 全部落地** | #200 | +| **PKG-2A PG 二进制分发源(方案 A 上游直拉,beta 硬阻塞里唯一零用户依赖项收官)**:fetch-binaries.sh 双模式重写——upstream 默认(Win=EDB 官方 zip `upstream.lock` 锁 URL+SHA256+裁剪 bin/lib/share;mac=runner brew postgresql@17+make-relocatable,主版本匹配),`PG_ARTIFACT_BASE_URL` 保留为方案 B 覆盖;`.partial` 原子落位+必需工具校验(含 pg_dump/pg_restore);release.yml 去门控无条件 fetch;回归测试入 ci release-scripts;**顺带 QG-6 的 checksums.txt**(publish job 全资产 sha256);本机真栈验收=EDB 裁剪产物过 realpg E2E 全链 | 本 PR | ## M1 补遗(审计发现的悬空项) @@ -158,4 +160,5 @@ | GAP-3 评审 P3 残留(#199):①方向读错期 lane_toolbar 置灰未做(二元域无损毁,但 toggle 到不了 horizontal 的怪异 UX);②横幅三源 `??` 链+单 `_dismissed` 遮蔽——关掉 edges 错后并发 lanes/direction 错不上屏(改集合);③非 InkError→errorUnknown 后无任何日志线索(此前 raw toString 至少可报障),建议 error 分支补 log 或 ProviderObserver.providerDidFail | 🅿️ | ①②低害 UX;③可观测性,随日志面收口 | | ON-5 评审 P3 残留(#198):①golden sentinel 单点——删 studio_empty.png 五测静默 skip 而 node_card 仍 ran>0 骗过整 job 守卫;评审提的 `skipped>0&&baselines>0→fail` 会误伤增量铸线 bootstrap(本 PR 自身流程即反例),需更细粒度方案;②成本断言 0.01×1 测不出漏乘 batch,补 maxBatchSize>1→\$0.02 用例;③ci.yml 与 update-goldens.yml 双 pin FLUTTER_VERSION 升级必须同步+重铸 | 🅿️ | ①设计再议 ②一测的事 ③升级 checklist 项 | | ON-3 评审 P3 残留(#196):卸载 ffmpeg 后设置页旧 Available 滞留(hit 缓存 app 级,设置页不调 invalidate;导出失败路径会自愈);PATH 命中显示裸 `ffmpeg` 当路径 | 🅿️ | 低害:重启/导出失败自愈;若做刷新按钮同窗顺修 | +| PKG-2A 评审 P3 残留:fetch-binaries macOS upstream 无架构核对——arm64 机上 INKFRAME_PG_PLATFORM=macos-x64 会把 arm64 二进制落进 x64 目录且本机 verify 照过(操作员失误场景;release.yml 现只建 arm64;make-relocatable 旧脚本同病) | 🅿️ | 随 macos-13 x64 matrix(PKG-7 真做)同窗加 uname 对 PLATFORM 的核对 | | ON-2b 评审 P3 三条(#195):①真 PG 回滚测只走 projects+canvas 两仓储,建议扩成与 createSample 同构四步;②泳道带厚 400 魔数散落三处(接口默认/注释/测试),建议提 kDefaultLaneSize;③示例 laneStylePrompt 走 zh 本地化与 base_style_presets「模型合约保英文」惯例有张力(用户可见可编辑,判定可接受)——产品可拍板改为仅本地化 label | 🅿️ | ①②低成本顺窗;③产品取舍,英文语系 provider 出图质量考量 | diff --git a/docs/BUILD-RELEASE.md b/docs/BUILD-RELEASE.md index e2fd0767..b3b0684e 100644 --- a/docs/BUILD-RELEASE.md +++ b/docs/BUILD-RELEASE.md @@ -116,35 +116,57 @@ CI 由 git tag 推导:`v0.1.0-beta.1` → build-name = `0.1.0-beta.1`,build- ``` macos/Runner/Resources/pg/ ├── macos-arm64/ -│ ├── bin/ # initdb, pg_ctl, postgres -│ └── lib/ # 共享库 +│ ├── bin/ # initdb, pg_ctl, postgres, pg_dump, pg_restore +│ ├── lib/ # 依赖闭包(vendored dylib)+ lib/postgresql 模块 +│ └── share/postgresql/ # initdb 所需 bki/timezone/sql └── macos-x64/ # 同上 windows/runner/resources/pg/ └── windows-x64/ - ├── bin/ # initdb.exe, pg_ctl.exe, postgres.exe - └── lib/ + ├── bin/ # initdb.exe, pg_ctl.exe, postgres.exe, pg_dump.exe, pg_restore.exe + ├── lib/ + └── share/ # initdb 所需(已裁 doc/man/locale) build/pg/linux-x64/ # 仅本地烟测,不进发布 ``` -**大小预算**:每平台 ~60 MB,打包压缩后(DMG/MSIX)约 20 MB。 +**大小预算**:Windows 实测 82 MB(EDB 官方 zip 裁剪后;zip/安装包压缩后显著更小); +macOS 为 make-relocatable 产物(~60 MB 量级,以 release CI 实测为准)。 ### 3.2 拉取脚本 -`scripts/pg/fetch-binaries.sh`: +`scripts/pg/fetch-binaries.sh` 两种模式(PKG-2A): -- 从 `PG_ARTIFACT_BASE_URL` 指向的对象存储下载(未配置 → `NOT_CONFIGURED` 非零退出,日常开发用本地 PG 即可,见 `docs/SETUP.md`) -- 版本锁 + SHA256 校验按 `scripts/pg/pg-version.txt`(17.2) -- 幂等:`bin/postgres --version` 已匹配则跳过 -- 失败时明确报错,不静默降级 +- **upstream(默认,方案 A,零配置)**: + - `windows-x64`:EDB 官方 zip 直拉,URL + SHA256 锁定在 `scripts/pg/upstream.lock`; + 只取 `pgsql/{bin,lib,share}` 并裁掉 `share/{doc,man,locale}`(pgAdmin/StackBuilder/include 不拷) + - `macos-*`:本机/runner 的 Homebrew `postgresql@17` → `make-relocatable-macos.sh` + 生成自洽可分发目录(brew 补丁位浮动 → 校验只锁主版本 17) + - `linux-x64`:无 upstream 源(仅本地烟测平台,用系统 PG 或 bucket 模式) +- **bucket(方案 B 覆盖)**:设 `PG_ARTIFACT_BASE_URL` 时从对象存储拉 + `$BASE/$VERSION/$PLATFORM.tar.gz`(+ `.sha256`),SHA256 校验按远端 sidecar + +公共不变量: + +- 版本锁按 `scripts/pg/pg-version.txt`(17.2;Windows/bucket 精确匹配) +- 落位原子:装配+校验全在 `.partial`,通过才对换,失败零残留 +- 校验统一:必需工具 `postgres/initdb/pg_ctl/pg_dump/pg_restore`(`PgBinaryLocation` + 契约,pg_dump/pg_restore 为 LB-10/LB-22 备份还原所需)+ 版本匹配 +- 幂等:现存目标过同一校验即短路,不重复下载 +- 回归测试:`test/scripts/fetch_binaries_test.sh`(ci.yml release-scripts job) ```bash -# 本地首次 / 清理后执行 +# 本地首次 / 清理后执行(Windows 从 Git Bash;macOS 需先 brew install postgresql@17) +bash scripts/pg/fetch-binaries.sh + +# 对象存储覆盖(方案 B) export PG_ARTIFACT_BASE_URL=https:///inkframe/pg bash scripts/pg/fetch-binaries.sh ``` +风险与处置:EDB 撤下/变更旧版 zip → SHA256 锁死使脚本硬报错(绝不静默换包), +届时下载新包实测 SHA 后更新 `upstream.lock`。 + ### 3.3 运行时定位 `PgBinaryLocator` 的查找顺序(高到低): @@ -535,13 +557,17 @@ gh release create v0.1.0 \ - **触发**:push tag `v*`(由 `scripts/release-tag.sh` 打 tag 后自动触发)+ `workflow_dispatch`(演练)。 - **job**: - `build-macos`(macos-14,arm64)/ `build-windows`(windows-latest): - `(fetch PG, if vars.PG_ARTIFACT_BASE_URL) → pub get → analyze → test --exclude-tags pg →` + `fetch PG(无条件:默认 upstream 直拉,vars.PG_ARTIFACT_BASE_URL 配置时切对象存储)→ pub get → analyze → test --exclude-tags pg →` `flutter build → zip unsigned 产物(无条件上传)→ 签名/公证(缺凭据自跳过)→ 打包(best-effort)→ upload-artifact` - `publish`(ubuntu,仅真实 tag):download artifacts → `softprops/action-gh-release@v2` 幂等附加资产 到该 tag 的 Release(`--generate-notes`,不依赖 CHANGELOG 文件)。 - **守卫模型**: - - PG 二进制 fetch:repo variable `PG_ARTIFACT_BASE_URL` 配置时才跑;缺则 `::warning::` 并继续 - (产物不含嵌入式 PG,仅验证构建/链接)。 + - PG 二进制 fetch:**无条件执行**(PKG-2A)——默认 upstream 直拉(Windows=EDB zip 按 + `upstream.lock` SHA256 锁定;macOS=runner `brew install postgresql@17` + make-relocatable, + brew 步**不以 cache-hit 门控**——缓存树未过脚本校验会走重建路径,必须有 keg 兜底, + 否则坏缓存 = 每次 re-run 恢复同一份坏树的死循环);repo variable + `PG_ARTIFACT_BASE_URL` 配置时切对象存储。产物恒含嵌入式 PG。 + - publish job 生成 `checksums.txt`(全部资产的 sha256,QG-6)并随资产上传。 - 签名/公证/MSIX 签名:脚本自检环境,缺凭据 → 打印 `SKIPPED` 退出 0,不阻断。 - 原始构建产物(zip 的 `.app` / `Release` 目录)**无条件上传** —— 零密钥也有可下载工件。 - **与 §13 早期愿景稿的差异(已就现实修正)**: @@ -560,13 +586,13 @@ gh release create v0.1.0 \ | `distribute_options.yaml` | flutter_distributor MSIX 配置骨架 | publisher 待 EV 证书 subject | | `scripts/smoke/{macos-smoke.sh,windows-smoke.ps1}` | 双平台 boot 烟测(本地可复现) | — | -### 13.4 待提供的机密 / 变量(流水线就绪前的唯一缺口) +### 13.4 待提供的机密 / 变量(剩余缺口=签名/公证凭据) -配齐后 release.yml 的签名/公证/PG 打包自动生效,无需改代码: +配齐后 release.yml 的签名/公证自动生效,无需改代码(PG 打包已零配置默认生效,PKG-2A): | 名称 | 类型 | 用途 | |---|---|---| -| `PG_ARTIFACT_BASE_URL` | repo **variable**(或改 secret) | 嵌入式 PG 二进制对象存储 base URL(`fetch-binaries.sh` 读) | +| `PG_ARTIFACT_BASE_URL` | repo **variable**(可选) | **不再是缺口**:缺省走 upstream 直拉(PKG-2A);仅当想切对象存储分发(方案 B)时配置 | | `MACOS_CERT_P12_BASE64` / `MACOS_CERT_PASSWORD` | secret | Developer ID Application 证书(.p12 base64)+ 密码 | | `MACOS_SIGN_IDENTITY` | secret | 签名身份串 `Developer ID Application: Name (TEAMID)` | | `APPLE_ID` / `APPLE_TEAM_ID` / `APPLE_APP_SPECIFIC_PASSWORD` | secret | notarytool 公证凭据 | diff --git a/docs/MASTERPLAN.md b/docs/MASTERPLAN.md index d4ba6b64..4018b34b 100644 --- a/docs/MASTERPLAN.md +++ b/docs/MASTERPLAN.md @@ -13,7 +13,7 @@ ## 0. 总览:里程碑与主线 ``` -现状(2026-07-14):M1 ✅ M2 ✅ M3 🔵(四方向首切片+导出 UI 入口 #143) alpha.10 已发布(release.yml 首跑,双平台 unsigned 产物;欠 PG 分发源/checksums/Latest 卫生);M5 backend 线飞推(LB-01~04/06/07/08/09/13a/13b/14/16/17/19 已合)+**UI 线连发**(GAP-8/#164、GAP-4/#167、PL-4a/#168、PL-6/#170、CV-1+PL-1/#174、PL-2/#176、画布体验大改 #179/#180)+M4 破冰(XM-1/#177+XM-1b/#181)+发布线 UPD-1/#173、LEG-1/#147;D-7/D-8/D-BE-2/D-10 已拍(见 §9) +现状(2026-07-18):M1 ✅ M2 ✅ M3 🔵(四方向首切片+导出 UI 入口 #143) alpha.10 已发布(release.yml 首跑,双平台 unsigned 产物;PG 分发源+checksums 已随 PKG-2A 落,欠 Latest 卫生);M5 backend 线飞推(LB-01~04/06/07/08/09/13a/13b/14/16/17/19 已合)+**UI 线连发**(GAP-8/#164、GAP-4/#167、PL-4a/#168、PL-6/#170、CV-1+PL-1/#174、PL-2/#176、画布体验大改 #179/#180)+M4 破冰(XM-1/#177+XM-1b/#181)+发布线 UPD-1/#173、LEG-1/#147;D-7/D-8/D-BE-2/D-10 已拍(见 §9) │ M4 「能力完整」……… M3 各方向二/三切片:storyboard 流水线成型、聚合器可视化配置、 │ 画廊可复用、导出可用可靠 + 角色进阶 @@ -211,15 +211,19 @@ UI 文档内 BOARD 行号引用一律以**文字锚**为准(本 PR 自己给 BOA > [`superpowers/plans/2026-07-07-launch-release-engineering.md`](superpowers/plans/2026-07-07-launch-release-engineering.md); > 调研支撑见 [`research/2026-07-07-release-engineering.md`](research/2026-07-07-release-engineering.md)。 -**现状要害**(2026-07-08 更新):release.yml 已随 alpha.10 首跑成功(双平台 build+publish 一次通过, -macOS arm64 zip/dmg + Windows x64 zip,均 unsigned);仍欠:**PG 分发源未配**(gh variable 空,产物不含 -嵌入式 PG,干净机装不上)、alpha.6 仍错挂 "Latest"(alpha.2–.6 prerelease=false 未 PATCH)、release -notes 双重生成致重复+基线错(#66 起)、无 checksums;good-first-issue 池为 0。 +**现状要害**(2026-07-18 更新):release.yml 已随 alpha.10 首跑成功(双平台 build+publish 一次通过, +macOS arm64 zip/dmg + Windows x64 zip,均 unsigned);**PG 分发源已解**(PKG-2A 方案 A 上游直拉, +零 variable 配置,下个 tag 起产物含嵌入式 PG——Windows 侧本机真栈已验,mac 侧待 release CI +首跑验证;checksums.txt 亦随之落地);仍欠:alpha.6 仍错挂 +"Latest"(alpha.2–.6 prerelease=false 未 PATCH)、release notes 双重生成致重复+基线错(#66 起); +good-first-issue 池为 0。 **模型侧可立即并行、零用户依赖**:PKG-1 流水线首演练(✅ 已随 alpha.10 以真实 tag 实质完成, -notes 双重生成问题随 PKG-6 收口)、PKG-2A PG 上游直拉(M)、 +notes 双重生成问题随 PKG-6 收口)、PKG-2A PG 上游直拉 ✅ 本 PR(EDB zip SHA 锁定裁剪+ +brew relocate,release.yml 无条件 fetch;mac 侧待 release CI 首跑验证)、 PKG-5 安装文档(S,复审补充:Defender/EDR 排除段、卸载与数据、迁移新机 SOP、零遥测承诺句)、 -PKG-6 Release 卫生(XS)、QG-1 回归清单 v2(S)、QG-6 checksums+清单落地(S)、 +PKG-6 Release 卫生(XS)、QG-1 回归清单 v2(S)、QG-6 checksums+清单落地(S,checksums.txt +已随 PKG-2A 落,余 BUILD-RELEASE §9 分层)、 UPD-1 应用内检查更新 ✅ #173(零新增依赖,ProcessRunner 开系统浏览器)、WEB-1 Pages 官网(M,FAQ 补 「为什么没有 Linux」)、WEB-2 示例模板(M)、COM-1 gfi 池重建(S,候选与 backend W0/W1 卡 双占用需协调)、COM-2 Issue 模板+SECURITY.md scope 对账(XS)、COM-3 CONTRIBUTING 英文摘要(S)。 @@ -241,7 +245,8 @@ beta 准入追加第 9 条:**第三方许可 NOTICE 上线**。用户必办追 unsigned+安装文档说明 SmartScreen 绕行,1.0 前补。**beta.1 准入的签名两条(第 2/3 条)执行顺延至补购后** - **U7** 数据升级政策拍板(→ D-4,牵动铁律文本与 SCRAM 覆盖面) - alpha.10 已按"不等 U1/U2"路线发出(release.yml 产 unsigned 双平台产物); - **beta.1 被 U1+U2+PG 分发源+QG-4 升级演练硬阻塞**(D-4 已拍 ADR-0012、UPD-1 已随 #173 交付,均从阻塞名单移除)。 + **beta.1 被 U1+U2+QG-4 升级演练硬阻塞**(D-4 已拍 ADR-0012、UPD-1 已随 #173、 + PG 分发源已随 PKG-2A 交付,均从阻塞名单移除)。 ## 6. 模型接入路线(2026-07 调研,全表与来源见 [`research/2026-07-07-model-landscape.md`](research/2026-07-07-model-landscape.md)) diff --git a/docs/SETUP.md b/docs/SETUP.md index a7768aac..502309be 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -157,22 +157,22 @@ The release build embeds bundled PG binaries under `scripts/pg/fetch-binaries.sh` 拉取(Windows 上从 Git Bash 运行): ```bash -# from Git Bash -export PG_ARTIFACT_BASE_URL=https:///inkframe/pg +# from Git Bash — zero config: downloads the official EDB zip pinned by +# scripts/pg/upstream.lock (URL + SHA256), trims it to bin/lib/share ./scripts/pg/fetch-binaries.sh ``` -**Expected output:** with `PG_ARTIFACT_BASE_URL` set, the script verifies the SHA256 -against `scripts/pg/pg-version.txt` (17.2) and prints `[fetch-binaries] OK`. With the -URL **unset** it prints `[fetch-binaries] NOT_CONFIGURED` and exits non-zero — that is -expected for normal dev, because day-to-day development uses your locally installed PG -17 (step 3) rather than the embedded binaries. You only need this step before producing -a release package. - -**预期输出:** 设了 `PG_ARTIFACT_BASE_URL` 时,脚本会按 `scripts/pg/pg-version.txt`(17.2) -校验 SHA256 并打印 `[fetch-binaries] OK`。**没设** URL 时打印 `[fetch-binaries] NOT_CONFIGURED` -并以非零退出 —— 这对日常开发是正常的,因为平时开发用的是你本地装的 PG 17(第 3 步),而不是嵌入 -二进制。只有要打发布包时才需要这一步。 +**Expected output:** the script downloads the EDB PostgreSQL zip, verifies the SHA256 +pinned in `scripts/pg/upstream.lock`, trims it, and prints `[fetch-binaries] OK +postgres (PostgreSQL) 17.2 → windows/runner/resources/pg/windows-x64`. Day-to-day +development still uses your locally installed PG 17 (step 3) rather than the embedded +binaries — you only need this step before producing a release package. (Setting +`PG_ARTIFACT_BASE_URL` switches the script to an object-storage source instead.) + +**预期输出:** 脚本会下载 EDB 官方 PostgreSQL zip,按 `scripts/pg/upstream.lock` 锁定的 +SHA256 校验并裁剪,打印 `[fetch-binaries] OK postgres (PostgreSQL) 17.2 → …`。日常开发 +仍用你本地装的 PG 17(第 3 步),只有要打发布包时才需要这一步。(设 `PG_ARTIFACT_BASE_URL` +可切换到对象存储源。) ### Run it / 跑起来 diff --git a/macos/Podfile b/macos/Podfile index 9299a66a..6db09edf 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -41,8 +41,9 @@ post_install do |installer| end # 注入「拷贝嵌入式 PostgreSQL 进 .app」构建阶段(幂等)。 - # 资源在 macos/Runner/Resources/pg//,由 scripts/pg/make-relocatable-macos.sh 生成 - # (或 CI 从对象存储拉取)。在签名前运行,把 PG 拷进 Contents/Resources/pg/。 + # 资源在 macos/Runner/Resources/pg//,由 scripts/pg/fetch-binaries.sh 置备 + # (macOS upstream 分支即委托 make-relocatable-macos.sh;对象存储为覆盖模式)。 + # 在签名前运行,把 PG 拷进 Contents/Resources/pg/。 require 'xcodeproj' runner_project_path = File.join(__dir__, 'Runner.xcodeproj') if File.exist?(runner_project_path) diff --git a/scripts/pg/fetch-binaries.sh b/scripts/pg/fetch-binaries.sh index ff635b9b..8e40e491 100755 --- a/scripts/pg/fetch-binaries.sh +++ b/scripts/pg/fetch-binaries.sh @@ -1,32 +1,42 @@ #!/usr/bin/env bash -# fetch-binaries.sh —— 从对象存储拉取锁定版本的 PostgreSQL 二进制,落地到 -# resources 子目录供打包。PRD §22.1 规格: -# macOS: macos/Runner/Resources/pg/bin + pg/lib (分 arm64 / x64 两套) -# Windows: windows/runner/resources/pg/bin + pg/lib (仅 x64) +# fetch-binaries.sh —— 嵌入式 PostgreSQL 二进制置备(PKG-2A)。 +# 落点(与 PgBinaryLocator 约定一致): +# macOS: macos/Runner/Resources/pg//{bin,lib,share} +# Windows: windows/runner/resources/pg/windows-x64/{bin,lib,share} # -# 设计目标: -# - 幂等:已存在且 SHA256 校验通过则跳过下载 -# - 版本锁:从 scripts/pg/pg-version.txt 读取,与远端 manifest 严格比对 -# - 校验失败 → exit 1(禁止继续打包) +# 两种模式: +# upstream(默认,方案 A,零配置零用户动作): +# windows-x64 EDB 官方 zip 直拉,URL+SHA256 锁定在 scripts/pg/upstream.lock; +# 只取 pgsql/{bin,lib,share} 并裁掉 share/{doc,man,locale} +# macos-* Homebrew postgresql@ → make-relocatable-macos.sh 生成 +# 自洽可分发目录(brew 补丁位浮动 → 只做主版本匹配) +# linux-x64 不支持(仅本地烟测平台,用系统 PG 或 bucket 模式) +# bucket(方案 B 覆盖):设 PG_ARTIFACT_BASE_URL 时从对象存储拉 +# $BASE/$VERSION/$PLATFORM.tar.gz(+ .sha256),语义与历史版本一致 # -# v0.1.0 未上对象存储,本脚本先输出 NOT_CONFIGURED 并提示手动放置路径, -# 保证 CI / 开发机不会误以为下载成功。真实对象存储地址由 T7 打包流水线填入。 +# 不变量: +# - 落位原子:装配+校验全在 .partial,通过后才对换;任何失败零残留 +# - 校验统一:必需工具(postgres/initdb/pg_ctl/pg_dump/pg_restore,PgBinaryLocation +# 契约)+ 版本匹配(windows/bucket 精确含 pg-version.txt;macos upstream 主版本) +# - 幂等:现存目标通过同一校验即短路退出,不重复下载 set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" cd "$REPO_ROOT" VERSION_FILE="scripts/pg/pg-version.txt" +LOCK_FILE="scripts/pg/upstream.lock" + if [[ ! -f "$VERSION_FILE" ]]; then echo "[fetch-binaries] 缺少 $VERSION_FILE" >&2 exit 1 fi - PG_VERSION="$(tr -d '[:space:]' < "$VERSION_FILE")" if [[ -z "$PG_VERSION" ]]; then echo "[fetch-binaries] pg-version.txt 为空" >&2 exit 1 fi +PG_MAJOR="${PG_VERSION%%.*}" # 目标平台由调用方以 INKFRAME_PG_PLATFORM 指定;默认按运行机器推断。 PLATFORM="${INKFRAME_PG_PLATFORM:-}" @@ -42,77 +52,173 @@ if [[ -z "$PLATFORM" ]]; then fi case "$PLATFORM" in - macos-arm64|macos-x64) TARGET_DIR="macos/Runner/Resources/pg/$PLATFORM" ;; - windows-x64) TARGET_DIR="windows/runner/resources/pg/$PLATFORM" ;; - linux-x64) TARGET_DIR="build/pg/$PLATFORM" ;; # 仅本地烟测,不进发布 + macos-arm64|macos-x64) TARGET_DIR="macos/Runner/Resources/pg/$PLATFORM"; EXE="" ;; + windows-x64) TARGET_DIR="windows/runner/resources/pg/$PLATFORM"; EXE=".exe" ;; + linux-x64) TARGET_DIR="build/pg/$PLATFORM"; EXE="" ;; # 仅本地烟测,不进发布 *) echo "[fetch-binaries] 未知平台:$PLATFORM" >&2; exit 1 ;; esac +PARTIAL_DIR="$TARGET_DIR.partial" + +MODE=upstream +[[ -n "${PG_ARTIFACT_BASE_URL:-}" ]] && MODE=bucket + +echo "[fetch-binaries] version=$PG_VERSION platform=$PLATFORM mode=$MODE target=$TARGET_DIR" -BIN_DIR="$TARGET_DIR/bin" -LIB_DIR="$TARGET_DIR/lib" +REQUIRED_TOOLS=(postgres initdb pg_ctl pg_dump pg_restore) + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +lock_get() { + local key="$1" val + # tr -d '\r':防 CRLF 形态工作树把行尾混进 URL/SHA(.gitattributes 已锁 LF,此为双保险) + val="$(sed -n "s/^${key}=//p" "$LOCK_FILE" 2>/dev/null | head -1 | tr -d '\r')" + if [[ -z "$val" ]]; then + echo "[fetch-binaries] $LOCK_FILE 缺少 ${key}" >&2 + return 1 + fi + echo "$val" +} + +# 校验一棵已装配的 PG 目录:必需工具齐全 + lib/share 在位 + 版本匹配。$1=目录 +verify_tree() { + local dir="$1" t ver + for t in "${REQUIRED_TOOLS[@]}"; do + if [[ ! -f "$dir/bin/$t$EXE" ]]; then + echo "[fetch-binaries] 校验失败:缺 bin/$t$EXE($dir)" >&2 + return 1 + fi + done + # initdb 运行必需 share(postgres.bki/timezone),postgres 运行必需 lib——残缺树不得短路 + for t in lib share; do + if [[ ! -d "$dir/$t" ]]; then + echo "[fetch-binaries] 校验失败:缺 $t/ 目录($dir)" >&2 + return 1 + fi + done + ver="$("$dir/bin/postgres$EXE" --version 2>/dev/null || true)" + if [[ "$MODE" == "upstream" && "$PLATFORM" == macos-* ]]; then + # brew 补丁位浮动(17.x),只锁主版本 + if ! grep -qE "\(PostgreSQL\) ${PG_MAJOR}\." <<<"$ver"; then + echo "[fetch-binaries] 校验失败:版本主号不匹配 got='$ver' want major=$PG_MAJOR" >&2 + return 1 + fi + else + # 版本号右侧锚定:17.2 不得被 17.20/17.21 满足 + if ! grep -qE "\(PostgreSQL\) ${PG_VERSION//./\\.}([^0-9]|$)" <<<"$ver"; then + echo "[fetch-binaries] 校验失败:版本不匹配 got='$ver' want=$PG_VERSION" >&2 + return 1 + fi + fi + echo "$ver" +} -echo "[fetch-binaries] version=$PG_VERSION platform=$PLATFORM target=$TARGET_DIR" +# 陈旧 .partial(上次进程被杀的残留)先清,不受下方幂等短路遮蔽 +rm -rf "$PARTIAL_DIR" -# 幂等校验:已有 postgres / pg_ctl / initdb 则视为就绪。 -if [[ -x "$BIN_DIR/postgres" || -x "$BIN_DIR/postgres.exe" ]]; then - FOUND_VERSION="$("$BIN_DIR/postgres" --version 2>/dev/null || "$BIN_DIR/postgres.exe" --version 2>/dev/null || true)" - if [[ "$FOUND_VERSION" == *"$PG_VERSION"* ]]; then - echo "[fetch-binaries] 已就绪:$FOUND_VERSION" +# 幂等短路:现存目标过同一校验即无事可做。 +if [[ -d "$TARGET_DIR" ]]; then + if READY_VER="$(verify_tree "$TARGET_DIR")"; then + echo "[fetch-binaries] 已就绪:$READY_VER" exit 0 fi - echo "[fetch-binaries] 版本不匹配:$FOUND_VERSION ≠ $PG_VERSION,重新拉取" + echo "[fetch-binaries] 现存目标未过校验,重建" rm -rf "$TARGET_DIR" fi -# 对象存储 URL:v0.1.0 尚未接入,留占位。 -PG_ARTIFACT_BASE_URL="${PG_ARTIFACT_BASE_URL:-}" -if [[ -z "$PG_ARTIFACT_BASE_URL" ]]; then - cat <<'EOF' >&2 -[fetch-binaries] NOT_CONFIGURED - ⚠️ PG_ARTIFACT_BASE_URL 未设置,无法下载二进制。 - 开发机:使用 Homebrew PG 17(详见 CONTRIBUTING.md「本地 PostgreSQL」章节) - 打包机:请先配置对象存储 base URL,格式: - export PG_ARTIFACT_BASE_URL=https:///inkframe/pg - 预期文件:$PG_ARTIFACT_BASE_URL/$PG_VERSION/$PLATFORM.tar.gz - $PG_ARTIFACT_BASE_URL/$PG_VERSION/$PLATFORM.tar.gz.sha256 -EOF - exit 1 -fi - -ARCHIVE="$PLATFORM.tar.gz" -SUM_URL="$PG_ARTIFACT_BASE_URL/$PG_VERSION/$ARCHIVE.sha256" -ARCHIVE_URL="$PG_ARTIFACT_BASE_URL/$PG_VERSION/$ARCHIVE" - TMP_DIR="$(mktemp -d)" -trap 'rm -rf "$TMP_DIR"' EXIT - -echo "[fetch-binaries] 下载 $ARCHIVE_URL" -curl -fL --retry 3 -o "$TMP_DIR/$ARCHIVE" "$ARCHIVE_URL" -curl -fL --retry 3 -o "$TMP_DIR/$ARCHIVE.sha256" "$SUM_URL" - -EXPECTED="$(awk '{print $1}' "$TMP_DIR/$ARCHIVE.sha256")" -if command -v shasum >/dev/null 2>&1; then - ACTUAL="$(shasum -a 256 "$TMP_DIR/$ARCHIVE" | awk '{print $1}')" -else - ACTUAL="$(sha256sum "$TMP_DIR/$ARCHIVE" | awk '{print $1}')" -fi - -if [[ "$EXPECTED" != "$ACTUAL" ]]; then - echo "[fetch-binaries] SHA256 校验失败 expected=$EXPECTED actual=$ACTUAL" >&2 - exit 1 -fi - -mkdir -p "$TARGET_DIR" -tar -xzf "$TMP_DIR/$ARCHIVE" -C "$TARGET_DIR" --strip-components=1 +trap 'rm -rf "$TMP_DIR" "$PARTIAL_DIR"' EXIT + +download() { # $1=url $2=输出路径 + echo "[fetch-binaries] 下载 $1" + curl -fL --retry 3 -o "$2" "$1" +} + +extract_zip() { # $1=zip $2=目标目录 + mkdir -p "$2" + if command -v unzip >/dev/null 2>&1; then + unzip -q "$1" -d "$2" + elif command -v 7z >/dev/null 2>&1; then + 7z x -y -o"$2" "$1" >/dev/null + else + powershell.exe -NoProfile -Command \ + "Expand-Archive -LiteralPath '$(cygpath -w "$1")' -DestinationPath '$(cygpath -w "$2")'" + fi +} + +fetch_bucket() { + local archive="$PLATFORM.tar.gz" expected actual + download "$PG_ARTIFACT_BASE_URL/$PG_VERSION/$archive" "$TMP_DIR/$archive" + download "$PG_ARTIFACT_BASE_URL/$PG_VERSION/$archive.sha256" "$TMP_DIR/$archive.sha256" + expected="$(awk '{print $1}' "$TMP_DIR/$archive.sha256")" + actual="$(sha256_of "$TMP_DIR/$archive")" + if [[ "$expected" != "$actual" ]]; then + echo "[fetch-binaries] SHA256 校验失败 expected=$expected actual=$actual" >&2 + exit 1 + fi + mkdir -p "$PARTIAL_DIR" + tar -xzf "$TMP_DIR/$archive" -C "$PARTIAL_DIR" --strip-components=1 +} + +fetch_upstream_windows() { + local url sha actual + url="$(lock_get WINDOWS_X64_URL)" + sha="$(lock_get WINDOWS_X64_SHA256)" + download "$url" "$TMP_DIR/pg.zip" + actual="$(sha256_of "$TMP_DIR/pg.zip")" + if [[ "$sha" != "$actual" ]]; then + echo "[fetch-binaries] SHA256 校验失败(EDB 包被变更或下载损坏)expected=$sha actual=$actual" >&2 + echo " 上游 URL 若已失效,更新 $LOCK_FILE 并重新实测锁定。" >&2 + exit 1 + fi + extract_zip "$TMP_DIR/pg.zip" "$TMP_DIR/extract" + if [[ ! -d "$TMP_DIR/extract/pgsql/bin" ]]; then + echo "[fetch-binaries] EDB zip 布局异常:缺 pgsql/bin(上游打包结构变了?)" >&2 + exit 1 + fi + # 只取运行所需三目录(pgAdmin/StackBuilder/include/doc/symbols 全部不拷),再裁运行期无用物: + # share/{doc,man,locale} 文档与 NLS + # lib/*.lib MSVC 导入库(构建期产物) + # bin/wx*.dll StackBuilder 的 wxWidgets GUI 依赖(PG 本体不加载) + mkdir -p "$PARTIAL_DIR" + cp -R "$TMP_DIR/extract/pgsql/bin" "$TMP_DIR/extract/pgsql/lib" "$TMP_DIR/extract/pgsql/share" "$PARTIAL_DIR/" + rm -rf "$PARTIAL_DIR/share/doc" "$PARTIAL_DIR/share/man" "$PARTIAL_DIR/share/locale" + rm -f "$PARTIAL_DIR"/lib/*.lib "$PARTIAL_DIR"/bin/wx*.dll + chmod -R u+x "$PARTIAL_DIR/bin" # zip 不保执行位(Windows 本机无害,CI/测试环境必需) +} + +fetch_upstream_macos() { + local formula="postgresql@$PG_MAJOR" src="" + if command -v brew >/dev/null 2>&1; then + src="$(brew --prefix "$formula" 2>/dev/null || true)" + fi + if [[ -z "$src" || ! -x "$src/bin/postgres" ]]; then + echo "[fetch-binaries] 找不到 Homebrew $formula(src='$src')" >&2 + echo " 先 brew install $formula 再重跑(CI 由 release.yml 安装)。" >&2 + exit 1 + fi + bash "$REPO_ROOT/scripts/pg/make-relocatable-macos.sh" "$src" "$REPO_ROOT/$PARTIAL_DIR" +} + +case "$MODE:$PLATFORM" in + bucket:*) fetch_bucket ;; + upstream:windows-x64) fetch_upstream_windows ;; + upstream:macos-*) fetch_upstream_macos ;; + upstream:linux-x64) + echo "[fetch-binaries] linux-x64 无 upstream 源(仅本地烟测平台):" >&2 + echo " 用系统 PG(SETUP.md),或设 PG_ARTIFACT_BASE_URL 走对象存储。" >&2 + exit 1 + ;; +esac -echo "[fetch-binaries] 解压完成,验证 postgres 版本" -if [[ -x "$BIN_DIR/postgres" ]]; then - "$BIN_DIR/postgres" --version -elif [[ -x "$BIN_DIR/postgres.exe" ]]; then - "$BIN_DIR/postgres.exe" --version -else - echo "[fetch-binaries] 解压后找不到 postgres 可执行文件" >&2 +if ! FINAL_VER="$(verify_tree "$PARTIAL_DIR")"; then + echo "[fetch-binaries] 装配产物未过校验,放弃(零残留)" >&2 exit 1 fi - -echo "[fetch-binaries] OK" +mv "$PARTIAL_DIR" "$TARGET_DIR" +echo "[fetch-binaries] OK $FINAL_VER → $TARGET_DIR" diff --git a/scripts/pg/upstream.lock b/scripts/pg/upstream.lock new file mode 100644 index 00000000..56e1da94 --- /dev/null +++ b/scripts/pg/upstream.lock @@ -0,0 +1,6 @@ +# PKG-2A upstream 锁定(方案 A:官方上游直拉,零对象存储依赖)。 +# 改版本:先改 pg-version.txt,再同步本文件 URL 与 SHA256(下载后本地 sha256sum 实测)。 +# macOS 无锁定条目:源是 runner 上 Homebrew postgresql@(补丁位随 brew 浮动, +# 供应链信任交给 brew;fetch-binaries.sh 只做主版本匹配校验)。 +WINDOWS_X64_URL=https://get.enterprisedb.com/postgresql/postgresql-17.2-3-windows-x64-binaries.zip +WINDOWS_X64_SHA256=3e55fcfd81a042b3cfac481a11ef5fb9741f61f25fa96fcd0e450fee120c3c38 diff --git a/test/scripts/fetch_binaries_test.sh b/test/scripts/fetch_binaries_test.sh new file mode 100644 index 00000000..bf849a1e --- /dev/null +++ b/test/scripts/fetch_binaries_test.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash +# fetch-binaries.sh 回归测试(PKG-2A)。沙箱内跑:把脚本拷进临时仓库骨架, +# curl/brew 走 PATH stub,make-relocatable 用沙箱 stub —— 不碰真网络、不碰真仓库。 +set -euo pipefail + +REAL_SCRIPT="$(cd "$(dirname "$0")" && pwd)/../../scripts/pg/fetch-binaries.sh" +FIXTURES="$(cd "$(dirname "$0")" && pwd)/fixtures" +FAILURES=0 + +pass() { echo " ✅ $1"; } +fail() { echo " ❌ $1"; FAILURES=$((FAILURES + 1)); } + +# GNU/BSD 双轨(macOS 无 sha256sum) +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' + else shasum -a 256 "$1" | awk '{print $1}'; fi +} + +FIXTURE_ZIP="$FIXTURES/fake-edb-pgsql.zip" +FIXTURE_ZIP_MISSING_TOOL="$FIXTURES/fake-edb-missing-pg-restore.zip" +FIXTURE_SHA="$(sha256_of "$FIXTURE_ZIP")" +FIXTURE_MISSING_SHA="$(sha256_of "$FIXTURE_ZIP_MISSING_TOOL")" + +# 沙箱 = 迷你仓库骨架:scripts/pg/{fetch-binaries.sh,pg-version.txt,upstream.lock} + PATH stub 目录 +setup_sandbox() { + local dir + dir="$(mktemp -d)" + mkdir -p "$dir/scripts/pg" "$dir/bin" + cp "$REAL_SCRIPT" "$dir/scripts/pg/fetch-binaries.sh" + echo "17.2" > "$dir/scripts/pg/pg-version.txt" + cat > "$dir/scripts/pg/upstream.lock" < "$dir/bin/curl" <<'CURL' +#!/usr/bin/env bash +out=""; url="" +args=("$@") +for ((i=0; i<${#args[@]}; i++)); do + case "${args[$i]}" in + -o) out="${args[$((i+1))]}" ;; + http*) url="${args[$i]}" ;; + esac +done +echo "$url" >> "${CURL_LOG:?}" +if [[ -n "${CURL_SERVE:-}" && -f "${CURL_SERVE}" ]]; then + cp "$CURL_SERVE" "$out"; exit 0 +fi +if [[ -n "${CURL_SERVE_DIR:-}" ]]; then + base="$(basename "$url")" + [[ -f "$CURL_SERVE_DIR/$base" ]] && { cp "$CURL_SERVE_DIR/$base" "$out"; exit 0; } +fi +exit 22 +CURL + chmod +x "$dir/bin/curl" + echo "$dir" +} + +# 在沙箱内跑脚本:$1=沙箱 $2=平台,其余透传环境(VAR=x 形式) +run_fetch() { + local dir="$1" platform="$2"; shift 2 + ( cd "$dir" && PATH="$dir/bin:$PATH" INKFRAME_PG_PLATFORM="$platform" env "$@" bash scripts/pg/fetch-binaries.sh ) +} + +WIN_TARGET="windows/runner/resources/pg/windows-x64" +MAC_TARGET="macos/Runner/Resources/pg/macos-arm64" + +# --- Task 1: upstream windows-x64 happy path ------------------------------- +echo "=== Task 1: upstream windows happy path ===" +SB="$(setup_sandbox)" +export CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +set +e +CURL_SERVE="$FIXTURE_ZIP" run_fetch "$SB" windows-x64 CURL_LOG="$CURL_LOG" CURL_SERVE="$FIXTURE_ZIP" >/dev/null 2>&1 +ec=$? +set -e +[[ $ec -eq 0 ]] && pass "exit 0" || fail "exit $ec (want 0)" +[[ -d "$SB/$WIN_TARGET/bin" && -d "$SB/$WIN_TARGET/lib" && -d "$SB/$WIN_TARGET/share" ]] \ + && pass "bin/lib/share 落位" || fail "bin/lib/share 缺失" +missing=0 +for t in postgres initdb pg_ctl pg_dump pg_restore; do + [[ -f "$SB/$WIN_TARGET/bin/$t.exe" ]] || { fail "缺 $t.exe"; missing=1; } +done +[[ $missing -eq 0 ]] && pass "必需工具齐全(postgres/initdb/pg_ctl/pg_dump/pg_restore)" +[[ ! -e "$SB/$WIN_TARGET/share/doc" && ! -e "$SB/$WIN_TARGET/share/man" && ! -e "$SB/$WIN_TARGET/share/locale" ]] \ + && pass "share/doc|man|locale 已裁剪" || fail "share 未裁剪" +[[ ! -e "$SB/$WIN_TARGET/lib/libpq.lib" ]] && pass "lib/*.lib 已裁剪" || fail "lib/*.lib 未裁剪" +[[ -z "$(ls "$SB/$WIN_TARGET/bin/" | grep '^wx')" ]] && pass "bin/wx*.dll 已裁剪" || fail "bin/wx*.dll 未裁剪" +[[ -f "$SB/$WIN_TARGET/lib/libpq.dll" ]] && pass "运行期 dll 保留" || fail "运行期 dll 被误删" +[[ ! -e "$SB/$WIN_TARGET/pgAdmin 4" && ! -e "$SB/$WIN_TARGET/include" && ! -e "$SB/$WIN_TARGET/StackBuilder" ]] \ + && pass "pgAdmin/StackBuilder/include 未拷入" || fail "多拷了 EDB 附属目录" +[[ ! -e "$SB/$WIN_TARGET.partial" ]] && pass "无 .partial 残留" || fail ".partial 残留" +grep -q "example.test/postgresql-17.2-3" "$CURL_LOG" && pass "按 lock URL 下载" || fail "未按 lock URL 下载" +[[ "$(wc -l < "$CURL_LOG")" -eq 1 ]] && pass "upstream 模式只下载 1 个文件" || fail "下载次数异常" + +# --- Task 2: 幂等重跑不再下载 ---------------------------------------------- +echo "=== Task 2: idempotent rerun ===" +set +e +run_fetch "$SB" windows-x64 CURL_LOG="$CURL_LOG" CURL_SERVE="$FIXTURE_ZIP" >/dev/null 2>&1 +ec=$? +set -e +[[ $ec -eq 0 ]] && pass "重跑 exit 0" || fail "重跑 exit $ec" +[[ "$(wc -l < "$CURL_LOG")" -eq 1 ]] && pass "重跑零下载(已就绪短路)" || fail "重跑触发了下载" +rm -rf "$SB" + +# --- Task 3: SHA256 不匹配 → 失败且零残留 ---------------------------------- +echo "=== Task 3: sha mismatch ===" +SB="$(setup_sandbox)" +CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +sed -i.bak "s/^WINDOWS_X64_SHA256=.*/WINDOWS_X64_SHA256=$(printf 'deadbeef%.0s' 1 2 3 4 5 6 7 8)/" "$SB/scripts/pg/upstream.lock" +set +e +run_fetch "$SB" windows-x64 CURL_LOG="$CURL_LOG" CURL_SERVE="$FIXTURE_ZIP" >/dev/null 2>&1 +ec=$? +set -e +[[ $ec -ne 0 ]] && pass "sha 不匹配 → 非零退出" || fail "sha 不匹配竟然成功" +[[ ! -e "$SB/$WIN_TARGET" && ! -e "$SB/$WIN_TARGET.partial" ]] && pass "失败零残留" || fail "失败留下残留目录" +rm -rf "$SB" + +# --- Task 4: 缺必需工具(无 pg_restore)→ 失败且零残留 ---------------------- +echo "=== Task 4: missing required tool ===" +SB="$(setup_sandbox)" +CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +sed -i.bak "s/^WINDOWS_X64_SHA256=.*/WINDOWS_X64_SHA256=$FIXTURE_MISSING_SHA/" "$SB/scripts/pg/upstream.lock" +set +e +run_fetch "$SB" windows-x64 CURL_LOG="$CURL_LOG" CURL_SERVE="$FIXTURE_ZIP_MISSING_TOOL" >/dev/null 2>&1 +ec=$? +set -e +[[ $ec -ne 0 ]] && pass "缺 pg_restore → 非零退出" || fail "缺工具竟然成功" +[[ ! -e "$SB/$WIN_TARGET" && ! -e "$SB/$WIN_TARGET.partial" ]] && pass "失败零残留" || fail "失败留下残留目录" +rm -rf "$SB" + +# --- Task 5: lock 缺键 → 失败 ---------------------------------------------- +echo "=== Task 5: lock missing key ===" +SB="$(setup_sandbox)" +CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +grep -v '^WINDOWS_X64_SHA256=' "$SB/scripts/pg/upstream.lock" > "$SB/scripts/pg/upstream.lock.tmp" +mv "$SB/scripts/pg/upstream.lock.tmp" "$SB/scripts/pg/upstream.lock" +set +e +out="$(run_fetch "$SB" windows-x64 CURL_LOG="$CURL_LOG" CURL_SERVE="$FIXTURE_ZIP" 2>&1)" +ec=$? +set -e +[[ $ec -ne 0 ]] && pass "lock 缺 SHA 键 → 非零退出" || fail "lock 缺键竟然成功" +echo "$out" | grep -qi "WINDOWS_X64_SHA256" && pass "报错点名缺失键" || fail "报错未点名缺失键" +rm -rf "$SB" + +# --- Task 6: bucket 模式(PG_ARTIFACT_BASE_URL)优先且语义不变 -------------- +echo "=== Task 6: bucket mode override ===" +SB="$(setup_sandbox)" +CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +# 运行时造 bucket tar.gz:内容布局 = /{bin,lib,share}(strip-components=1 语义) +BUCKET_DIR="$SB/bucketsrv" +mkdir -p "$BUCKET_DIR/stage/pg/bin" "$BUCKET_DIR/stage/pg/lib" "$BUCKET_DIR/stage/pg/share" +printf '#!/usr/bin/env bash\necho "postgres (PostgreSQL) 17.2"\n' > "$BUCKET_DIR/stage/pg/bin/postgres.exe" +for t in initdb pg_ctl pg_dump pg_restore; do + printf '#!/usr/bin/env bash\nexit 0\n' > "$BUCKET_DIR/stage/pg/bin/$t.exe" +done +chmod +x "$BUCKET_DIR/stage/pg/bin/"*.exe +echo lib > "$BUCKET_DIR/stage/pg/lib/libpq.dll" +echo bki > "$BUCKET_DIR/stage/pg/share/postgres.bki" +tar -czf "$BUCKET_DIR/windows-x64.tar.gz" -C "$BUCKET_DIR/stage" pg +sha256_of "$BUCKET_DIR/windows-x64.tar.gz" > "$BUCKET_DIR/windows-x64.tar.gz.sha256" +set +e +run_fetch "$SB" windows-x64 CURL_LOG="$CURL_LOG" CURL_SERVE_DIR="$BUCKET_DIR" \ + PG_ARTIFACT_BASE_URL="https://bucket.test/inkframe/pg" >/dev/null 2>&1 +ec=$? +set -e +[[ $ec -eq 0 ]] && pass "bucket 模式 exit 0" || fail "bucket 模式 exit $ec" +grep -q "bucket.test/inkframe/pg/17.2/windows-x64.tar.gz" "$CURL_LOG" \ + && pass "走对象存储 URL(模式优先级正确)" || fail "未走对象存储 URL" +[[ "$(wc -l < "$CURL_LOG")" -eq 2 ]] && pass "bucket 模式下载 tar.gz + .sha256 两件" || fail "bucket 下载次数异常" +[[ -f "$SB/$WIN_TARGET/bin/postgres.exe" ]] && pass "bucket 落位" || fail "bucket 未落位" +rm -rf "$SB" + +# --- Task 7: linux-x64 upstream 不支持 ------------------------------------- +echo "=== Task 7: linux upstream unsupported ===" +SB="$(setup_sandbox)" +CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +set +e +out="$(run_fetch "$SB" linux-x64 CURL_LOG="$CURL_LOG" 2>&1)" +ec=$? +set -e +[[ $ec -ne 0 ]] && pass "linux upstream → 非零退出" || fail "linux upstream 竟然成功" +echo "$out" | grep -q "PG_ARTIFACT_BASE_URL" && pass "报错指路(系统 PG / 对象存储)" || fail "报错未指路" +rm -rf "$SB" + +# --- Task 8: macOS upstream 委托 make-relocatable + 主版本匹配 -------------- +echo "=== Task 8: macOS upstream (brew + relocate stub) ===" +setup_mac_sandbox() { + local dir="$1" brew_ver="$2" + # brew stub + cat > "$dir/bin/brew" <<'BREW' +#!/usr/bin/env bash +if [[ "${1:-}" == "--prefix" ]]; then echo "${FAKE_BREW_PREFIX:?}"; exit 0; fi +exit 1 +BREW + chmod +x "$dir/bin/brew" + # 假 brew keg + mkdir -p "$dir/fakebrew/bin" + printf '#!/usr/bin/env bash\necho "postgres (PostgreSQL) %s"\n' "$brew_ver" > "$dir/fakebrew/bin/postgres" + chmod +x "$dir/fakebrew/bin/postgres" + # make-relocatable stub:记录参数,产出含全部必需工具的 DEST + cat > "$dir/scripts/pg/make-relocatable-macos.sh" <<'REL' +#!/usr/bin/env bash +set -euo pipefail +echo "relocate $*" >> "${REL_LOG:?}" +SRC="$1"; DEST="$2" +rm -rf "$DEST" +mkdir -p "$DEST/bin" "$DEST/lib" "$DEST/share/postgresql" +for t in postgres initdb pg_ctl pg_dump pg_restore; do + cp "$SRC/bin/postgres" "$DEST/bin/$t" + chmod +x "$DEST/bin/$t" +done +REL + chmod +x "$dir/scripts/pg/make-relocatable-macos.sh" +} + +# 8a: brew 17.6(补丁位浮动)→ 主版本 17 匹配 → 成功 +SB="$(setup_sandbox)" +CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +REL_LOG="$SB/rel.log"; : > "$REL_LOG" +setup_mac_sandbox "$SB" "17.6" +set +e +run_fetch "$SB" macos-arm64 CURL_LOG="$CURL_LOG" REL_LOG="$REL_LOG" FAKE_BREW_PREFIX="$SB/fakebrew" >/dev/null 2>&1 +ec=$? +set -e +[[ $ec -eq 0 ]] && pass "brew 17.6 → 主版本匹配 exit 0" || fail "brew 17.6 exit $ec" +grep -q "relocate $SB/fakebrew" "$REL_LOG" && pass "以 brew prefix 为源委托 relocate" || fail "relocate 源参数错误" +[[ -f "$SB/$MAC_TARGET/bin/postgres" && -f "$SB/$MAC_TARGET/bin/pg_restore" ]] \ + && pass "macOS 落位含必需工具" || fail "macOS 未落位" +[[ ! -e "$SB/$MAC_TARGET.partial" ]] && pass "无 .partial 残留" || fail ".partial 残留" +[[ "$(wc -l < "$CURL_LOG")" -eq 0 ]] && pass "macOS upstream 零网络下载" || fail "macOS upstream 触发下载" +rm -rf "$SB" + +# 8b: brew 18.0 → 主版本不匹配 → 失败且零残留 +SB="$(setup_sandbox)" +CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +REL_LOG="$SB/rel.log"; : > "$REL_LOG" +setup_mac_sandbox "$SB" "18.0" +set +e +run_fetch "$SB" macos-arm64 CURL_LOG="$CURL_LOG" REL_LOG="$REL_LOG" FAKE_BREW_PREFIX="$SB/fakebrew" >/dev/null 2>&1 +ec=$? +set -e +[[ $ec -ne 0 ]] && pass "brew 18.0 → 主版本不匹配非零退出" || fail "18.0 竟然成功" +[[ ! -e "$SB/$MAC_TARGET" && ! -e "$SB/$MAC_TARGET.partial" ]] && pass "失败零残留" || fail "失败留下残留目录" +rm -rf "$SB" + +# 8c: 无 brew → 明确报错指路 +SB="$(setup_sandbox)" +CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +set +e +out="$(run_fetch "$SB" macos-arm64 CURL_LOG="$CURL_LOG" 2>&1)" +ec=$? +set -e +[[ $ec -ne 0 ]] && pass "无 brew keg → 非零退出" || fail "无 brew 竟然成功" +echo "$out" | grep -q "postgresql@17" && pass "报错指路 brew install postgresql@17" || fail "报错未指路" +rm -rf "$SB" + +# --- Task 9: 坏现存树触发重建 + 陈旧 .partial 清理 --------------------------- +echo "=== Task 9: rebuild on corrupt tree + stale .partial cleanup ===" +SB="$(setup_sandbox)" +CURL_LOG="$SB/curl.log"; : > "$CURL_LOG" +set +e +run_fetch "$SB" windows-x64 CURL_LOG="$CURL_LOG" CURL_SERVE="$FIXTURE_ZIP" >/dev/null 2>&1 +set -e +# 破坏现存树(删 share——残缺树不得骗过幂等短路)+ 制造上次被杀的陈旧 .partial +rm -rf "$SB/$WIN_TARGET/share" +mkdir -p "$SB/$WIN_TARGET.partial/junk" +set +e +run_fetch "$SB" windows-x64 CURL_LOG="$CURL_LOG" CURL_SERVE="$FIXTURE_ZIP" >/dev/null 2>&1 +ec=$? +set -e +[[ $ec -eq 0 ]] && pass "坏树重跑 exit 0" || fail "坏树重跑 exit $ec" +[[ "$(wc -l < "$CURL_LOG")" -eq 2 ]] && pass "残缺树未短路,触发重新下载" || fail "残缺树被幂等短路放过" +[[ -d "$SB/$WIN_TARGET/share" ]] && pass "share 已随重建恢复" || fail "share 未恢复" +[[ ! -e "$SB/$WIN_TARGET.partial" ]] && pass "陈旧 .partial 已清理" || fail "陈旧 .partial 残留" +rm -rf "$SB" + +echo "" +exit $FAILURES diff --git a/test/scripts/fixtures/fake-edb-missing-pg-restore.zip b/test/scripts/fixtures/fake-edb-missing-pg-restore.zip new file mode 100644 index 00000000..48443e1b Binary files /dev/null and b/test/scripts/fixtures/fake-edb-missing-pg-restore.zip differ diff --git a/test/scripts/fixtures/fake-edb-pgsql.zip b/test/scripts/fixtures/fake-edb-pgsql.zip new file mode 100644 index 00000000..a3d46ca0 Binary files /dev/null and b/test/scripts/fixtures/fake-edb-pgsql.zip differ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 9b069552..d3819c49 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -110,7 +110,7 @@ install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" # 嵌入式 PostgreSQL(Windows):打进 /resources/pg/windows-x64/, # 与 macOS Podfile 的 "Bundle Embedded PostgreSQL" 阶段对应;运行期由 # pg_binary_locator 解析 /resources/pg/windows-x64/bin。 -# 源目录由 scripts/pg(对象存储或现场构建)填充。 +# 源目录由 scripts/pg/fetch-binaries.sh 填充(默认 EDB 上游直拉,PG_ARTIFACT_BASE_URL 时对象存储)。 set(INKFRAME_PG_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runner/resources/pg/windows-x64") if(EXISTS "${INKFRAME_PG_DIR}/bin/postgres.exe") install(DIRECTORY "${INKFRAME_PG_DIR}/"