#!/bin/zsh set -euo pipefail script_dir=${0:A:h} project="$script_dir/VoiceCat.Mac/VoiceCat.Mac.csproj" configuration=Release dry_run=0 dotnet_host=${VOICECAT_DOTNET:-} if [[ -z "$dotnet_host" ]]; then if [[ -x /usr/local/share/dotnet/dotnet ]]; then dotnet_host=/usr/local/share/dotnet/dotnet else dotnet_host=$(command -v dotnet) fi fi if [[ ${1:-} == "--dry-run" ]]; then dry_run=1 fi publish_properties=(-p:ArchiveOnBuild=false) if (( dry_run )); then # Hardened-runtime library validation requires the executable and every bundled dylib to # have the same real Team ID. Ad-hoc signatures have no Team ID, so a hardened ad-hoc app # passes codesign verification but is rejected by dyld at launch. Keep local validation # ad-hoc and reserve the hardened runtime for the Developer ID distribution path below. publish_properties+=(-p:UseHardenedRuntime=false) fi "$dotnet_host" publish "$project" -c "$configuration" --no-restore "${publish_properties[@]}" built_app="$script_dir/VoiceCat.Mac/bin/Release/net10.0-macos27.0/osx-arm64/VoiceCat.app" if [[ ! -d "$built_app" ]]; then print -u2 "VoiceCat.app was not produced at $built_app" exit 1 fi # Signing mutates every nested runtime binary. Work on a disposable distribution copy so a # validation run can never poison MSBuild's incremental output or its signing caches. distribution="$script_dir/VoiceCat.Mac/bin/Release/distribution" app="$distribution/VoiceCat.app" if [[ -e "$app" ]]; then rm -rf "$app" fi mkdir -p "$distribution" ditto "$built_app" "$app" entitlements="$script_dir/VoiceCat.Mac/VoiceCat.Mac.entitlements" sign_nested_code() { local identity=$1 local timestamp_option=${2:-} local runtime_option=${3:-} local nested local sign_options=(--force) [[ -n "$timestamp_option" ]] && sign_options+=("$timestamp_option") [[ -n "$runtime_option" ]] && sign_options+=("$runtime_option") # Do not use codesign --deep for signing. It is intended as an emergency repair operation # and can apply the app's entitlements to nested code. Sign each bundled Mach-O library first, # then sign the outer app once with its own entitlements. while IFS= read -r -d '' nested; do codesign "${sign_options[@]}" --sign "$identity" "$nested" done < <(find "$app/Contents" -type f \( -name '*.dylib' -o -name '*.so' \) -print0) } install_verified_primary_bundle() { # dotnet publish must create the workload bundle before it can be normalized. Replace that # misleading intermediate with the verified result so both documented output paths work. rm -rf "$built_app" ditto "$app" "$built_app" } if (( dry_run )); then sign_nested_code - codesign --force --entitlements "$entitlements" --sign - "$app" codesign --verify --deep --strict "$app" spctl --assess --type execute "$app" 2>/dev/null || true install_verified_primary_bundle print "Validated ad-hoc build: $app" exit 0 fi identity=${VOICECAT_CODESIGN_IDENTITY:-} if [[ -z "$identity" ]]; then print -u2 "Set VOICECAT_CODESIGN_IDENTITY to a Developer ID Application identity." exit 2 fi sign_nested_code "$identity" --timestamp --options=runtime codesign --force --timestamp --options runtime \ --entitlements "$entitlements" \ --sign "$identity" "$app" codesign --verify --deep --strict --verbose=2 "$app" install_verified_primary_bundle archive="$distribution/VoiceCat-macOS-arm64.zip" ditto -c -k --keepParent "$app" "$archive" if [[ -n ${APPLE_ID:-} && -n ${APPLE_TEAM_ID:-} && -n ${APPLE_APP_PASSWORD:-} ]]; then xcrun notarytool submit "$archive" --wait \ --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_PASSWORD" xcrun stapler staple "$app" xcrun stapler validate "$app" spctl --assess --type execute --verbose=2 "$app" ditto -c -k --keepParent "$app" "$archive" print "Signed, notarized, and stapled: $archive" else print "Signed archive produced without notarization: $archive" print "Set APPLE_ID, APPLE_TEAM_ID, and APPLE_APP_PASSWORD to submit it." fi