mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-01 15:43:45 +00:00
build: add signed and notarized DMG packaging
This commit is contained in:
@@ -77,6 +77,7 @@ IDEWorkspaceChecks.plist
|
||||
####
|
||||
# New project build artifacts
|
||||
ShadowsocksX-NG2/.build/
|
||||
ShadowsocksX-NG2/dist/
|
||||
ShadowsocksX-NG2/DerivedData/
|
||||
|
||||
####
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
# https://taskfile.dev/
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
packaging:
|
||||
taskfile: ./Taskfile.package.yml
|
||||
flatten: true
|
||||
|
||||
vars:
|
||||
PROJECT_NAME: "ShadowsocksX-NG2"
|
||||
BUILD_DIR: "{{ .TASKFILE_DIR }}/.build"
|
||||
DERIVED_DATA_DIR: "{{ .BUILD_DIR }}/derivedData"
|
||||
DERIVED_DATA_DIR: '{{default (printf "%s/derivedData" .BUILD_DIR) .DERIVED_DATA_DIR}}'
|
||||
TEST_TIMEOUT_ARGS: '-test-timeouts-enabled YES -default-test-execution-time-allowance 120 -maximum-test-execution-time-allowance 300'
|
||||
|
||||
tasks:
|
||||
@@ -121,12 +126,17 @@ tasks:
|
||||
-scheme ShadowsocksX-NG2
|
||||
-configuration Debug
|
||||
-destination 'platform=macOS,arch=arm64'
|
||||
-derivedDataPath {{ .DERIVED_DATA_DIR }}
|
||||
-derivedDataPath {{.DERIVED_DATA_DIR | shellQuote}}
|
||||
{{if .RESULT_BUNDLE_PATH}}-resultBundlePath {{.RESULT_BUNDLE_PATH | shellQuote}}{{end}}
|
||||
build
|
||||
|
||||
build:release:
|
||||
desc: Build the ShadowsocksX-NG2 macOS application with Release Config
|
||||
preconditions:
|
||||
- sh: '{{if or (empty .MARKETING_VERSION) (regexMatch "^[0-9]+\\.[0-9]+\\.[0-9]+$" .MARKETING_VERSION)}}true{{else}}false{{end}}'
|
||||
msg: 'MARKETING_VERSION must have three numeric components (for example, 2.1.0).'
|
||||
- sh: '{{if or (empty .CURRENT_PROJECT_VERSION) (regexMatch "^[0-9]+(\\.[0-9]+){0,2}$" .CURRENT_PROJECT_VERSION)}}true{{else}}false{{end}}'
|
||||
msg: 'CURRENT_PROJECT_VERSION must have one to three numeric components.'
|
||||
deps:
|
||||
- gen:prj
|
||||
cmds:
|
||||
@@ -136,7 +146,9 @@ tasks:
|
||||
-scheme ShadowsocksX-NG2
|
||||
-configuration Release
|
||||
-destination 'platform=macOS,arch=arm64'
|
||||
-derivedDataPath {{ .DERIVED_DATA_DIR }}
|
||||
-derivedDataPath {{.DERIVED_DATA_DIR | shellQuote}}
|
||||
{{if .MARKETING_VERSION}}{{printf "MARKETING_VERSION=%s" .MARKETING_VERSION | shellQuote}}{{end}}
|
||||
{{if .CURRENT_PROJECT_VERSION}}{{printf "CURRENT_PROJECT_VERSION=%s" .CURRENT_PROJECT_VERSION | shellQuote}}{{end}}
|
||||
build
|
||||
|
||||
format:
|
||||
@@ -166,7 +178,7 @@ tasks:
|
||||
-project ShadowsocksX-NG2.xcodeproj
|
||||
-scheme ShadowsocksX-NG2
|
||||
-destination 'platform=macOS,arch=arm64'
|
||||
-derivedDataPath {{ .DERIVED_DATA_DIR }}
|
||||
-derivedDataPath {{.DERIVED_DATA_DIR | shellQuote}}
|
||||
{{if .RESULT_BUNDLE_PATH}}-resultBundlePath {{.RESULT_BUNDLE_PATH | shellQuote}}{{end}}
|
||||
{{.TEST_TIMEOUT_ARGS}}
|
||||
test
|
||||
@@ -192,7 +204,7 @@ tasks:
|
||||
-project ShadowsocksX-NG2.xcodeproj
|
||||
-scheme ShadowsocksX-NG2Smoke
|
||||
-destination 'platform=macOS,arch=arm64'
|
||||
-derivedDataPath {{ .DERIVED_DATA_DIR }}
|
||||
-derivedDataPath {{.DERIVED_DATA_DIR | shellQuote}}
|
||||
{{if .RESULT_BUNDLE_PATH}}-resultBundlePath {{.RESULT_BUNDLE_PATH | shellQuote}}{{end}}
|
||||
{{.TEST_TIMEOUT_ARGS}}
|
||||
test
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# https://taskfile.dev/
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
DIST_DIR: '{{.TASKFILE_DIR}}/dist'
|
||||
DMG_REQUIREMENT: '=anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.13] exists'
|
||||
|
||||
tasks:
|
||||
package:
|
||||
desc: Build, sign, notarize and publish an arm64 Release DMG
|
||||
dir: '{{.TASKFILE_DIR}}'
|
||||
requires:
|
||||
vars: [NOTARY_PROFILE]
|
||||
preconditions:
|
||||
- sh: 'test -n {{.NOTARY_PROFILE | shellQuote}}'
|
||||
msg: 'NOTARY_PROFILE must name credentials stored by notarytool store-credentials.'
|
||||
- sh: '{{if or (empty .MARKETING_VERSION) (regexMatch "^[0-9]+\\.[0-9]+\\.[0-9]+$" .MARKETING_VERSION)}}true{{else}}false{{end}}'
|
||||
msg: 'MARKETING_VERSION must have three numeric components (for example, 2.1.0).'
|
||||
- sh: '{{if or (empty .CURRENT_PROJECT_VERSION) (regexMatch "^[0-9]+(\\.[0-9]+){0,2}$" .CURRENT_PROJECT_VERSION)}}true{{else}}false{{end}}'
|
||||
msg: 'CURRENT_PROJECT_VERSION must have one to three numeric components.'
|
||||
vars:
|
||||
RUN_DIR:
|
||||
sh: |
|
||||
mkdir -p {{.BUILD_DIR | shellQuote}}/packaging
|
||||
mktemp -d {{.BUILD_DIR | shellQuote}}/packaging/run-XXXXXX
|
||||
cmds:
|
||||
- 'printf "Packaging workspace: %s\n" {{.RUN_DIR | shellQuote}}'
|
||||
- for: [xcodebuild, xcodegen, hdiutil, codesign, ditto, plutil, shasum, spctl, link]
|
||||
cmd: 'command -v {{.ITEM}} > /dev/null'
|
||||
- xcrun --find stapler
|
||||
- >-
|
||||
xcrun notarytool history --keychain-profile {{.NOTARY_PROFILE | shellQuote}}
|
||||
--output-format json > {{.RUN_DIR | shellQuote}}/notary-history.json
|
||||
- Scripts/fetch-external-binaries.sh
|
||||
- task: test:smoke
|
||||
vars:
|
||||
DERIVED_DATA_DIR: '{{.RUN_DIR}}/derivedData'
|
||||
RESULT_BUNDLE_PATH: '{{.RUN_DIR}}/smoke.xcresult'
|
||||
- task: build:release
|
||||
vars:
|
||||
DERIVED_DATA_DIR: '{{.RUN_DIR}}/derivedData'
|
||||
- task: package:image
|
||||
vars:
|
||||
RUN_DIR: '{{.RUN_DIR}}'
|
||||
APP_PATH: '{{.RUN_DIR}}/derivedData/Build/Products/Release/ShadowsocksX-NG2.app'
|
||||
|
||||
package:image:
|
||||
internal: true
|
||||
dir: '{{.TASKFILE_DIR}}'
|
||||
vars:
|
||||
APP_VERSION:
|
||||
sh: plutil -extract CFBundleShortVersionString raw -expect string -o - {{.APP_PATH | shellQuote}}/Contents/Info.plist
|
||||
APP_BUILD:
|
||||
sh: plutil -extract CFBundleVersion raw -expect string -o - {{.APP_PATH | shellQuote}}/Contents/Info.plist
|
||||
IMAGE_NAME: 'ShadowsocksX-NG2-{{.APP_VERSION}}-{{.APP_BUILD}}-arm64.dmg'
|
||||
IMAGE_PATH: '{{.RUN_DIR}}/{{.IMAGE_NAME}}'
|
||||
preconditions:
|
||||
- sh: '{{if regexMatch "^[0-9]+\\.[0-9]+\\.[0-9]+$" .APP_VERSION}}true{{else}}false{{end}}'
|
||||
msg: 'Built App has an invalid CFBundleShortVersionString.'
|
||||
- sh: '{{if regexMatch "^[0-9]+(\\.[0-9]+){0,2}$" .APP_BUILD}}true{{else}}false{{end}}'
|
||||
msg: 'Built App has an invalid CFBundleVersion.'
|
||||
- sh: 'test ! -e {{printf "%s/%s" .DIST_DIR .IMAGE_NAME | shellQuote}} && test ! -L {{printf "%s/%s" .DIST_DIR .IMAGE_NAME | shellQuote}}'
|
||||
msg: 'This release already exists in dist; existing releases are never overwritten.'
|
||||
cmds:
|
||||
- Scripts/packaging-gate.sh {{.APP_PATH | shellQuote}}
|
||||
- mkdir {{.RUN_DIR | shellQuote}}/stage
|
||||
- ditto {{.APP_PATH | shellQuote}} {{.RUN_DIR | shellQuote}}/stage/ShadowsocksX-NG2.app
|
||||
- ln -s /Applications {{.RUN_DIR | shellQuote}}/stage/Applications
|
||||
- >-
|
||||
hdiutil create -volname ShadowsocksX-NG2 -format UDZO
|
||||
-srcfolder {{.RUN_DIR | shellQuote}}/stage {{.IMAGE_PATH | shellQuote}}
|
||||
- codesign --display {{printf "--extract-certificates=%s/signing-" .RUN_DIR | shellQuote}} {{.APP_PATH | shellQuote}}
|
||||
- task: package:sign
|
||||
vars:
|
||||
RUN_DIR: '{{.RUN_DIR}}'
|
||||
DMG_PATH: '{{.IMAGE_PATH}}'
|
||||
- task: package:notarize
|
||||
vars:
|
||||
DMG_PATH: '{{.IMAGE_PATH}}'
|
||||
|
||||
package:sign:
|
||||
internal: true
|
||||
vars:
|
||||
CERTIFICATE_HASH:
|
||||
sh: shasum -a 1 {{.RUN_DIR | shellQuote}}/signing-0
|
||||
IDENTITY: '{{first (splitList " " .CERTIFICATE_HASH)}}'
|
||||
preconditions:
|
||||
- sh: '{{if regexMatch "^[0-9a-fA-F]{40}$" .IDENTITY}}true{{else}}false{{end}}'
|
||||
msg: 'Cannot identify the certificate that signed the App.'
|
||||
cmds:
|
||||
- >-
|
||||
codesign --sign {{.IDENTITY | shellQuote}} --timestamp
|
||||
--identifier com.qiuyuzhou.ShadowsocksX-NG2.dmg {{.DMG_PATH | shellQuote}}
|
||||
|
||||
package:notarize:
|
||||
desc: Notarize a signed DMG, staple its ticket and publish after verification
|
||||
dir: '{{.TASKFILE_DIR}}'
|
||||
requires:
|
||||
vars: [DMG_PATH, NOTARY_PROFILE]
|
||||
preconditions:
|
||||
- sh: 'test -n {{.NOTARY_PROFILE | shellQuote}}'
|
||||
msg: 'NOTARY_PROFILE must name credentials stored by notarytool store-credentials.'
|
||||
- sh: '{{if and (hasPrefix "/" .DMG_PATH) (hasSuffix ".dmg" .DMG_PATH)}}true{{else}}false{{end}}'
|
||||
msg: 'DMG_PATH must be an absolute path ending in .dmg.'
|
||||
- sh: 'test -f {{.DMG_PATH | shellQuote}}'
|
||||
msg: 'DMG_PATH does not name a file.'
|
||||
- sh: 'test ! -e {{printf "%s/%s" .DIST_DIR (base .DMG_PATH) | shellQuote}} && test ! -L {{printf "%s/%s" .DIST_DIR (base .DMG_PATH) | shellQuote}}'
|
||||
msg: 'This release already exists in dist; existing releases are never overwritten.'
|
||||
vars:
|
||||
LOG_DIR:
|
||||
sh: |
|
||||
mkdir -p {{.BUILD_DIR | shellQuote}}/notarization
|
||||
mktemp -d {{.BUILD_DIR | shellQuote}}/notarization/run-XXXXXX
|
||||
cmds:
|
||||
- 'printf "Notarization evidence: %s\n" {{.LOG_DIR | shellQuote}}'
|
||||
- codesign --verify --strict -R {{.DMG_REQUIREMENT | shellQuote}} {{.DMG_PATH | shellQuote}}
|
||||
- shasum -a 256 {{.DMG_PATH | shellQuote}} > {{.LOG_DIR | shellQuote}}/input.sha256
|
||||
- >-
|
||||
xcrun notarytool submit {{.DMG_PATH | shellQuote}}
|
||||
--keychain-profile {{.NOTARY_PROFILE | shellQuote}} --output-format json
|
||||
> {{.LOG_DIR | shellQuote}}/submission.json
|
||||
2> {{.LOG_DIR | shellQuote}}/submission.stderr.log
|
||||
- task: package:wait
|
||||
vars:
|
||||
LOG_DIR: '{{.LOG_DIR}}'
|
||||
- xcrun stapler staple {{.DMG_PATH | shellQuote}}
|
||||
- xcrun stapler validate {{.DMG_PATH | shellQuote}}
|
||||
- codesign --verify --strict -R {{.DMG_REQUIREMENT | shellQuote}} {{.DMG_PATH | shellQuote}}
|
||||
- spctl --assess --type open --context context:primary-signature --verbose {{.DMG_PATH | shellQuote}}
|
||||
- task: package:publish
|
||||
vars:
|
||||
DMG_PATH: '{{.DMG_PATH}}'
|
||||
|
||||
package:wait:
|
||||
internal: true
|
||||
vars:
|
||||
SUBMISSION_ID:
|
||||
sh: plutil -extract id raw -expect string -o - {{.LOG_DIR | shellQuote}}/submission.json
|
||||
preconditions:
|
||||
- sh: '{{if regexMatch "^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$" .SUBMISSION_ID}}true{{else}}false{{end}}'
|
||||
msg: 'No valid notarization submission ID; inspect submission.json and submission.stderr.log.'
|
||||
cmds:
|
||||
- defer:
|
||||
task: package:notary-log
|
||||
vars:
|
||||
SUBMISSION_ID: '{{.SUBMISSION_ID}}'
|
||||
LOG_DIR: '{{.LOG_DIR}}'
|
||||
- >-
|
||||
xcrun notarytool wait {{.SUBMISSION_ID | shellQuote}} --timeout 30m
|
||||
--keychain-profile {{.NOTARY_PROFILE | shellQuote}} --output-format json
|
||||
> {{.LOG_DIR | shellQuote}}/result.json
|
||||
2> {{.LOG_DIR | shellQuote}}/wait.stderr.log
|
||||
- task: package:accepted
|
||||
vars:
|
||||
LOG_DIR: '{{.LOG_DIR}}'
|
||||
|
||||
package:accepted:
|
||||
internal: true
|
||||
vars:
|
||||
NOTARY_STATUS:
|
||||
sh: plutil -extract status raw -expect string -o - {{.LOG_DIR | shellQuote}}/result.json
|
||||
preconditions:
|
||||
- sh: '{{if eq .NOTARY_STATUS "Accepted"}}true{{else}}false{{end}}'
|
||||
msg: 'Apple did not accept this submission; inspect result.json and notary-log.json.'
|
||||
cmds:
|
||||
- 'printf "Notarization status: %s\n" {{.NOTARY_STATUS | shellQuote}}'
|
||||
|
||||
package:notary-log:
|
||||
internal: true
|
||||
cmds:
|
||||
- cmd: >-
|
||||
xcrun notarytool log {{.SUBMISSION_ID | shellQuote}}
|
||||
--keychain-profile {{.NOTARY_PROFILE | shellQuote}}
|
||||
{{.LOG_DIR | shellQuote}}/notary-log.json
|
||||
2> {{.LOG_DIR | shellQuote}}/notary-log.stderr.log
|
||||
ignore_error: true
|
||||
|
||||
package:publish:
|
||||
internal: true
|
||||
vars:
|
||||
PUBLISH_DIR:
|
||||
sh: |
|
||||
mkdir -p {{.DIST_DIR | shellQuote}}
|
||||
mktemp -d {{.DIST_DIR | shellQuote}}/.package-XXXXXX
|
||||
OUTPUT_PATH: '{{.DIST_DIR}}/{{base .DMG_PATH}}'
|
||||
cmds:
|
||||
- defer: 'rm -rf {{.PUBLISH_DIR | shellQuote}}'
|
||||
- ditto {{.DMG_PATH | shellQuote}} {{.PUBLISH_DIR | shellQuote}}/release.dmg
|
||||
# Linking within dist publishes a complete file atomically and refuses replacement.
|
||||
- link {{.PUBLISH_DIR | shellQuote}}/release.dmg {{.OUTPUT_PATH | shellQuote}}
|
||||
- 'printf "Release DMG: %s\n" {{.OUTPUT_PATH | shellQuote}}'
|
||||
+51
-2
@@ -37,13 +37,62 @@
|
||||
|
||||
## 发布门槛
|
||||
|
||||
发布前运行发布门槛检查:
|
||||
### 签名并公证的 DMG
|
||||
|
||||
从 `ShadowsocksX-NG2/` 执行。首次准备外部二进制及 Developer ID 签名配置的方法见上文。钥匙串中必须有对应证书及私钥。
|
||||
|
||||
先用 Apple 的工具保存公证凭据。此命令交互输入 App 专用密码;凭据保存在钥匙串中,不写入仓库或 Taskfile:
|
||||
|
||||
```bash
|
||||
xcrun notarytool store-credentials shadowsocks-release \
|
||||
--apple-id YOUR_APPLE_ID --team-id YOUR_TEAM_ID
|
||||
```
|
||||
|
||||
完整发布入口:
|
||||
|
||||
```bash
|
||||
task package NOTARY_PROFILE=shadowsocks-release \
|
||||
MARKETING_VERSION=2.1.0 CURRENT_PROJECT_VERSION=21
|
||||
```
|
||||
|
||||
`MARKETING_VERSION` 和 `CURRENT_PROJECT_VERSION` 分别可选。未传的参数使用工程默认值;传入的参数只覆盖本次 Release 构建,不改写 `project.yml`。`MARKETING_VERSION` 接受三个以点分隔的数字,`CURRENT_PROJECT_VERSION` 接受一至三个以点分隔的数字。单独运行 `task build:release` 也支持这两个覆盖参数。
|
||||
|
||||
流程由 `Taskfile.package.yml` 编排:检查公证凭据 → 获取外部二进制 → 真实进程冒烟 → Release 构建 → App 打包门槛 → 制作压缩 DMG → DMG 签名 → Apple 公证 → 装订票据 → 签名、票据及 Gatekeeper 检查 → 发布到 `dist/`。构建只支持 arm64。DMG 只包含 App 和指向 `/Applications` 的快捷方式,不增加背景图或 Finder 自动化依赖。
|
||||
|
||||
DMG 复用 App 实际签名证书的 SHA-1 指纹选择签名身份,使用安全时间戳。公证仅接收 `NOTARY_PROFILE` 指定的钥匙串配置名称。发布产物示例为 `dist/ShadowsocksX-NG2-2.1.0-21-arm64.dmg`,名称中的版本从构建产物的 Info.plist 读取。已有同名发布产物时明确失败,不覆盖文件。
|
||||
|
||||
每次构建使用独立的 `.build/packaging/run-*/`,其中保留签名 DMG、构建产物与冒烟结果。每次公证在 `.build/notarization/run-*/` 保存提交响应、提交编号、等待结果与 Apple 诊断日志。控制台打印这两个目录。提交和等待分别执行,等待中断时仍保留提交编号。公证等待上限为 30 分钟;本地等待结束后 Apple 仍可能继续处理。
|
||||
|
||||
公证失败后,可以直接重跑保留的签名 DMG,无须重新构建:
|
||||
|
||||
```bash
|
||||
task package:notarize \
|
||||
DMG_PATH=/absolute/path/to/ShadowsocksX-NG2-2.1.0-21-arm64.dmg \
|
||||
NOTARY_PROFILE=shadowsocks-release
|
||||
```
|
||||
|
||||
此入口会重新提交公证。若此前仅等待超时,可以先用保留的提交编号查询 Apple 状态,避免重复提交:
|
||||
|
||||
```bash
|
||||
xcrun notarytool info SUBMISSION_ID --keychain-profile shadowsocks-release
|
||||
```
|
||||
|
||||
`package:notarize` 仅在 Apple 返回 `Accepted` 后装订 DMG 票据。签名、票据和 Gatekeeper 检查全部通过后,才发布完整文件到 `dist/`。失败时不发布新文件。此流程只装订 DMG 票据,不另外分发独立 App。公证重跑入口不代替完整入口中的冒烟和 App 打包门槛;它接收此前完整入口生成的签名 DMG。
|
||||
|
||||
`task clean` 删除 `.build/` 中间产物及日志,保留 `dist/`。仓库忽略这两类产物。此流程不上传 GitHub Release。
|
||||
|
||||
工具用法见 Apple 的[公证流程](https://developer.apple.com/documentation/security/customizing-the-notarization-workflow)与[分发打包说明](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution)。
|
||||
|
||||
### 单独检查 App
|
||||
|
||||
已有构建产物时,可以单独运行发布门槛检查:
|
||||
|
||||
```bash
|
||||
# 发布门槛(必须全绿):真实进程冒烟 + 打包门槛
|
||||
task test:smoke
|
||||
task build:release
|
||||
Scripts/packaging-gate.sh \
|
||||
.build/derivedData/Build/Products/Debug/ShadowsocksX-NG2.app
|
||||
.build/derivedData/Build/Products/Release/ShadowsocksX-NG2.app
|
||||
```
|
||||
|
||||
打包门槛只接受 Developer ID 签名的产物:Team 期望值取自 `Configs/Signing.local.xcconfig`(可用 `EXPECTED_TEAM_ID=<Team ID>` 显式覆盖),两者都没有时该断言直接失败,不静默跳过。无证书的 ad-hoc Debug 产物会在此明确失败(`gate: hint` 会说明原因),这是预期行为。
|
||||
|
||||
Reference in New Issue
Block a user