[build] add macOS signing and notarization to GitHub Actions

This commit is contained in:
dijunkun
2026-10-29 11:09:12 +08:00
parent 199d29a6dd
commit 613b58f582
11 changed files with 545 additions and 8 deletions
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
# Never echo commands containing imported certificate or notarization secrets.
set +x
if [[ "${GITHUB_ACTIONS:-}" != "true" ]]; then
echo "This script prepares an isolated GitHub Actions signing keychain." >&2
exit 1
fi
: "${RUNNER_TEMP:?RUNNER_TEMP is required}"
: "${GITHUB_ENV:?GITHUB_ENV is required}"
: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}"
for secret_name in MACOS_APP_CERTIFICATE_P12_BASE64 MACOS_INSTALLER_CERTIFICATE_P12_BASE64 \
MACOS_P12_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
if [[ -z "${!secret_name:-}" ]]; then
echo "Missing required Actions secret: $secret_name" >&2
exit 1
fi
done
if [[ ! "$APPLE_TEAM_ID" =~ ^[A-Z0-9]{10}$ ]]; then
echo "APPLE_TEAM_ID must contain 10 uppercase letters or digits." >&2
exit 1
fi
SIGNING_DIR="$RUNNER_TEMP/crossdesk-macos-signing"
KEYCHAIN_PATH="$SIGNING_DIR/signing.keychain-db"
APP_P12="$SIGNING_DIR/application.p12"
INSTALLER_P12="$SIGNING_DIR/installer.p12"
NOTARY_PROFILE="crossdesk-notary"
# Refuse to overwrite anything left by an unrelated task. Each hosted matrix
# runner has its own RUNNER_TEMP, and the workflow also cleans up with always().
umask 077
mkdir "$SIGNING_DIR"
cleanup_setup() {
local status=$?
rm -f "$APP_P12" "$INSTALLER_P12"
if [[ "$status" -ne 0 ]]; then
security delete-keychain "$KEYCHAIN_PATH" >/dev/null 2>&1 || true
rm -rf "$SIGNING_DIR"
fi
exit "$status"
}
trap cleanup_setup EXIT
printf '%s' "$MACOS_APP_CERTIFICATE_P12_BASE64" | base64 --decode > "$APP_P12"
printf '%s' "$MACOS_INSTALLER_CERTIFICATE_P12_BASE64" | base64 --decode > "$INSTALLER_P12"
KEYCHAIN_PASSWORD="$(openssl rand -hex 32)"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
for certificate in "$APP_P12" "$INSTALLER_P12"; do
security import "$certificate" -P "$MACOS_P12_PASSWORD" \
-t cert -f pkcs12 -k "$KEYCHAIN_PATH" \
-T /usr/bin/codesign -T /usr/bin/productbuild -T /usr/bin/productsign
done
# Allow the signing tools to use only this job's imported keys without a GUI
# password prompt. The user's login keychain and its access controls are untouched.
security set-key-partition-list -S apple-tool:,apple:,codesign: -s \
-k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" >/dev/null
find_identity() {
local policy="$1"
local prefix="$2"
security find-identity -v -p "$policy" "$KEYCHAIN_PATH" | \
awk -F '"' -v prefix="$prefix" -v suffix=" ($APPLE_TEAM_ID)" '
index($2, prefix) == 1 &&
substr($2, length($2) - length(suffix) + 1) == suffix {
print $2
count++
}
END { if (count != 1) exit 1 }'
}
if ! APP_IDENTITY="$(find_identity codesigning 'Developer ID Application: ')"; then
echo "Expected one valid Developer ID Application identity for APPLE_TEAM_ID, including its private key." >&2
exit 1
fi
if ! INSTALLER_IDENTITY="$(find_identity basic 'Developer ID Installer: ')"; then
echo "Expected one valid Developer ID Installer identity for APPLE_TEAM_ID, including its private key." >&2
exit 1
fi
xcrun notarytool store-credentials "$NOTARY_PROFILE" \
--apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" --keychain "$KEYCHAIN_PATH" --validate
{
printf 'CROSSDESK_MACOS_KEYCHAIN=%s\n' "$KEYCHAIN_PATH"
printf 'CROSSDESK_MACOS_SIGN_IDENTITY=%s\n' "$APP_IDENTITY"
printf 'CROSSDESK_MACOS_INSTALLER_IDENTITY=%s\n' "$INSTALLER_IDENTITY"
} >> "$GITHUB_ENV"
# Keep notarization separate from packaging so CI can retain a signed package
# and the submission ID when Apple's processing exceeds the wait timeout.
printf 'notary_profile=%s\n' "$NOTARY_PROFILE" >> "$GITHUB_OUTPUT"
echo "Prepared Developer ID application and installer identities for team $APPLE_TEAM_ID."
+81
View File
@@ -24,6 +24,11 @@ on:
required: false
type: boolean
default: false
sign_macos:
description: "Sign and notarize macOS installers (automatic for v* release tags)."
required: false
type: boolean
default: false
permissions:
actions: read
@@ -220,7 +225,9 @@ jobs:
VERSION_NUM: ${{ needs.prepare-version.outputs.version }}
MACOSX_DEPLOYMENT_TARGET: "14.0"
EXPECTED_MACOS_SDK: "26.5"
MACOS_SIGNING_REQUIRED: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.sign_macos) }}
strategy:
fail-fast: false
matrix:
include:
- arch: x64
@@ -237,6 +244,27 @@ jobs:
package_script: ./apps/desktop/scripts/macos/pkg_arm64.sh
steps:
- name: Validate macOS signing secrets
if: env.MACOS_SIGNING_REQUIRED == 'true'
shell: bash
env:
MACOS_APP_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_APP_CERTIFICATE_P12_BASE64 }}
MACOS_INSTALLER_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_P12_BASE64 }}
MACOS_P12_PASSWORD: ${{ secrets.MACOS_P12_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
missing=0
for name in MACOS_APP_CERTIFICATE_P12_BASE64 MACOS_INSTALLER_CERTIFICATE_P12_BASE64 \
MACOS_P12_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
if [[ -z "${!name:-}" ]]; then
echo "::error::Missing required Actions secret: ${name}"
missing=1
fi
done
exit "$missing"
- name: Select Xcode 26.6
shell: bash
run: |
@@ -275,7 +303,21 @@ jobs:
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=false -vyD
xmake b -vy crossdesk
- name: Import Developer ID certificates and notarization credentials
id: macos_signing
if: env.MACOS_SIGNING_REQUIRED == 'true'
shell: bash
env:
MACOS_APP_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_APP_CERTIFICATE_P12_BASE64 }}
MACOS_INSTALLER_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_P12_BASE64 }}
MACOS_P12_PASSWORD: ${{ secrets.MACOS_P12_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: bash .github/scripts/setup_macos_signing.sh
- name: Package CrossDesk app
id: macos_package
run: |
chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${VERSION_NUM}
@@ -296,17 +338,56 @@ jobs:
./apps/desktop/scripts/macos/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
- name: Notarize and staple macOS installer
id: macos_notarization
if: env.MACOS_SIGNING_REQUIRED == 'true'
timeout-minutes: 65
shell: bash
env:
CROSSDESK_MACOS_NOTARY_PROFILE: ${{ steps.macos_signing.outputs.notary_profile }}
CROSSDESK_MACOS_NOTARY_TIMEOUT: 60m
CROSSDESK_MACOS_NOTARY_S3_ACCELERATION: "false"
run: |
bash ./apps/desktop/scripts/macos/notarize_pkg.sh \
"crossdesk-macos-${{ matrix.arch }}-${VERSION_NUM}.pkg"
- name: Upload macOS notarization diagnostics
if: always() && env.MACOS_SIGNING_REQUIRED == 'true'
uses: actions/upload-artifact@v6
with:
name: macos-notarization-${{ matrix.arch }}-${{ env.VERSION_NUM }}
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg.notary*.json
if-no-files-found: ignore
retention-days: 14
- name: Preserve signed installer after notarization failure
if: failure() && env.MACOS_SIGNING_REQUIRED == 'true' && steps.macos_package.outcome == 'success'
uses: actions/upload-artifact@v6
with:
name: macos-pending-notarization-${{ matrix.arch }}-${{ env.VERSION_NUM }}
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg
if-no-files-found: error
retention-days: 14
- name: Upload build artifacts
uses: actions/upload-artifact@v6
with:
name: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg
if-no-files-found: error
- name: Move files to release dir
run: |
mkdir -p release
cp crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg release/
- name: Remove macOS signing credentials
if: always() && env.MACOS_SIGNING_REQUIRED == 'true'
shell: bash
run: |
security delete-keychain "${RUNNER_TEMP}/crossdesk-macos-signing/signing.keychain-db" 2>/dev/null || true
rm -rf "${RUNNER_TEMP:?}/crossdesk-macos-signing"
# iOS (also available as a standalone workflow)
build-ios:
name: Build iOS
+4 -4
View File
@@ -127,9 +127,9 @@ fi
# The package cache remains intact for debugging and other consumers.
strip -S -x "$FRAMEWORKS_DIR/$SLINT_NAME"
# install_name_tool and strip invalidate the linker's ad-hoc signature. Sign nested code
# first, then seal the complete app bundle so local packages remain launchable.
codesign --force --sign - "$FRAMEWORKS_DIR/$SLINT_NAME"
codesign --force --deep --sign - "$APP_BUNDLE"
# install_name_tool and strip invalidate existing signatures. Sign only after
# all bundle changes, using Developer ID when configured or ad-hoc for local use.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bash "$SCRIPT_DIR/sign_app.sh" "$APP_BUNDLE"
echo "Bundled Slint runtime: $SLINT_LIBRARY"
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
</dict>
</plist>
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 || -z "${CROSSDESK_MACOS_NOTARY_PROFILE:-}" ]]; then
echo "Usage: CROSSDESK_MACOS_NOTARY_PROFILE=<keychain-profile> $0 <signed.pkg>" >&2
echo "Create the profile with: xcrun notarytool store-credentials <keychain-profile>" >&2
exit 2
fi
PACKAGE_FILE="$1"
if [[ ! -f "$PACKAGE_FILE" || "$PACKAGE_FILE" != *.pkg ]]; then
echo "Expected a signed .pkg file: $PACKAGE_FILE" >&2
exit 1
fi
pkgutil --check-signature "$PACKAGE_FILE"
NOTARY_ARGS=(--keychain-profile "$CROSSDESK_MACOS_NOTARY_PROFILE")
if [[ -n "${CROSSDESK_MACOS_KEYCHAIN:-}" ]]; then
NOTARY_ARGS+=(--keychain "$CROSSDESK_MACOS_KEYCHAIN")
fi
UPLOAD_ARGS=()
case "${CROSSDESK_MACOS_NOTARY_S3_ACCELERATION:-true}" in
true) UPLOAD_ARGS+=(--s3-acceleration) ;;
false) UPLOAD_ARGS+=(--no-s3-acceleration) ;;
*)
echo "CROSSDESK_MACOS_NOTARY_S3_ACCELERATION must be true or false." >&2
exit 2
;;
esac
# Retain the submission response and log beside the installer for diagnostics
# and for resuming a submission if Apple's service takes longer than expected.
SUBMIT_FILE="${PACKAGE_FILE}.notary-submit.json"
RESULT_FILE="${PACKAGE_FILE}.notary.json"
LOG_FILE="${PACKAGE_FILE}.notary-log.json"
SUBMIT_STATUS=0
xcrun notarytool submit "$PACKAGE_FILE" "${NOTARY_ARGS[@]}" "${UPLOAD_ARGS[@]}" \
--no-wait --output-format json > "$SUBMIT_FILE" || SUBMIT_STATUS=$?
cat "$SUBMIT_FILE"
SUBMISSION_ID="$(plutil -extract id raw -o - "$SUBMIT_FILE" 2>/dev/null || true)"
if [[ "$SUBMIT_STATUS" -ne 0 || -z "$SUBMISSION_ID" ]]; then
echo "Notarization upload failed; response: $SUBMIT_FILE" >&2
echo "Check notarytool history before retrying; Apple may have allocated a submission ID." >&2
exit 1
fi
echo "Waiting for notarization: $SUBMISSION_ID"
WAIT_STATUS=0
xcrun notarytool wait "$SUBMISSION_ID" "${NOTARY_ARGS[@]}" \
--timeout "${CROSSDESK_MACOS_NOTARY_TIMEOUT:-30m}" \
--output-format json > "$RESULT_FILE" || WAIT_STATUS=$?
cat "$RESULT_FILE"
STATUS="$(plutil -extract status raw -o - "$RESULT_FILE" 2>/dev/null || true)"
xcrun notarytool log "$SUBMISSION_ID" "${NOTARY_ARGS[@]}" "$LOG_FILE" || true
if [[ "$WAIT_STATUS" -ne 0 || "$STATUS" != "Accepted" ]]; then
echo "Notarization did not complete successfully (status: ${STATUS:-unknown})." >&2
echo "Submission: ${SUBMISSION_ID:-unavailable}; response: $RESULT_FILE" >&2
exit 1
fi
xcrun stapler staple "$PACKAGE_FILE"
xcrun stapler validate "$PACKAGE_FILE"
spctl --assess --type install --verbose=2 "$PACKAGE_FILE"
echo "Notarized and stapled: $PACKAGE_FILE"
+36 -2
View File
@@ -13,6 +13,31 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
MACOS_MIN_VERSION="14.0"
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <version>" >&2
exit 2
fi
# For distribution, configure both full Developer ID certificate names.
# Setting CROSSDESK_MACOS_NOTARY_PROFILE also notarizes and staples the pkg.
PRODUCT_SIGN_ARGS=()
if [[ -n "${CROSSDESK_MACOS_INSTALLER_IDENTITY:-}" || -n "${CROSSDESK_MACOS_NOTARY_PROFILE:-}" ||
"${CROSSDESK_MACOS_SIGN_IDENTITY:--}" != "-" ]]; then
if [[ "${CROSSDESK_MACOS_SIGN_IDENTITY:-}" != "Developer ID Application: "* ||
"${CROSSDESK_MACOS_INSTALLER_IDENTITY:-}" != "Developer ID Installer: "* ]]; then
echo "Signed packaging requires CROSSDESK_MACOS_SIGN_IDENTITY and CROSSDESK_MACOS_INSTALLER_IDENTITY." >&2
exit 1
fi
if [[ "${CROSSDESK_MACOS_SIGN_IDENTITY##* (}" != "${CROSSDESK_MACOS_INSTALLER_IDENTITY##* (}" ]]; then
echo "Application and installer certificates must belong to the same team." >&2
exit 1
fi
PRODUCT_SIGN_ARGS+=(--sign "$CROSSDESK_MACOS_INSTALLER_IDENTITY" --timestamp)
if [[ -n "${CROSSDESK_MACOS_KEYCHAIN:-}" ]]; then
PRODUCT_SIGN_ARGS+=(--keychain "$CROSSDESK_MACOS_KEYCHAIN")
fi
fi
cd "$PROJECT_ROOT"
normalize_app_version() {
@@ -175,6 +200,7 @@ cat > build_pkg_temp/Distribution <<EOF
EOF
productbuild \
${PRODUCT_SIGN_ARGS[@]+"${PRODUCT_SIGN_ARGS[@]}"} \
--distribution build_pkg_temp/Distribution \
--package-path build_pkg_temp \
--resources build_pkg_resources \
@@ -182,6 +208,15 @@ productbuild \
echo "PKG package created: ${PKG_NAME}"
if [[ -n "${CROSSDESK_MACOS_INSTALLER_IDENTITY:-}" ]]; then
pkgutil --check-signature "${PKG_NAME}"
fi
if [[ -n "${CROSSDESK_MACOS_NOTARY_PROFILE:-}" ]]; then
bash "${SCRIPT_DIR}/notarize_pkg.sh" "${PKG_NAME}"
elif [[ -n "${CROSSDESK_MACOS_INSTALLER_IDENTITY:-}" ]]; then
echo "Package is signed but not notarized. Set CROSSDESK_MACOS_NOTARY_PROFILE before public distribution."
fi
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
@@ -215,8 +250,7 @@ APPLESCRIPT
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources "${APP_BUNDLE}"
echo "PKG package created successfully."
echo "package ${APP_BUNDLE}"
echo "installer ${PKG_NAME}"
+36 -2
View File
@@ -13,6 +13,31 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
MACOS_MIN_VERSION="14.0"
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <version>" >&2
exit 2
fi
# For distribution, configure both full Developer ID certificate names.
# Setting CROSSDESK_MACOS_NOTARY_PROFILE also notarizes and staples the pkg.
PRODUCT_SIGN_ARGS=()
if [[ -n "${CROSSDESK_MACOS_INSTALLER_IDENTITY:-}" || -n "${CROSSDESK_MACOS_NOTARY_PROFILE:-}" ||
"${CROSSDESK_MACOS_SIGN_IDENTITY:--}" != "-" ]]; then
if [[ "${CROSSDESK_MACOS_SIGN_IDENTITY:-}" != "Developer ID Application: "* ||
"${CROSSDESK_MACOS_INSTALLER_IDENTITY:-}" != "Developer ID Installer: "* ]]; then
echo "Signed packaging requires CROSSDESK_MACOS_SIGN_IDENTITY and CROSSDESK_MACOS_INSTALLER_IDENTITY." >&2
exit 1
fi
if [[ "${CROSSDESK_MACOS_SIGN_IDENTITY##* (}" != "${CROSSDESK_MACOS_INSTALLER_IDENTITY##* (}" ]]; then
echo "Application and installer certificates must belong to the same team." >&2
exit 1
fi
PRODUCT_SIGN_ARGS+=(--sign "$CROSSDESK_MACOS_INSTALLER_IDENTITY" --timestamp)
if [[ -n "${CROSSDESK_MACOS_KEYCHAIN:-}" ]]; then
PRODUCT_SIGN_ARGS+=(--keychain "$CROSSDESK_MACOS_KEYCHAIN")
fi
fi
cd "$PROJECT_ROOT"
normalize_app_version() {
@@ -175,6 +200,7 @@ cat > build_pkg_temp/Distribution <<EOF
EOF
productbuild \
${PRODUCT_SIGN_ARGS[@]+"${PRODUCT_SIGN_ARGS[@]}"} \
--distribution build_pkg_temp/Distribution \
--package-path build_pkg_temp \
--resources build_pkg_resources \
@@ -182,6 +208,15 @@ productbuild \
echo "PKG package created: ${PKG_NAME}"
if [[ -n "${CROSSDESK_MACOS_INSTALLER_IDENTITY:-}" ]]; then
pkgutil --check-signature "${PKG_NAME}"
fi
if [[ -n "${CROSSDESK_MACOS_NOTARY_PROFILE:-}" ]]; then
bash "${SCRIPT_DIR}/notarize_pkg.sh" "${PKG_NAME}"
elif [[ -n "${CROSSDESK_MACOS_INSTALLER_IDENTITY:-}" ]]; then
echo "Package is signed but not notarized. Set CROSSDESK_MACOS_NOTARY_PROFILE before public distribution."
fi
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
@@ -215,8 +250,7 @@ APPLESCRIPT
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources "${APP_BUNDLE}"
echo "PKG package created successfully."
echo "package ${APP_BUNDLE}"
echo "installer ${PKG_NAME}"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <app-bundle>" >&2
echo "Set CROSSDESK_MACOS_SIGN_IDENTITY to a full Developer ID Application name." >&2
echo "Without an identity, the bundle receives an ad-hoc signature for local use." >&2
exit 2
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_BUNDLE="$1"
SIGN_IDENTITY="${CROSSDESK_MACOS_SIGN_IDENTITY:--}"
SIGN_ARGS=(--force --sign "$SIGN_IDENTITY")
APP_ARGS=()
case "$SIGN_IDENTITY" in
-) ;;
"Developer ID Application: "*)
SIGN_ARGS+=(--options runtime --timestamp)
APP_ARGS+=(--entitlements "$SCRIPT_DIR/entitlements.plist")
;;
*)
echo "CROSSDESK_MACOS_SIGN_IDENTITY must be a full Developer ID Application name or '-'." >&2
exit 1
;;
esac
if [[ -n "${CROSSDESK_MACOS_KEYCHAIN:-}" ]]; then
SIGN_ARGS+=(--keychain "$CROSSDESK_MACOS_KEYCHAIN")
fi
for executable in CrossDesk crossdesk_updater; do
if [[ ! -x "$APP_BUNDLE/Contents/MacOS/$executable" ]]; then
echo "Missing app executable: $APP_BUNDLE/Contents/MacOS/$executable" >&2
exit 1
fi
done
if [[ ! -d "$APP_BUNDLE/Contents/Frameworks" ]]; then
echo "Missing bundled frameworks: $APP_BUNDLE/Contents/Frameworks" >&2
exit 1
fi
# Sign nested code explicitly, from the inside out. The updater also runs
# outside the bundle, and must retain its own valid Developer ID signature.
while IFS= read -r -d '' library; do
codesign "${SIGN_ARGS[@]}" "$library"
done < <(find "$APP_BUNDLE/Contents/Frameworks" -type f -name '*.dylib' -print0)
codesign "${SIGN_ARGS[@]}" "$APP_BUNDLE/Contents/MacOS/crossdesk_updater"
codesign "${SIGN_ARGS[@]}" ${APP_ARGS[@]+"${APP_ARGS[@]}"} "$APP_BUNDLE"
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
echo "App signed with: $SIGN_IDENTITY"
+39
View File
@@ -127,4 +127,43 @@ fi
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
# A distribution build must not silently fall back to an ad-hoc/development
# identity, omit a secure timestamp, or leave its updater/runtime unsigned.
APP_SIGNATURE="$(codesign --display --verbose=4 "$APP_BUNDLE" 2>&1)"
if [[ "${CROSSDESK_MACOS_SIGN_IDENTITY:--}" != "-" ||
"$APP_SIGNATURE" == *"Authority=Developer ID Application: "* ]]; then
EXPECTED_TEAM="$(printf '%s\n' "$APP_SIGNATURE" | sed -n 's/^TeamIdentifier=//p')"
verify_distribution_signature() {
local target="$1"
local signature=""
codesign --verify --strict --verbose=2 \
-R='anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists' \
"$target"
signature="$(codesign --display --verbose=4 "$target" 2>&1)"
if ! printf '%s\n' "$signature" | grep -q '^CodeDirectory .*flags=.*runtime'; then
echo "Hardened Runtime is missing: $target" >&2
return 1
fi
if ! printf '%s\n' "$signature" | grep -q '^Timestamp='; then
echo "Secure signing timestamp is missing: $target" >&2
return 1
fi
if ! printf '%s\n' "$signature" | grep -Fxq "TeamIdentifier=$EXPECTED_TEAM"; then
echo "Signing team mismatch: $target" >&2
return 1
fi
if [[ "${CROSSDESK_MACOS_SIGN_IDENTITY:--}" != "-" ]] &&
! printf '%s\n' "$signature" | grep -Fxq "Authority=$CROSSDESK_MACOS_SIGN_IDENTITY"; then
echo "Unexpected signing identity: $target" >&2
return 1
fi
}
verify_distribution_signature "$APP_BUNDLE"
verify_distribution_signature "$UPDATER"
while IFS= read -r -d '' library; do
verify_distribution_signature "$library"
done < <(find "$FRAMEWORKS_DIR" -type f -name '*.dylib' -print0)
fi
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
+61
View File
@@ -59,6 +59,67 @@ xmake r crossdesk
打包脚本会生成包含 Slint 运行库的 `.app` 和 `.pkg`。屏幕录制、辅助功能授权与应用路径及签名有关;测试被控能力时请使用固定路径下的应用包。
### Developer ID 签名与公证
默认打包对应用使用 ad-hoc 签名,不签名安装包,适用于本地开发。对外分发时,需要在钥匙串安装同一团队的 `Developer ID Application`、`Developer ID Installer` 证书及对应私钥,并设置以下环境变量(替换证书名称与团队 ID):
```bash
export CROSSDESK_MACOS_SIGN_IDENTITY='Developer ID Application: Your Organization (TEAMID)'
export CROSSDESK_MACOS_INSTALLER_IDENTITY='Developer ID Installer: Your Organization (TEAMID)'
# 仅首次配置:按提示输入 Apple ID 和 App 专用密码,保存到本机钥匙串。
xcrun notarytool store-credentials crossdesk-notary --team-id TEAMID
export CROSSDESK_MACOS_NOTARY_PROFILE=crossdesk-notary
# 先完成上面的 release 编译;Intel 改用 pkg_x64.sh。
./apps/desktop/scripts/macos/pkg_arm64.sh 1.4.3
```
脚本依次签署 Slint 动态库、更新器与应用,启用 Hardened Runtime、安全时间戳及所需资源权限,再使用 Installer 证书签署 `.pkg`。配置公证 Profile 后会提交 Apple 公证,仅在返回 `Accepted` 后附加票据并校验 Gatekeeper。未配置 Profile 时只生成已签名但未公证的安装包;签名与公证不代替用户对屏幕录制、辅助功能等权限的授权。
已有签名安装包可以单独提交公证:
```bash
CROSSDESK_MACOS_NOTARY_PROFILE=crossdesk-notary \
bash apps/desktop/scripts/macos/notarize_pkg.sh crossdesk-macos-arm64-1.4.3.pkg
```
上传成功后会立即显示提交 ID,并保存 `.notary-submit.json`;公证结果及日志保存在安装包旁的 `.notary.json`、`.notary-log.json` 文件中。失败或超时会终止打包并保留产物及提交 ID。默认等待 30 分钟,可用 `CROSSDESK_MACOS_NOTARY_TIMEOUT` 调整。超时后可用 `xcrun notarytool wait <提交ID> --keychain-profile crossdesk-notary` 继续等待,确认 `Accepted` 后执行 `xcrun stapler staple <安装包>`、`xcrun stapler validate <安装包>` 和 `spctl --assess --type install --verbose=2 <安装包>`。
若加速上传出现连接超时,可设置 `CROSSDESK_MACOS_NOTARY_S3_ACCELERATION=false`,使用普通 S3 上传通道重试;默认值为 `true`。上传失败也可能已在 Apple 端创建记录,重试前先用 `xcrun notarytool history --keychain-profile crossdesk-notary` 检查提交历史。
自定义钥匙串可通过 `CROSSDESK_MACOS_KEYCHAIN` 指定,证书私钥与公证 Profile 都需要存放在该钥匙串。不要将私钥、P12 文件或密码提交到仓库。苹果说明:[Developer ID 证书](https://developer.apple.com/help/account/certificates/create-developer-id-certificates/)、[公证要求](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)。
### GitHub Actions 自动签名与公证
[Build and Release 工作流](../.github/workflows/build.yml) 对 `v*` 发布标签自动启用 macOS 签名、公证与票据附加;手动运行时可勾选 `sign_macos`。普通分支推送及未勾选的手动构建沿用 ad-hoc 签名。两种架构均在各自 runner 上完成处理,只有公证及 Gatekeeper 校验成功的签名构建才上传正式产物;缺少 Secrets 或公证失败会阻止发布。
先在本机“钥匙串访问 → 我的证书”中,分别将 `Developer ID Application` 和 `Developer ID Installer` **连同对应私钥**导出为两个 `.p12` 文件,并为这两个文件设置相同的导出密码。只有 `.cer` 证书文件不能用于 CI 签名。文件保存在工程之外,例如 `~/Downloads/crossdesk-application.p12` 和 `~/Downloads/crossdesk-installer.p12`。
在 GitHub 仓库 **Settings → Secrets and variables → Actions → New repository secret** 中配置:
| Secret | 内容 |
| --- | --- |
| `MACOS_APP_CERTIFICATE_P12_BASE64` | Application 证书及私钥的 `.p12` 文件的 Base64 |
| `MACOS_INSTALLER_CERTIFICATE_P12_BASE64` | Installer 证书及私钥的 `.p12` 文件的 Base64 |
| `MACOS_P12_PASSWORD` | 导出两个 `.p12` 时设置的共同密码 |
| `APPLE_ID` | 拥有该开发团队权限的 Apple 账户邮箱 |
| `APPLE_APP_SPECIFIC_PASSWORD` | 为公证生成的 App 专用密码 |
| `APPLE_TEAM_ID` | 两张证书所属的 10 位团队 ID |
在本机分别执行以下命令,将剪贴板内容粘贴到对应的 Base64 Secret。先保存第一项,再执行第二条命令:
```bash
base64 -i "$HOME/Downloads/crossdesk-application.p12" | pbcopy
base64 -i "$HOME/Downloads/crossdesk-installer.p12" | pbcopy
```
本机保存的 `crossdesk-notary` 钥匙串 Profile 不会同步到 GitHub runner。工作流使用上述 Secrets 在临时钥匙串中重建 Profile,自动选择与团队 ID 匹配的有效证书,配置签名工具无需交互访问这些临时私钥,并在任务结束时删除证书和公证凭据。无需额外设置钥匙串密码 Secret,也无需为当前 Developer ID 分发流程配置 provisioning profile。
配置后先手动运行工作流:填写 `desktop_version`,勾选 `sign_macos`,确认 ARM64、Intel 的 `Notarize and staple macOS installer` 步骤均成功,再推送发布标签。需要先将工作流和配套脚本提交、推送到 GitHub,新的开关和步骤才会生效。
CI 最多等待公证 60 分钟;若 Apple 仍在处理,任务失败并阻止发布,但会保留 `macos-pending-notarization-*` 安装包及 `macos-notarization-*` JSON 诊断产物 14 天。下载产物后,按上一节用已保存的提交 ID 查询并附加票据,无需重复上传同一个包。不要将 pending 产物当作已经完成公证的发布包。自动签名流程不改变安装器当前的权限重置逻辑;辅助功能授权是否保留还需单独调整安装脚本。
## Linux
Linux 构建支持 Ubuntu 20.04 及以上版本的 amd64 和 arm64。发布安装包以
+61
View File
@@ -56,6 +56,67 @@ The current CI sets macOS 14.0 as the minimum target for both Intel and Apple Si
The scripts create an `.app` and `.pkg` containing the Slint runtime. Screen Recording and Accessibility grants depend on the app path and signature; use a consistent application bundle when testing host functionality.
### Developer ID signing and notarization
By default, packaging uses ad-hoc app signatures and leaves the installer unsigned for local development. For distribution, install both `Developer ID Application` and `Developer ID Installer` certificates and their private keys from the same team in your keychain. Configure their full names and your team ID:
```bash
export CROSSDESK_MACOS_SIGN_IDENTITY='Developer ID Application: Your Organization (TEAMID)'
export CROSSDESK_MACOS_INSTALLER_IDENTITY='Developer ID Installer: Your Organization (TEAMID)'
# One-time setup: enter your Apple ID and app-specific password when prompted.
xcrun notarytool store-credentials crossdesk-notary --team-id TEAMID
export CROSSDESK_MACOS_NOTARY_PROFILE=crossdesk-notary
# Build in release mode first, as above. Use pkg_x64.sh for Intel.
./apps/desktop/scripts/macos/pkg_arm64.sh 1.4.3
```
The scripts sign the Slint runtime, updater and app, enable Hardened Runtime with secure timestamps and required resource entitlements, and sign the `.pkg` using the Installer certificate. When a notary profile is configured, they submit to Apple, staple only an `Accepted` result, and verify Gatekeeper assessment. Without a profile, the result is signed but not notarized. Signing and notarization do not replace user consent for Screen Recording, Accessibility or other protected resources.
To notarize an existing signed installer separately:
```bash
CROSSDESK_MACOS_NOTARY_PROFILE=crossdesk-notary \
bash apps/desktop/scripts/macos/notarize_pkg.sh crossdesk-macos-arm64-1.4.3.pkg
```
After a successful upload, the submission ID is printed immediately and saved in `.notary-submit.json`. The result and log are saved next to the installer as `.notary.json` and `.notary-log.json`. Failure or timeout stops packaging and preserves the artifact and submission ID. The default wait is 30 minutes; override it with `CROSSDESK_MACOS_NOTARY_TIMEOUT`. After a timeout, resume waiting with `xcrun notarytool wait <submission-id> --keychain-profile crossdesk-notary`; after confirming `Accepted`, run `xcrun stapler staple <package>`, `xcrun stapler validate <package>` and `spctl --assess --type install --verbose=2 <package>`.
If accelerated uploads time out, set `CROSSDESK_MACOS_NOTARY_S3_ACCELERATION=false` to retry using the standard S3 upload endpoint; the default is `true`. A failed upload may still allocate a submission record at Apple, so check `xcrun notarytool history --keychain-profile crossdesk-notary` before retrying.
Set `CROSSDESK_MACOS_KEYCHAIN` to use a custom keychain containing both signing identities and the notary profile. Never commit private keys, P12 files or passwords. See Apple's [Developer ID certificate guide](https://developer.apple.com/help/account/certificates/create-developer-id-certificates/) and [notarization requirements](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution).
### Signing and notarization in GitHub Actions
The [Build and Release workflow](../.github/workflows/build.yml) automatically signs, notarizes and staples macOS installers for `v*` release tags. Manual runs can enable `sign_macos`; ordinary branch pushes and unchecked manual runs retain ad-hoc signing. Each architecture is processed on its own runner. Signed builds upload their normal artifacts only after notarization and Gatekeeper verification succeed; missing secrets or notarization failures block publication.
In **Keychain Access → My Certificates**, export the `Developer ID Application` and `Developer ID Installer` identities **with their private keys** into two `.p12` files, using the same export password for both. A `.cer` file alone is insufficient. Keep these files outside the repository, for example `~/Downloads/crossdesk-application.p12` and `~/Downloads/crossdesk-installer.p12`.
Add these repository secrets under **Settings → Secrets and variables → Actions → New repository secret**:
| Secret | Value |
| --- | --- |
| `MACOS_APP_CERTIFICATE_P12_BASE64` | Base64 of the Application certificate/private-key `.p12` |
| `MACOS_INSTALLER_CERTIFICATE_P12_BASE64` | Base64 of the Installer certificate/private-key `.p12` |
| `MACOS_P12_PASSWORD` | Shared export password for both `.p12` files |
| `APPLE_ID` | Apple account email with access to the developer team |
| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password generated for notarization |
| `APPLE_TEAM_ID` | The 10-character team ID of both certificates |
Run these commands separately and paste each clipboard value into its corresponding Base64 secret. Save the first secret before running the second command:
```bash
base64 -i "$HOME/Downloads/crossdesk-application.p12" | pbcopy
base64 -i "$HOME/Downloads/crossdesk-installer.p12" | pbcopy
```
Your local `crossdesk-notary` profile does not synchronize to GitHub runners. The workflow recreates it in a temporary keychain, selects valid identities matching the team ID, allows signing tools to access only those imported keys without interactive prompts, and deletes signing/notarization credentials at the end of the job. No extra keychain password secret or provisioning profile is required for the current Developer ID distribution setup.
After committing and pushing the workflow and supporting scripts, first run it manually with `desktop_version` set and `sign_macos` enabled. Confirm that `Notarize and staple macOS installer` succeeds for both ARM64 and Intel before pushing a release tag.
CI waits up to 60 minutes for notarization. If Apple is still processing, the job fails and blocks publication, while preserving `macos-pending-notarization-*` installer artifacts and `macos-notarization-*` JSON diagnostics for 14 days. Download them and use the saved submission ID as described above to resume checking and staple the package without uploading it again. Pending artifacts are not completed notarized releases. CI signing does not change the installer's existing permission-reset behavior; preserving Accessibility grants requires a separate installer change.
## Linux
Linux builds support Ubuntu 20.04 or later on amd64 and arm64. Release packages