#! /bin/sh
#shellcheck disable=SC3043

#
# Creates macOS .app bundle from pd build.
#
# Make sure pd has been configured and built
# before running this.
#
# Inspired by Inkscape's osx-app.sh builder.
#
# Dan Wilcox danomatika.com
#

# stop on error
set -e

verbose=
universal=
fix_usrlocallib=true
included_wish=true
EXTERNAL_EXT=d_fat
TK=
SYS_TK=Current
WISH=
PD_VERSION=

# ad hoc by default
SIGNATURE_ID="-"

# source dir, relative to this script
SRC=..

# build dir, relative to working directory
custom_builddir=false
BUILD=..

# which installation method to chose
# either 'manually' or 'make'
# per default we use 'make' for custom-builddirs
# and 'manually' otherwise
install_type=

# PlistBuddy command for editing app bundle Info.plist from template
PLIST_BUDDY=/usr/libexec/PlistBuddy

# a scratchdir that is automatically cleaned up on exit
scratchdir=$(mktemp -d)
trap 'rm -rf "${scratchdir}"' EXIT INT TERM


# echo to stderr
error() {
    echo "$*" 1>&2
}

# Help message
#----------------------------------------------------------
help() {
cat <<EOF
Usage: osx-app.sh [OPTIONS] [VERSION]

  Creates a Pd .app bundle for macOS using a Tk Wish.app wrapper

  Uses the included Tk Wish.app at mac/stuff/wish-shell.tgz by default

Options:
  -h,--help           display this help message

  -v,--verbose        verbose copy prints

  -w,--wish APP       use a specific Wish.app

  --sign SIGNATURE_ID use SIGNATURE_ID for signing the app.
                      the default is "-", which means ad-hoc signing

  -s,--system-tk VER  use a version of the Tk Wish.app installed on the system,
                      searches in /Library first then /System/Library second,
                      naming is "8.4", "8.5", "Current", etc

  -t,--tk VER         use a version of Wish.app with embedded Tcl/Tk
                      frameworks, downloads and builds using tcltk-wish.sh

  --universal         "universal" multi-arch build when using -t,--tk:
                      a combination of ppc, i386, x86_64, and/or arm64
                      depending on detected macOS SDK

  --builddir DIR      set pd build directory path

  --installtype TYP   select how files are collected into the .app bundle
                      "manually" - use builtin logic to determine files
                      "make" - use 'make install'
                      not setting this value, will use 'make' if you set
                      the '--builddir', and 'manually' otherwise

  --dont-fix-usrlocallib   for arm64 binaries (only), ensure that dependencies
                      are not only searched for in /usr/local/lib,
                      but also in /opt/homebrew/lib

Arguments:

  VERSION             optional string to use in file name ie. Pd-VERSION.app
                      configure --version output used for app plist if not set

Examples:

    # create Pd.app with included Tk 8.4 Wish,
    # version string set automatically
    osx-app.sh

    # create Pd-0.47-0.app with included Tk 8.4 Wish,
    # uses specified version string
    osx-app.sh 0.47-0

    # create Pd-0.47-0.app with the system's Tk 8.4 Wish.app
    osx-app.sh --system-tk 8.4 0.47-0

    # create Pd-0.47-0.app by downloading & building Tk 8.5.19
    osx-app.sh --tk 8.5.19 0.47-0

    # same as above, but with a "universal" multi-arch build
    osx-app.sh --tk 8.5.19 --universal 0.47-0

    # use Wish-8.6.5.app manually built with tcltk-wish.sh
    osx-app.sh --wish Wish-8.6.5 0.47-0

EOF
}

install_manually() {
    # pd app bundle destination path
    local DEST="${APP}/Contents/Resources"

    # install binaries
    mkdir -p "${DEST}/bin"
    cp -R $verbose "${BUILD}/src/pd"          "${DEST}/bin/"
    cp -R $verbose "${BUILD}/src/pdsend"      "${DEST}/bin/"
    cp -R $verbose "${BUILD}/src/pdreceive"   "${DEST}/bin/"
    cp -R $verbose "${BUILD}/src/pd-watchdog" "${DEST}/bin/" || true

    # install resources
    mkdir -p "${DEST}/po"
    cp -R $verbose "${SRC}/doc"         "${DEST}/"
    cp -R $verbose "${SRC}/extra"       "${DEST}/"
    cp -R $verbose "${SRC}/tcl"         "${DEST}/"
    rm -f "${DEST}/tcl/pd.ico" "${DEST}/tcl/pd-gui.in"
    if [ ! "${BUILD}" -ef "${SRC}" ] ; then # are src and build dirs not the same?
        # compiled externals are in the build dir
        cp -R $verbose "${BUILD}/extra" "${DEST}/"
    fi

    # install translations if they were built
    if [ -e "${BUILD}/po/es.msg" ] ; then
        mkdir -p "${DEST}/po"
        cp $verbose "${BUILD}/po"/*.msg "${DEST}/po/"
    else
        error "No localizations found. Skipping po dir..."
    fi

    # install headers
    mkdir -p "${DEST}/src"
    cp $verbose "${SRC}/src"/*.h "${DEST}/src/"

    # clean extra folders
    cd "${DEST}/extra"
    rm -f makefile.subdir
    find ./* -prune -type d | while read -r ext; do
        ext_lib="${ext}.${EXTERNAL_EXT}"
        if [ -e "${ext}/.libs/${ext_lib}" ] ; then

            # remove any symlinks to the compiled external
            rm -f "${ext}"/*."${EXTERNAL_EXT}"

            # mv compiled external into main folder
            mv "${ext}"/.libs/*."${EXTERNAL_EXT}" "${ext}/"

            # remove libtool build folders & unneeded build files
            rm -rf "${ext}/.libs"
            rm -rf "${ext}/.deps"
            rm -f "${ext}"/*.c "${ext}"/*.o "${ext}"/*.lo "${ext}"/*.la
            rm -f "${ext}"/GNUmakefile* "${ext}"/makefile*
        fi
    done
    cd - > /dev/null # quiet

    cp stuff/pd.icns "${DEST}/"
    cp stuff/pd-file.icns "${DEST}/"
    cp -R "${SRC}/font" "${DEST}/"

    # install licenses
    cp $verbose "${SRC}/README.txt"  "${DEST}/"
    cp $verbose "${SRC}/LICENSE.txt" "${DEST}/"


    # clean Makefiles
    find "${DEST}" -name "Makefile*" -type f -delete

    # create any needed symlinks
    ln -s tcl "${DEST}/Scripts"
}

install_make() {
    local DEST="${APP}/Contents/Resources"
    make install -C "${BUILD}" DESTDIR="${DEST}" prefix=/ libdir=/ pkglibdir=/ datarootdir=/ pkgdatadir=/ includedir=/src pkgincludedir=/src

    find "${DEST}" -type f -name "*.la" -delete

    # create any needed symlinks
    ln -s tcl "${DEST}/Scripts"
}

register_l10n() {
    # register translations in the plist file
    # usage: register_l10n "${INFO_PLIST}" "${BUILD}/po"
    #   adds an entry for each translation found in '${BUILD}/po' to '${INFO_PLIST}'
    local po
    find "$2" -maxdepth 1 -type f -name "*.msg" -exec basename {} .msg ";" | sort | while read -r po; do
        "${PLIST_BUDDY}" -c "Print :CFBundleLocalizations" "$1" >/dev/null 2>&1 || \
            "${PLIST_BUDDY}" -c "Add CFBundleLocalizations array" "$1"
        "${PLIST_BUDDY}" -c "Add :CFBundleLocalizations: string ${po}" "$1"
    done
}

usrlocallib_to_opthomebrewlib() {
    # for amd64 binaries, replace all dependencies on libs in /usr/local/lib
    # with @rpath dependencies
    # (and add /usr/local/lib and /opt/homebrew/lib) to RPATH
  local infile
  local in
  local lipodir
  local thinfile
  local a
  local archs
  local locallib

  if [ "$verbose" != "" ] ; then
    error "Changing /usr/lcocal/lib dependencies to RPATH with /opt/homebrew/lib"
  fi

  infile="$1"
  in="$(basename "${infile}")"
  lipodir="${scratchdir}/lipo"

  # check if this is a binary
  lipo -archs "${infile}" >/dev/null 2>&1 || return 1

  # split fat binary into thin ones
  archs="$(lipo -archs "${infile}")"
  if [ "$(echo "${archs}" | grep arm64 | wc -w)" -gt 1 ]; then
    # fat binary with multiple architectures
    for a in ${archs}; do
      mkdir -p "${lipodir}/${a}"
      lipo "${infile}" -extract "${a}" -output "${lipodir}/${a}/${in}" || return 1
    done
  elif [ "${archs}" = "arm64" ]; then
    # thin or fat binary, with only a single architecture
    mkdir -p "${lipodir}/arm64/"
    cp "${infile}" "${lipodir}/arm64/${in}"
  fi

  # do we have an arm64 variant?
  thinfile="${lipodir}/arm64/${in}"
  test -e "${thinfile}" || return 1

  # replace all dependencies on /usr/local/lib with @rpath
  # add both /usr/local/lib and /opt/homebrew/lib to RPATH
  otool -L "${thinfile}" | awk '{print $1}' | grep "/usr/local/lib/" | while read -r locallib; do
    install_name_tool \
      -change "${locallib}" "@rpath/$(basename "${locallib}")" \
      -add_rpath /usr/local/lib \
      -add_rpath /opt/homebrew/lib \
      "${thinfile}" || return 1
  done

  # create a fat binary from the mangled thin ones
  find "${lipodir}" -type f -print0 | xargs -0 lipo -output "${infile}" -create || return 1
  return 0
}


# Parse command line arguments
#----------------------------------------------------------
while [ "$1" != "" ] ; do
    case $1 in
        --sign)
            shift 1
            if [ $# = 0 ] ; then
                error "--sign option requires a SIGNATURE_ID argument"
                exit 1
            fi
            SIGNATURE_ID=$1
            ;;
        -t|--tk)
            shift 1
            if [ $# = 0 ] ; then
                error "-t,--tk option requires a VER argument"
                exit 1
            fi
            TK=$1
            included_wish=false
            ;;
        -s|--system-tk)
            if [ $# != 0 ] ; then
                shift 1
                SYS_TK=$1
            fi
            included_wish=false
            ;;
        -w|--wish)
            if [ $# = 0 ] ; then
                error "-w,--wish option requires an APP argument"
                exit 1
            fi
            shift 1
            WISH=${1%/} # remove trailing slash
            included_wish=false
            ;;
        --universal)
            universal=--universal
            ;;
        --builddir)
            if [ $# = 0 ] ; then
                error "--builddir options requires a DIR argument"
                exit 1
            fi
            shift 1
            BUILD=${1%/} # remove trailing slash
            custom_builddir=true
            ;;
        --installtype)
            if [ $# = 0 ] ; then
                error "--installtype option requires a TYPE argument"
                exit 1
            fi
            shift 1
            case "$1" in
                manual|manually)
                    install_type="manual"
                    ;;
                make)
                    install_type="make"
                    ;;
                *)
                    error "invalid installtype (must be 'manually' or 'make')"
                    exit 1
                    ;;
            esac
            ;;
        --dont-fix-userlocalib)
            fix_usrlocallib=false
            ;;
        -v|--verbose)
            verbose=-v
            ;;
        -h|--help)
            help
            exit 0
            ;;
        *)
            break ;;
    esac
    shift 1
done

# check for version argument and set app path in the dir the script is run from
if [ "$1" != "" ] ; then
    APP="$(pwd)/Pd-${1}.app"
else
    # version not specified
    APP="$(pwd)/Pd.app"
fi

# Go
#----------------------------------------------------------

# use a default install-type if none was requested
if [ "${install_type}" = "" ] ; then
   if [ "${custom_builddir}" = true ] ; then
       install_type="make"
   else
       install_type="manual"
   fi
fi

# make sure custom build directory is an absolute path
if [ "${custom_builddir}" = true ] ; then
    if [ "${BUILD}" = "${BUILD#/}" ] ; then
       BUILD="$(pwd)/${BUILD}"
    fi
fi

# change to the dir of this script
cd "$(dirname "$0")"

# grab package version from configure --version output: line 1, word 3
# aka "pd configure 0.47.1" -> "0.47.1"
PD_VERSION=$(${SRC}/configure --version | head -n 1 | cut -d " " -f 3)

# remove old app if found
if [ -d "${APP}" ] ; then
    rm -rf "${APP}"
fi

# check if pd is already built
if [ ! -e "${BUILD}/src/pd" ] ; then
    error "Looks like pd hasn't been built yet. Maybe run make first?"
    exit 1
fi

if [ "${fix_usrlocallib}" = true ]; then
    usrlocallib_to_opthomebrewlib "${BUILD}/src/pd" || error "Couldn't fix /usr/local/lib dependencies."
fi

if [ "$verbose" != "" ] ; then
    error "==== Creating ${APP}"
fi

# extract included Wish app
if [ "${included_wish}" = true ] ; then
    tar xzf stuff/wish-shell.tgz
    if [ -e "Wish Shell.app" ] ; then
        mv "Wish Shell.app" Wish.app
    fi
    WISH=Wish.app

# build Wish or use the system Wish
elif [ "${WISH}" = "" ] ; then
    if [ "${TK}" != "" ] ; then
        error "Using custom ${TK} Wish.app"
        ./tcltk-wish.sh ${universal} "${TK}"
        WISH="Wish-${TK}.app"
    elif [ "${SYS_TK}" != "" ] ; then
        error "Using system ${SYS_TK} Wish.app"
        tk_path=/Library/Frameworks/Tk.framework/Versions
        # check /Library first, then fall back to /System/Library
        if [ ! -e "${tk_path}/${SYS_TK}/Resources/Wish.app" ] ; then
            tk_path="/System${tk_path}"
            if [ ! -e "${tk_path}/${SYS_TK}/Resources/Wish.app" ] ; then
                error "Wish.app not found"
                exit 1
            fi
        fi
        cp -R $verbose "${tk_path}/${SYS_TK}/Resources/Wish.app" .
        WISH=Wish.app
    fi

# make a local copy if using a given Wish.app
else

    # get absolute path in original location
    cd - > /dev/null # quiet
    WISH=$(cd "$(dirname "${WISH}")"; pwd)/$(basename "${WISH}")
    cd - > /dev/null # quiet
    if [ ! -e "${WISH}" ] ; then
        [ ! -e "${WISH}.app" ] || WISH="${WISH}.app"
    fi
    if [ ! -e "${WISH}" ] ; then
        error "${WISH} not found"
        exit 1
    fi
    error "Using $(basename "${WISH}")"

    # copy
    WISH_TMP=$(basename "${WISH}")-tmp
    cp -R "${WISH}" "${WISH_TMP}"
    WISH="${WISH_TMP}"
fi

# sanity check
if [ ! -e "${WISH}" ] ; then
    error "${WISH} not found"
    exit 1
fi

# change app name and rename resources
# try to handle both older "Wish Shell.app" & newer "Wish.app"
mv "${WISH}" "${APP}"
chmod -R u+w "${APP}"

if [ -e "${APP}/Contents/version.plist" ] ; then
    rm "${APP}/Contents/version.plist"
fi
# older "Wish Shell.app" does not have a symlink but a real file
if [ -f "${APP}/Contents/MacOS/Wish Shell" ] && \
   [ ! -L "${APP}/Contents/MacOS/Wish Shell" ] ; then
    mv "${APP}/Contents/MacOS/Wish Shell" "${APP}/Contents/MacOS/Pd"
    mv "${APP}/Contents/Resources/Wish Shell.rsrc" "${APP}/Contents/Resources/Pd.rsrc"
else
    mv "${APP}/Contents/MacOS/Wish" "${APP}/Contents/MacOS/Pd"
    if [ -e "${APP}/Contents/Resources/Wish.rsrc" ] ; then
        mv "${APP}/Contents/Resources/Wish.rsrc" "${APP}/Contents/Resources/Pd.rsrc"
    else
        mv "${APP}/Contents/Resources/Wish.sdef" "${APP}/Contents/Resources/Pd.sdef"
    fi
fi
rm -f "${APP}/Contents/MacOS/Wish"
rm -f "${APP}/Contents/MacOS/Wish Shell"

# prepare bundle resources
cp stuff/Info.plist "${APP}/Contents/"
rm "${APP}/Contents/Resources/Wish.icns"

INFO_PLIST="${APP}/Contents/Info.plist"

# set version identifiers & contextual strings in Info.plist,
# version strings can only use 0-9 and periods, so replace "-" & "test" with "."
PLIST_VERSION=$(echo "${PD_VERSION}" | sed -e 's/[^0-9]/./g' -e 's/\.\.*/./g' -e 's/^\.//' -e 's/\.$//')
"${PLIST_BUDDY}" -c "Set:CFBundleVersion \"$PLIST_VERSION\"" "${INFO_PLIST}"
"${PLIST_BUDDY}" -c "Set:CFBundleShortVersionString \"$PLIST_VERSION\"" "${INFO_PLIST}"
# remove deprecated key as this will display in Finder instead of the version
"${PLIST_BUDDY}" -c "Delete:CFBundleGetInfoString" "${INFO_PLIST}"

case "${install_type}" in
    make)
        install_make
        ;;
    *)
        install_manually
        ;;
esac

# add locale entries to the plist based on available .msg files
# commented out for 0.48-1 because it seems to misbehave - open/save dialogs
# are opening in a random language (and meanwhile Pd doesn't yet respect
# current language setting - I don't know what's going on here.  -msp
# re-enabled for 0.55 -jmz
register_l10n "${INFO_PLIST}" "${APP}/Contents/Resources/po"


# "code signing" which also sets entitlements
# note: "-" identity results in "ad-hoc signing" aka no signing is performed
# for one, this allows loading un-validated external libraries on macOS 10.15+:
# https://cutecoder.org/programming/shared-framework-hardened-runtime
codesign $verbose --force --sign "${SIGNATURE_ID}" --entitlements stuff/pd.entitlements \
    "${APP}/Contents/Frameworks/Tcl.framework/Versions/Current"
codesign $verbose --force --sign "${SIGNATURE_ID}" --entitlements stuff/pd.entitlements \
    "${APP}/Contents/Frameworks/Tk.framework/Versions/Current"
codesign $verbose --deep  --sign "${SIGNATURE_ID}" --entitlements stuff/pd.entitlements \
    "${APP}"

# finish up
touch "${APP}"

if [ "$verbose" != "" ] ; then
    error  "==== Finished ${APP}"
fi
