Files
create/package/scripts/macos_sign_and_notarize.zsh
forbes 87a0af0b0f phase 1: copy Kindred-only files onto upstream/main (FreeCAD 1.2.0-dev)
Wholesale copy of all Kindred Create additions that don't conflict with
upstream FreeCAD code:

- kindred-icons/ (1444 Catppuccin Mocha SVG icon overrides)
- src/Mod/Create/ (Kindred Create workbench)
- src/Gui/ Kindred source files (FileOrigin, OriginManager,
  OriginSelectorWidget, CommandOrigin, BreadcrumbToolBar, EditingContext)
- src/Gui/Icons/ (Kindred branding and silo icons)
- src/Gui/PreferencePacks/KindredCreate/
- src/Gui/Stylesheets/ (KindredCreate.qss, images_dark-light/)
- package/ (rattler-build recipe)
- docs/ (architecture, guides, specifications)
- .gitea/ (CI workflows, issue templates)
- mods/silo, mods/ztools submodules
- .gitmodules (Kindred submodule URLs)
- resources/ (kindred-create.desktop, kindred-create.xml)
- banner-logo-light.png, CONTRIBUTING.md
2026-02-13 14:03:58 -06:00

254 lines
9.7 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"
}
function run_codesign_extension {
local target="$1"
local entitlements_file="$2"
echo "Signing extension $target with entitlements $entitlements_file"
/usr/bin/codesign --options runtime -f -s ${SIGNING_KEY_ID} --timestamp --entitlements "$entitlements_file" "$target"
}
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
# Skip .appex executables as they will be signed separately with their bundles
if [[ "$exe" != */Contents/PlugIns/*.appex/* ]]; then
run_codesign "${exe}"
fi
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"
# Sign new Swift QuickLook extensions (macOS 15.0+) with their specific entitlements
# These must be signed before the app itself to avoid overriding the extension signatures
if [ -d "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns" ]; then
# Find the entitlements files relative to script location
# Script is in package/scripts/, entitlements are in src/MacAppBundle/QuickLook/modern/
SCRIPT_DIR="${0:A:h}" # zsh equivalent of dirname with full path resolution
PREVIEW_ENTITLEMENTS="${SCRIPT_DIR}/../../src/MacAppBundle/QuickLook/modern/PreviewExtension.entitlements"
THUMBNAIL_ENTITLEMENTS="${SCRIPT_DIR}/../../src/MacAppBundle/QuickLook/modern/ThumbnailExtension.entitlements"
# Sign individual executables within .appex bundles first
if [ -f "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex/Contents/MacOS/FreeCADThumbnailExtension" ]; then
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex/Contents/MacOS/FreeCADThumbnailExtension"
fi
if [ -f "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex/Contents/MacOS/FreeCADPreviewExtension" ]; then
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex/Contents/MacOS/FreeCADPreviewExtension"
fi
# Then sign the .appex bundles themselves with extension-specific entitlements
if [ -d "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex" ] && [ -f "$THUMBNAIL_ENTITLEMENTS" ]; then
run_codesign_extension "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex" "$THUMBNAIL_ENTITLEMENTS"
fi
if [ -d "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex" ] && [ -f "$PREVIEW_ENTITLEMENTS" ]; then
run_codesign_extension "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex" "$PREVIEW_ENTITLEMENTS"
fi
fi
# 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