#!/usr/bin/env bash # # Builds the direct-download macOS artifact: a Developer ID–signed, notarized # .dmg of the VniDropDirect target (the Sparkle-enabled build). Produces: # - apple/dist/VniDrop-.dmg (signed + stapled when notarizing) # # This is the direct-distribution counterpart to the App Store archive flow; it # never touches the App Store `VniDrop` target. The Rust crate is not modified. # # Usage: apple/scripts/build-dmg.sh # # Environment: # DEVELOPER_ID_APP Codesign identity, e.g. "Developer ID Application: … (TEAMID)". # Auto-detected from the keychain when unset. # DEVELOPMENT_TEAM Apple team ID (10 chars). Auto-derived from the identity. # NOTARY_PROFILE Name of a `xcrun notarytool store-credentials` keychain # profile. When set, the DMG is notarized and stapled; when # unset the build still produces a signed DMG and prints the # pending notarization step (useful before creds exist). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" APPLE_DIR="$REPO_ROOT/apple" DIST_DIR="$APPLE_DIR/dist" BUILD_DIR="$APPLE_DIR/.build-dmg" PROJECT="$APPLE_DIR/VniDrop.xcodeproj" SCHEME="VniDropDirect" CONFIG="Release-Direct" APP_NAME="VniDrop" VERSION_RESOLVER="$REPO_ROOT/packaging/version/resolve-version.sh" VERSION_CONFIG_GENERATOR="$REPO_ROOT/packaging/version/generate-apple-xcconfig.sh" VERSION="$("$VERSION_RESOLVER" product)" export VNIDROP_BUILD_TIME_UTC="${VNIDROP_BUILD_TIME_UTC:-$(date -u +%Y%m%d%H%M%S)}" BUILD_NUMBER="$("$VERSION_RESOLVER" apple-direct-build)" "$VERSION_RESOLVER" verify >/dev/null BUILD_METADATA="$DIST_DIR/$APP_NAME-$VERSION.build-info.json" # --- Resolve signing identity ------------------------------------------------ if [ -z "${DEVELOPER_ID_APP:-}" ]; then DEVELOPER_ID_APP="$(security find-identity -v -p codesigning 2>/dev/null \ | sed -nE 's/.*"(Developer ID Application: [^"]+)".*/\1/p' | head -1)" fi if [ -z "${DEVELOPER_ID_APP:-}" ]; then echo "error: no 'Developer ID Application' identity found in the keychain." >&2 echo " Create one in Xcode ▸ Settings ▸ Accounts, or set DEVELOPER_ID_APP." >&2 exit 1 fi if [ -z "${DEVELOPMENT_TEAM:-}" ]; then # The team ID is the 10-char code in the trailing parenthesis of the identity. DEVELOPMENT_TEAM="$(printf '%s' "$DEVELOPER_ID_APP" | sed -nE 's/.*\(([A-Z0-9]{10})\)$/\1/p')" fi echo "==> Direct build v$VERSION (CFBundleVersion $BUILD_NUMBER)" echo " identity: $DEVELOPER_ID_APP" echo " team: ${DEVELOPMENT_TEAM:-}" # --- Build core + regenerate project ---------------------------------------- # Release core needs LTO disabled (workspace thin-LTO miscompiles proc-macros). echo "==> Building Rust core (release)" CARGO_PROFILE_RELEASE_LTO=false "$SCRIPT_DIR/build-core.sh" release echo "==> Regenerating Xcode project" "$VERSION_CONFIG_GENERATOR" all ( cd "$APPLE_DIR" && xcodegen generate >/dev/null ) rm -rf "$BUILD_DIR" && mkdir -p "$BUILD_DIR" "$DIST_DIR" ARCHIVE="$BUILD_DIR/$APP_NAME.xcarchive" EXPORT_DIR="$BUILD_DIR/export" # --- Archive + export (Developer ID) ---------------------------------------- echo "==> Archiving $SCHEME ($CONFIG)" xcodebuild archive \ -project "$PROJECT" \ -scheme "$SCHEME" \ -configuration "$CONFIG" \ -destination 'generic/platform=macOS' \ -archivePath "$ARCHIVE" \ MARKETING_VERSION="$VERSION" \ DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \ CODE_SIGN_STYLE=Manual \ CODE_SIGN_IDENTITY="$DEVELOPER_ID_APP" \ | xcbeautify 2>/dev/null || true [ -d "$ARCHIVE" ] || { echo "error: archive failed" >&2; exit 1; } echo "==> Exporting Developer ID app" EXPORT_OPTS="$BUILD_DIR/ExportOptions.plist" sed "s/\${DEVELOPMENT_TEAM}/$DEVELOPMENT_TEAM/" \ "$SCRIPT_DIR/ExportOptions-DeveloperID.plist" > "$EXPORT_OPTS" xcodebuild -exportArchive \ -archivePath "$ARCHIVE" \ -exportPath "$EXPORT_DIR" \ -exportOptionsPlist "$EXPORT_OPTS" APP="$EXPORT_DIR/$APP_NAME.app" [ -d "$APP" ] || { echo "error: export failed" >&2; exit 1; } ACTUAL_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \ "$APP/Contents/Info.plist")" ACTUAL_BUILD="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' \ "$APP/Contents/Info.plist")" [ "$ACTUAL_VERSION" = "$VERSION" ] || { echo "error: exported app version $ACTUAL_VERSION does not match $VERSION" >&2 exit 1 } [ "$ACTUAL_BUILD" = "$BUILD_NUMBER" ] || { echo "error: exported app build $ACTUAL_BUILD does not match $BUILD_NUMBER" >&2 exit 1 } echo "==> Enforcing hardened-runtime signature" "$SCRIPT_DIR/sign-exported-app.sh" \ "$APP" \ "$DEVELOPER_ID_APP" \ "$APPLE_DIR/VniDrop/Resources/VniDropDirect.entitlements" # --- Build the DMG ----------------------------------------------------------- DMG="$DIST_DIR/$APP_NAME-$VERSION.dmg" rm -f "$DMG" STAGING="$BUILD_DIR/dmg-staging" rm -rf "$STAGING" && mkdir -p "$STAGING" cp -R "$APP" "$STAGING/" ln -s /Applications "$STAGING/Applications" echo "==> Building DMG" if command -v create-dmg >/dev/null 2>&1; then create-dmg \ --volname "$APP_NAME" \ --app-drop-link 380 205 \ --icon "$APP_NAME.app" 130 205 \ --window-size 540 380 \ --no-internet-enable \ "$DMG" "$STAGING" >/dev/null || { # create-dmg exits non-zero if it can't set the fancy layout; fall back. [ -f "$DMG" ] || hdiutil create -volname "$APP_NAME" -srcfolder "$STAGING" \ -ov -format UDZO "$DMG" >/dev/null } else hdiutil create -volname "$APP_NAME" -srcfolder "$STAGING" \ -ov -format UDZO "$DMG" >/dev/null fi echo "==> Signing DMG" codesign --force --sign "$DEVELOPER_ID_APP" --timestamp "$DMG" # --- Notarize + staple ------------------------------------------------------- if [ -n "${NOTARY_PROFILE:-}" ]; then echo "==> Notarizing (profile: $NOTARY_PROFILE)" NOTARY_LOG="$DIST_DIR/$APP_NAME-$VERSION.notary-log.json" "$SCRIPT_DIR/notarize.sh" "$DMG" "$NOTARY_PROFILE" "$NOTARY_LOG" echo "==> Stapling" xcrun stapler staple "$DMG" xcrun stapler validate "$DMG" spctl -a -vvv --type install "$DMG" || true else echo "==> NOTARY_PROFILE unset — skipping notarization." echo " The DMG is signed but NOT notarized; Gatekeeper will block it until" echo " you run 'xcrun notarytool store-credentials' and re-run with NOTARY_PROFILE set." fi SIZE="$(stat -f%z "$DMG")" jq -n \ --arg productVersion "$VERSION" \ --arg directBuildNumber "$BUILD_NUMBER" \ --arg artifact "$(basename "$DMG")" \ '{ productVersion: $productVersion, directBuildNumber: $directBuildNumber, distribution: "direct", artifact: $artifact }' > "$BUILD_METADATA" echo "==> Done." echo " dmg: $DMG" echo " version: $VERSION" echo " build: $BUILD_NUMBER" echo " size: $SIZE bytes"