diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..ef2a7e8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,28 @@ +#!/bin/bash +cd /Volumes/Developer/Applications/DbSyn +&& rm -rf ./build +&& +xcodebuild +-project +DBSyn.xcodeproj +-scheme +DBSyn +-configuration +Debug +-destination +'platform=macOS,arch=arm64' +-derivedDataPath +./build +clean +build +&& +{ +pkill +-x +DBSyn +2>/dev/null +|| +true; +open +./build/Build/Products/Debug/DBSyn.app; +} \ No newline at end of file diff --git a/scripts/release-dbsyn-direct.sh b/scripts/release-dbsyn-direct.sh deleted file mode 100755 index c27c5f5..0000000 --- a/scripts/release-dbsyn-direct.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# DBSyn — Developer ID direct distribution (signed and notarized app + DMG) -# Install at: /scripts/release-dbsyn-direct.sh -# Run from any directory: ./scripts/release-dbsyn-direct.sh - -APP_NAME="DBSyn" -SCHEME="DBSyn" -PROJECT="DBSyn.xcodeproj" -TEAM_ID="${TEAM_ID:-39R6U3437D}" -SIGNING_IDENTITY="${SIGNING_IDENTITY:-B99D353C88996866C49E4389B375645F76870A3F}" -NOTARY_PROFILE="${NOTARY_PROFILE:-orion-notary}" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -if [[ -d "$SCRIPT_DIR/$PROJECT" ]]; then - ROOT_DIR="$SCRIPT_DIR" -elif [[ -d "$SCRIPT_DIR/../$PROJECT" ]]; then - ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -else - echo "ERROR: $PROJECT not found next to this script or in its parent directory." >&2 - exit 1 -fi - -BUILD_DIR="$ROOT_DIR/build/direct-distribution" -ARCHIVE_PATH="$BUILD_DIR/$APP_NAME.xcarchive" -EXPORT_DIR="$BUILD_DIR/export" -DMG_STAGE="$BUILD_DIR/dmg-stage" -EXPORT_OPTIONS="$BUILD_DIR/ExportOptions.plist" -APP_ZIP="$BUILD_DIR/$APP_NAME-notarization.zip" -RELEASE_DIR="$ROOT_DIR/releases" -DMG_PATH="$RELEASE_DIR/$APP_NAME.dmg" - -for tool in xcodebuild xcrun codesign spctl ditto hdiutil shasum security; do - command -v "$tool" >/dev/null || { echo "ERROR: Missing $tool" >&2; exit 1; } -done -[[ -d "$ROOT_DIR/$PROJECT" ]] || { echo "ERROR: Missing $PROJECT" >&2; exit 1; } -security find-identity -v -p codesigning | grep -Fq "$SIGNING_IDENTITY" || { - echo "ERROR: Signing identity $SIGNING_IDENTITY not available in Keychain." >&2 - exit 1 -} -# Check that notarytool profile exists before spending time compiling. -xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null || { - echo "ERROR: Notarytool profile '$NOTARY_PROFILE' is unavailable or invalid." >&2 - exit 1 -} - -printf '\n========== DBSyn DIRECT DISTRIBUTION ==========\n' -cd "$ROOT_DIR" -rm -rf "$BUILD_DIR" -mkdir -p "$BUILD_DIR" "$EXPORT_DIR" "$DMG_STAGE" "$RELEASE_DIR" -# Keep previous releases until a new notarized DMG has been successfully built. - -cat > "$EXPORT_OPTIONS" < - - - methoddeveloper-id - signingStyleautomatic - teamID$TEAM_ID - -PLIST - -echo '==> Archiving DBSyn (Release)...' -xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \ - -destination 'generic/platform=macOS' -archivePath "$ARCHIVE_PATH" \ - DEVELOPMENT_TEAM="$TEAM_ID" CODE_SIGN_STYLE=Automatic \ - clean archive - -echo '==> Exporting Developer ID application...' -xcodebuild -exportArchive -archivePath "$ARCHIVE_PATH" \ - -exportPath "$EXPORT_DIR" -exportOptionsPlist "$EXPORT_OPTIONS" -APP_PATH="$EXPORT_DIR/$APP_NAME.app" - -# ------------------------------------------------------------ -# Enforce Developer ID signing with hardened runtime -# ------------------------------------------------------------ - -echo -echo "==> Signing DBSyn with hardened runtime..." - -codesign \ - --force \ - --deep \ - --options runtime \ - --timestamp \ - --sign "$SIGNING_IDENTITY" \ - "$APP_PATH" - -echo "==> Verifying hardened runtime..." - -codesign --verify --deep --strict --verbose=2 "$APP_PATH" - -SIGNATURE_INFO="$(codesign -dv --verbose=4 "$APP_PATH" 2>&1)" - -printf '%s\n' "$SIGNATURE_INFO" - -if [[ "$SIGNATURE_INFO" != *"(runtime)"* ]]; then - echo "ERROR: Hardened runtime signing failed." - exit 1 -fi - -echo "Hardened runtime verified." - - -[[ -d "$APP_PATH" ]] || { echo "ERROR: Exported app missing: $APP_PATH" >&2; exit 1; } - -echo '==> Checking application signature and hardened runtime...' -codesign --verify --deep --strict --verbose=2 "$APP_PATH" -codesign -dv --verbose=4 "$APP_PATH" 2>&1 | grep -E 'Identifier=|Authority=|TeamIdentifier=|Runtime Version=|flags=' || true -# codesign -d prints details to stderr and may return a nonzero status even -# when the signature is valid. Under `set -o pipefail`, piping it to grep -# can therefore reject a correctly signed app. Capture once and inspect. -SIGNATURE_INFO="$(codesign -dv --verbose=4 "$APP_PATH" 2>&1 || true)" -ACTUAL_TEAM="$(printf '%s\n' "$SIGNATURE_INFO" | sed -n 's/^TeamIdentifier=//p' | head -n 1)" -[[ "$ACTUAL_TEAM" == "$TEAM_ID" ]] || { echo "ERROR: Unexpected signing team: $ACTUAL_TEAM" >&2; exit 1; } -if ! grep -Eq 'flags=0x[[:xdigit:]]+\(runtime\)' <<< "$SIGNATURE_INFO"; then - echo 'ERROR: App is not signed with the hardened runtime required for notarization.' >&2 - exit 1 -fi -echo '==> Hardened runtime verified.' - -echo '==> Notarizing application...' -ditto -c -k --keepParent "$APP_PATH" "$APP_ZIP" -xcrun notarytool submit "$APP_ZIP" --keychain-profile "$NOTARY_PROFILE" --wait -xcrun stapler staple "$APP_PATH" -xcrun stapler validate "$APP_PATH" -spctl --assess --type execute --verbose=4 "$APP_PATH" - -echo '==> Creating installer DMG...' -ditto "$APP_PATH" "$DMG_STAGE/$APP_NAME.app" -ln -s /Applications "$DMG_STAGE/Applications" -TMP_DMG="$BUILD_DIR/$APP_NAME.dmg" -hdiutil create -volname "$APP_NAME" -srcfolder "$DMG_STAGE" -ov -format UDZO "$TMP_DMG" - -echo '==> Signing and notarizing DMG...' -codesign --force --timestamp --sign "$SIGNING_IDENTITY" "$TMP_DMG" -codesign --verify --verbose=2 "$TMP_DMG" -xcrun notarytool submit "$TMP_DMG" --keychain-profile "$NOTARY_PROFILE" --wait -xcrun stapler staple "$TMP_DMG" -xcrun stapler validate "$TMP_DMG" -spctl --assess --type open --context context:primary-signature --verbose=4 "$TMP_DMG" - -# Publish only after all checks have passed. -cp -f "$TMP_DMG" "$DMG_PATH" -echo '==> Release ready:' -ls -lh "$DMG_PATH" -shasum -a 256 "$DMG_PATH" -printf '\n========== DBSyn RELEASE COMPLETE ==========\n%s\n' "$DMG_PATH" diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..7d47afe --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,157 @@ +#!/bin/bash +set -euo pipefail + +# DBSyn direct distribution: Developer ID signed, notarized app and DMG. +# macOS/Xcode required. Install as scripts/release-dbsyn-direct.sh. +APP_NAME=DBSyn +SCHEME=DBSyn +PROJECT=DBSyn.xcodeproj +TEAM_ID="${TEAM_ID:-39R6U3437D}" +SIGNING_IDENTITY="${SIGNING_IDENTITY:-B99D353C88996866C49E4389B375645F76870A3F}" +NOTARY_PROFILE="${NOTARY_PROFILE:-orion-notary}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ -d "$SCRIPT_DIR/$PROJECT" ]]; then ROOT_DIR="$SCRIPT_DIR" +elif [[ -d "$SCRIPT_DIR/../$PROJECT" ]]; then ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +else echo "ERROR: Cannot locate $PROJECT" >&2; exit 1; fi +BUILD_DIR="$ROOT_DIR/build/direct-distribution" +ARCHIVE_PATH="$BUILD_DIR/$APP_NAME.xcarchive" +EXPORT_DIR="$BUILD_DIR/export" +EXPORT_OPTIONS="$BUILD_DIR/ExportOptions.plist" +APP_PATH="$EXPORT_DIR/$APP_NAME.app" +APP_ZIP="$BUILD_DIR/$APP_NAME-notarization.zip" +DMG_STAGE="$BUILD_DIR/dmg-stage" +RW_DMG="$BUILD_DIR/$APP_NAME-rw.dmg" +TMP_DMG="$BUILD_DIR/$APP_NAME.dmg" +RELEASE_DIR="$ROOT_DIR/releases" +DMG_PATH="$RELEASE_DIR/$APP_NAME.dmg" +MOUNT_POINT="$BUILD_DIR/dmg-mount" + +for tool in xcodebuild xcrun codesign spctl ditto hdiutil shasum security swift sips /usr/libexec/PlistBuddy; do + command -v "$tool" >/dev/null || { echo "ERROR: Missing $tool" >&2; exit 1; } +done +if ! security find-identity -v -p codesigning | grep -F "$SIGNING_IDENTITY" >/dev/null; then + echo "ERROR: Developer ID identity not found: $SIGNING_IDENTITY" >&2; exit 1 +fi +xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null || { + echo "ERROR: Notarytool profile '$NOTARY_PROFILE' unavailable" >&2; exit 1; +} + +cd "$ROOT_DIR" +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" "$EXPORT_DIR" "$DMG_STAGE" "$RELEASE_DIR" "$MOUNT_POINT" +cat > "$EXPORT_OPTIONS" < + + +methoddeveloper-id +signingStyleautomatic +teamID$TEAM_ID + +PLIST + +echo '==> Archiving DBSyn with hardened runtime...' +xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \ + -destination 'generic/platform=macOS' -archivePath "$ARCHIVE_PATH" \ + DEVELOPMENT_TEAM="$TEAM_ID" CODE_SIGN_STYLE=Automatic \ + ENABLE_HARDENED_RUNTIME=YES \ + clean archive + +echo '==> Exporting Developer ID app...' +xcodebuild -exportArchive -archivePath "$ARCHIVE_PATH" \ + -exportPath "$EXPORT_DIR" -exportOptionsPlist "$EXPORT_OPTIONS" +[[ -d "$APP_PATH" ]] || { echo "ERROR: Missing exported app" >&2; exit 1; } + +# Xcode's generated Info.plist can omit CFBundleIconFile even when the +# build setting specifies it. Set it on the exported bundle BEFORE the +# final Developer ID signature, so the signed app includes the icon key. +ICON_PATH="$APP_PATH/Contents/Resources/DBSyn.icns" +[[ -s "$ICON_PATH" ]] || { echo "ERROR: Missing bundled icon $ICON_PATH" >&2; exit 1; } +PLIST_PATH="$APP_PATH/Contents/Info.plist" +ICON_KEY="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH" 2>/dev/null || true)" +if [[ "$ICON_KEY" != DBSyn && "$ICON_KEY" != DBSyn.icns ]]; then + echo "==> Setting CFBundleIconFile in exported app (before signing)..." + if /usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH" >/dev/null 2>&1; then + /usr/libexec/PlistBuddy -c 'Set :CFBundleIconFile DBSyn.icns' "$PLIST_PATH" + else + /usr/libexec/PlistBuddy -c 'Add :CFBundleIconFile string DBSyn.icns' "$PLIST_PATH" + fi +fi +ICON_KEY="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH")" +[[ "$ICON_KEY" == DBSyn || "$ICON_KEY" == DBSyn.icns ]] || { + echo "ERROR: CFBundleIconFile still incorrect: '$ICON_KEY'" >&2; exit 1; +} +echo "==> Application icon verified: $ICON_KEY" +sips -g pixelWidth -g pixelHeight "$ICON_PATH" + +# Export may omit hardened runtime; sign exported app explicitly. +# Sign embedded code first if your project adds frameworks/helpers in future. +echo '==> Developer ID signing with hardened runtime...' +codesign --force --deep --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$APP_PATH" +codesign --verify --deep --strict --verbose=2 "$APP_PATH" +SIGNATURE_INFO="$(codesign -dv --verbose=4 "$APP_PATH" 2>&1 || true)" +printf '%s\n' "$SIGNATURE_INFO" | grep -E 'Identifier=|Authority=|TeamIdentifier=|flags=' || true +[[ "$SIGNATURE_INFO" == *"TeamIdentifier=$TEAM_ID"* ]] || { echo 'ERROR: Wrong signing team' >&2; exit 1; } +[[ "$SIGNATURE_INFO" == *'(runtime)'* ]] || { echo 'ERROR: Hardened runtime missing' >&2; exit 1; } + +echo '==> Notarizing and stapling app...' +ditto -c -k --keepParent "$APP_PATH" "$APP_ZIP" +xcrun notarytool submit "$APP_ZIP" --keychain-profile "$NOTARY_PROFILE" --wait +xcrun stapler staple "$APP_PATH" +xcrun stapler validate "$APP_PATH" +spctl --assess --type execute --verbose=4 "$APP_PATH" + +echo '==> Preparing writable DMG...' +ditto "$APP_PATH" "$DMG_STAGE/$APP_NAME.app" +ln -s /Applications "$DMG_STAGE/Applications" +# Create writable image first: the custom volume icon must be set ON the +# mounted filesystem, not on the staging directory. +hdiutil create -size 100m -fs HFS+ -volname "$APP_NAME" -format UDRW -ov "$RW_DMG" +MOUNTED=0 +cleanup() { + if [[ "$MOUNTED" == 1 ]]; then hdiutil detach "$MOUNT_POINT" -quiet || true; fi +} +trap cleanup EXIT +hdiutil attach "$RW_DMG" -nobrowse -noverify -mountpoint "$MOUNT_POINT" -quiet +MOUNTED=1 +ditto "$DMG_STAGE/$APP_NAME.app" "$MOUNT_POINT/$APP_NAME.app" +ln -s /Applications "$MOUNT_POINT/Applications" +cp "$ICON_PATH" "$MOUNT_POINT/.VolumeIcon.icns" +xcrun SetFile -a C "$MOUNT_POINT" +hdiutil detach "$MOUNT_POINT" -quiet +MOUNTED=0 +hdiutil convert "$RW_DMG" -format UDZO -o "$TMP_DMG" -ov + +# A DMG *file* icon is different from its mounted volume icon. Use AppKit to +# assign a Finder custom icon to the DMG file before signing/notarization. +# This creates Finder metadata (custom icon resource/finder flag). +ICON_SWIFT="$BUILD_DIR/set-dmg-file-icon.swift" +cat > "$ICON_SWIFT" <<'SWIFT' +import AppKit +import Foundation +let arguments = CommandLine.arguments +precondition(arguments.count == 3, "Usage: icon.swift ") +guard let icon = NSImage(contentsOfFile: arguments[1]) else { + fputs("ERROR: Cannot load ICNS image\n", stderr); exit(1) +} +guard NSWorkspace.shared.setIcon(icon, forFile: arguments[2], options: []) else { + fputs("ERROR: NSWorkspace could not set DMG Finder icon\n", stderr); exit(1) +} +print("Custom Finder icon assigned to DMG file") +SWIFT +echo '==> Assigning Finder icon to DMG file...' +swift "$ICON_SWIFT" "$ICON_PATH" "$TMP_DMG" + +echo '==> Signing and notarizing DMG...' +codesign --force --timestamp --sign "$SIGNING_IDENTITY" "$TMP_DMG" +codesign --verify --verbose=2 "$TMP_DMG" +xcrun notarytool submit "$TMP_DMG" --keychain-profile "$NOTARY_PROFILE" --wait +xcrun stapler staple "$TMP_DMG" +xcrun stapler validate "$TMP_DMG" +spctl --assess --type open --context context:primary-signature --verbose=4 "$TMP_DMG" + +# Publish using ditto so Finder metadata/resource forks survive the copy. +ditto "$TMP_DMG" "$DMG_PATH" +echo '==> Release ready:' +ls -lh "$DMG_PATH" +shasum -a 256 "$DMG_PATH" +printf '\n========== DBSyn RELEASE COMPLETE ==========\n%s\n' "$DMG_PATH"