#!/usr/bin/env bash # Creates two self-contained macOS .app bundles: # EWSDR.app — normal launch # EWSDR_opengl.app — launches with --opengl flag via wrapper script # # Usage: ./bundle_app.sh [path/to/Source.app] [output/dir] # If no arguments given, auto-detects the .app in bin/aarch64-darwin/ # # Skips system libs: /System/, /usr/lib/, @executable_path, @loader_path, @rpath set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Project root holds bin/, icon/, dist/. This script lives in other/, so go up one. PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # ── Optional runtime libraries ──────────────────────────────────────────────── # These are loaded dynamically (dlopen) rather than linked, so they never show # up in the executable's otool -L. They are only needed for optional hardware # (PlutoSDR / AD936x). Bundle them — together with their dependency trees — only # if they are present on the build machine; otherwise the bundle builds fine # without them. OPTIONAL_LIB_NAMES=(libiio libad9361) OPTIONAL_LIB_DIRS=(/usr/local/lib /opt/homebrew/lib /opt/local/lib) # ── Locate source .app ──────────────────────────────────────────────────────── if [[ $# -ge 1 ]]; then SRC_APP="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" else SRC_APP="$(find "$PROJECT_ROOT/bin" -maxdepth 3 -name "*.app" | head -1)" [[ -n "$SRC_APP" ]] || { echo "ERROR: no .app found under bin/"; exit 1; } fi # ── Output directory ────────────────────────────────────────────────────────── if [[ $# -ge 2 ]]; then OUT_DIR="$2" else OUT_DIR="$PROJECT_ROOT/dist" fi mkdir -p "$OUT_DIR" # ── Generate .icns from icon/ folder ───────────────────────────────────────── ICNS_PATH="" ICON_SRC="$PROJECT_ROOT/icon" if [[ -d "$ICON_SRC" ]]; then echo "Generating EWSDR.icns..." ICONSET_TMP="$(mktemp -d)/EWSDR.iconset" mkdir -p "$ICONSET_TMP" # iconset naming: icon_WxH.png and icon_WxH@2x.png (where @2x = double pixels) [[ -f "$ICON_SRC/icon16.png" ]] && cp "$ICON_SRC/icon16.png" "$ICONSET_TMP/icon_16x16.png" [[ -f "$ICON_SRC/icon32.png" ]] && cp "$ICON_SRC/icon32.png" "$ICONSET_TMP/icon_16x16@2x.png" [[ -f "$ICON_SRC/icon32.png" ]] && cp "$ICON_SRC/icon32.png" "$ICONSET_TMP/icon_32x32.png" [[ -f "$ICON_SRC/icon64.png" ]] && cp "$ICON_SRC/icon64.png" "$ICONSET_TMP/icon_32x32@2x.png" [[ -f "$ICON_SRC/icon128.png" ]] && cp "$ICON_SRC/icon128.png" "$ICONSET_TMP/icon_128x128.png" [[ -f "$ICON_SRC/icon256.png" ]] && cp "$ICON_SRC/icon256.png" "$ICONSET_TMP/icon_128x128@2x.png" [[ -f "$ICON_SRC/icon256.png" ]] && cp "$ICON_SRC/icon256.png" "$ICONSET_TMP/icon_256x256.png" if [[ -f "$ICON_SRC/icon.png" ]]; then sips -z 512 512 "$ICON_SRC/icon.png" --out "$ICONSET_TMP/icon_256x256@2x.png" >/dev/null sips -z 512 512 "$ICON_SRC/icon.png" --out "$ICONSET_TMP/icon_512x512.png" >/dev/null cp "$ICON_SRC/icon.png" "$ICONSET_TMP/icon_512x512@2x.png" fi ICNS_PATH="$OUT_DIR/EWSDR.icns" iconutil -c icns "$ICONSET_TMP" -o "$ICNS_PATH" rm -rf "$(dirname "$ICONSET_TMP")" echo " -> $ICNS_PATH" echo "" fi # ── Resolve source executable name ─────────────────────────────────────────── SRC_EXEC_NAME="$(defaults read "$SRC_APP/Contents/Info" CFBundleExecutable 2>/dev/null || true)" [[ -z "$SRC_EXEC_NAME" ]] && SRC_EXEC_NAME="$(basename "$SRC_APP" .app)" # Resolve the real binary path (Lazarus may place it alongside the .app) SRC_BIN="" CANDIDATE="$(dirname "$SRC_APP")/$SRC_EXEC_NAME" if [[ -f "$CANDIDATE" ]]; then SRC_BIN="$CANDIDATE" fi if [[ -z "$SRC_BIN" ]]; then SRC_EXEC="$SRC_APP/Contents/MacOS/$SRC_EXEC_NAME" if [[ -L "$SRC_EXEC" ]]; then RAW="$(readlink "$SRC_EXEC")" if [[ "$RAW" == /* ]]; then SRC_BIN="$RAW" else SRC_BIN="$(cd "$(dirname "$SRC_EXEC")" && cd "$(dirname "$RAW")" && pwd)/$(basename "$RAW")" fi elif [[ -f "$SRC_EXEC" ]]; then SRC_BIN="$SRC_EXEC" fi fi [[ -f "$SRC_BIN" ]] || { echo "ERROR: executable not found for $SRC_EXEC_NAME"; exit 1; } # ── Helpers ─────────────────────────────────────────────────────────────────── is_system_lib() { local path="$1" [[ "$path" == /System/* ]] && return 0 [[ "$path" == /usr/lib/* ]] && return 0 [[ "$path" == @executable_path/* ]] && return 0 [[ "$path" == @loader_path/* ]] && return 0 [[ "$path" == @rpath/* ]] && return 0 return 1 } # ── Build a self-contained .app bundle ─────────────────────────────────────── # Args: build_bundle() { local OUT_APP="$1" local BUNDLE_NAME="$2" local BUNDLE_ID="$3" local FWK_DIR="$OUT_APP/Contents/Frameworks" echo "==> Building $OUT_APP" rm -rf "$OUT_APP" cp -R "$SRC_APP" "$OUT_APP" mkdir -p "$FWK_DIR" rm -f "$OUT_APP/Contents/MacOS/wdspWisdom"* # Install the real binary (replace symlink left by Lazarus) local EXEC_PATH="$OUT_APP/Contents/MacOS/$SRC_EXEC_NAME" rm -f "$EXEC_PATH" cp "$SRC_BIN" "$EXEC_PATH" chmod +x "$EXEC_PATH" # Update bundle metadata /usr/libexec/PlistBuddy -c "Set :CFBundleName $BUNDLE_NAME" \ "$OUT_APP/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" \ "$OUT_APP/Contents/Info.plist" # Install icon if [[ -n "${ICNS_PATH:-}" && -f "$ICNS_PATH" ]]; then mkdir -p "$OUT_APP/Contents/Resources" cp "$ICNS_PATH" "$OUT_APP/Contents/Resources/EWSDR.icns" /usr/libexec/PlistBuddy -c "Set :CFBundleIconFile EWSDR" \ "$OUT_APP/Contents/Info.plist" 2>/dev/null || \ /usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string EWSDR" \ "$OUT_APP/Contents/Info.plist" fi # Recursive dylib collector declare -A PROCESSED=() collect_and_copy() { local binary="$1" local deps deps="$(otool -L "$binary" | tail -n +2 | awk '{print $1}')" while IFS= read -r dep; do [[ -z "$dep" ]] && continue is_system_lib "$dep" && continue local libname libname="$(basename "$dep")" local dst="$FWK_DIR/$libname" if [[ -f "$dst" || -f "$dep" ]]; then install_name_tool -change "$dep" \ "@executable_path/../Frameworks/$libname" \ "$binary" 2>/dev/null || true fi [[ -n "${PROCESSED[$libname]:-}" ]] && continue if [[ ! -f "$dep" ]]; then echo " WARNING: $dep not found, skipping" continue fi echo " Bundling $libname" cp "$dep" "$dst" chmod 755 "$dst" PROCESSED[$libname]=1 install_name_tool -id "@executable_path/../Frameworks/$libname" "$dst" collect_and_copy "$dst" done <<< "$deps" } # Seed an optional, dlopen'd dylib into the bundle (with its dependency tree). # Arg: path to a dylib (possibly a symlink, e.g. /usr/local/lib/libiio.dylib). seed_optional_lib() { local link="$1" # Follow the symlink chain to the real file. local real real="$(cd "$(dirname "$link")" && pwd)/$(basename "$link")" while [[ -L "$real" ]]; do local target target="$(readlink "$real")" [[ "$target" == /* ]] || target="$(dirname "$real")/$target" real="$target" done [[ -f "$real" ]] || { echo " (optional $(basename "$link") not found)"; return; } # Bundle the real file under the basename advertised by its install id, # so dependents referencing that name (and our alias symlinks) resolve. local idname idname="$(basename "$(otool -D "$real" | tail -n +2 | head -1)")" [[ -n "$idname" ]] || idname="$(basename "$real")" local dst="$FWK_DIR/$idname" if [[ -z "${PROCESSED[$idname]:-}" ]]; then echo " Bundling optional $idname" cp "$real" "$dst" chmod 755 "$dst" PROCESSED[$idname]=1 install_name_tool -id "@executable_path/../Frameworks/$idname" "$dst" collect_and_copy "$dst" fi # Recreate the symlink aliases (libfoo.dylib, libfoo.N.dylib) alongside # it, so a dlopen() by any of those names finds the bundled copy. local srcdir base an srcdir="$(dirname "$link")" base="${idname%%.*}" for alias in "$srcdir/$base".dylib "$srcdir/$base".*.dylib; do [[ -e "$alias" ]] || continue an="$(basename "$alias")" [[ "$an" == "$idname" ]] && continue [[ -e "$FWK_DIR/$an" ]] && continue ln -s "$idname" "$FWK_DIR/$an" done } echo " Scanning dependencies..." collect_and_copy "$EXEC_PATH" echo " Seeding optional dynamically-loaded libs..." for optname in "${OPTIONAL_LIB_NAMES[@]}"; do for optdir in "${OPTIONAL_LIB_DIRS[@]}"; do if [[ -e "$optdir/$optname.dylib" ]]; then seed_optional_lib "$optdir/$optname.dylib" break fi done done while IFS= read -r existing; do local libname libname="$(basename "$existing")" [[ -n "${PROCESSED[$libname]:-}" ]] && continue echo " Re-scanning bundled $libname" PROCESSED[$libname]=1 collect_and_copy "$existing" done < <(find "$FWK_DIR" -name "*.dylib" -type f) # Verify local BAD=0 while IFS= read -r binary; do while IFS= read -r dep; do [[ -z "$dep" ]] && continue is_system_lib "$dep" && continue echo " UNRESOLVED in $(basename "$binary"): $dep" BAD=1 done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}') done < <(find "$OUT_APP" -type f \( -name "*.dylib" -o -perm +0111 \)) [[ $BAD -ne 0 ]] && echo " WARNING: some dependencies are still unresolved." # Sign echo " Signing (ad-hoc)..." find "$FWK_DIR" -name "*.dylib" -type f | while IFS= read -r lib; do codesign --sign - --force "$lib" 2>/dev/null || true done codesign --sign - --force --deep "$OUT_APP" echo " Done: $OUT_APP" echo "" } # ── EWSDR.app ───────────────────────────────────────────────────────────────── echo "Source: $SRC_APP ($SRC_BIN)" echo "" build_bundle "$OUT_DIR/EWSDR.app" "EWSDR" "com.company.ewsdr" # ── EWSDR_opengl.app ────────────────────────────────────────────────────────── OPENGL_APP="$OUT_DIR/EWSDR_opengl.app" echo "==> Building $OPENGL_APP" rm -rf "$OPENGL_APP" cp -R "$OUT_DIR/EWSDR.app" "$OPENGL_APP" # Update metadata /usr/libexec/PlistBuddy -c "Set :CFBundleName EWSDR OpenGL" \ "$OPENGL_APP/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.company.ewsdr-opengl" \ "$OPENGL_APP/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleExecutable EWSDR_opengl" \ "$OPENGL_APP/Contents/Info.plist" # Wrapper script that passes --opengl to the real binary cat > "$OPENGL_APP/Contents/MacOS/EWSDR_opengl" << EOF #!/usr/bin/env bash DIR="\$(cd "\$(dirname "\$0")" && pwd)" exec "\$DIR/$SRC_EXEC_NAME" --opengl "\$@" EOF chmod +x "$OPENGL_APP/Contents/MacOS/EWSDR_opengl" # Re-sign with updated contents echo " Signing (ad-hoc)..." find "$OPENGL_APP/Contents/Frameworks" -name "*.dylib" -type f | while IFS= read -r lib; do codesign --sign - --force "$lib" 2>/dev/null || true done codesign --sign - --force --deep "$OPENGL_APP" echo " Done: $OPENGL_APP" echo "" echo "All bundles ready in $OUT_DIR/" # ── Clean up dist artifacts that are not part of the bundles ───────────────── rm -f "$ICNS_PATH" find "$OUT_DIR" -maxdepth 1 -name ".DS_Store" -delete