FreeCAD WebAssembly development snapshot
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled
This commit is contained in:
266
package/scripts/macos_sign_and_notarize.zsh
Executable file
266
package/scripts/macos_sign_and_notarize.zsh
Executable file
@@ -0,0 +1,266 @@
|
||||
#!/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"
|
||||
|
||||
# Sign legacy QuickLook generator if present (not built for macOS 15.0+)
|
||||
if [ -f "${CONTAINING_FOLDER}/${APP_NAME}/Contents/Library/QuickLook/QuicklookFCStd.qlgenerator/Contents/MacOS/QuicklookFCStd" ]; then
|
||||
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/Library/QuickLook/QuicklookFCStd.qlgenerator/Contents/MacOS/QuicklookFCStd"
|
||||
fi
|
||||
|
||||
# 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..."
|
||||
local staple_attempt=0
|
||||
while ! xcrun stapler staple "${DMG_NAME}"; do
|
||||
(( staple_attempt++ ))
|
||||
if [[ $staple_attempt -ge 5 ]]; then
|
||||
print "❌ Failed to staple after 5 attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
print "Staple attempt $staple_attempt failed, retrying in $((staple_attempt * 10))s..."
|
||||
sleep $((staple_attempt * 10))
|
||||
done
|
||||
print "Stapled: ${DMG_NAME}"
|
||||
rm -f "${ID_FILE}"
|
||||
else
|
||||
rc=$?
|
||||
print "❌ Notarization failed (code $rc)." >&2
|
||||
exit "$rc"
|
||||
fi
|
||||
51
package/scripts/write_version_info.py
Executable file
51
package/scripts/write_version_info.py
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# call this file from within the FreeCAD git repo
|
||||
# this script creates a file with the important version information
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
sys.path.append(f"{os.getcwd()}/src/Tools")
|
||||
import SubWCRev
|
||||
|
||||
gitInfo = SubWCRev.GitControl()
|
||||
gitInfo.extractInfo("","")
|
||||
|
||||
i = open("src/Build/Version.h.cmake")
|
||||
content = []
|
||||
for line in i.readlines():
|
||||
line = line.replace("${PACKAGE_WCREF}",gitInfo.rev)
|
||||
line = line.replace("${PACKAGE_WCDATE}",gitInfo.date)
|
||||
line = line.replace("${PACKAGE_WCURL}",gitInfo.url)
|
||||
content.append(line)
|
||||
|
||||
with open("src/Build/Version.h.cmake", "w") as o:
|
||||
content.append('// Git relevant stuff\n')
|
||||
content.append('#define FCRepositoryHash "%s"\n' % (gitInfo.hash))
|
||||
content.append('#define FCRepositoryBranch "%s"\n' % (gitInfo.branch))
|
||||
o.writelines(content)
|
||||
|
||||
with open("src/Tools/SubWCRev.py", "r") as f:
|
||||
new_subwcrev = f.read()
|
||||
new_subwcrev = new_subwcrev.replace("lines = i.writeVersion(lines)",
|
||||
"#lines = i.writeVersion(lines) # this source package already has git info, we do nothing here")
|
||||
|
||||
with open("src/Tools/SubWCRev.py", "w") as f:
|
||||
f.writelines(new_subwcrev)
|
||||
|
||||
with open(os.sys.argv[1], "w") as f:
|
||||
f.write(f"rev_number: {gitInfo.rev}\n")
|
||||
f.write(f"branch_name: {gitInfo.branch}\n")
|
||||
f.write(f"commit_date: {gitInfo.date}\n")
|
||||
f.write(f"commit_hash: {gitInfo.hash}\n")
|
||||
f.write(f"remote_url: {gitInfo.url}\n")
|
||||
|
||||
p = subprocess.Popen(["git", "-c", "user.name='github-actions[bot]'", "-c",
|
||||
"user.email='41898282+github-actions[bot]@users.noreply.github.com'", "commit", "-a", "-m",
|
||||
"add git version information"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
out, err = p.communicate()
|
||||
|
||||
print(out.decode())
|
||||
print(err.decode())
|
||||
Reference in New Issue
Block a user