diff --git a/.github/workflows/cd-debian13.yml b/.github/workflows/cd-debian13.yml index c37f08d4e9..9e98ebcc03 100644 --- a/.github/workflows/cd-debian13.yml +++ b/.github/workflows/cd-debian13.yml @@ -7,9 +7,10 @@ on: description: 'Set version manually (e.g. 1.2.3)' required: false default: '' - push: - tags: - - '*' + # 测试期注释:velopack CD 联调的测试 tag 不触发 Debian 打包,恢复时取消注释 + # push: + # tags: + # - '*' jobs: debian13-build: diff --git a/.github/workflows/cd-fedora.yml b/.github/workflows/cd-fedora.yml index 6e96ed722c..983bcea360 100644 --- a/.github/workflows/cd-fedora.yml +++ b/.github/workflows/cd-fedora.yml @@ -7,9 +7,10 @@ on: description: 'Set version manually (e.g. 1.2.3)' required: false default: '' - push: - tags: - - '*' + # 测试期注释:velopack CD 联调的测试 tag 不触发 Fedora 打包,恢复时取消注释 + # push: + # tags: + # - '*' jobs: fedora-build: diff --git a/.github/workflows/cd_on_macos_arm64.yml b/.github/workflows/cd_on_macos_arm64.yml index d4a87aa55e..87fdb2bd38 100644 --- a/.github/workflows/cd_on_macos_arm64.yml +++ b/.github/workflows/cd_on_macos_arm64.yml @@ -16,9 +16,11 @@ jobs: os: [macos-14] arch: [arm64] runs-on: ${{ matrix.os }} - timeout-minutes: 45 + # Windows CD 为 45 分钟;mac 侧多出 vpk 签名 + notarytool 公证等待 + # (package.scm 时代公证单独给了 60 分钟超时),放宽到 60。 + timeout-minutes: 60 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 1 - uses: xmake-io/github-action-setup-xmake@v1 @@ -27,30 +29,218 @@ jobs: actions-cache-folder: '.xmake-cache' - name: xmake repo --update run: xmake repo --update - - name: Config and build goldfish + # Noto 字体(与 Windows CD 相同来源):原由 package.scm 内部下载, + # DMG 流程移除后必须在此落位,install 才会收进 .app 的 Resources。 + - name: Noto fonts run: | - xmake config -m release -a ${{ matrix.arch }} -vD --loro=yes --yes - xmake build goldfish-bin - ls -la TeXmacs/plugins/goldfish/bin/ || echo "goldfish bin dir not found" - - name: Package with Goldfish + mkdir -p TeXmacs/fonts/opentype/noto + cd TeXmacs/fonts/opentype/noto + curl -fsSLO https://github.com/XmacsLabs/mogan/releases/download/v1.2.9.7/NotoSansCJK-Bold.ttc + curl -fsSLO https://github.com/XmacsLabs/mogan/releases/download/v1.2.9.7/NotoSansCJK-Regular.ttc + curl -fsSLO https://github.com/XmacsLabs/mogan/releases/download/v1.2.9.7/NotoSerifCJK-Bold.ttc + curl -fsSLO https://github.com/XmacsLabs/mogan/releases/download/v1.2.9.7/NotoSerifCJK-Regular.ttc + - name: config + run: xmake config -m release -a ${{ matrix.arch }} -vD --loro=yes --yes + - name: build + run: xmake build -vD stem + - name: install + run: xmake install -vD stem + # vpk 1.2.0 的 apphost 需要 .NET 10 runtime;macos-14 arm64 镜像自带的 + # dotnet 版本较旧且不在 apphost 默认搜索路径(首跑实测 You must install + # .NET)。setup-dotnet 显式装 .NET 10 并设置 DOTNET_ROOT。 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Install vpk + shell: pwsh + run: | + dotnet tool install -g vpk --version 1.2.0 + # pwsh 在 Unix 无 USERPROFILE,用 HOME + Add-Content -Path $env:GITHUB_PATH -Value "$($env:HOME)/.dotnet/tools" + # Apple 签名/公证 setup(mac 独有;Windows 的正式签名在 SafeNet 签名机 + # 事后完成,mac 未签名 pkg 会被 Gatekeeper 直接拦截,故在 CI 内完成)。 + # keychain 命令序列照搬 packages/macos/package.scm 的 setup-keychain; + # 身份名动态解析后经 VPK_SIGN_* 环境变量注入 pack_velopack.lua(见 + # tools/release/pack_velopack.lua 的 mac 签名参数透传),stage 侧检测到 + # VPK_SIGN_APP_IDENTITY 即跳过 ad-hoc、交给 vpk 深签。 + - name: Setup Apple signing env: APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | - chmod +x TeXmacs/plugins/goldfish/bin/goldfish - TeXmacs/plugins/goldfish/bin/goldfish packages/macos/package.scm - - name: Upload + set -e + KEYCHAIN="$HOME/Library/Keychains/mogan-signing.keychain-db" + KEYCHAIN_PASS="$(openssl rand -base64 32)" + CERT=/tmp/mogan_cert.p12 + printf '%s' "$APPLE_CERTIFICATE_P12_BASE64" | base64 -D -o "$CERT" + security delete-keychain "$KEYCHAIN" 2>/dev/null || true + security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" + security set-keychain-settings -lut 21600 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" + security import "$CERT" -P "$APPLE_CERTIFICATE_PASSWORD" -k "$KEYCHAIN" -T /usr/bin/codesign || + security import "$CERT" -P "$APPLE_CERTIFICATE_PASSWORD" -k "$KEYCHAIN" + curl -fsSL -o /tmp/apple_devid_ca.cer https://www.apple.com/certificateauthority/DeveloperIDCA.cer + curl -fsSL -o /tmp/apple_devid_ca_g2.cer https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer + security add-certificates -k "$KEYCHAIN" /tmp/apple_devid_ca.cer /tmp/apple_devid_ca_g2.cer + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASS" "$KEYCHAIN" + LOGIN_KC="$HOME/Library/Keychains/login.keychain-db" + if [ -f "$LOGIN_KC" ]; then + security list-keychains -d user -s "$KEYCHAIN" "$LOGIN_KC" /Library/Keychains/System.keychain + else + security list-keychains -d user -s "$KEYCHAIN" /Library/Keychains/System.keychain + fi + security default-keychain -s "$KEYCHAIN" + # notarytool 凭据 profile(App Store Connect API key,同 package.scm) + KEY_DIR="$HOME/.appstoreconnect/private_keys" + mkdir -p "$KEY_DIR" + API_KEY="$KEY_DIR/AuthKey_${APPLE_API_KEY_ID}.p8" + printf '%s' "$APPLE_API_KEY_P8" | base64 -D -o "$API_KEY" + chmod 600 "$API_KEY" + xcrun notarytool store-credentials mogan-notary \ + --key "$API_KEY" --key-id "$APPLE_API_KEY_ID" \ + --issuer "$APPLE_API_ISSUER_ID" --keychain "$KEYCHAIN" + # 身份名动态解析(P12 里证书的完整主题名因续期而变,不硬编码) + APP_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" | + sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -1)" + INSTALL_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" | + sed -n 's/.*"\(Developer ID Installer:[^"]*\)".*/\1/p' | head -1)" + if [ -n "$APP_IDENTITY" ]; then + echo "VPK_SIGN_APP_IDENTITY=$APP_IDENTITY" >> "$GITHUB_ENV" + else + echo "::error::keychain 中未找到 Developer ID Application 证书" + exit 1 + fi + # P12 可能只含 Application 证书;缺 Installer 时 vpk 跳过 .pkg 签名并 + # 告警(app 仍签名),属可接受降级 + if [ -n "$INSTALL_IDENTITY" ]; then + echo "VPK_SIGN_INSTALL_IDENTITY=$INSTALL_IDENTITY" >> "$GITHUB_ENV" + echo "INSTALL_IDENTITY=$INSTALL_IDENTITY" + else + echo "INSTALL_IDENTITY=(未找到 Developer ID Installer,pkg 将不签名)" + fi + echo "VPK_NOTARY_PROFILE=mogan-notary" >> "$GITHUB_ENV" + echo "VPK_KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV" + echo "APP_IDENTITY=$APP_IDENTITY" + - name: Stage Velopack + run: xmake l tools/release/stage_velopack.lua + # 从 GitHub Release 拉取上一版本的发布压缩包作为各通道的 delta 基线: + # 逻辑与 Windows CD 一致(列 100 个 release、排除 draft 与当前 tag、 + # beta 必查 + 无 -rc 才查 stable、stable 只认非 rc release、同版本跳过、 + # 解 zip 取该通道 full 包拷入 build/velopack_release_/)。 + # 与 Windows 的两处差异: + # 1) zip 匹配模式带 -osx-arm64 平台段——同一 release 上还有 Windows 的 + # mogan-release-*-win-x64-.zip,nupkg 是平台专属的,不带 + # 平台段会抓错平台基线; + # 2) 不做裸 nupkg 回退——Windows 的该回退对应其过渡期历史,mac 沿用 + # 会抓到 Windows 的旧 full 包。 + # 某通道首次发布(GitHub 上尚无 mac 归档 zip)时跳过属正常:没有基线 + # 时 pack 只出 full 包,是合法状态。 + # GITHUB_TOKEN 不会自动注入为环境变量,必须显式传入 secrets.GITHUB_TOKEN; + # 为空时回退为公开仓库的匿名访问。 + - name: Fetch delta baselines from GitHub Release + shell: pwsh + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $headers = @{ + Accept = "application/vnd.github+json" + "User-Agent" = "mogan-cd" + } + if ($env:GITHUB_TOKEN) { + $headers["Authorization"] = "Bearer $($env:GITHUB_TOKEN)" + } + $releases = Invoke-RestMethod -Headers $headers ` + -Uri "https://api.github.com/repos/${{ github.repository }}/releases?per_page=100" + $releases = $releases | + Where-Object { -not $_.draft -and $_.tag_name -ne $env:GITHUB_REF_NAME } + # 本次要打的通道:beta 必打;stable 仅 tag 不含 -rc 时打 + # (tag 名由 bump-version 流水线产生,-rc 与否即通道信息,无需透传)。 + $tag = "${{ github.ref_name }}" + $channels = @("beta") + if ($tag -notmatch '-rc') { $channels += "stable" } + $vars = Get-Content xmake/vars.lua -Raw + $currentVer = [regex]::Match($vars, 'XMACS_VERSION\s*=\s*"([^"]+)"').Groups[1].Value + foreach ($channel in $channels) { + Write-Host "== 通道 $channel:查找 delta 基线 ==" + foreach ($release in $releases) { + # stable 基线只认非 rc tag 的 release(rc release 无 stable 包) + if ($channel -eq "stable" -and $release.tag_name -match '-rc') { + continue + } + Remove-Item build/velopack_baseline -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path build/velopack_baseline -Force | Out-Null + # 按平台 + 通道名匹配 zip(分开上传的格式,名带平台与通道后缀, + # 只下载目标通道的归档,避免拉取无关平台的 1.5GB 包) + $zipPattern = "mogan-release-*-osx-arm64-$channel.zip" + $zip = $release.assets | + Where-Object { $_.name -like $zipPattern } | + Select-Object -First 1 + if (-not $zip) { continue } + /usr/bin/curl -fsSL "$($zip.browser_download_url)" -o build/velopack_baseline.zip + Expand-Archive -Path build/velopack_baseline.zip ` + -DestinationPath build/velopack_baseline -Force + $full = Get-ChildItem build/velopack_baseline -Filter "*-$channel-full.nupkg" | + Select-Object -First 1 + if (-not $full) { continue } + $baselineVer = $full.Name -replace '^Mogan-', '' ` + -replace "-$channel-full\.nupkg$", '' + if ($baselineVer -eq $currentVer) { + Write-Host "基线版本与当前版本相同($baselineVer),跳过(重跑同一版本)" + break + } + $outDir = "build/velopack_release_$channel" + New-Item -ItemType Directory -Path $outDir -Force | Out-Null + Copy-Item $full.FullName $outDir/ + Write-Host "已就位 $channel delta 基线: $($full.Name)" + break + } + } + # vpk pack 在 outputDir 存在上一版本(上一步拉取的 full 包)时自动生成 + # delta 增量包;首次发布无上一版本,只有全量包。beta/stable 各用独立 + # 输出目录(vpk 把 outputDir 当 channel 累积目录,混用会互相干扰), + # 产物文件名带 channel 后缀,最后合并进同一 release 归档。 + - name: Pack Velopack (beta) + env: + VPK_CHANNEL: beta + VPK_OUTPUT_DIR: build/velopack_release_beta + run: xmake l tools/release/pack_velopack.lua + - name: Pack Velopack (stable) + if: ${{ !contains(github.ref_name, '-rc') }} + env: + VPK_CHANNEL: stable + VPK_OUTPUT_DIR: build/velopack_release_stable + run: xmake l tools/release/pack_velopack.lua + # 每个通道各自打一个 zip(Release 资产与下游 delta 基线按通道取用): + # 文件名带平台与通道后缀 mogan-release--osx-arm64-.zip, + # 命名规则与 Windows CD 的 win-x64 归档一致。 + - name: Create release archive + shell: pwsh + run: | + $vars = Get-Content xmake/vars.lua -Raw + $ver = [regex]::Match($vars, 'XMACS_VERSION\s*=\s*"([^"]+)"').Groups[1].Value + $dirs = Get-ChildItem build -Directory -Filter 'velopack_release_*' | + Sort-Object Name + foreach ($d in $dirs) { + $channel = $d.Name -replace '^velopack_release_', '' + $archive = "build/mogan-release-$ver-osx-arm64-$channel.zip" + Compress-Archive -Path "$($d.FullName)/*" -DestinationPath $archive ` + -CompressionLevel Optimal -Force + Get-Item $archive | Select-Object Name, Length + } + - name: Upload uses: actions/upload-artifact@v4 if: ${{ !startsWith(github.ref, 'refs/tags/') }} with: - path: build/*.dmg + path: | + build/velopack_release_*/* + build/mogan-release-*.zip - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: prerelease: true - files: build/*.dmg \ No newline at end of file + files: build/mogan-release-*.zip diff --git a/.github/workflows/cd_research_on_windows.yml b/.github/workflows/cd_research_on_windows.yml index 071f6aa5c0..be518009fc 100644 --- a/.github/workflows/cd_research_on_windows.yml +++ b/.github/workflows/cd_research_on_windows.yml @@ -73,9 +73,11 @@ jobs: # stable full。所有发布都标了 prerelease,releases/latest 接口拿不到, # 这里列出最近 100 个 release,排除 draft 和当前 tag(重跑同一 tag 时 # 避免把自己当基线),按通道各自向后遍历,wget 下载对应通道的 zip。 - # 压缩包分两种:新版每通道各一个 zip(mogan-release--win-x64- - # .zip,按通道名只下载目标通道的归档);旧版是全部通道合并的 - # 单一 zip(无通道后缀,兼容回退)。解压拣出对应通道的 full 包 + # 压缩包分两种:新版每平台每通道各一个 zip(mogan-release-- + # -.zip,按平台与通道名匹配归档——mac CD 会向同一 + # release 上传 osx-arm64 归档,nupkg 是平台专属的,不带平台段会抓错 + # 基线);旧版是全部通道合并的单一 zip(无通道后缀,仅 Windows 时代 + # 存在,兼容回退)。解压拣出对应通道的 full 包 # (stable 需逐个 release 尝试:rc tag 的 zip 里没有 stable 包,自然 # 跳过)。某通道首次发布或 GitHub 上尚无压缩包时跳过属正常(没有基线 # 时 pack 只出 full 包,是合法状态)。 @@ -116,16 +118,17 @@ jobs: } Remove-Item build/velopack_baseline -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path build/velopack_baseline -Force | Out-Null - # 优先按通道名匹配 zip(分开上传的新格式,名带通道后缀, - # 只下载目标通道的归档,避免拉取无关通道的 1.5GB 包);找不到 - # 再回退旧合并 zip(无通道后缀,内含全部通道的包)。 - $zipPattern = if ($channel -eq "beta") { '*beta*.zip' } else { '*stable*.zip' } + # 按平台 + 通道名匹配 zip(分开上传的新格式,名带平台与通道 + # 后缀,只下载目标通道的归档,避免拉取无关通道/平台的 1.5GB + # 包);找不到再回退旧合并 zip(无通道后缀、仅 Windows 时代 + # 存在,内含全部通道的包)。 + $zipPattern = "mogan-release-*-win-x64-$channel.zip" $zip = $release.assets | Where-Object { $_.name -like $zipPattern } | Select-Object -First 1 if (-not $zip) { $zip = $release.assets | - Where-Object { $_.name -like 'mogan-release-*.zip' } | + Where-Object { $_.name -like 'mogan-release-*-win-x64.zip' } | Select-Object -First 1 } if ($zip) { diff --git a/3rdparty/velopack/README.md b/3rdparty/velopack/README.md index ca63549f6d..a164601772 100644 --- a/3rdparty/velopack/README.md +++ b/3rdparty/velopack/README.md @@ -4,9 +4,39 @@ 许可证:MIT(见上游仓库 LICENSE)。 -当前仅 vendor Windows x64 资产: -- `include/Velopack.h`、`include/Velopack.hpp`:官方 C/C++ 头文件(C++ 为 C API 的薄封装)。 -- `lib/velopack_libc_win_x64_msvc.dll`:动态库本体。 -- `lib/velopack_libc_win_x64_msvc.dll.lib`:MSVC 导入库。 +当前 vendor 的资产: -macOS / Linux / Windows arm64 等资产后续按平台补充(zip 内其余文件未 vendor)。 +- Windows x64: + - `include/Velopack.h`、`include/Velopack.hpp`:官方 C/C++ 头文件(C++ 为 C API 的薄封装)。 + - `lib/velopack_libc_win_x64_msvc.dll`:动态库本体。 + - `lib/velopack_libc_win_x64_msvc.dll.lib`:MSVC 导入库。 +- macOS(universal x86_64 + arm64): + - `lib/libvelopack_libc.dylib`:来自 zip 内 `lib/velopack_libc_osx.dylib`,经下述三步 + 预处理后按 `lib` 前缀命名(见「macOS 预处理」)。`lib-static/` 下的 `.a` 未 vendor: + dylib 已预链 Rust 依赖,免去手动补 framework/link 参数。 + +Linux / Windows arm64 等资产后续按平台补充(zip 内其余文件未 vendor)。 + +## macOS 预处理 + +上游 dylib 直接链接会产生两个问题,vendor 时已做如下处理(复现命令): + +1. 上游 install id 是 CI 构建机的绝对路径(`/Users/runner/work/...`),直接链接会把 + 该路径写进可执行文件的依赖表,运行时找不到库。改名 + 重写 id 为 `@rpath/`: + ```bash + mv libvelopack_libc_osx.dylib libvelopack_libc.dylib # lib 前缀使 -lvelopack_libc 可解析 + install_name_tool -id @rpath/libvelopack_libc.dylib libvelopack_libc.dylib + ``` + 加载名必须与磁盘文件名一致(loader 按 install id 的文件名在各 rpath 目录查找), + 故 id 带 lib 前缀,不能对齐 Windows 的无前缀 `velopack_libc.dll`。 +2. `install_name_tool` 改 id 会使原有 linker-signed 签名失效,arm64 上签名损坏的 + dylib 会被 AMFI 拒载,必须 ad-hoc 重签: + ```bash + codesign --force --sign - --timestamp=none libvelopack_libc.dylib + ``` + +注意:与 Windows 的改名时机不同——Windows 在安装期把 DLL 改名为 +`velopack_libc.dll`(见 `xmake/targets/stem.lua`),macOS 因链接期/运行期文件名 +必须与磁盘文件一致,只能在 vendor 时改好提交。 + +dylib 的 `LC_BUILD_VERSION` minos 为 11.0,低于应用自身要求,无兼容性约束。 diff --git a/3rdparty/velopack/lib/libvelopack_libc.dylib b/3rdparty/velopack/lib/libvelopack_libc.dylib new file mode 100644 index 0000000000..4bf568d14c Binary files /dev/null and b/3rdparty/velopack/lib/libvelopack_libc.dylib differ diff --git a/TeXmacs/progs/texmacs/menus/preferences-widgets.scm b/TeXmacs/progs/texmacs/menus/preferences-widgets.scm index d91dcba42d..1b7163276c 100644 --- a/TeXmacs/progs/texmacs/menus/preferences-widgets.scm +++ b/TeXmacs/progs/texmacs/menus/preferences-widgets.scm @@ -920,7 +920,7 @@ ) ;list ;; 更新通道(Stable/Beta 单选 combo):真实存储单值 update-channel, ;; 写值经 set-field 特例路由到 updater-switch-channel 的两次确认。 - ;; 仅 Velopack 更新器平台(Windows 安装版)显示。 + ;; 仅 Velopack 更新器平台(Windows / macOS arm64 安装版)显示。 (if (use-plugin-updater?) (list (list (pref-update-channel) "Update channel" diff --git a/TeXmacs/progs/texmacs/menus/tests/preferences-widgets-test.scm b/TeXmacs/progs/texmacs/menus/tests/preferences-widgets-test.scm index fbe3cdbcc5..0c5b49a71c 100644 --- a/TeXmacs/progs/texmacs/menus/tests/preferences-widgets-test.scm +++ b/TeXmacs/progs/texmacs/menus/tests/preferences-widgets-test.scm @@ -57,7 +57,12 @@ (check (length (caddr (tab-ref meta "keyboard"))) => (if (os-macos?) 15 14)) (check (length (caddr (tab-ref meta "mathematics"))) => 11) (check (length (caddr (tab-ref meta "convert"))) => 0) - (check (length (caddr (tab-ref meta "other"))) => (if (os-macos?) 17 15)) + ;; other: 平台基础字段 + Velopack 更新器开启时多 1 个 Update channel 字段。 + ;; 用 use-plugin-updater? 而非硬编码,osx-x64 未接入 velopack 时仍正确。 + (check (length (caddr (tab-ref meta "other"))) + => + (+ (if (os-macos?) 17 15) (if (use-plugin-updater?) 1 0)) + ) ;check ) ;let ) ;define diff --git a/devel/0519.md b/devel/0519.md new file mode 100644 index 0000000000..9bc5f4fced --- /dev/null +++ b/devel/0519.md @@ -0,0 +1,166 @@ +# 0519: macOS arm64 Velopack 打包与自动更新 + +## 2026/08/27 macOS arm64 接入 Velopack + +### What(本次改动做了什么) + +把 macOS arm64 接入与 Windows 相同的 Velopack 发布/更新链路: + +1. vendor macOS libc 运行时动态库(上游 release 1.2.0 的 universal + `velopack_libc_osx.dylib`,前置任务单独提交); +2. xmake/C++ 运行时集成:`USE_PLUGIN_VELOPACK` 扩展到 macOS arm64, + 启动钩子/更新器后端/feed 平台段全部放开平台 guard; +3. release 脚本(`stage_velopack.lua`/`pack_velopack.lua`)增加 macOS + 分支,产出 vpk 原生 `.pkg` 安装器 + portable `.zip` + full/delta + nupkg + `releases..json` feed。 + +### Why(为什么这么做) + +- 0512 立下的目标是三平台共用 Velopack 的发布版本/渠道/客户端更新 API, + 各平台产出各自安装格式;macOS 是第二个接入的平台。 +- 现有 macOS 发布(.dmg)无自动更新;Velopack delta 增量 + 应用内更新 + 与 Windows 侧体验拉齐。 +- 发布物选 vpk 原生 `.pkg` + `.zip`(不做 DMG):`.pkg` 走系统安装流程 + (用户可选 /Applications 或 ~/Applications),更新链路天然支持;DMG + 可后续从 portable zip 自行生成,不在本任务。 + +### How(怎么做的) + +按提交顺序分四步: + +1. **任务文档**(本文件)。 +2. **vendor macOS dylib(前置任务,单独提交)**:从 + `velopack_libc_1.2.0.zip` 取 `lib/velopack_libc_osx.dylib` + (universal x86_64+arm64),改名 `libvelopack_libc.dylib`(lib 前缀 + 使 `add_links("velopack_libc")` 可解析;macOS 链接期文件名须与磁盘 + 名一致,故 vendor 时改名,与 Windows 安装期改名不同), + `install_name_tool -id @rpath/libvelopack_libc.dylib` 重写 install id + (上游是 CI 构建路径;id 必须与磁盘文件名一致,loader 按该文件名在 + rpath 目录查找),随后 `codesign --force --sign -` 重签(改 id + 会破坏原签名,arm64 上签名损坏的 dylib 会被 AMFI 拒载)。 + lib-static 的 `.a` 不 vendor:dylib 已预链 Rust 依赖,免手动补 + framework。 +3. **C++/xmake 运行时集成**: + - `xmake/velopack.lua`:`add_velopack_runtime()` 平台分发,macOS + arm64 直链 dylib;`velopack_startup_test` 目标扩到 mac。 + - `xmake/velopack.lua` 增加 `velopack_libc` shared target(on_build 拷 + 预编译产物,targetdir 定在 `/Frameworks`):qt.widgetapp + 的部署规则会把 shared 依赖的 targetfile 自动拷进 .app 的 + `Contents/Frameworks`,裸二进制(dev 运行/CI scheme 测试走 + `target:targetfile()`,bundle 外)靠自带 `@executable_path/Frameworks` + rpath 找到同一份。自写 after_build 拷贝不可行——qt 部署规则会 + `os.tryrm` 整个 .app 重建,时机上必然被清掉。 + - `xmake/targets/libmogan.lua`:`USE_PLUGIN_VELOPACK = win x64 或 + macosx arm64`,不按构建模式 gate(CI releasedbg 跑 scheme 测试, + 模式 gate 会使测试期望随模式漂移)。 + - `research.cpp` 启动钩子、`tm_updater.cpp`/`tm_velopack.cpp` 后端 + 选择 guard 扩为 `OS_WIN || OS_MACOS`。 + - `tm_velopack.cpp` feed 平台段改编译期宏 `UPDATER_FEED_PLATFORM` + (`win-x64`/`osx-arm64`/`osx-x64`),替换硬编码 `win-x64`;不走 + config.h.xmake(单消费者不值得动全局配置头)。 + - `preferences-widgets-test.scm`:other tab 字段数期望改为 + `(+ (if (os-macos?) 17 15) (if (use-plugin-updater?) 1 0))`, + 不硬编码 18(osx-x64 未开启时仍正确)。 +4. **release 脚本 macOS 分支**: + - `stage_velopack.lua`:按 `os.host()` 分发;定位 + `build/macosx/*/release/MoganSTEM.app`,`ditto` 拷贝(保 Qt + framework 符号链接,xmake `os.cp` 默认解引用会打坏 bundle); + 从 `packages/macos/Info.plist.in` 生成真 plist(替换 + `@STEM_NAME@`/`@XMACS_VERSION@`,不依赖 stem_packager——它 + release-only、依赖 create-dmg、会产 DMG);ad-hoc 签 staging + 副本;结构与禁发文件校验。 + - `pack_velopack.lua`:vpk 定位加 mac 候选路径;args 用 + `--runtime osx-arm64`、`--mainExe MoganSTEM`、`--packDir` 指向 + `.app`;显式传 `--channel`(vpk mac 默认 channel 是 `osx`, + 不传会拆渠道);签名参数 `VPK_SIGN_APP_IDENTITY` 等五个环境变量 + 透传(本阶段默认不签,凭据留给 CI);产物从 + `assets..json` 发现 `.pkg`/`.zip` 后按 + `MoganSTEM-v<版本>-arm64-<渠道>-Setup.pkg`/`-Portable.zip` 改名; + `prune_old_full` 后处理平台无关,不动。 + +feed 平台段为 `osx-arm64`(服务端 `/api/v1/public/update/osx-arm64` +目录不在本任务范围)。版本纪律沿用 0512 的 `rc.N` 预发布格式。 + +### 不做什么 + +- CI workflow 改造(`cd_on_macos_arm64.yml` 仍走 package.scm 产 DMG, + 接入 velopack 是后续任务); +- DMG 产物;osx-x64(dylib 为 universal,后续加成本低); +- 正式 Developer ID 签名/公证(环境变量已接好,凭据后续配)。 + +### 涉及文件 + +- `3rdparty/velopack/lib/libvelopack_libc.dylib`(新)、`3rdparty/velopack/README.md` +- `xmake/velopack.lua`、`xmake/targets/stem.lua`、`xmake/targets/libmogan.lua` +- `src/Mogan/Research/research.cpp`、`src/Plugins/Updater/tm_updater.cpp`、 + `src/Plugins/Updater/tm_velopack.cpp` +- `tools/velopack/velopack_startup_test.cpp` +- `TeXmacs/progs/texmacs/menus/preferences-widgets.scm`(注释)、 + `TeXmacs/progs/texmacs/menus/tests/preferences-widgets-test.scm` +- `tools/release/stage_velopack.lua`、`tools/release/pack_velopack.lua` + +### 如何测试 + +```bash +# 运行时集成(releasedbg,对齐 macOS CI) +xmake f -c -m releasedbg --yes && xmake b stem +otool -L build/macosx/arm64/releasedbg/MoganSTEM.app/Contents/MacOS/MoganSTEM | grep velopack +xmake r updater-test && xmake r preferences-widgets-test +xmake b velopack_startup_test && xmake r velopack_startup_test +xmake run --group=scheme_tests + +# 打包(release) +xmake f -c -m release --yes && xmake b stem && xmake i stem +xmake l tools/release/stage_velopack.lua +xmake l tools/release/pack_velopack.lua +``` + +## 2026/08/27 验证结果 + +### 运行时集成(releasedbg,对齐 ci-macos-arm64.yml) + +- `otool -L` 裸二进制与 .app 内二进制均记录 `@rpath/libvelopack_libc.dylib`; + dylib 两处落位:`/Frameworks/`(裸二进制,dev 运行与 CI scheme + 测试走 `target:targetfile()`)与 `.app/Contents/Frameworks/`(qt 部署规则 + 按 shared 依赖拷贝)。 +- `xmake r updater-test`:9 correct, 0 failed——macOS 首次以 `tm_velopack` + 激活状态跑该测试(Windows CI 不跑 scheme 测试,Linux 编的是基类空实现)。 +- `xmake r preferences-widgets-test`:434 correct, 0 failed(other tab + 18 字段,含 Update channel)。 +- `xmake r velopack_startup_test`:输出 `velopack startup hook: ok`。 +- `xmake run --group=scheme_tests` 全组合计 1152 correct, 0 failed。 + +### 打包(release,本机 ad-hoc,未接正式签名) + +- stage:4203 文件 / 420.4 MB;`plutil -lint` 通过;CFBundleIdentifier= + app.mogan、CFBundleExecutable=MoganSTEM、版本 2026.3.2。 +- vpk 1.2.0(dotnet tool,需 `DOTNET_ROOT=/opt/homebrew/opt/dotnet/libexec`) + 实测 macOS 产物名与 Windows 同约定:`Mogan--Setup.pkg` / + `Mogan--Portable.zip`;改名后为 + `MoganSTEM-v2026.3.2-arm64-stable-{Setup.pkg,Portable.zip}`(pkg 168MB / + zip 172MB),assets.stable.json 同步更新。注意 assets.json 是顶层数组 + (字段 `RelativeFileName`),与 releases.json 的 `{Assets:[...]}` 结构不同。 +- 无签名参数时 vpk 明确告警跳过签名/公证(Package will not be signed or + notarized),流程不失败。 +- delta:同 outputDir 下以 `VPK_VERSION=2026.3.3` 二次 pack,产出 + `Mogan-2026.3.3-stable-delta.nupkg`(4141 unchanged / 1 patched / 10 + removed),增量链可用(测试产物已清理)。 + +### 端到端 + +- Portable.zip 解压到 /tmp 直接运行:headless 冒烟 + `(use-plugin-updater?)` ⇒ `#t`、`(updater-state)` ⇒ 0(IDLE),进程正常 + 退出——bundle 内 dylib 加载、`$TEXMACS_PATH` 从 bundle 解析均正常。 +- GUI `open` 启动存活(越过 2s 的 updater-initialize 窗口,feed 尚未就绪 + 也不崩);首选项 → Other 可见 Update channel。 +- .pkg 解包检查:Distribution 引用 app.mogan@2026.3.2 主 bundle, + `enable_currentUserHome="true"` 支持 ~/Applications 安装。 + +### 后续(非本任务) + +- CI 接入(cd_on_macos_arm64.yml 或新 workflow)+ Developer ID 签名/公证 + (`VPK_SIGN_APP_IDENTITY` 等五个环境变量已接好); +- 服务端 `/api/v1/public/update/osx-arm64` feed; +- 本机跑 vpk 需 dotnet + `dotnet tool install -g vpk`;brew 安装的 dotnet + 不在 vpk 默认 runtime 搜索路径,pack 脚本已自动探测注入 + `DOTNET_ROOT=/opt/homebrew/opt/dotnet/libexec`,无需手动 export。 diff --git a/devel/0520.md b/devel/0520.md new file mode 100644 index 0000000000..050ed24430 --- /dev/null +++ b/devel/0520.md @@ -0,0 +1,108 @@ +# 0520: macOS CD 接入 Velopack(对齐 Windows CD) + +## 2026/08/28 macOS CD 从 DMG 切换到 Velopack + +### What(本次改动做了什么) + +重写 `.github/workflows/cd_on_macos_arm64.yml`:移除旧的 goldfish + +package.scm DMG 打包/签名/公证流程,改为与 +`cd_research_on_windows.yml` 行为一致的 Velopack 流程——build/install stem、 +安装 vpk、stage、从 GitHub Release 拉取各通道 delta 基线、beta/stable 双通道 +pack、按通道产出归档 zip `mogan-release-<版本>-osx-arm64-<渠道>.zip`、 +上传 artifact / 发布 Release。CI 内完成 Developer ID 签名 + 公证(复用现有 +APPLE_* secrets 构建 keychain 与 notarytool profile,经 0519 接好的 +VPK_SIGN_* 环境变量注入 vpk)。 + +### Why(为什么这么做) + +- 0519 已落地 macOS 侧 Velopack 运行时与本地 stage/pack 脚本,CI 是最后一环; + release 产物从 DMG 迁移到 vpk 原生 .pkg + portable zip + nupkg feed, + 获得 delta 增量更新与应用内自动更新能力。 +- 严格镜像 Windows CD 的步骤结构、通道规则与基线回退语义,两平台发布行为 + 可预测、可对照排障。 +- 签名公证留在 CI 内完成(而非像 Windows 那样事后签名机):mac 未签名 pkg + 下载即被 Gatekeeper 拦截,且现有 secrets 齐备,vpk 原生支持。 + +### How(怎么做的) + +1. **Noto 字体外置**:原 package.scm 内部下载的 4 个 CJK ttc 改为独立步骤 + (与 Windows CD 相同 URL),在 `xmake install stem` 前落进 + `TeXmacs/fonts/opentype/noto/`。 +2. **签名 setup(mac 独有步骤)**:keychain 构建命令序列照搬 + `packages/macos/package.scm`(create/unlock/import P12/import Developer ID + CA/set-key-partition-list/搜索列表+默认钥匙串);App Store Connect API key + 解出后 `xcrun notarytool store-credentials` 建 profile;身份名动态解析 + (`security find-identity`),`VPK_SIGN_APP_IDENTITY`/ + `VPK_SIGN_INSTALL_IDENTITY`(存在才导出)/`VPK_NOTARY_PROFILE`/ + `VPK_KEYCHAIN` 写 GITHUB_ENV。 +3. **基线拉取**:逐行镜像 Windows CD 的 pwsh 逻辑(列 100 个 release、排除 + draft 与当前 tag、beta 必查 + 无 `-rc` 才查 stable、stable 只认非 rc + release、同版本跳过、解 zip 取 `*-$channel-full.nupkg` 拷入 + `build/velopack_release_<渠道>/`)。差异仅两处:zip 匹配模式带 + `-osx-arm64` 平台段(release 上已有 Windows 同前缀 zip,不带平台段会抓 + 错平台基线);**不做裸 nupkg 回退**(Windows 的该回退对应其过渡期历史, + mac 沿用会抓到 Windows 旧 full)。`wget` 换 `curl`。 +4. **双通道 pack / 归档 / 发布**:与 Windows CD 完全一致——beta 每个 tag 打、 + tag 不含 `-rc` 加打 stable,各用独立 outputDir;归档 + `build/mogan-release-<版本>-osx-arm64-<渠道>.zip`;非 tag 上传 artifact、 + tag 以 prerelease 发布 `build/mogan-release-*.zip`。 + +### 已知关联缺陷(本次不处理) + +Windows CD 的基线 zip 匹配模式(`*beta*.zip` 等)不带平台段,mac 归档上传到 +同一 release 后会被其误抓为基线;用户决定另行任务收紧 Windows CD 模式。 + +### 不做什么 + +- 不动 Windows CD(含上述缺陷); +- `packages/macos/package.scm` 文件保留在仓库(不再被 CD 引用,后续另行清理); +- osx-x64;服务端 feed 目录(/api/v1/public/update/osx-arm64)。 + +### 涉及文件 + +- `devel/0520.md`(本文件) +- `.github/workflows/cd_on_macos_arm64.yml`(重写) + +### 如何测试 + +- 本地:`python3 -c "import yaml; yaml.safe_load(open('.github/workflows/cd_on_macos_arm64.yml'))"` + 过语法;actionlint(若可用)过两个 workflow;与 Windows CD 逐步骤结构对照。 +- 真实验证依赖首个 tag:观察双通道 pack、基线拉取日志(首版无基线只出 full + 属正常)、zip 命名与 release 资产、vpk 日志确认签名/公证完成。签名链路 + (vpk × hardened runtime × notarytool profile)是唯一无法本地验证的部分, + 若公证被拒,在 `--signEntitlements` 补最小权利文件。 + +## 2026/08/28 CD 三轮 tag 实测(PR #4451) + +测试 tag:`v2026.4.0` → `v2026.4.1-rc.1` → `v2026.4.1`(均 prerelease), +Windows 与 macOS CD 同跑同 tag。首轮 mac 两次失败,定位并修复两处: + +1. **vpk 找不到 .NET runtime**:vpk 1.2.0 的 apphost 需要 net10.0, + macos-14 arm64 镜像自带 dotnet 版本旧且不在 apphost 搜索路径。修复: + `actions/setup-dotnet@v4` 装 .NET 10(自动设 `DOTNET_ROOT`)。 +2. **公证判 Invalid**:两个未签名嵌套二进制——(a) xmake 安装 stem 时把 + velopack shared 依赖额外装进 `Contents/Resources/lib/`(vpk 的 + `codesign --deep` 只遍历 bundle/framework/PlugIns 结构,签不到), + 修复:stem 的 after_install 清理该错位副本(与重复二进制清理同模式), + stage 加硬校验兜底;(b) `Resources/.../goldfish` 裸可执行文件同样对 + `--deep` 不可见,修复:stage 在配置了 `VPK_SIGN_APP_IDENTITY` 时用 + 正式身份逐个预签 Resources 裸 Mach-O(`.dSYM` 按旧 DMG 流程先例不签), + bundle 本体仍交给 vpk 深签。修复后公证 **Accepted**(beta/stable 各 + 一次,~90s/次)+ staple 通过 + spctl 校验通过。 + +三轮结果(两平台行为一致): + +| 轮次 | tag | 断言 | 结果 | +|------|-----|------|------| +| 1 | v2026.4.0(无 rc) | 双渠道产出 | ✓ win/mac × beta/stable 四归档;win 基线取真实 release 2026.3.2 的对应通道 full(新模式 `mogan-release-*-win-x64-.zip` 生效),mac 首发无基线只出 full | +| 2 | v2026.4.1-rc.1(rc) | 仅 beta + 以轮 1 beta 为基线 | ✓ 两平台仅 beta 归档;基线 `Mogan-2026.4.0-beta-full.nupkg`;delta 2026.4.0→2026.4.1-rc.1 | +| 3 | v2026.4.1(无 rc) | 双渠道 + 基线穿越 | ✓ 四归档;stable 基线 `Mogan-2026.4.0-stable-full.nupkg`(跳过 rc release);beta 基线 `Mogan-2026.4.1-rc.1-beta-full.nupkg`;delta 2026.4.0→2026.4.1 / 2026.4.1-rc.1→2026.4.1 | + +测试遗留(合并前需决策): + +- 三个测试 release+tag(v2026.4.0 / v2026.4.1-rc.1 / v2026.4.1)在仓库中, + 未来正式版本号必须高于 2026.4.1,或删除测试 release+tag 后基线链自动 + 回落到真实 release; +- 分支上 `xmake/vars.lua` 当前为测试版本 2026.4.1,合并时需视发布节奏处理; +- Debian13/Fedora CD 的 tag 触发仍是注释状态,正式发版前须恢复(两文件 + 内有标注)。 diff --git a/src/Mogan/Research/research.cpp b/src/Mogan/Research/research.cpp index 8aa197cd70..98153046e2 100644 --- a/src/Mogan/Research/research.cpp +++ b/src/Mogan/Research/research.cpp @@ -39,7 +39,7 @@ #include "tm_url.hpp" #include "tm_window.hpp" -#if defined(OS_WIN) +#if defined(OS_WIN) || defined(OS_MACOS) #include "Velopack.hpp" #endif @@ -165,8 +165,8 @@ int main (int argc, char** argv) { // Velopack 启动钩子:处理待安装的更新(无安装时为空操作)。 - // 必须早于任何系统初始化与参数解析,且仅限 Windows。 -#if defined(OS_WIN) + // 必须早于任何系统初始化与参数解析(Windows 与 macOS 安装版均需要)。 +#if defined(OS_WIN) || defined(OS_MACOS) Velopack::VelopackApp::Build ().Run (); #endif diff --git a/src/Plugins/Updater/tm_updater.cpp b/src/Plugins/Updater/tm_updater.cpp index 2592fffeea..644393ad8e 100644 --- a/src/Plugins/Updater/tm_updater.cpp +++ b/src/Plugins/Updater/tm_updater.cpp @@ -13,7 +13,7 @@ #include "config.h" #include "scheme.hpp" -#if defined(OS_WIN) && defined(USE_PLUGIN_VELOPACK) +#if defined(USE_PLUGIN_VELOPACK) && (defined(OS_WIN) || defined(OS_MACOS)) #include "tm_velopack.hpp" #endif @@ -22,7 +22,7 @@ tm_updater::instance () { static tm_updater* _instance= NULL; if (!_instance) { -#if defined(OS_WIN) && defined(USE_PLUGIN_VELOPACK) +#if defined(USE_PLUGIN_VELOPACK) && (defined(OS_WIN) || defined(OS_MACOS)) _instance= new tm_velopack (); #else _instance= new tm_updater (); diff --git a/src/Plugins/Updater/tm_velopack.cpp b/src/Plugins/Updater/tm_velopack.cpp index 45b5e480f8..130b36bbeb 100644 --- a/src/Plugins/Updater/tm_velopack.cpp +++ b/src/Plugins/Updater/tm_velopack.cpp @@ -10,7 +10,7 @@ #include "tm_configure.hpp" -#if defined(USE_PLUGIN_VELOPACK) && defined(OS_WIN) +#if defined(USE_PLUGIN_VELOPACK) && (defined(OS_WIN) || defined(OS_MACOS)) #include "preferences.hpp" #include "string.hpp" @@ -45,13 +45,25 @@ feed_base_url () { return "https://liiistem.cn"; } +// feed 平台段:与 vpk pack 的 --runtime 一致,服务端按平台目录分发各自的 +// releases..json 与 nupkg。只在本编译单元消费,不走 config.h。 +#if defined(OS_WIN) +#define UPDATER_FEED_PLATFORM "win-x64" +#elif defined(OS_MACOS) && defined(__aarch64__) +#define UPDATER_FEED_PLATFORM "osx-arm64" +#elif defined(OS_MACOS) && defined(__x86_64__) +#define UPDATER_FEED_PLATFORM "osx-x64" +#else +#error "unsupported velopack feed platform" +#endif + // 完整 feed URL = base URL + 按社区版/商业版(IS_COMMUNITY 宏)选定的路径段 static std::string feed_url (const std::string& base) { #ifdef IS_COMMUNITY - return base + "/api/v1/public/update/win-x64"; + return base + "/api/v1/public/update/" UPDATER_FEED_PLATFORM; #else - return base + "/api/v1/public/commercial/update/win-x64"; + return base + "/api/v1/public/commercial/update/" UPDATER_FEED_PLATFORM; #endif } @@ -389,4 +401,5 @@ tm_velopack::applyUpdate () { return true; } -#endif // defined (USE_PLUGIN_VELOPACK) && defined (OS_WIN) +#endif // defined (USE_PLUGIN_VELOPACK) && (defined (OS_WIN) || defined + // (OS_MACOS)) diff --git a/tools/release/pack_velopack.lua b/tools/release/pack_velopack.lua index 28401bd308..8f5d4ff662 100644 --- a/tools/release/pack_velopack.lua +++ b/tools/release/pack_velopack.lua @@ -1,7 +1,8 @@ ------------------------------------------------------------------------------- -- -- MODULE : pack_velopack.lua --- DESCRIPTION : 调用 vpk pack 生成 Velopack 发布(Setup.exe / releases.*.json / packages/) +-- DESCRIPTION : 调用 vpk pack 生成 Velopack 发布(Windows: Setup.exe / Portable.zip; +-- macOS: .pkg / Portable.zip;均含 releases.*.json 与 packages/) -- COPYRIGHT : (C) 2026 Xmacs Labs -- -- This software falls under the GNU general public license version 3 or later. @@ -15,8 +16,25 @@ -- vpk pack 把 --packDir 的扁平目录打成安装包与全量/增量包,并生成 -- releases..json 供客户端查询更新。 -- --- 关于签名:CI/本地默认不签名(VPK_SIGN_PARAMS 为空)。正式签名在 SafeNet --- 签名机上通过环境变量 VPK_SIGN_PARAMS 注入 signtool 参数完成。SafeNet +-- macOS 分支(os.host() == "macosx"): +-- - --packDir 直接指向 stage 产出的 MoganSTEM.app(vpk 原样使用现成 bundle, +-- 无需 --icon/--bundleId/--plist——那些仅用于让 vpk 自己生成 bundle); +-- - --channel 必须显式传:vpk 在 macOS 的默认 channel 是 "osx",漏传会把 +-- feed 悄悄拆到 osx 渠道; +-- - --runtime osx-arm64 与客户端 feed 平台段(tm_velopack.cpp 的 +-- UPDATER_FEED_PLATFORM)对应,服务端按平台目录分发; +-- - 签名默认关闭(本地验证):不传任何 --sign* 参数时 vpk 不做 Developer ID +-- 签名/公证。正式发布经环境变量注入: +-- VPK_SIGN_APP_IDENTITY → --signAppIdentity(Developer ID Application) +-- VPK_SIGN_INSTALL_IDENTITY → --signInstallIdentity(Developer ID Installer) +-- VPK_SIGN_ENTITLEMENTS → --signEntitlements(hardened runtime 权利文件) +-- VPK_NOTARY_PROFILE → --notaryProfile(notarytool 凭据 profile) +-- VPK_KEYCHAIN → --keychain +-- - vpk 的 macOS 产物文件名上游文档未写明,改名逻辑从 assets..json +-- 的 Assets 里按扩展名(.pkg/.zip)发现原文件名,不硬编码猜测。 +-- +-- 关于签名(Windows):CI/本地默认不签名(VPK_SIGN_PARAMS 为空)。正式签名在 +-- SafeNet 签名机上通过环境变量 VPK_SIGN_PARAMS 注入 signtool 参数完成。SafeNet -- 交互式令牌(interactive token)不允许并发签名,签名机上须固定 -- --signParallel 1;该参数与 --signParams 一起由签名机脚本传入,这里只 -- 注释说明,不写死进默认命令。 @@ -38,21 +56,35 @@ -- -- 环境变量覆盖: -- VPK_PATH vpk 可执行文件;默认自动定位 --- VPK_PACK_DIR 默认 build/velopack_staging +-- VPK_PACK_DIR 默认 Windows: build/velopack_staging +-- macOS: build/velopack_staging/MoganSTEM.app -- VPK_OUTPUT_DIR 默认 build/velopack_release -- VPK_CHANNEL 默认 stable -- VPK_VERSION 默认从 xmake/vars.lua 解析 XMACS_VERSION --- VPK_SIGN_PARAMS 默认空字符串;签名机注入 signtool 参数 +-- VPK_SIGN_PARAMS Windows signtool 参数;默认空字符串,签名机注入 -- VPK_PRUNE_ONLY 只跑后处理(清理历史 full 包 + manifest),不重新 pack +-- +-- macOS 注意:brew 安装的 dotnet 不在 vpk apphost 的默认 runtime 搜索路径, +-- 未设 DOTNET_ROOT 时本脚本自动探测 brew 路径注入(/opt/homebrew/opt/dotnet/ +-- libexec 等);官方 pkg 安装的 dotnet 在默认路径 /usr/local/share/dotnet,无需处理。 ------------------------------------------------------------------------------- --- 定位 vpk:显式环境变量优先,其次常见安装路径,最后回退到 PATH 上的 vpk +-- 定位 vpk:显式环境变量优先,其次按宿主平台的常见安装路径,最后回退到 PATH local vpk = os.getenv ("VPK_PATH") if vpk == nil or vpk == "" then - local candidates = { - path.join (os.getenv ("USERPROFILE") or "", ".dotnet/tools/vpk.exe"), - path.join ("C:/Program Files/dotnet/tools/vpk.exe"), - } + local candidates + if os.host () == "macosx" then + candidates = { + path.join (os.getenv ("HOME") or "", ".dotnet/tools/vpk"), + "/opt/homebrew/share/dotnet/tools/vpk", + "/usr/local/share/dotnet/tools/vpk", + } + else + candidates = { + path.join (os.getenv ("USERPROFILE") or "", ".dotnet/tools/vpk.exe"), + path.join ("C:/Program Files/dotnet/tools/vpk.exe"), + } + end for _, c in ipairs (candidates) do if os.isfile (c) then vpk = c @@ -81,8 +113,13 @@ if version == nil or version == "" then end end +local is_mac = os.host () == "macosx" + local pack_dir = os.getenv ("VPK_PACK_DIR") -if pack_dir == nil or pack_dir == "" then pack_dir = "build/velopack_staging" end +if pack_dir == nil or pack_dir == "" then + pack_dir = is_mac and "build/velopack_staging/MoganSTEM.app" + or "build/velopack_staging" +end local out_dir = os.getenv ("VPK_OUTPUT_DIR") if out_dir == nil or out_dir == "" then out_dir = "build/velopack_release" end local channel = os.getenv ("VPK_CHANNEL") @@ -92,10 +129,215 @@ local prune_only = os.getenv ("VPK_PRUNE_ONLY") == "1" pack_dir = path.absolute (path.join (os.projectdir (), pack_dir)) out_dir = path.absolute (path.join (os.projectdir (), out_dir)) + +-- 发布物改名:沿用既有命名 MoganSTEM-v<版本>-<平台>-<渠道>-Setup/Portable。 +-- vpk 默认名是 --*;改名后同步更新 assets..json 里的 +-- 引用,避免下载链接失效。 +local function rename_asset (old_name, new_name) + local old_path= path.join (out_dir, old_name) + local new_path= path.join (out_dir, new_name) + if os.isfile (old_path) then + if os.isfile (new_path) then + os.rm (new_path) -- 同名残留先清掉,避免 os.mv 目标已存在失败 + end + os.mv (old_path, new_path) + cprint ("${green}已改名: " .. new_name .. "${clear}") + return true + end + return false +end + +local function swap_name_in_assets (assets_file, old_name, new_name) + local content = io.readfile (assets_file) or "" + -- Lua 模式中 - . 是 magic 字符,先转义再替换 + local escaped = old_name:gsub ("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") + content = content:gsub (escaped, function () return new_name end) + io.writefile (assets_file, content) +end + +------------------------------------------------------------------------------- +-- 后处理:清理发布目录中的历史 full 包,release 只留当前版本产物。 +-- vpk pack 把 outputDir 当作 channel 累积目录:上一版本 full 会留在 outputDir +-- 并被写进 releases..json。那份旧 full 只是算 delta 的基线,客户端 +-- 走 delta 用的是本地 packages 目录里的旧 full(见联调日志),feed 上的旧 full +-- 不会被拉取,纯属冗余(回滚另走 OSS 保留策略)。这里删掉旧 full 文件并在 +-- manifest 中去掉对应条目,只保留当前版本 full 与历史 delta。outputDir 仍 +-- 保留当前版本 full,作为下一次 pack 的 delta 基线,不受影响。 +------------------------------------------------------------------------------- +local function prune_old_full (out_dir, channel, version) + local json = import ("core.base.json") + local current_full = "Mogan-" .. version .. "-" .. channel .. "-full.nupkg" + + -- 1) 删除旧版本 full 包文件(当前版本 full 保留,作下次 delta 基线) + for _, f in ipairs (os.files (path.join (out_dir, "Mogan-*-" .. channel .. "-full.nupkg"))) do + if path.filename (f) ~= current_full then + os.rm (f) + cprint ("${yellow}已清理历史 full: " .. path.filename (f) .. "${clear}") + end + end + + -- 2) releases..json 去掉旧 full 条目,保留当前 full 与全部 delta。 + -- 注意 xmake lua 沙箱无 pcall / try-catch(见本文件 git 历史),防御靠 + -- 内容结构检查:缺 "Assets" 就跳过,vpk 生成的 manifest 正常情况必然合法。 + local rel_file= path.join (out_dir, "releases." .. channel .. ".json") + if os.isfile (rel_file) then + local content = io.readfile (rel_file) or "" + if content:find ('"Assets"') then + local data= json.decode (content) + if type (data) == "table" and type (data.Assets) == "table" then + local kept= {} + for _, a in ipairs (data.Assets) do + if a.Type ~= "Full" or a.Version == version then + table.insert (kept, a) + end + end + data.Assets= kept + io.writefile (rel_file, json.encode (data)) + cprint ("${green}releases." .. channel .. ".json 已去除历史 full 条目${clear}") + end + else + cprint ("${yellow}warning: " .. path.filename (rel_file) .. " 缺少 Assets 结构,跳过 manifest 清理${clear}") + end + end +end + +-- 打包完成后做后处理;VPK_PRUNE_ONLY=1 时独立重跑清理 +local function postprocess () + prune_old_full (out_dir, channel, version) +end + +if prune_only then + postprocess () + return +end + +------------------------------------------------------------------------------- +-- macOS 打包 +------------------------------------------------------------------------------- +if is_mac then + -- 先决条件:staging .app 结构齐备 + if not os.isdir (pack_dir) then + cprint ("${bright red}error: 暂存 .app 不存在: " .. pack_dir .. "${clear}") + cprint ("${yellow}请先运行 xmake l tools/release/stage_velopack.lua${clear}") + os.exit (1) + end + if not os.isfile (path.join (pack_dir, "Contents/MacOS/MoganSTEM")) then + cprint ("${bright red}error: 暂存 .app 缺 Contents/MacOS/MoganSTEM: " .. pack_dir .. "${clear}") + os.exit (1) + end + if not os.isfile (path.join (pack_dir, "Contents/Info.plist")) then + cprint ("${bright red}error: 暂存 .app 缺 Contents/Info.plist: " .. pack_dir .. "${clear}") + os.exit (1) + end + + os.mkdir (out_dir) + + -- 命令参数固定为 Velopack 锁定集;签名参数仅在环境变量非空时追加。 + -- macOS 上 vpk 的默认 channel 是 "osx",必须显式传 --channel 保持 stable/beta。 + local args = { + "pack", + "--packId", "Mogan", + "--packVersion", version, + "--packDir", pack_dir, + "--mainExe", "MoganSTEM", + "--channel", channel, + "--outputDir", out_dir, + "--runtime", "osx-arm64", + "--packTitle", "Mogan STEM", + "--packAuthors", "Xmacs Labs", + } + local mac_sign_env = { + {"VPK_SIGN_APP_IDENTITY", "--signAppIdentity"}, + {"VPK_SIGN_INSTALL_IDENTITY", "--signInstallIdentity"}, + {"VPK_SIGN_ENTITLEMENTS", "--signEntitlements"}, + {"VPK_NOTARY_PROFILE", "--notaryProfile"}, + {"VPK_KEYCHAIN", "--keychain"}, + } + for _, pair in ipairs (mac_sign_env) do + local val = os.getenv (pair[1]) or "" + if val ~= "" then + table.insert (args, pair[2]) + table.insert (args, val) + end + end + + cprint ("${cyan}vpk pack 调用:${clear}") + for _, a in ipairs (args) do + print (" " .. a) + end + print ("") + + -- brew dotnet 需要 DOTNET_ROOT 才能找到 runtime;已设则不覆盖 + local exec_envs = nil + if (os.getenv ("DOTNET_ROOT") or "") == "" then + for _, root in ipairs ({"/opt/homebrew/opt/dotnet/libexec", + "/usr/local/opt/dotnet/libexec"}) do + if os.isdir (root) then + exec_envs = {DOTNET_ROOT = root} + cprint ("${yellow}DOTNET_ROOT 未设置,注入: " .. root .. "${clear}") + break + end + end + end + + -- try=true:非零退出不抛异常,由本脚本统一报错退出 + local code = os.execv (vpk, args, {try = true, envs = exec_envs}) + if code ~= 0 then + cprint ("${bright red}error: vpk pack 失败,退出码 " .. code .. "${clear}") + os.exit (1) + end + cprint ("${green}vpk pack 完成: " .. out_dir .. "${clear}") + + -- 产物改名:vpk 的 macOS 原始文件名(实测 1.2.0 为 Mogan--Setup.pkg / + -- -Portable.zip,与 Windows 同约定)从 assets..json 发现——它是顶层数组, + -- 每项 {RelativeFileName, Type};按 Type + 扩展名匹配,不依赖上游文件名猜测。 + local assets_file = path.join (out_dir, "assets." .. channel .. ".json") + if not os.isfile (assets_file) then + cprint ("${bright red}error: 未找到 " .. assets_file .. ",无法定位 vpk 产物文件名${clear}") + for _, f in ipairs (os.files (path.join (out_dir, "*"))) do + cprint (" " .. path.filename (f)) + end + os.exit (1) + end + local json = import ("core.base.json") + local entries = json.decode (io.readfile (assets_file) or "") + if type (entries) ~= "table" then + cprint ("${bright red}error: " .. assets_file .. " 不是合法 JSON 数组${clear}") + os.exit (1) + end + local pkg_old, zip_old + for _, a in ipairs (entries) do + local fn = path.filename (tostring (a.RelativeFileName or "")) + if a.Type == "Installer" and fn:match ("%.pkg$") then pkg_old = fn end + if a.Type == "Portable" and fn:match ("%.zip$") then zip_old = fn end + end + local suffix = "-arm64-" .. channel + local pkg_new = "MoganSTEM-v" .. version .. suffix .. "-Setup.pkg" + local zip_new = "MoganSTEM-v" .. version .. suffix .. "-Portable.zip" + if not pkg_old or not rename_asset (pkg_old, pkg_new) then + cprint ("${bright red}error: 未在 assets 中找到 .pkg 安装器(原名为 " .. + tostring (pkg_old) .. ")${clear}") + os.exit (1) + end + if not zip_old or not rename_asset (zip_old, zip_new) then + cprint ("${bright red}error: 未在 assets 中找到 .zip 便携包(原名为 " .. + tostring (zip_old) .. ")${clear}") + os.exit (1) + end + swap_name_in_assets (assets_file, pkg_old, pkg_new) + swap_name_in_assets (assets_file, zip_old, zip_new) + + postprocess () + return +end + +------------------------------------------------------------------------------- +-- Windows 打包 +------------------------------------------------------------------------------- + local icon = path.absolute (path.join (os.projectdir (), "packages/windows/Xmacs.ico")) --- 先决条件:暂存根与图标必须存在(prune-only 模式跳过,只依赖 outputDir) -if not prune_only then +-- 先决条件:暂存根与图标必须存在 if not os.isdir (pack_dir) then cprint ("${bright red}error: 暂存目录不存在: " .. pack_dir .. "${clear}") cprint ("${yellow}请先运行 xmake l tools/release/stage_velopack.lua${clear}") @@ -150,20 +392,6 @@ cprint ("${green}vpk pack 完成: " .. out_dir .. "${clear}") -- 发布物改名:沿用旧 NSIS 命名 MoganSTEM-v<版本>-64bit-<渠道>-Setup/Portable。 -- vpk 默认名是 --Setup.exe / -Portable.zip;改名后同步更新 -- assets..json 里的 Installer/Portable 引用,避免下载链接失效。 -local function rename_asset (old_name, new_name) - local old_path= path.join (out_dir, old_name) - local new_path= path.join (out_dir, new_name) - if os.isfile (old_path) then - if os.isfile (new_path) then - os.rm (new_path) -- 同名残留先清掉,避免 os.mv 目标已存在失败 - end - os.mv (old_path, new_path) - cprint ("${green}已改名: " .. new_name .. "${clear}") - return true - end - return false -end - local setup_old = "Mogan-" .. channel .. "-Setup.exe" local setup_new = "MoganSTEM-v" .. version .. "-64bit-" .. channel .. "-Setup.exe" local portable_old= "Mogan-" .. channel .. "-Portable.zip" @@ -181,63 +409,8 @@ end -- 同步 assets..json 中的文件名引用 local assets_file= path.join (out_dir, "assets." .. channel .. ".json") if os.isfile (assets_file) then - local content= io.readfile (assets_file) or "" - local function swap_name (old_name, new_name) - -- Lua 模式中 - . 是 magic 字符,先转义再替换 - local escaped= old_name:gsub ("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") - content= content:gsub (escaped, function () return new_name end) - end - swap_name (setup_old, setup_new) - swap_name (portable_old, portable_new) - io.writefile (assets_file, content) -end -end -- if not prune_only - -------------------------------------------------------------------------------- --- 后处理:清理发布目录中的历史 full 包,release 只留当前版本产物。 --- vpk pack 把 outputDir 当作 channel 累积目录:上一版本 full 会留在 outputDir --- 并被写进 releases..json。那份旧 full 只是算 delta 的基线,客户端 --- 走 delta 用的是本地 packages 目录里的旧 full(见联调日志),feed 上的旧 full --- 不会被拉取,纯属冗余(回滚另走 OSS 保留策略)。这里删掉旧 full 文件并在 --- manifest 中去掉对应条目,只保留当前版本 full 与历史 delta。outputDir 仍 --- 保留当前版本 full,作为下一次 pack 的 delta 基线,不受影响。 -------------------------------------------------------------------------------- -local function prune_old_full (out_dir, channel, version) - local json = import ("core.base.json") - local current_full = "Mogan-" .. version .. "-" .. channel .. "-full.nupkg" - - -- 1) 删除旧版本 full 包文件(当前版本 full 保留,作下次 delta 基线) - for _, f in ipairs (os.files (path.join (out_dir, "Mogan-*-" .. channel .. "-full.nupkg"))) do - if path.filename (f) ~= current_full then - os.rm (f) - cprint ("${yellow}已清理历史 full: " .. path.filename (f) .. "${clear}") - end - end - - -- 2) releases..json 去掉旧 full 条目,保留当前 full 与全部 delta。 - -- 注意 xmake lua 沙箱无 pcall / try-catch(见本文件 git 历史),防御靠 - -- 内容结构检查:缺 "Assets" 就跳过,vpk 生成的 manifest 正常情况必然合法。 - local rel_file= path.join (out_dir, "releases." .. channel .. ".json") - if os.isfile (rel_file) then - local content= io.readfile (rel_file) or "" - if content:find ('"Assets"') then - local data= json.decode (content) - if type (data) == "table" and type (data.Assets) == "table" then - local kept= {} - for _, a in ipairs (data.Assets) do - if a.Type ~= "Full" or a.Version == version then - table.insert (kept, a) - end - end - data.Assets= kept - io.writefile (rel_file, json.encode (data)) - cprint ("${green}releases." .. channel .. ".json 已去除历史 full 条目${clear}") - end - else - cprint ("${yellow}warning: " .. path.filename (rel_file) .. " 缺少 Assets 结构,跳过 manifest 清理${clear}") - end - end + swap_name_in_assets (assets_file, setup_old, setup_new) + swap_name_in_assets (assets_file, portable_old, portable_new) end --- 打包完成后做后处理;VPK_PRUNE_ONLY=1 时独立重跑清理 -prune_old_full (out_dir, channel, version) +postprocess () diff --git a/tools/release/stage_velopack.lua b/tools/release/stage_velopack.lua index fe9f90d7d6..f9591eefd0 100644 --- a/tools/release/stage_velopack.lua +++ b/tools/release/stage_velopack.lua @@ -1,7 +1,7 @@ ------------------------------------------------------------------------------- -- -- MODULE : stage_velopack.lua --- DESCRIPTION : 将 stem 安装树装配为 Velopack 扁平暂存根 +-- DESCRIPTION : 装配 Velopack 暂存目录(Windows 扁平暂存根 / macOS .app 副本) -- COPYRIGHT : (C) 2026 Xmacs Labs -- -- This software falls under the GNU general public license version 3 or later. @@ -20,7 +20,7 @@ -- 辅助二进制(pandoc.exe 等)保留在暂存根 bin/ 子目录;vc_redist.x64.exe -- 只作为运行库提取源使用、不随包携带:Qt DLL 为 /MD,依赖 VC++ 14.3 运行库, -- 本脚本从官方 vc_redist.x64.exe 提取 vcruntime140/msvcp140 等 DLL 放进暂存根 --- 做 app-local 部署(见步骤 4),安装期无需联网。该文件由 Qt 部署 +-- 做 app-local 部署(见 Windows 流程步骤 4),安装期无需联网。该文件由 Qt 部署 -- (windeployqt)在 xmake install 时从构建机 VS 的 VC\Redist 目录自动拷入 -- 安装树 bin/(与旧 NSIS 流程一致)。find-binary 与 pandoc 等按 -- $TEXMACS_PATH/bin 查找;.pdb 调试符号不发布。 @@ -35,21 +35,227 @@ -- 实际产物,见 xmake/targets/stem.lua 的 add_installfiles 前缀捕获) -- 判断依据:src/TeXmacs 是否为目录;否则按已知条目清单从 src 根收集。 -- +-- macOS 流程(os.host() == "macosx"):vpk pack 的 --packDir 直接吃 .app bundle, +-- 暂存只需把 release 构建出的 .app 完整复制一份供 vpk 改写/签名(不动构建树): +-- 1) 拷贝用 ditto:Qt framework 内含符号链接(QtCore -> Versions/Current/QtCore), +-- xmake os.cp 默认解引用会悄悄打坏 bundle; +-- 2) build 目录里的 Info.plist 是 qt.widgetapp 占位符(org.example.*),真 plist +-- 由本脚本从 packages/macos/Info.plist.in 生成——不依赖 stem_packager(它 +-- release-only、依赖 create-dmg、还会顺手产 DMG); +-- 3) ad-hoc 重签 staging 副本,保证无签名配置(VPK_SIGN_APP_IDENTITY 为空)时 +-- bundle 也可加载;正式签名交给 vpk(CI 注入环境变量)。 +-- macOS 的 $TEXMACS_PATH 解析是 exedir * "../Resources/share/moganlab" +-- (init_texmacs.cpp macOS 分支),bundle 布局天然满足,无需摊平。 +-- -- 本脚本只做装配与校验,不打包;打包见 pack_velopack.lua。 -- -- 环境变量覆盖: --- VPK_STAGING_SRC 默认 build/packages/stem/data(xmake install stem 产物) +-- VPK_STAGING_SRC 默认 Windows: build/packages/stem/data(xmake install stem 产物) +-- macOS: build/macosx//release/MoganSTEM.app -- VPK_STAGING_OUT 默认 build/velopack_staging ------------------------------------------------------------------------------- +-- 解析版本号:从 xmake/vars.lua 的 XMACS_VERSION 取值(与 pack_velopack.lua 同源) +local function xmacs_version () + local f = io.open (path.join (os.projectdir (), "xmake/vars.lua"), "r") + if f == nil then + cprint ("${bright red}error: 无法读取 xmake/vars.lua${clear}") + os.exit (1) + end + local content = f:read ("*a") + f:close () + local version = content:match ('XMACS_VERSION%s*=%s*"(.-)"') + if version == nil then + cprint ("${bright red}error: xmake/vars.lua 中未找到 XMACS_VERSION${clear}") + os.exit (1) + end + return version +end + +-- 全树扫描:运行期日志/构建中间产物不得发布。逐目录递归而不是 **/* 批量 glob: +-- 实测 xmake 的 **/* 会漏掉深层大文件(如 fonts/opentype/noto 下的 CJK 字体), +-- 逐目录扫描才是可靠全集。返回 {文件列表, 总字节数, 禁发命中列表}。 +local function scan_tree (root) + local all_files = {} + local forbidden = {} + local total_bytes = 0 + local function scan (d) + for _, f in ipairs (os.files (path.join (d, "*"))) do + table.insert (all_files, f) + total_bytes = total_bytes + (os.filesize (f) or 0) + local name = path.filename (f) + if name:match ("%.log$") or name:match ("%.tmp$") or name:match ("%.pdb$") + or name:match ("~$") then + table.insert (forbidden, f) + end + end + for _, sub in ipairs (os.dirs (path.join (d, "*"))) do + -- .git 是版本库元数据目录,绝对不允许进入发布树 + if path.filename (sub) == ".git" then + table.insert (forbidden, sub) + end + scan (sub) + end + end + scan (root) + return all_files, total_bytes, forbidden +end + local src = os.getenv ("VPK_STAGING_SRC") -if src == nil or src == "" then src = "build/packages/stem/data" end local out = os.getenv ("VPK_STAGING_OUT") if out == nil or out == "" then out = "build/velopack_staging" end +local out_dir = path.absolute (path.join (os.projectdir (), out)) + +------------------------------------------------------------------------------- +-- macOS 流程 +------------------------------------------------------------------------------- +if os.host () == "macosx" then + -- 1) 定位源 .app:默认 release 构建产物(arch 唯一,多个即歧义硬失败) + local app + if src ~= nil and src ~= "" then + app = path.absolute (path.join (os.projectdir (), src)) + if not os.isdir (app) then + cprint ("${bright red}error: 源 .app 不存在: " .. app .. "${clear}") + os.exit (1) + end + else + local found = os.dirs (path.join (os.projectdir (), "build/macosx/*/release/MoganSTEM.app")) + if #found == 0 then + cprint ("${bright red}error: 未找到 release 构建的 MoganSTEM.app${clear}") + cprint ("${yellow}请先执行 xmake f -m release && xmake b stem && xmake i stem${clear}") + os.exit (1) + elseif #found > 1 then + cprint ("${bright red}error: 多个架构的 MoganSTEM.app,请用 VPK_STAGING_SRC 指定其一:${clear}") + for _, a in ipairs (found) do cprint (" " .. a) end + os.exit (1) + end + app = found[1] + end + + -- 2) 清空重建暂存根:保证可重复执行,且不含上次残留 + if os.exists (out_dir) then os.rm (out_dir) end + os.mkdir (out_dir) + local dst_app = path.join (out_dir, "MoganSTEM.app") + + -- 3) ditto 拷贝:保留 Qt framework 符号链接与签名元数据(os.cp 会解引用打坏) + local code = os.execv ("ditto", {app, dst_app}, {try = true}) + if code ~= 0 then + cprint ("${bright red}error: ditto 拷贝失败,退出码 " .. code .. "${clear}") + os.exit (1) + end + + -- 4) 生成真 Info.plist(覆盖 qt.widgetapp 占位符):与 stem_packager 同一模板, + -- 同样的 @(.-)@ 替换语义,占位符残留即失败 + local version = xmacs_version () + local plist_in = path.join (os.projectdir (), "packages/macos/Info.plist.in") + local content = io.readfile (plist_in) + if content == nil then + cprint ("${bright red}error: 无法读取 " .. plist_in .. "${clear}") + os.exit (1) + end + local vars = {STEM_NAME = "MoganSTEM", XMACS_VERSION = version, OSXVERMIN = ""} + content = content:gsub ("@([%u_]+)@", function (k) + return assert (vars[k], "Info.plist.in 出现未知占位符 @" .. k .. "@") + end) + io.writefile (path.join (dst_app, "Contents", "Info.plist"), content) + + -- 5) 结构校验(硬失败):主程序、plist 三键、扁平数据、velopack dylib、 + -- 无重复主程序残留 + local ok = true + local function need (cond, msg) + if not cond then + cprint ("${bright red}error: " .. msg .. "${clear}") + ok = false + end + end + need (os.isfile (path.join (dst_app, "Contents/MacOS/MoganSTEM")), + "缺 Contents/MacOS/MoganSTEM(是否执行过 xmake b stem?)") + need (content:find ("app.mogan", 1, true) ~= nil, "Info.plist 缺 CFBundleIdentifier app.mogan") + need (content:find ("MoganSTEM", 1, true) ~= nil, + "Info.plist 缺 CFBundleExecutable MoganSTEM") + need (content:find (version, 1, true) ~= nil, "Info.plist 缺版本号 " .. version) + for _, sub in ipairs ({"progs", "fonts", "doc", "plugins"}) do + need (os.isdir (path.join (dst_app, "Contents/Resources/share/moganlab", sub)), + "缺 Contents/Resources/share/moganlab/" .. sub .. "(是否执行过 xmake i stem?)") + end + need (os.isfile (path.join (dst_app, "Contents/Frameworks/libvelopack_libc.dylib")), + "缺 Contents/Frameworks/libvelopack_libc.dylib(velopack_libc shared 目标应随构建部署)") + if os.isfile (path.join (dst_app, "Contents/Resources/lib/libvelopack_libc.dylib")) then + cprint ("${bright red}error: dylib 错位出现在 Contents/Resources/lib/(应在 Frameworks)," .. + "stem 的 after_install 应已清理${clear}") + ok = false + end + if os.isfile (path.join (dst_app, "Contents/Resources/bin/MoganSTEM")) then + cprint ("${bright red}error: Resources/bin/MoganSTEM 重复主程序未清除${clear}") + ok = false + end + if os.isdir (path.join (dst_app, "Contents/Resources/share/moganlab/tests")) then + cprint ("${yellow}warn: 含 tests/ 回归样例目录,请确认是否应随包发布${clear}") + end + if not ok then os.exit (1) end + + -- 6) 签名。正式身份(VPK_SIGN_APP_IDENTITY)时只预签 vpk --deep 覆盖不到的 + -- Contents/Resources 裸 Mach-O(helper 可执行文件/dylib,如 goldfish): + -- codesign --deep 只遍历 bundle/framework/PlugIns 这类结构,Resources 里 + -- 的裸二进制对它不可见,未签名会被公证判 Invalid;bundle 本体交给 vpk + -- 深签。.dSYM 里的 DWARF 虽是 Mach-O,按旧 DMG 流程先例不签(可过公证)。 + -- 无正式身份时 ad-hoc 签整个 bundle,保证本地验证可加载。 + local sign_identity = os.getenv ("VPK_SIGN_APP_IDENTITY") or "" + if sign_identity ~= "" then + local macho = {} + local function collect (d) + for _, sub in ipairs (os.dirs (path.join (d, "*"))) do + if not path.filename (sub):match ("%.dSYM$") then collect (sub) end + end + for _, f in ipairs (os.files (path.join (d, "*"))) do + local out = os.iorunv ("/usr/bin/file", {"-b", f}) + if out and tostring (out):match ("^Mach%-O") then + table.insert (macho, f) + end + end + end + collect (path.join (dst_app, "Contents", "Resources")) + for _, f in ipairs (macho) do + cprint ("预签 Resources 裸 Mach-O: " .. path.relative (f, dst_app)) + code = os.execv ("codesign", + {"--force", "--options", "runtime", "--timestamp", + "--sign", sign_identity, f}, {try = true}) + if code ~= 0 then + cprint ("${bright red}error: 预签失败,退出码 " .. code .. ": " .. f .. "${clear}") + os.exit (1) + end + end + cprint ("${green}已预签 " .. #macho .. " 个 Resources 裸 Mach-O(bundle 交给 vpk 深签)${clear}") + else + code = os.execv ("codesign", {"--force", "--deep", "--sign", "-", dst_app}, {try = true}) + if code ~= 0 then + cprint ("${bright red}error: ad-hoc 签名失败,退出码 " .. code .. "${clear}") + os.exit (1) + end + end + + -- 7) 禁发文件扫描 + 汇总 + local all_files, total_bytes, forbidden = scan_tree (dst_app) + if #forbidden > 0 then + for _, f in ipairs (forbidden) do + cprint ("${bright red}error: 含禁止发布内容: " .. f .. "${clear}") + end + os.exit (1) + end + cprint ("${green}staging 完成: " .. dst_app .. "${clear}") + cprint (" 文件数: " .. #all_files) + cprint (string.format (" 总大小: %.1f MB", total_bytes / 1048576)) + os.exit (0) +end + +------------------------------------------------------------------------------- +-- Windows 流程 +------------------------------------------------------------------------------- + +if src == nil or src == "" then src = "build/packages/stem/data" end -- xmake l 的工作目录即项目根,但显式求绝对路径更稳妥 local src_dir = path.absolute (path.join (os.projectdir (), src)) -local out_dir = path.absolute (path.join (os.projectdir (), out)) -- 源安装树必须存在(xmake install stem 的产物),否则无从装配 if not os.isdir (src_dir) then @@ -228,27 +434,7 @@ end -- 出现即说明拷贝逻辑有漏洞。用逐目录递归而不是 **/* 批量 glob:实测 xmake 的 -- **/* 会漏掉深层大文件(如 fonts/opentype/noto 下的 CJK 字体),逐目录扫描才是 -- 可靠全集。 -local all_files = {} -local forbidden = {} -local total_bytes = 0 -local function scan (d) - for _, f in ipairs (os.files (path.join (d, "*"))) do - table.insert (all_files, f) - total_bytes = total_bytes + (os.filesize (f) or 0) - local name = path.filename (f) - if name:match ("%.log$") or name:match ("%.tmp$") or name:match ("%.pdb$") or name:match ("~$") then - table.insert (forbidden, f) - end - end - for _, sub in ipairs (os.dirs (path.join (d, "*"))) do - -- .git 是版本库元数据目录,绝对不允许进入安装树 - if path.filename (sub) == ".git" then - table.insert (forbidden, sub) - end - scan (sub) - end -end -scan (out_dir) +local all_files, total_bytes, forbidden = scan_tree (out_dir) if #forbidden > 0 then for _, f in ipairs (forbidden) do cprint ("${bright red}error: 含禁止发布内容: " .. f .. "${clear}") diff --git a/tools/velopack/velopack_startup_test.cpp b/tools/velopack/velopack_startup_test.cpp index 60a95b4947..d38560d025 100644 --- a/tools/velopack/velopack_startup_test.cpp +++ b/tools/velopack/velopack_startup_test.cpp @@ -2,13 +2,15 @@ #include #include -// 仅验证 Velopack C++ runtime 启动钩子可编译/链接;未安装环境下 Run() 为空操作。 -int main () { -#if defined (_WIN32) && defined (_M_X64) +// 仅验证 Velopack C++ runtime 启动钩子可编译/链接;未安装环境下 Run() +// 为空操作。 +int +main () { +#if (defined(_WIN32) && defined(_M_X64)) || defined(__APPLE__) try { Velopack::VelopackApp::Build ().Run (); std::cout << "velopack startup hook: ok\n"; - } catch (const std::exception &e) { + } catch (const std::exception& e) { std::cerr << "velopack startup hook failed: " << e.what () << "\n"; return 1; } diff --git a/xmake/targets/libmogan.lua b/xmake/targets/libmogan.lua index 661c32c260..366a2020b0 100644 --- a/xmake/targets/libmogan.lua +++ b/xmake/targets/libmogan.lua @@ -21,7 +21,7 @@ target("libmogan") do set_runtimes("MT") add_defines("_USE_MATH_DEFINES") end - if is_plat("windows") and is_arch("x64") then + if (is_plat("windows") and is_arch("x64")) or (is_plat("macosx") and is_arch("arm64")) then -- Velopack C++ runtime:头文件路径供后续 tm_velopack 使用;链接项对静态库仅 -- 在最终 exe 链接时生效,stem 已自带,此处双保险。 add_velopack_runtime () @@ -159,7 +159,8 @@ target("libmogan") do USE_PLUGIN_TEX = true, USE_PLUGIN_ISPELL = true, USE_PLUGIN_PDF = has_config("pdfhummus"), - USE_PLUGIN_VELOPACK = is_plat("windows") and is_arch("x64"), + USE_PLUGIN_VELOPACK = (is_plat("windows") and is_arch("x64")) + or (is_plat("macosx") and is_arch("arm64")), USE_PLUGIN_HTML = true, USE_MUPDF_RENDERER = has_config("mupdf"), USE_STARTUP_TAB = has_config("startup_tab"), diff --git a/xmake/targets/stem.lua b/xmake/targets/stem.lua index d3616322b9..05753749ec 100644 --- a/xmake/targets/stem.lua +++ b/xmake/targets/stem.lua @@ -183,12 +183,16 @@ target("stem") do add_includedirs("$(builddir)", {public = true}) add_files("$(projectdir)/src/Mogan/Research/research.cpp") - -- Velopack C++ runtime:启动钩子编译/链接 + 动态库随 bin/ 发布 - if is_plat("windows") and is_arch("x64") then + -- Velopack C++ runtime:启动钩子编译/链接 + 动态库随包发布 + if (is_plat("windows") and is_arch("x64")) or (is_plat("macosx") and is_arch("arm64")) then add_velopack_runtime () - -- 导入库内嵌 DLL 名为 velopack_libc.dll,发布时改名,exe 才能加载 - add_installfiles ("$(projectdir)/3rdparty/velopack/lib/velopack_libc_win_x64_msvc.dll", - {prefixdir = "bin", filename = "velopack_libc.dll"}) + if is_plat("windows") then + -- 导入库内嵌 DLL 名为 velopack_libc.dll,发布时改名,exe 才能加载 + add_installfiles ("$(projectdir)/3rdparty/velopack/lib/velopack_libc_win_x64_msvc.dll", + {prefixdir = "bin", filename = "velopack_libc.dll"}) + end + -- macOS 的 dylib 部署见下方 after_build:构建目录的 .app 即 dev 运行/CI 测试/ + -- 打包共用物,须进 Contents/Frameworks,而非安装树 end -- install tm files for testing purpose @@ -364,6 +368,30 @@ target("stem") do os.rm(duplicate_binary) print("Removed duplicate app binary: " .. duplicate_binary) end + if is_arch("arm64") then + -- xmake 安装 stem 时会把 shared 依赖(velopack_libc)的产物额外装进 + -- installdir/lib(即 Contents/Resources/lib),dep 上的空 on_install + -- 拦不住。该 dylib 的正式位置是 Contents/Frameworks(构建期由 qt + -- 部署规则落位),Resources/lib 下的副本会让公证失败(vpk 的 + -- --deep 签不到 Resources 里的裸 dylib)。 + local stray = path.join(target:installdir(), "lib", "libvelopack_libc.dylib") + if os.isfile(stray) then + os.rm(stray) + os.rmdir(path.join(target:installdir(), "lib")) + print("Removed stray velopack dylib: " .. stray) + end + end + -- 幂等保险:只 install 不 rebuild 的场景下补拷 Velopack dylib + -- (正常路径由构建期 velopack_libc shared 依赖经 qt 部署规则落位) + if is_arch("arm64") and has_config("qt_frontend") then + local frameworks = path.join(target:installdir(), "..", "Frameworks") + if os.isdir(frameworks) and + not os.isfile(path.join(frameworks, "libvelopack_libc.dylib")) then + os.cp(path.join(os.projectdir(), "3rdparty/velopack/lib/libvelopack_libc.dylib"), + path.join(frameworks, "libvelopack_libc.dylib")) + print("Deployed libvelopack_libc.dylib (after_install)") + end + end end end) end diff --git a/xmake/vars.lua b/xmake/vars.lua index 5226a9811b..ee086445fc 100644 --- a/xmake/vars.lua +++ b/xmake/vars.lua @@ -9,7 +9,7 @@ -- It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE -- in the root directory or . -XMACS_VERSION="2026.3.2" +XMACS_VERSION="2026.4.1" TEXMACS_VERSION = "2.1.2" GOLDFISH_VERSION = "17.11.2" DEVEL_VERSION = TEXMACS_VERSION diff --git a/xmake/velopack.lua b/xmake/velopack.lua index ff08d48181..f3f26444a3 100644 --- a/xmake/velopack.lua +++ b/xmake/velopack.lua @@ -1,33 +1,79 @@ --- Velopack C/C++ runtime(Windows x64)集成。 +-- Velopack C/C++ runtime(Windows x64 / macOS arm64)集成。 -- add_velopack_runtime () 仅供 target("stem") / target("libmogan") 块内调用: -- xmake 的 target 作用域 API(add_includedirs/add_links/...)绑定当前活动 target, -- 普通 Lua 函数在 target 块内调用即可生效。 function add_velopack_runtime () - if not (is_plat ("windows") and is_arch ("x64")) then return end - add_includedirs ("$(projectdir)/3rdparty/velopack/include") - add_linkdirs ("$(projectdir)/3rdparty/velopack/lib") - -- 链接导入库:文件名形如 velopack_libc_win_x64_msvc.dll.lib - add_links ("velopack_libc_win_x64_msvc.dll") + if is_plat ("windows") and is_arch ("x64") then + add_includedirs ("$(projectdir)/3rdparty/velopack/include") + add_linkdirs ("$(projectdir)/3rdparty/velopack/lib") + -- 链接导入库:文件名形如 velopack_libc_win_x64_msvc.dll.lib + add_links ("velopack_libc_win_x64_msvc.dll") + elseif is_plat ("macosx") and is_arch ("arm64") then + add_includedirs ("$(projectdir)/3rdparty/velopack/include") + add_linkdirs ("$(projectdir)/3rdparty/velopack/lib") + -- dylib 的 install id 已在 vendor 时写为 @rpath/libvelopack_libc.dylib + -- (与磁盘文件名一致),链接后 exe/bundle 自带的 + -- @executable_path/Frameworks rpath 即可解析。 + -- 无源码的 shared 依赖不产生链接项,链接由上面的 add_links 显式完成; + -- add_deps 只是让 qt.widgetapp 部署规则把 dylib 拷进 .app 的 + -- Contents/Frameworks(该机制按 shared 依赖的 targetfile 落位)。 + add_links ("velopack_libc") + add_deps ("velopack_libc") + end +end + +-- macOS:把 vendored dylib 包成 shared target。qt.widgetapp 的部署规则 +-- (xmake rules/qt/deploy/macosx.lua)会把 shared 依赖的 targetfile 自动拷进 +-- .app 的 Contents/Frameworks,且时机在 bundle 重建之后——自写 after_build 拷贝 +-- 会被规则的 os.tryrm 整包清掉。targetdir 定到 /Frameworks, +-- 裸二进制(dev 运行 / CI scheme 测试用 target:targetfile())靠自带的 +-- @executable_path/Frameworks rpath 找到它,.app 由部署规则落位,一处受益两处。 +if is_plat ("macosx") and is_arch ("arm64") then + target ("velopack_libc") do + set_kind ("shared") + set_group ("velopack") + -- 与 stem 同层(build/macosx/$(arch)/$(mode))下的 Frameworks/ + set_targetdir (path.join ("$(builddir)", "macosx", "$(arch)", "$(mode)", "Frameworks")) + -- 预编译产物,无源码:on_build 即“编译”,dylib 的 install id 已在 + -- vendor 时写为 @rpath/velopack_libc.dylib(见 3rdparty/velopack/README.md) + on_build (function (target) + os.cp (path.join (os.projectdir (), "3rdparty/velopack/lib/libvelopack_libc.dylib"), + target:targetfile ()) + end) + on_install (function (target) end) -- 部署走 qt 规则(Frameworks),不做独立安装 + end end -- 最小验证程序:仅验证 Velopack C++ 启动钩子可编译/链接,未安装环境下 Run() 为空操作。 -- 不放在 tests/ 下:根 xmake.lua 会自动发现 tests/**_test.cpp 并链接 libmogan/libmoebius, -- 会与此处目标重名冲突,且该验证程序不应依赖项目库。 -if is_plat ("windows") and is_arch ("x64") then +if (is_plat ("windows") and is_arch ("x64")) or + (is_plat ("macosx") and is_arch ("arm64")) then target ("velopack_startup_test") do set_kind ("binary") set_group ("velopack") set_languages ("c++17") - set_runtimes ("MT") + if is_plat ("windows") then + set_runtimes ("MT") + end add_velopack_runtime () add_files ("$(projectdir)/tools/velopack/velopack_startup_test.cpp") - -- 运行时需在 exe 旁找到 DLL。导入库内嵌的 DLL 名为 velopack_libc.dll, - -- 故发布时须把 vendored 的 velopack_libc_win_x64_msvc.dll 改名为 velopack_libc.dll。 - -- (after_build 不替代默认编译/链接) - after_build (function (target) - os.cp (path.join (os.projectdir (), "3rdparty/velopack/lib/velopack_libc_win_x64_msvc.dll"), - path.join (target:targetdir (), "velopack_libc.dll")) - end) + if is_plat ("windows") then + -- 运行时需在 exe 旁找到 DLL。导入库内嵌的 DLL 名为 velopack_libc.dll, + -- 故发布时须把 vendored 的 velopack_libc_win_x64_msvc.dll 改名为 velopack_libc.dll。 + -- (after_build 不替代默认编译/链接) + after_build (function (target) + os.cp (path.join (os.projectdir (), "3rdparty/velopack/lib/velopack_libc_win_x64_msvc.dll"), + path.join (target:targetdir (), "velopack_libc.dll")) + end) + else + -- 裸二进制无 bundle,dylib 放 exe 旁并指 @loader_path 搜索 + add_rpathdirs ("@loader_path") + after_build (function (target) + os.cp (path.join (os.projectdir (), "3rdparty/velopack/lib/libvelopack_libc.dylib"), + path.join (target:targetdir (), "libvelopack_libc.dylib")) + end) + end end end