[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
+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+)"