diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2583ee..c2a45e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ # A release: push a tag `v` matching Cargo.toml's version. # # verify the tag, the CHANGELOG section, the update keys -# macos universal app: signed, notarized, stapled; update zip and DMG +# macos per-architecture apps: signed, notarized, stapled; update zip and DMG # windows per-user Inno Setup installer (unsigned for now) # linux AppImage # publish (environment `release`, approved by hand) uploads to R2 behind @@ -83,8 +83,13 @@ jobs: macos: needs: verify runs-on: macos-14 + strategy: + fail-fast: false + matrix: + arch: [aarch64, x86_64] timeout-minutes: 120 env: + ARCH: ${{ matrix.arch }} VERSION: ${{ needs.verify.outputs.version }} SHELLRS_UPDATE_CHANNEL: ${{ needs.verify.outputs.channel }} SHELLRS_COMMIT: ${{ github.sha }} @@ -92,18 +97,17 @@ jobs: steps: - uses: actions/checkout@v4 - run: rustup toolchain install - - run: rustup target add aarch64-apple-darwin x86_64-apple-darwin + - run: rustup target add "$ARCH-apple-darwin" - uses: Swatinem/rust-cache@v2 - - name: Build both halves and join them + with: + key: macos-${{ matrix.arch }} + - name: Build the architecture-specific app run: | export SHELLRS_BUILD_DATE=$(date -u +%Y-%m-%d) - cargo build --release --locked --bin shellrs --target aarch64-apple-darwin - cargo build --release --locked --bin shellrs --target x86_64-apple-darwin + cargo build --release --locked --bin shellrs --target "$ARCH-apple-darwin" mkdir -p dist build - lipo -create -output build/shellrs \ - target/aarch64-apple-darwin/release/shellrs \ - target/x86_64-apple-darwin/release/shellrs - packaging/macos/bundle.sh build/shellrs "$VERSION" "$GITHUB_RUN_NUMBER" build + lipo -verify_arch "${ARCH/aarch64/arm64}" "target/$ARCH-apple-darwin/release/shellrs" + packaging/macos/bundle.sh "target/$ARCH-apple-darwin/release/shellrs" "$VERSION" "$GITHUB_RUN_NUMBER" build - name: Sign env: CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -147,12 +151,12 @@ jobs: xcrun stapler staple build/ShellRS.app # The update package: the stapled bundle, zipped the way the # updater unzips it (ditto keeps the signature intact). - ditto -c -k --keepParent build/ShellRS.app "dist/ShellRS-$VERSION-macos-universal.app.zip" + ditto -c -k --keepParent build/ShellRS.app "dist/ShellRS-$VERSION-macos-$ARCH.app.zip" # The first-install disk image, with the Applications shortcut. mkdir -p "$RUNNER_TEMP/dmg" cp -R build/ShellRS.app "$RUNNER_TEMP/dmg/" ln -s /Applications "$RUNNER_TEMP/dmg/Applications" - dmg="dist/ShellRS-$VERSION-macos-universal.dmg" + dmg="dist/ShellRS-$VERSION-macos-$ARCH.dmg" # hdiutil's own estimate of the image size once came out too small # for the app ("No space left on device"), so the size is given, # with room to spare; the compressed image is about half the app. @@ -171,7 +175,7 @@ jobs: spctl -a -vv -t install "$dmg" - uses: actions/upload-artifact@v4 with: - name: macos + name: macos-${{ matrix.arch }} path: dist/* windows: @@ -265,7 +269,7 @@ jobs: - name: Collect run: | mkdir -p dist - cp artifacts/macos/* artifacts/windows/* artifacts/linux/* dist/ + cp artifacts/macos-aarch64/* artifacts/macos-x86_64/* artifacts/windows/* artifacts/linux/* dist/ cp artifacts/notes/notes.md notes.md ls -l dist - name: Signing key diff --git a/README.md b/README.md index 75563ba..8325817 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,8 @@ Download from [shellrs.com/download](https://shellrs.com/download), which offers | System | Package | Notes | | --- | --- | --- | -| macOS 11+ (Apple Silicon and Intel) | `ShellRS--macos-universal.dmg` | Signed and notarized. Open the DMG and drag ShellRS into Applications | +| macOS 11+ (Apple Silicon) | `ShellRS--macos-aarch64.dmg` | Signed and notarized. Open the DMG and drag ShellRS into Applications | +| macOS 11+ (Intel) | `ShellRS--macos-x86_64.dmg` | Signed and notarized. Open the DMG and drag ShellRS into Applications | | Windows x64 | `ShellRS--windows-x86_64-setup.exe` | Installs per user, no administrator rights needed. The installer is not signed yet; if SmartScreen stops it, choose "More info › Run anyway" | | Linux x64 | `ShellRS--linux-x86_64.AppImage` | `chmod +x` and run. Needs glibc 2.35 or newer (Ubuntu 22.04, Debian 12 and later) | diff --git a/README.zh-CN.md b/README.zh-CN.md index 7f4938b..1befcd8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -119,7 +119,8 @@ ShellRS 把 Xshell 式的主机管理和多标签终端、WinSCP 式的双栏 SF | 系统 | 安装包 | 说明 | | --- | --- | --- | -| macOS 11+(Apple Silicon 和 Intel) | `ShellRS-<版本>-macos-universal.dmg` | 已签名并经过 Apple 公证。打开 DMG,把 ShellRS 拖进「应用程序」 | +| macOS 11+(Apple Silicon) | `ShellRS-<版本>-macos-aarch64.dmg` | 已签名并经过 Apple 公证。打开 DMG,把 ShellRS 拖进「应用程序」 | +| macOS 11+(Intel) | `ShellRS-<版本>-macos-x86_64.dmg` | 已签名并经过 Apple 公证。打开 DMG,把 ShellRS 拖进「应用程序」 | | Windows x64 | `ShellRS-<版本>-windows-x86_64-setup.exe` | 按当前用户安装,不需要管理员权限。安装程序暂未签名,SmartScreen 拦下时点「更多信息 › 仍要运行」 | | Linux x64 | `ShellRS-<版本>-linux-x86_64.AppImage` | `chmod +x` 后直接运行,需要 glibc 2.35 以上(Ubuntu 22.04、Debian 12 及更新的版本) | diff --git a/docs/release.md b/docs/release.md index 4ea6f3a..fa38f54 100644 --- a/docs/release.md +++ b/docs/release.md @@ -5,7 +5,7 @@ 在 `CHANGELOG.md` 里写好 `## [x.y.z]` 一节,把 `Cargo.toml` 的版本改成 `x.y.z`,提交后推送 tag `vx.y.z`。`.github/workflows/release.yml` 依次: 1. 核对 tag 与 `Cargo.toml` 一致,截出更新说明,确认代码里有发布公钥; -2. 构建并打包:macOS 合成 universal,签名、公证、staple,产出升级用的 `.app.zip` 和首次安装用的 `.dmg`;Windows 用 Inno Setup 打安装程序;Linux 打 AppImage; +2. 构建并打包:macOS 按 `aarch64`(Apple Silicon)和 `x86_64`(Intel)分别构建、签名、公证、staple,各自产出升级用的 `.app.zip` 和首次安装用的 `.dmg`;Windows 用 Inno Setup 打安装程序;Linux 打 AppImage; 3. 在 `release` 环境里人工批准后:给每个包签名,上传到 R2(`dl.shellrs.com/releases/<版本>/`),建 GitHub Release,**最后**才替换更新清单 `dl.shellrs.com/update/v1/<通道>.json` 并清 CDN 缓存,然后调用官网的部署挂钩重新构建 shellrs.com(官网的下载页和更新日志在构建时读这份清单)。清单里每个包有两个下载地址:先 R2,下载失败时客户端接着试 GitHub Release(仓库是公开的,下载不用登录)。 版本号带 `-` 的(如 `0.3.0-beta.1`)只发到 beta 通道;正式版同时成为 beta 通道的最新版。在 Actions 里手动运行这个 workflow 只构建打包、不发布,用来演练。客户端只接受比自己新的版本,所以发错的版本撤不回来:把清单改回上一版能阻止更多人升级,修复要发新的补丁版。 @@ -19,3 +19,5 @@ - **GitHub secrets**:仓库级的 `APPLE_CERTIFICATE_P12`(base64)、`APPLE_CERTIFICATE_PASSWORD`、`APPLE_SIGNING_IDENTITY`、`APPLE_TEAM_ID`、`APPLE_API_KEY_P8`(base64)、`APPLE_API_KEY_ID`、`APPLE_API_ISSUER`;`release` 环境(设为需要审批)里的 `MINISIGN_SECRET_KEY`、`MINISIGN_PASSWORD`、`R2_ACCOUNT_ID`、`R2_ACCESS_KEY_ID`、`R2_SECRET_ACCESS_KEY`、`R2_BUCKET`、`CF_ZONE_ID`、`CF_API_TOKEN`、`SITE_DEPLOY_HOOK`(官网部署挂钩的地址,拿到它的人能触发官网重新构建,泄露了就删掉重建)。 清单格式见 `packaging/manifest.example.json`,由 `packaging/make-manifest.sh` 生成:一个文件里是清单原文和它的 minisign 签名,签名的 trusted comment 写明通道和版本。格式只增加字段、不改已有的;已发出去的 ShellRS 一直按 `/update/v1/` 这个地址检查。 + +macOS 产物名分别为 `ShellRS-<版本>-macos-aarch64.{app.zip,dmg}` 和 `ShellRS-<版本>-macos-x86_64.{app.zip,dmg}`。更新清单保留原有的两个平台键,分别记录对应架构包的地址、大小和 SHA-256;已有 universal 客户端按当前运行的架构升级。Apple Silicon 上通过 Rosetta 运行 Intel 版时仍会收到 Intel 包,需要手动安装 ARM 版才能切换。官网在独立仓库中,下载页也需提供 Apple Silicon / Intel 两个入口,读取清单中各自的 installer 地址。 diff --git a/packaging/make-manifest.sh b/packaging/make-manifest.sh index cbc84ff..9efdba7 100755 --- a/packaging/make-manifest.sh +++ b/packaging/make-manifest.sh @@ -23,8 +23,10 @@ out=$5 base="https://dl.shellrs.com/releases/$version" mirror=${SHELLRS_MIRROR_BASE:-} -mac="ShellRS-$version-macos-universal.app.zip" -dmg="ShellRS-$version-macos-universal.dmg" +mac_arm="ShellRS-$version-macos-aarch64.app.zip" +mac_intel="ShellRS-$version-macos-x86_64.app.zip" +dmg_arm="ShellRS-$version-macos-aarch64.dmg" +dmg_intel="ShellRS-$version-macos-x86_64.dmg" win="ShellRS-$version-windows-x86_64-setup.exe" linux="ShellRS-$version-linux-x86_64.AppImage" @@ -58,10 +60,12 @@ jq -n -j \ --arg published "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --rawfile notes "$notes" \ --arg notes_url "https://shellrs.com/changelog#$version" \ - --argjson mac "$(asset "$mac")" \ + --argjson mac_arm "$(asset "$mac_arm")" \ + --argjson mac_intel "$(asset "$mac_intel")" \ --argjson win "$(asset "$win")" \ --argjson linux "$(asset "$linux")" \ - --arg dmg "$base/$dmg" \ + --arg dmg_arm "$base/$dmg_arm" \ + --arg dmg_intel "$base/$dmg_intel" \ --arg win_url "$base/$win" \ --arg linux_url "$base/$linux" \ '{ @@ -74,14 +78,14 @@ jq -n -j \ notes: $notes, notes_url: $notes_url, assets: { - "macos-aarch64": $mac, - "macos-x86_64": $mac, + "macos-aarch64": $mac_arm, + "macos-x86_64": $mac_intel, "windows-x86_64": $win, "linux-x86_64": $linux }, installers: { - "macos-aarch64": $dmg, - "macos-x86_64": $dmg, + "macos-aarch64": $dmg_arm, + "macos-x86_64": $dmg_intel, "windows-x86_64": $win_url, "linux-x86_64": $linux_url } diff --git a/packaging/manifest.example.json b/packaging/manifest.example.json index ace8e68..b98b45a 100644 --- a/packaging/manifest.example.json +++ b/packaging/manifest.example.json @@ -10,16 +10,16 @@ "assets": { "macos-aarch64": { "urls": [ - "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-macos-universal.app.zip", - "https://github.com/since2006/shell-rs/releases/download/v0.2.0/ShellRS-0.2.0-macos-universal.app.zip" + "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-macos-aarch64.app.zip", + "https://github.com/since2006/shell-rs/releases/download/v0.2.0/ShellRS-0.2.0-macos-aarch64.app.zip" ], "size": 41234567, "sha256": "0000000000000000000000000000000000000000000000000000000000000000" }, "macos-x86_64": { "urls": [ - "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-macos-universal.app.zip", - "https://github.com/since2006/shell-rs/releases/download/v0.2.0/ShellRS-0.2.0-macos-universal.app.zip" + "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-macos-x86_64.app.zip", + "https://github.com/since2006/shell-rs/releases/download/v0.2.0/ShellRS-0.2.0-macos-x86_64.app.zip" ], "size": 41234567, "sha256": "0000000000000000000000000000000000000000000000000000000000000000" @@ -42,8 +42,8 @@ } }, "installers": { - "macos-aarch64": "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-macos-universal.dmg", - "macos-x86_64": "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-macos-universal.dmg", + "macos-aarch64": "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-macos-aarch64.dmg", + "macos-x86_64": "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-macos-x86_64.dmg", "windows-x86_64": "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-windows-x86_64-setup.exe", "linux-x86_64": "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-linux-x86_64.AppImage" } diff --git a/src/update/platform.rs b/src/update/platform.rs index 75cef27..a27f18c 100644 --- a/src/update/platform.rs +++ b/src/update/platform.rs @@ -1,10 +1,9 @@ //! Which package of a release fits this machine. //! -//! The keys are part of the manifest format, so they never change. A -//! universal macOS build runs its native half, so an Apple Silicon Mac asks -//! for `macos-aarch64` even though both keys point at the same package; an -//! Intel-only build under Rosetta would ask for `macos-x86_64` and keep -//! getting Intel builds, which is why macOS ships universal. +//! The keys are part of the manifest format, so they never change. macOS +//! packages match the running binary's architecture. Existing universal +//! builds select their native half's package; an Intel build under Rosetta +//! keeps receiving Intel packages until the user installs the ARM build. /// The manifest's key for this build's platform. pub fn platform_key() -> &'static str { diff --git a/src/update/verify.rs b/src/update/verify.rs index ede7452..c328332 100644 --- a/src/update/verify.rs +++ b/src/update/verify.rs @@ -319,7 +319,8 @@ pub(crate) mod tests { let dist = dir.path().join("dist"); std::fs::create_dir(&dist).unwrap(); for name in [ - "ShellRS-0.2.0-macos-universal.app.zip", + "ShellRS-0.2.0-macos-aarch64.app.zip", + "ShellRS-0.2.0-macos-x86_64.app.zip", "ShellRS-0.2.0-windows-x86_64-setup.exe", "ShellRS-0.2.0-linux-x86_64.AppImage", ] { @@ -355,15 +356,36 @@ pub(crate) mod tests { }; assert_eq!(manifest.version, Version::new(0, 2, 0)); assert_eq!(manifest.notes, "### 新增\n\n- 在线升级\n"); - let mac = &manifest.assets["macos-aarch64"]; - assert_eq!(mac, &manifest.assets["macos-x86_64"]); - // dl.shellrs.com first, the GitHub release only when it fails. + for arch in ["aarch64", "x86_64"] { + let key = format!("macos-{arch}"); + let name = format!("ShellRS-0.2.0-{key}.app.zip"); + let asset = &manifest.assets[&key]; + // Each architecture has its own package and both download sources. + assert_eq!( + asset.urls, + [ + format!("https://dl.shellrs.com/releases/0.2.0/{name}"), + format!( + "https://github.com/since2006/shell-rs/releases/download/v0.2.0/{name}" + ), + ] + ); + assert_eq!(verify_file(&dist.join(&name), asset), Ok(())); + assert_eq!( + manifest.installers[&key], + format!("https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-{key}.dmg") + ); + } + assert_ne!( + manifest.assets["macos-aarch64"].sha256, + manifest.assets["macos-x86_64"].sha256 + ); assert_eq!( - mac.urls, - [ - "https://dl.shellrs.com/releases/0.2.0/ShellRS-0.2.0-macos-universal.app.zip", - "https://github.com/since2006/shell-rs/releases/download/v0.2.0/ShellRS-0.2.0-macos-universal.app.zip", - ] + verify_file( + &dist.join("ShellRS-0.2.0-macos-x86_64.app.zip"), + &manifest.assets["macos-aarch64"], + ), + Err(UpdateError::Corrupt) ); let package = dist.join("ShellRS-0.2.0-linux-x86_64.AppImage"); assert_eq!(