#!/usr/bin/env bash
#
# Assemble a macOS .app bundle for a Nucleic build channel.
#
#   scripts/package-app.sh {dev|canary|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          xyz.blakeslee.nucleic.desktop.dev
#   canary   nucleic-canary     Nucleic Canary.app       xyz.blakeslee.nucleic.desktop.canary
#   beta     nucleic-beta       Nucleic Beta.app         xyz.blakeslee.nucleic.desktop.beta
#   rc       nucleic-rc         Nucleic RC.app           xyz.blakeslee.nucleic.desktop.rc
#   stable   nucleic            Nucleic.app              xyz.blakeslee.nucleic.desktop.release
#
# (The production channel keyword stays "stable" everywhere in the build system; only its
# bundle-id suffix is "release", per the requested xyz.blakeslee.nucleic.desktop.* scheme.)
#
# Version comes from ./VERSION (NUCLEIC_MARKETING_VERSION + NUCLEIC_BUILD_NUMBER), maintained by
# scripts/release-macos.sh. This script only reads it; see the Version section below.
#
# Env overrides:
#   NUCLEIC_CONFIG   build configuration (default: release)
#   NUCLEIC_VERSION  marketing version / CFBundleShortVersionString (default: ./VERSION, else 0.1.0)
#   NUCLEIC_BUILD    build number / CFBundleVersion (default: ./VERSION, else git commit count)
#   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. Two formats, in priority order:
#   • Resources/AppIcon.icon  — Icon Composer package (macOS 26 Liquid Glass). Carries the
#     themed appearances (default/dark/clear/tinted); actool compiles it to Assets.car.
#   • Resources/AppIcon.icns  — legacy flat icon, no themed variants.
# Per-channel overrides take precedence over the generic names so each channel can ship a
# visually distinct icon. Two override locations, root wins over Resources/:
#   • ./app-logo-<channel>.{icon,icns}          (repo root — e.g. ./app-logo-canary.icon)
#   • Resources/AppIcon-<channel>.{icon,icns}   (Resources/)

set -euo pipefail

CHANNEL="${1:-dev}"
CONFIG="${NUCLEIC_CONFIG:-release}"
SIGN_ID="${NUCLEIC_SIGN_ID:--}"
# MARKETING_VERSION / BUILD_NUMBER are resolved below, after ROOT, from ./VERSION.

case "$CHANNEL" in
  dev)    PRODUCT=nucleic-local;  APP_NAME="Nucleic Dev";    BUNDLE_ID="xyz.blakeslee.nucleic.desktop.dev" ;;
  canary) PRODUCT=nucleic-canary; APP_NAME="Nucleic Canary"; BUNDLE_ID="xyz.blakeslee.nucleic.desktop.canary" ;;
  beta)   PRODUCT=nucleic-beta;   APP_NAME="Nucleic Beta";   BUNDLE_ID="xyz.blakeslee.nucleic.desktop.beta" ;;
  rc)     PRODUCT=nucleic-rc;     APP_NAME="Nucleic RC";     BUNDLE_ID="xyz.blakeslee.nucleic.desktop.rc" ;;
  stable) PRODUCT=nucleic;        APP_NAME="Nucleic";        BUNDLE_ID="xyz.blakeslee.nucleic.desktop.release" ;;
  *) echo "usage: $0 {dev|canary|beta|rc|stable}" >&2; exit 2 ;;
esac

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

# ── Version ──────────────────────────────────────────────────────────────────────
# Single source of truth is ./VERSION (KEY=value, bumped per release by scripts/release-macos.sh).
# A direct env override still wins for one-off builds (NUCLEIC_VERSION / NUCLEIC_BUILD), and a
# checkout with no VERSION file falls back to 0.1.0 and the git commit count — so `make app-beta`
# and old trees never break. This script only READS the version; only release-macos.sh bumps it.
NUCLEIC_MARKETING_VERSION=""; NUCLEIC_BUILD_NUMBER=""
# shellcheck source=/dev/null
[ -f "$ROOT/VERSION" ] && . "$ROOT/VERSION"
MARKETING_VERSION="${NUCLEIC_VERSION:-${NUCLEIC_MARKETING_VERSION:-0.1.0}}"

# 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 "<base> <n>.swift" with an existing "<base>.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

# Per-mount scratch dir so a build from the VM share doesn't poison the host's .build/
# (and vice versa). Empty on the host; a --scratch-path redirect on the /Volumes share.
# Must be passed to BOTH the build and the --show-bin-path query so they agree. See
# scripts/lib/build-scratch.sh and BUILD.md "Cross-environment builds".
SCRATCH_ARGS="$(bash "$ROOT/scripts/lib/build-scratch.sh" "$ROOT")"

echo "▸ Building $PRODUCT  (channel=$CHANNEL, config=$CONFIG)"
NUCLEIC_CHANNEL="$CHANNEL" swift build $SCRATCH_ARGS -c "$CONFIG" --product "$PRODUCT"
BIN_DIR="$(NUCLEIC_CHANNEL="$CHANNEL" swift build $SCRATCH_ARGS -c "$CONFIG" --product "$PRODUCT" --show-bin-path)"

# Build number: ./VERSION (release-maintained, monotonic) → env override → git commit count.
BUILD_NUMBER="${NUCLEIC_BUILD:-${NUCLEIC_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

# ── macOS-VM service: in-guest agent + provisioner ───────────────────────────────────────
# The one-click "Build base image" (Settings ▸ Virtual Machines) provisions the golden base fully:
# it stages the native NucleicVMAgent.app (computer use over vsock) and scripts/provision-macos-guest.sh
# into the guest over SSH. Bake both into Resources/macvm so the shipped app needs no Swift toolchain
# at runtime — MacVMEngine+Provision.swift resolves them from there. Best-effort: a build host without
# the Swift toolchain (or a non-arm64 build) ships without the agent, so computer use is unavailable
# but the exec-only base still works. The .app is signed inside-out below (nested code).
MACVM_RES="$CONTENTS/Resources/macvm"
VMAGENT_OUT="$ROOT/dist/.vmagent"
mkdir -p "$MACVM_RES"
cp "$ROOT/scripts/provision-macos-guest.sh" "$MACVM_RES/"
if NUCLEIC_VMAGENT_SIGN_IDENTITY="$SIGN_ID" "$ROOT/scripts/build-vm-agent.sh" "$VMAGENT_OUT" >/dev/null 2>&1 \
   && [ -d "$VMAGENT_OUT/NucleicVMAgent.app" ]; then
  cp -R "$VMAGENT_OUT/NucleicVMAgent.app" "$MACVM_RES/"
  cp "$VMAGENT_OUT/xyz.blakeslee.nucleic.vmagent.plist" "$MACVM_RES/" 2>/dev/null || true
  echo "  • embedded NucleicVMAgent.app + provisioner (macOS VM computer use)"
else
  echo "  • NucleicVMAgent.app not built (no Swift toolchain?) — computer use unavailable; provisioner still embedded"
fi

# Sparkle.framework — the in-app auto-updater (direct/non-App-Store distribution). SwiftPM
# emits it into the build products dir; it must live in Contents/Frameworks and be reachable
# from the executable via @executable_path/../Frameworks. It is deep-signed inside-out below.
SPARKLE_FW=""
for cand in "$BIN_DIR/Sparkle.framework" "$BIN_DIR/../Sparkle.framework"; do
  [ -d "$cand" ] && { SPARKLE_FW="$cand"; break; }
done
if [ -n "$SPARKLE_FW" ]; then
  mkdir -p "$CONTENTS/Frameworks"
  cp -R "$SPARKLE_FW" "$CONTENTS/Frameworks/"
  # The SwiftPM-linked binary may not already carry the embedded-frameworks rpath; add it
  # (idempotent — a duplicate just errors harmlessly and is ignored).
  install_name_tool -add_rpath "@executable_path/../Frameworks" "$CONTENTS/MacOS/$PRODUCT" 2>/dev/null || true
  echo "  • embedded Sparkle.framework (auto-update)"
else
  echo "  • Sparkle.framework not found in build products — auto-update will be inert"
fi

# TailscaleKit.framework — the embedded tsnet node behind the Tailnet sync transport. Present
# only when the build linked the local binary artifact (scripts/build-tailscalekit.sh); a build
# without it simply reports "not built in" from the Settings transport picker. Signed by the
# generic embedded-frameworks pass below.
for cand in "$BIN_DIR/TailscaleKit.framework" "$BIN_DIR/../TailscaleKit.framework"; do
  if [ -d "$cand" ]; then
    mkdir -p "$CONTENTS/Frameworks"
    cp -R "$cand" "$CONTENTS/Frameworks/"
    install_name_tool -add_rpath "@executable_path/../Frameworks" "$CONTENTS/MacOS/$PRODUCT" 2>/dev/null || true
    echo "  • embedded TailscaleKit.framework (Tailnet transport)"
    break
  fi
done

# 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. Prefer a themed Icon Composer package (.icon → Assets.car via actool);
# fall back to a flat .icns. Per-channel files win over the generic names.
ICON_PLIST=""
# Icon Composer package (themed). Priority: root per-channel override → Resources/ per-channel
# override → generic. The root ./app-logo-<channel>.icon lets a channel ship a distinct icon
# (e.g. the canary build's ./app-logo-canary.icon) without touching Resources/.
ICON_SET=""
[ -d "$ROOT/app-logo-$CHANNEL.icon" ] && ICON_SET="$ROOT/app-logo-$CHANNEL.icon"
[ -z "$ICON_SET" ] && [ -d "$ROOT/Resources/AppIcon-$CHANNEL.icon" ] && ICON_SET="$ROOT/Resources/AppIcon-$CHANNEL.icon"
[ -z "$ICON_SET" ] && [ -d "$ROOT/Resources/AppIcon.icon" ] && ICON_SET="$ROOT/Resources/AppIcon.icon"
# Legacy flat .icns fallback, same priority order.
ICON_ICNS=""
[ -f "$ROOT/app-logo-$CHANNEL.icns" ] && ICON_ICNS="$ROOT/app-logo-$CHANNEL.icns"
[ -z "$ICON_ICNS" ] && [ -f "$ROOT/Resources/AppIcon-$CHANNEL.icns" ] && ICON_ICNS="$ROOT/Resources/AppIcon-$CHANNEL.icns"
[ -z "$ICON_ICNS" ] && [ -f "$ROOT/Resources/AppIcon.icns" ] && ICON_ICNS="$ROOT/Resources/AppIcon.icns"

if [ -n "$ICON_SET" ]; then
  # actool consumes the .icon directly (no .xcassets wrapper) and emits Assets.car into
  # Contents/Resources. --app-icon names the icon; CFBundleIconName resolves that name
  # against the compiled catalog at launch and is what enables the themed appearances.
  # actool's partial plist is unusable on macOS (it omits CFBundleIconName), so we discard
  # it and write the key ourselves below. ICON_NAME = the .icon basename (matches per-channel).
  ICON_NAME="$(basename "$ICON_SET" .icon)"
  echo "  • compiling themed app icon: ${ICON_SET#"$ROOT/"} (CFBundleIconName=$ICON_NAME)"
  ACTOOL_PLIST="$(mktemp -t actool-partial-plist)"
  xcrun actool "$ICON_SET" \
    --compile "$CONTENTS/Resources" \
    --app-icon "$ICON_NAME" \
    --include-all-app-icons \
    --output-partial-info-plist "$ACTOOL_PLIST" \
    --enable-on-demand-resources NO \
    --development-region en \
    --target-device mac \
    --platform macosx \
    --minimum-deployment-target 26.0 \
    --output-format human-readable-text --notices --warnings --errors
  rm -f "$ACTOOL_PLIST"
  ICON_PLIST="
	<key>CFBundleIconName</key>
	<string>$ICON_NAME</string>"
  # Ship a flat .icns alongside if one exists — harmless belt-and-suspenders for contexts
  # that look for an icon file rather than the asset catalog.
  if [ -n "$ICON_ICNS" ]; then
    cp "$ICON_ICNS" "$CONTENTS/Resources/AppIcon.icns"
    ICON_PLIST="$ICON_PLIST
	<key>CFBundleIconFile</key>
	<string>AppIcon</string>"
  fi
elif [ -n "$ICON_ICNS" ]; then
  cp "$ICON_ICNS" "$CONTENTS/Resources/AppIcon.icns"
  ICON_PLIST="
	<key>CFBundleIconFile</key>
	<string>AppIcon</string>"
else
  echo "  • no app icon found (./app-logo-$CHANNEL.icon, Resources/AppIcon-$CHANNEL.icon, or Resources/AppIcon.icon) — using system default"
fi

# Sparkle auto-update keys. Embedded only for the distribution channels (beta/rc/stable) and
# only when a public EdDSA key is available — a dev/local build ships no feed, so its updater
# stays dormant and the "Check for Updates…" menu item hides itself. The per-channel feed lives
# under NUCLEIC_FEED_BASE (the R2 bucket behind updates.nucleic.blakeslee.xyz that hosts the
# appcasts + DMGs; the GitHub release is a documented fallback origin); the public key is
# the counterpart of the private signing key generated once by Sparkle's `generate_keys` (the
# public half is safe to commit at signing/sparkle_public_ed_key). Both are overridable by env.
FEED_BASE="${NUCLEIC_FEED_BASE:-https://updates.nucleic.blakeslee.xyz}"
SPARKLE_PUB="${NUCLEIC_SPARKLE_PUB:-}"
[ -z "$SPARKLE_PUB" ] && [ -f "$ROOT/signing/sparkle_public_ed_key" ] && \
  SPARKLE_PUB="$(tr -d '[:space:]' < "$ROOT/signing/sparkle_public_ed_key")"
SPARKLE_PLIST=""
if [ "$CHANNEL" != "dev" ] && [ -n "$SPARKLE_PUB" ]; then
  # Background poll cadence. Sparkle defaults to 86400s (once a day) when SUScheduledCheckInterval
  # is absent, which is what every channel but canary uses. Canary ships fast-moving builds, so it
  # checks every hour (3600s) to pull fixes sooner; only canary overrides the default.
  INTERVAL_PLIST=""
  if [ "$CHANNEL" = "canary" ]; then
    INTERVAL_PLIST="
	<key>SUScheduledCheckInterval</key>
	<integer>3600</integer>"
  fi
  SPARKLE_PLIST="
	<key>SUFeedURL</key>
	<string>$FEED_BASE/appcast-$CHANNEL.xml</string>
	<key>SUPublicEDKey</key>
	<string>$SPARKLE_PUB</string>
	<key>SUEnableAutomaticChecks</key>
	<true/>$INTERVAL_PLIST"
  echo "  • Sparkle feed: $FEED_BASE/appcast-$CHANNEL.xml"
  [ "$CHANNEL" = "canary" ] && echo "  • Sparkle check interval: 3600s (1h, canary)"
else
  echo "  • Sparkle feed: not embedded for this build (updater dormant)"
fi

cat > "$CONTENTS/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleExecutable</key>
	<string>$PRODUCT</string>
	<key>CFBundleIdentifier</key>
	<string>$BUNDLE_ID</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>$APP_NAME</string>
	<key>CFBundleDisplayName</key>
	<string>$APP_NAME</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>$MARKETING_VERSION</string>
	<key>CFBundleVersion</key>
	<string>$BUILD_NUMBER</string>$ICON_PLIST$SPARKLE_PLIST
	<key>LSMinimumSystemVersion</key>
	<string>26.0</string>
	<key>NSHighResolutionCapable</key>
	<true/>
	<key>NSPrincipalClass</key>
	<string>NSApplication</string>
	<key>NSSupportsAutomaticTermination</key>
	<true/>
	<key>NSSupportsSuddenTermination</key>
	<true/>
</dict>
</plist>
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"

# ── Code signing ────────────────────────────────────────────────────────────────
# Distribution builds are signed with a "Developer ID Application" identity, the
# hardened runtime (--options runtime, required by notarization), and a SECURE
# TIMESTAMP (also required by notarization). An ad-hoc/local build ("-") can't get a
# secure timestamp, so it's only requested for a real identity.
#
# Hardened runtime is ALSO conditional, and this is load-bearing: the hardened runtime
# forces library validation (CS_REQUIRE_LV), which demands every loaded Mach-O share the
# main executable's Team ID. For a real identity that's fine — the inner Sparkle helpers
# are re-signed with the same team below, so they match. But an AD-HOC binary has no team
# identity, and the app and Sparkle.framework are signed in separate ad-hoc operations, so
# the loader rejects Sparkle at launch with "mapping process and mapped file have different
# Team IDs" (EXC_CRASH / DYLD, even though `codesign --verify` — a static check — passes).
# Ad-hoc local builds don't need (and can't notarize with) the hardened runtime, so we omit
# it for them; that drops the library-validation requirement and the embedded Sparkle loads.
#
# The virtualization entitlement is applied to the main executable so the app can
# boot Linux VMs in-process (ContainerEngine). `com.apple.security.virtualization`
# is NOT a restricted/managed entitlement: it needs no special Apple approval and no
# provisioning profile — a Developer ID cert + notarization is enough. (The
# restricted entitlement we deliberately avoid is com.apple.vm.networking; see BUILD.md.)
# ENTITLEMENTS also depends on the identity. signing/nucleic.entitlements claims
# keychain-access-groups with the team prefix L7UDTQ6F5W. AMFI only honors a team-prefixed
# access group when the signature actually belongs to that team; an ad-hoc build ("-") has
# NO team identity, so baking the group in makes AMFI refuse to spawn the app at launch
# ("Launchd job spawn failed", NSPOSIXErrorDomain 163) even though `codesign --verify` passes.
# KeychainOwnedAccess already detects the missing Team ID and falls back to the login Keychain,
# so ad-hoc builds don't need the group at all — strip it to a virtualization-only entitlements
# file. A real Developer ID identity owns the team and gets the full file.
if [ "$SIGN_ID" = "-" ]; then
  TS_FLAG=(--timestamp=none)          # ad-hoc: no secure timestamp available
  RT_FLAG=()                          # ad-hoc: NO hardened runtime (would force library validation
                                      #         and reject the separately-ad-hoc-signed Sparkle)
  echo "▸ Code-signing ad-hoc (local only — not distributable, not notarizable; no hardened runtime)"
  ENTITLEMENTS="$ROOT/dist/.nucleic-adhoc.entitlements"
  cat > "$ENTITLEMENTS" <<'ENT'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.virtualization</key>
    <true/>
</dict>
</plist>
ENT
else
  TS_FLAG=(--timestamp)               # Developer ID: secure timestamp for notarization
  RT_FLAG=(--options runtime)         # Developer ID: hardened runtime, required by notarization
  echo "▸ Code-signing for distribution (identity: $SIGN_ID)"
  ENTITLEMENTS="$ROOT/signing/nucleic.entitlements"
fi

# Sign inside-out: every nested Mach-O MUST be signed BEFORE the enclosing bundle, or (for a
# real identity) notarization rejects the unsigned inner code. Sparkle.framework carries its own
# helper executables (the Autoupdate tool, the Updater app, and the Installer/Downloader XPC
# services) which codesign does NOT sign transitively when it seals the framework, so each is
# signed explicitly here, deepest-first, exactly as Sparkle's "sign outside Xcode" guidance says.
# (RT_FLAG carries the hardened runtime only for a real identity — see the note above.)
# (${RT_FLAG[@]+...} guard: RT_FLAG is empty for ad-hoc, and bash 3.2 — macOS's /bin/bash —
# errors on "${empty[@]}" under `set -u`; the guard expands to nothing when unset/empty.)
sign_one() { [ -e "$1" ] && codesign --force ${RT_FLAG[@]+"${RT_FLAG[@]}"} "${TS_FLAG[@]}" --sign "$SIGN_ID" "$1"; }
SPK="$CONTENTS/Frameworks/Sparkle.framework"
if [ -d "$SPK" ]; then
  V="$SPK/Versions/B"
  sign_one "$V/XPCServices/Installer.xpc"
  sign_one "$V/XPCServices/Downloader.xpc"
  sign_one "$V/Autoupdate"
  sign_one "$V/Updater.app"
  sign_one "$SPK"
fi
# Any other embedded frameworks (none today) — sign the bundle.
shopt -s nullglob
for nested in "$CONTENTS/Frameworks"/*.framework; do
  [ "$nested" = "$SPK" ] && continue
  codesign --force ${RT_FLAG[@]+"${RT_FLAG[@]}"} "${TS_FLAG[@]}" --sign "$SIGN_ID" "$nested"
done
shopt -u nullglob

# The embedded in-guest agent is nested code — sign it inside-out (like Sparkle) so the app's final
# --deep --strict verify passes and, for a real identity, its designated requirement is STABLE across
# builds (the guest's pre-granted TCC rows key on it; an ad-hoc DR changes every build — see
# scripts/build-vm-agent.sh). sign_one is a no-op when the agent wasn't built into the bundle.
sign_one "$CONTENTS/Resources/macvm/NucleicVMAgent.app"

# Finally the app bundle itself — signs the main executable with the virtualization
# entitlement and seals the bundle.
codesign --force ${RT_FLAG[@]+"${RT_FLAG[@]}"} "${TS_FLAG[@]}" \
  --entitlements "$ENTITLEMENTS" \
  --sign "$SIGN_ID" "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"

echo "✓ $APP"
echo "  bundle id   $BUNDLE_ID"
echo "  version     $MARKETING_VERSION (build $BUILD_NUMBER, commit $COMMIT)"
echo "  open with:  open \"$APP\""
