#!/usr/bin/env bash # # Assemble a macOS .app bundle for a Nucleic build channel. # # scripts/package-app.sh {dev|beta|rc|stable} # # Wraps the channel's bare SwiftPM executable in a proper .app: writes Info.plist, # ships the SwiftPM resource bundles (GRDB / SwiftTerm) inside Contents/Resources, # and ad-hoc code-signs so it launches locally. The channels build to distinct # app names + bundle IDs, so they coexist: # # channel product (process) app bundle bundle id # dev nucleic-local Nucleic Dev.app com.abkslm.nucleic.dev # beta nucleic-beta Nucleic Beta.app com.abkslm.nucleic.beta # rc nucleic-rc Nucleic RC.app com.abkslm.nucleic.rc # stable nucleic Nucleic.app com.abkslm.nucleic # # Env overrides: # NUCLEIC_CONFIG build configuration (default: release) # NUCLEIC_VERSION marketing version / CFBundleShortVersionString (default: 0.1.0) # NUCLEIC_SIGN_ID codesign identity (default: "-", ad-hoc). Set to a Developer ID # to sign for distribution (notarization/TestFlight is separate). # # An app icon is picked up automatically if present at Resources/AppIcon.icns # (or per-channel Resources/AppIcon-.icns). set -euo pipefail CHANNEL="${1:-dev}" CONFIG="${NUCLEIC_CONFIG:-release}" MARKETING_VERSION="${NUCLEIC_VERSION:-0.1.0}" SIGN_ID="${NUCLEIC_SIGN_ID:--}" case "$CHANNEL" in dev) PRODUCT=nucleic-local; APP_NAME="Nucleic Dev"; BUNDLE_ID="com.abkslm.nucleic.dev" ;; beta) PRODUCT=nucleic-beta; APP_NAME="Nucleic Beta"; BUNDLE_ID="com.abkslm.nucleic.beta" ;; rc) PRODUCT=nucleic-rc; APP_NAME="Nucleic RC"; BUNDLE_ID="com.abkslm.nucleic.rc" ;; stable) PRODUCT=nucleic; APP_NAME="Nucleic"; BUNDLE_ID="com.abkslm.nucleic" ;; *) echo "usage: $0 {dev|beta|rc|stable}" >&2; exit 2 ;; esac ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" # iCloud sync drops "conflict copies" beside source files — "Foo 2.swift", "Foo 3.swift" # — that are byte-identical to the original. SwiftPM globs the whole Sources/Tests tree, # so each copy compiles as a second definition and the build dies with "invalid # redeclaration". Sweep them before building. We only touch our own source trees, and # only files whose name is " .swift" with an existing ".swift" alongside, # so a deliberately-named file is never removed. clean_icloud_dupes() { local dupe base removed=0 while IFS= read -r -d '' dupe; do base="${dupe% [0-9].swift}.swift" [ -f "$base" ] || continue rm -f "$dupe" echo " removed iCloud conflict copy: ${dupe#"$ROOT/"}" removed=$((removed + 1)) done < <(find "$ROOT/Sources" "$ROOT/Tests" -type f -name '* [0-9].swift' -print0 2>/dev/null) [ "$removed" -gt 0 ] && echo "▸ Cleaned $removed iCloud conflict cop$([ "$removed" -eq 1 ] && echo y || echo ies) before build" return 0 } clean_icloud_dupes echo "▸ Building $PRODUCT (channel=$CHANNEL, config=$CONFIG)" NUCLEIC_CHANNEL="$CHANNEL" swift build -c "$CONFIG" --build-system native --product "$PRODUCT" BIN_DIR="$(NUCLEIC_CHANNEL="$CHANNEL" swift build -c "$CONFIG" --build-system native --product "$PRODUCT" --show-bin-path)" BUILD_NUMBER="$(git rev-list --count HEAD 2>/dev/null || echo 1)" COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)" APP="$ROOT/dist/$APP_NAME.app" CONTENTS="$APP/Contents" echo "▸ Assembling $APP" rm -rf "$APP" mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources" # Executable — keep the channel's product name so the OS process name is preserved # even when launched from the bundle. cp "$BIN_DIR/$PRODUCT" "$CONTENTS/MacOS/$PRODUCT" # SwiftPM resource bundles must ship inside the app — `Bundle.module` resolves them # via Bundle.main.resourceURL (= Contents/Resources). shopt -s nullglob for b in "$BIN_DIR"/*.bundle; do cp -R "$b" "$CONTENTS/Resources/" done shopt -u nullglob # Container runtime: NOTHING is required to be bundled. The kernel, the vminitd initfs, and the # sandbox image all download + cache automatically on first use. Optionally, a kernel staged at # Resources/vmlinux-arm64 (scripts/fetch-kernel.sh) is bundled here as an offline/dev fast-path so # the app skips that one download. if [ -f "$ROOT/Resources/vmlinux-arm64" ]; then cp "$ROOT/Resources/vmlinux-arm64" "$CONTENTS/Resources/vmlinux-arm64" echo " • bundled Resources/vmlinux-arm64 (kernel fast-path)" else echo " • kernel not bundled — it downloads automatically at runtime (optional: scripts/fetch-kernel.sh)" fi # Optional icon. ICON_PLIST="" ICON_SRC="" [ -f "$ROOT/Resources/AppIcon-$CHANNEL.icns" ] && ICON_SRC="$ROOT/Resources/AppIcon-$CHANNEL.icns" [ -z "$ICON_SRC" ] && [ -f "$ROOT/Resources/AppIcon.icns" ] && ICON_SRC="$ROOT/Resources/AppIcon.icns" if [ -n "$ICON_SRC" ]; then cp "$ICON_SRC" "$CONTENTS/Resources/AppIcon.icns" ICON_PLIST=" CFBundleIconFile AppIcon" fi cat > "$CONTENTS/Info.plist" < CFBundleDevelopmentRegion en CFBundleExecutable $PRODUCT CFBundleIdentifier $BUNDLE_ID CFBundleInfoDictionaryVersion 6.0 CFBundleName $APP_NAME CFBundleDisplayName $APP_NAME CFBundlePackageType APPL CFBundleShortVersionString $MARKETING_VERSION CFBundleVersion $BUILD_NUMBER$ICON_PLIST LSMinimumSystemVersion 26.0 NSHighResolutionCapable NSPrincipalClass NSApplication NSSupportsAutomaticTermination NSSupportsSuddenTermination PLIST printf 'APPL????' > "$CONTENTS/PkgInfo" # Strip extended attributes (resource forks / Finder info) that iCloud and Finder # attach to copied files — codesign rejects them with "resource fork, Finder # information, or similar detritus not allowed". SwiftPM ships some resource files # read-only, so make the tree writable first or xattr -c gets EACCES. chmod -R u+w "$APP" xattr -cr "$APP" echo "▸ Code-signing (identity: $SIGN_ID)" # The virtualization entitlement is required to boot Linux VMs in-process (ContainerEngine). # Honored with ad-hoc signing for local dev on Apple silicon; distribution needs it provisioned. codesign --force --options runtime --timestamp=none \ --entitlements "$ROOT/signing/nucleic.entitlements" \ --sign "$SIGN_ID" "$APP" codesign --verify --verbose=1 "$APP" echo "✓ $APP" echo " bundle id $BUNDLE_ID" echo " version $MARKETING_VERSION (build $BUILD_NUMBER, commit $COMMIT)" echo " open with: open \"$APP\""