Files
create/package/scripts/macos_sign_and_notarize.zsh
Adrian Insaurralde Avalos 46f3cf2f19 CI: improve release workflow
adapt weekly build workflow to do normal releases too, rename accordingly
skip macos singning setup if certificate not available (useful to run on forks)
add missing dmgbuild dependency for badge icons on macos
build windows installer in workflow, add needed dependencies to pixi.toml
reorganize packaging scripts that can be useful outside rattler-build too
do some cleanup
add .gitignore to rattler-build
Properly configure appimage updating depending on release type and upload zsync file
2025-11-23 10:54:51 -03:00

218 lines
7.3 KiB
Bash
Executable File

#!/bin/zsh
# SPDX-License-Identifier: LGPL-2.1-or-later
# Default values
SIGNING_KEY_ID="${FREECAD_SIGNING_KEY_ID}"
KEYCHAIN_PROFILE="FreeCAD"
CONTAINING_FOLDER="."
APP_NAME="FreeCAD.app"
VOLUME_NAME="FreeCAD"
DMG_NAME="FreeCAD-macOS-$(uname -m).dmg"
DMG_SETTINGS="dmg_settings.py"
# Function to display usage information
function usage {
echo "Usage: $0 [-k|--key-id <signing_key_id>] [-p|--keychain-profile <keychain_profile>]"
echo " [-d|--dir <containing_folder>] [-n|--app-name <app_name.app>]"
echo " [-v|--volume-name <volume_name>] [-o|--output <image_name.dmg>]"
echo " [-s|--dmg-settings <dmg_settings.py>]"
echo
echo "This script signs and notarizes a FreeCAD.app bundle. It expects that the bundle is in a folder"
echo "by itself (that folder will be used as the basis for the created disk image file, so anything"
echo "else in it will become part of the image). That folder should be located in the same folder as"
echo "this script."
echo
echo "If <signing_key_id> is not passed it defaults to env variable FREECAD_SIGNING_KEY_ID, it should"
echo "be a Developer ID Application certificate that has been installed into the login keychain."
echo "For a list of available keys see the output of"
echo " security find-identity -p basic -v"
echo "For instructions on how to configure the credentials for the tool for use before running this"
echo "script see the documentation for"
echo " xcrun notarytool store-credentials"
exit 1
}
# Parse command line arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
-k|--key-id)
SIGNING_KEY_ID="$2"
shift 2
;;
-p|--keychain-profile)
KEYCHAIN_PROFILE="$2"
shift 2
;;
-d|--dir)
CONTAINING_FOLDER="$2"
shift 2
;;
-n|--app-name)
APP_NAME="$2"
shift 2
;;
-v|--volume-name)
VOLUME_NAME="$2"
shift 2
;;
-o|--output)
DMG_NAME="$2"
shift 2
;;
-s|--dmg-settings)
DMG_SETTINGS="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo "Unknown parameter passed: $1"
usage
;;
esac
done
# Check if SIGNING_KEY_ID is set
if [ -z "$SIGNING_KEY_ID" ]; then
echo "Error: Signing key ID is required."
usage
fi
# Check for dmgbuild executable
if ! command -v dmgbuild &> /dev/null; then
echo 'Error: dmgbuild not installed. Please install it'
echo '- using pixi:'
echo 'pixi g install dmgbuild --with pyobjc-framework-Quartz'
echo '- using pip:'
echo 'pip3 install dmgbuild pyobjc-framework-Quartz'
exit 1
fi
function run_codesign {
echo "Signing $1"
/usr/bin/codesign --options runtime -f -s ${SIGNING_KEY_ID} --timestamp --entitlements entitlements.plist "$1"
}
IFS=$'\n'
dylibs=($(/usr/bin/find "${CONTAINING_FOLDER}/${APP_NAME}" -name "*.dylib"))
shared_objects=($(/usr/bin/find "${CONTAINING_FOLDER}/${APP_NAME}" -name "*.so"))
bundles=($(/usr/bin/find "${CONTAINING_FOLDER}/${APP_NAME}" -name "*.bundle"))
executables=($(/usr/bin/find "${CONTAINING_FOLDER}/${APP_NAME}" -type f -perm +111 -exec file {} + | grep "Mach-O 64-bit executable" | sed 's/:.*//g'))
IFS=$' \t\n' # The default
signed_files=("${dylibs[@]}" "${shared_objects[@]}" "${bundles[@]}" "${executables[@]}")
# This list of files is generated from:
# file `find . -type f -perm +111 -print` | grep "Mach-O 64-bit executable" | sed 's/:.*//g'
for exe in ${signed_files}; do
run_codesign "${exe}"
done
# Two additional files that must be signed that aren't caught by the above searches:
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/packages.txt"
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/Library/QuickLook/QuicklookFCStd.qlgenerator/Contents/MacOS/QuicklookFCStd"
# Finally, sign the app itself (must be done last)
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}"
# Create a disk image from the folder
echo "Creating disk image ${DMG_NAME}"
dmgbuild -s ${DMG_SETTINGS} -Dcontaining_folder="${CONTAINING_FOLDER}" -Dapp_name="${APP_NAME}" "${VOLUME_NAME}" "${DMG_NAME}"
ID_FILE="${DMG_NAME}.notarization_id"
# Submit it for notarization (requires that an App Store API Key has been set up in the notarytool)
# This is a *very slow* process, and occasionally the GitHub runners lose the internet connection for a short time
# during the run. So in order to be fault-tolerant, this script polls, instead of using --wait
submit_notarization_request() {
if [[ -s "${ID_FILE}" ]]; then
cat "${ID_FILE}"
return
fi
local out
if ! out=$(xcrun notarytool submit --keychain-profile "${KEYCHAIN_PROFILE}" \
--output-format json --no-progress "${DMG_NAME}" 2>&1); then
print -r -- "$out" >&2
return 1
fi
# We asked for JSON output so we had something stable, but of course parsing JSON with ZSH is ugly, so a quick bit of
# Python does it instead...
local id
id=$(print -r -- "$out" |
/usr/bin/python3 -c 'import sys, json; print(json.load(sys.stdin).get("id",""))'
)
[[ -n "$id" ]] || { print -r -- "Could not parse submission id" >&2; return 1; }
print -r -- "$id" > "${ID_FILE}"
print -r -- "$id" # ID is a string here, not an integer, so I can't just return it
}
wait_for_notarization_result() {
local id="$1" attempt=0
while :; do
if xcrun notarytool wait "$id" --keychain-profile "${KEYCHAIN_PROFILE}" \
--timeout 10m --no-progress >/dev/null; then
return 0
fi
(( attempt++ ))
# If the failure was transient (timeout/HTTP/connection) just retry, but make sure to check to see if the problem
# was actually that the signing failed before retrying.
local tmp_json
tmp_json=$(mktemp)
trap 'rm -f "$tmp_json"' EXIT INT TERM
xcrun notarytool info "$id" --keychain-profile "${KEYCHAIN_PROFILE}" --output-format json 2>/dev/null > "$tmp_json"
/usr/bin/python3 - "$tmp_json" <<'PY'
import sys, json
try:
with open(sys.argv[1]) as f:
s = (json.load(f).get("status") or "").lower()
if s in ("invalid", "rejected"):
sys.exit(2)
else:
sys.exit(0)
except Exception:
sys.exit(1)
PY
rc=$?
rm -f "$tmp_json"
if [[ $rc == 2 ]]; then
print -r -- "Notarization was not accepted by Apple:" >&2
xcrun notarytool log "$id" --keychain-profile "${KEYCHAIN_PROFILE}" >&2
return 3
fi
if [[ $attempt -gt 120 ]]; then
print -r -- "🏳️ Notarization is taking too long, bailing out. 🏳️" >&2
return 4
fi
sleep $(( (attempt<6?2**attempt:60) + RANDOM%5 )) # Increasing timeout plus jitter for multi-run safety
done
}
if ! id="$(submit_notarization_request)"; then
print -r -- "❌ Failed to submit notarization request" >&2
exit 1
fi
if [[ -z "$id" ]]; then
print -r -- "❌ Submission succeeded but no ID was returned" >&2
exit 1
fi
print "Notarization submission ID: $id"
if wait_for_notarization_result "$id"; then
print "✅ Notarization succeeded. Stapling..."
xcrun stapler staple "${DMG_NAME}"
print "Stapled: ${DMG_NAME}"
rm -f "${ID_FILE}"
else
rc=$?
print "❌ Notarization failed (code $rc)." >&2
exit "$rc"
fi