diff --git a/.gitignore b/.gitignore index c6ad953..355f586 100644 --- a/.gitignore +++ b/.gitignore @@ -101,6 +101,7 @@ lib/ # Application bundle for Mac OS *.app/ +dist/ wdspWisdom00 *.log ewsdr diff --git a/bundle_app.sh b/bundle_app.sh new file mode 100755 index 0000000..82fb55e --- /dev/null +++ b/bundle_app.sh @@ -0,0 +1,249 @@ +#!/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)" + +# ── Locate source .app ──────────────────────────────────────────────────────── +if [[ $# -ge 1 ]]; then + SRC_APP="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +else + SRC_APP="$(find "$SCRIPT_DIR/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="$SCRIPT_DIR/dist" +fi + +mkdir -p "$OUT_DIR" + +# ── Generate .icns from icon/ folder ───────────────────────────────────────── +ICNS_PATH="" +ICON_SRC="$SCRIPT_DIR/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" + } + + echo " Scanning dependencies..." + collect_and_copy "$EXEC_PATH" + + 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