summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2025-11-19 13:47:42 +0100
committerMichael Peter Christen <mc@yacy.net>2025-11-19 13:47:42 +0100
commit610400d2519f0e80da4b0a3acc9474f09c8ec1d2 (patch)
treec5671f6fca6d6dcf504a9f0385967a189a2eaa96
parent8d3c443ea571d3b71096f7749d3e6dd2c56ee5a2 (diff)
added mac app packager
-rwxr-xr-xpackage_macos_app.sh356
1 files changed, 356 insertions, 0 deletions
diff --git a/package_macos_app.sh b/package_macos_app.sh
new file mode 100755
index 000000000..f0b58d34e
--- /dev/null
+++ b/package_macos_app.sh
@@ -0,0 +1,356 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# -----------------------------------------------------------------------------
+# YaCy macOS Packaging Workflow (standalone script)
+#
+# Prerequisites:
+# - macOS host with Xcode command-line tools (needed for hdiutil/iconutil/sips)
+# - Ant/Java toolchain already used for YaCy builds
+# - Python 3 (only for resolving absolute paths)
+#
+# Typical Flow:
+# 1. Run this script (it defaults to invoking `ant clean all dist` and uses the
+# newest RELEASE/yacy_v*.tar.gz). Pass --skip-ant when reusing an existing
+# tarball.
+# 2. The script assembles YaCy.app plus YaCy-<version>.dmg directly inside RELEASE/.
+# 3. On first launch the .app copies its bundled DATA directory into
+# ~/Library/YaCy/DATA and always starts YaCy with `-gui ~/Library/YaCy`,
+# matching the GUI info dialog. Existing data is preserved on subsequent runs.
+#
+# Useful options:
+# --skip-ant Skip invoking Ant before packaging.
+# -r/--release-tar FILE Use a specific RELEASE/yacy_v*.tar.gz file.
+# -o/--output-dir DIR Where to place the resulting .app/.dmg (default RELEASE/).
+# -n/--app-name NAME Display name for the macOS bundle (default YaCy).
+# --volume-name NAME DMG volume label.
+# --payload-name NAME Subdirectory name under Contents/Resources inside the .app.
+# -i/--icon FILE Override the default addon/YaCy.icns icon.
+# --keep-work Preserve the staging folder for debugging.
+# --ant-cmd CMD Alternate Ant executable/binary path.
+#
+# Customizing the launcher:
+# The generated launcher lives at YaCy.app/Contents/MacOS/<app-name>. It changes
+# into the embedded payload, ensures ~/Library/YaCy/DATA exists (copying defaults
+# on first run), then runs Java with `net.yacy.yacy -gui ~/Library/YaCy`.
+# To tweak JVM flags or the data directory, edit this script's launcher section
+# (search for JAVA_ARGS) before rebuilding, or modify the generated launcher in
+# the .app bundle directly.
+# -----------------------------------------------------------------------------
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="${SCRIPT_DIR}"
+RELEASE_DIR="${PROJECT_ROOT}/RELEASE"
+DEFAULT_ICON="${PROJECT_ROOT}/addon/YaCy.icns"
+OUTPUT_DIR="${RELEASE_DIR}"
+APP_NAME="YaCy"
+VOLUME_NAME="YaCy"
+PAYLOAD_DIR_NAME="YaCy"
+RUN_ANT_BUILD=1
+ANT_CMD="ant"
+ANT_TARGETS=("clean" "all" "dist")
+RELEASE_TAR=""
+ICON_PATH=""
+KEEP_WORK=0
+
+usage() {
+ cat <<'EOF'
+Usage: ./package_macos_app.sh [options]
+
+Build the standard YaCy distribution with Ant (unless --skip-ant is provided),
+wrap the resulting release into a macOS .app bundle, and create a .dmg.
+
+Options:
+ -r, --release-tar <file> Path to an existing yacy_v*.tar.gz to wrap.
+ Defaults to the newest tarball under RELEASE/.
+ -i, --icon <file> Custom .icns file for the app icon.
+ -o, --output-dir <dir> Directory for the .app and .dmg (default RELEASE/).
+ -n, --app-name <name> Name displayed for the .app bundle (default YaCy).
+ --volume-name <name> Volume name for the DMG (default YaCy).
+ --payload-name <name> Directory name used inside Contents/Resources (default YaCy).
+ --skip-ant Do not run "ant clean all dist" automatically.
+ --ant-cmd <command> Ant executable to invoke (default ant).
+ --keep-work Keep the temporary staging directory (for debugging).
+ -h, --help Show this help message.
+EOF
+}
+
+abs_path() {
+ python3 - <<'PY' "$1"
+import os, sys
+print(os.path.abspath(sys.argv[1]))
+PY
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -r|--release-tar)
+ [[ $# -lt 2 ]] && { echo "Missing value for $1" >&2; exit 1; }
+ RELEASE_TAR="$(abs_path "$2")"
+ shift 2
+ ;;
+ -i|--icon)
+ [[ $# -lt 2 ]] && { echo "Missing value for $1" >&2; exit 1; }
+ ICON_PATH="$(abs_path "$2")"
+ shift 2
+ ;;
+ -o|--output-dir)
+ [[ $# -lt 2 ]] && { echo "Missing value for $1" >&2; exit 1; }
+ OUTPUT_DIR="$(abs_path "$2")"
+ shift 2
+ ;;
+ -n|--app-name)
+ [[ $# -lt 2 ]] && { echo "Missing value for $1" >&2; exit 1; }
+ APP_NAME="$2"
+ shift 2
+ ;;
+ --volume-name)
+ [[ $# -lt 2 ]] && { echo "Missing value for $1" >&2; exit 1; }
+ VOLUME_NAME="$2"
+ shift 2
+ ;;
+ --payload-name)
+ [[ $# -lt 2 ]] && { echo "Missing value for $1" >&2; exit 1; }
+ PAYLOAD_DIR_NAME="$2"
+ shift 2
+ ;;
+ --skip-ant)
+ RUN_ANT_BUILD=0
+ shift
+ ;;
+ --ant-cmd)
+ [[ $# -lt 2 ]] && { echo "Missing value for $1" >&2; exit 1; }
+ ANT_CMD="$2"
+ shift 2
+ ;;
+ --keep-work)
+ KEEP_WORK=1
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --)
+ shift
+ break
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+if [[ ${RUN_ANT_BUILD} -eq 1 ]]; then
+ echo "Running ${ANT_CMD} ${ANT_TARGETS[*]}..."
+ (cd "${PROJECT_ROOT}" && "${ANT_CMD}" "${ANT_TARGETS[@]}")
+fi
+
+if [[ -z "${RELEASE_TAR}" ]]; then
+ set +e
+ latest_tar="$(ls -t "${RELEASE_DIR}"/yacy_v*.tar.gz 2>/dev/null | head -n 1)"
+ status=$?
+ set -e
+ if [[ ${status} -ne 0 || -z "${latest_tar}" ]]; then
+ echo "No release tarball found under ${RELEASE_DIR}. Run 'ant clean all dist' first." >&2
+ exit 1
+ fi
+ RELEASE_TAR="$(abs_path "${latest_tar}")"
+fi
+
+if [[ ! -f "${RELEASE_TAR}" ]]; then
+ echo "Release tarball not found: ${RELEASE_TAR}" >&2
+ exit 1
+fi
+
+if [[ -z "${ICON_PATH}" ]]; then
+ if [[ -f "${DEFAULT_ICON}" ]]; then
+ ICON_PATH="${DEFAULT_ICON}"
+ else
+ echo "Warning: default icon not found (${DEFAULT_ICON}); the app bundle will not include an icon."
+ ICON_PATH=""
+ fi
+fi
+
+if [[ -n "${ICON_PATH}" && ! -f "${ICON_PATH}" ]]; then
+ echo "Icon file not found: ${ICON_PATH}" >&2
+ exit 1
+fi
+
+if ! command -v hdiutil >/dev/null 2>&1; then
+ echo "hdiutil is required to create a DMG. Please run this script on macOS." >&2
+ exit 1
+fi
+
+mkdir -p "${OUTPUT_DIR}"
+WORK_DIR="$(mktemp -d "${PROJECT_ROOT}/macos_app_work.XXXXXX")"
+
+cleanup() {
+ if [[ ${KEEP_WORK} -eq 0 ]]; then
+ rm -rf "${WORK_DIR}" 2>/dev/null || true
+ else
+ echo "Keeping work directory at ${WORK_DIR}"
+ fi
+}
+trap cleanup EXIT
+
+echo "Using release tarball: ${RELEASE_TAR}"
+tar -xzf "${RELEASE_TAR}" -C "${WORK_DIR}"
+
+PAYLOAD_SOURCE="$(find "${WORK_DIR}" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
+if [[ -z "${PAYLOAD_SOURCE}" ]]; then
+ echo "Could not find the extracted YaCy payload inside ${WORK_DIR}" >&2
+ exit 1
+fi
+
+APP_BUNDLE_SOURCE="${WORK_DIR}/${APP_NAME}.app"
+CONTENTS_DIR="${APP_BUNDLE_SOURCE}/Contents"
+MACOS_DIR="${CONTENTS_DIR}/MacOS"
+RESOURCES_DIR="${CONTENTS_DIR}/Resources"
+
+mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
+
+PAYLOAD_TARGET="${RESOURCES_DIR}/${PAYLOAD_DIR_NAME}"
+cp -R "${PAYLOAD_SOURCE}/." "${PAYLOAD_TARGET}"
+
+if [[ -n "${ICON_PATH}" ]]; then
+ ICON_NAME="$(basename "${ICON_PATH}")"
+ cp "${ICON_PATH}" "${RESOURCES_DIR}/${ICON_NAME}"
+else
+ ICON_NAME=""
+fi
+
+RELEASE_STUB="$(basename "${RELEASE_TAR}")"
+RELEASE_STUB="${RELEASE_STUB%.tar.gz}"
+APP_VERSION="${RELEASE_STUB#yacy_v}"
+
+INFO_PLIST="${CONTENTS_DIR}/Info.plist"
+cat > "${INFO_PLIST}" <<EOF
+<?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>English</string>
+ <key>CFBundleExecutable</key>
+ <string>${APP_NAME}</string>
+ <key>CFBundleIconFile</key>
+EOF
+if [[ -n "${ICON_NAME}" ]]; then
+ cat >> "${INFO_PLIST}" <<EOF
+ <string>${ICON_NAME}</string>
+EOF
+else
+ cat >> "${INFO_PLIST}" <<'EOF'
+ <string></string>
+EOF
+fi
+cat >> "${INFO_PLIST}" <<EOF
+ <key>CFBundleIdentifier</key>
+ <string>net.yacy.search</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>${APP_VERSION}</string>
+ <key>CFBundleVersion</key>
+ <string>${APP_VERSION}</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>10.13</string>
+ <key>NSHighResolutionCapable</key>
+ <true/>
+</dict>
+</plist>
+EOF
+
+LAUNCHER="${MACOS_DIR}/${APP_NAME}"
+cat > "${LAUNCHER}" <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+
+APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+YACY_HOME="${APP_ROOT}/Resources/%PAYLOAD_DIR%"
+cd "${YACY_HOME}"
+
+USER_DATA_HOME="${HOME}/Library/YaCy"
+TEMPLATE_DATA="${YACY_HOME}/DATA"
+mkdir -p "${USER_DATA_HOME}"
+
+if [[ ! -d "${USER_DATA_HOME}/DATA" ]]; then
+ echo "Initializing YaCy data directory at ${USER_DATA_HOME}/DATA"
+ if [[ -d "${TEMPLATE_DATA}" ]]; then
+ if command -v rsync >/dev/null 2>&1; then
+ rsync -a "${TEMPLATE_DATA}/" "${USER_DATA_HOME}/DATA/"
+ else
+ mkdir -p "${USER_DATA_HOME}/DATA"
+ cp -R "${TEMPLATE_DATA}/." "${USER_DATA_HOME}/DATA/"
+ fi
+ else
+ mkdir -p "${USER_DATA_HOME}/DATA"
+ fi
+fi
+
+JAVA_BIN=""
+if [[ -n "${JAVA_HOME:-}" && -x "${JAVA_HOME}/bin/java" ]]; then
+ JAVA_BIN="${JAVA_HOME}/bin/java"
+elif command -v java >/dev/null 2>&1; then
+ JAVA_BIN="$(command -v java)"
+elif command -v /usr/libexec/java_home >/dev/null 2>&1; then
+ JAVA_HOME="$(/usr/libexec/java_home 2>/dev/null || true)"
+ if [[ -n "${JAVA_HOME}" && -x "${JAVA_HOME}/bin/java" ]]; then
+ JAVA_BIN="${JAVA_HOME}/bin/java"
+ fi
+fi
+
+if [[ -z "${JAVA_BIN}" ]]; then
+ echo "Unable to find a Java runtime. Please install Java (JDK 11+) and try again." >&2
+ exit 1
+fi
+
+JAVA_ARGS=()
+JAVA_ARGS+=(-server)
+JAVA_ARGS+=(-Dfile.encoding=UTF-8)
+JAVA_ARGS+=(-Djava.awt.headless=false)
+
+if uname -m | grep -q 64; then
+ JAVA_ARGS+=(-Dsolr.directoryFactory=solr.MMapDirectoryFactory)
+fi
+
+CONFIG_FILE="DATA/SETTINGS/yacy.conf"
+if [[ -f "${CONFIG_FILE}" ]]; then
+ XMX="$(grep -E '^javastart_Xmx=' "${CONFIG_FILE}" | sed 's/^[^=]*=//' | tail -n 1)"
+ if [[ -n "${XMX}" ]]; then
+ JAVA_ARGS=("-${XMX}" "${JAVA_ARGS[@]}")
+ else
+ JAVA_ARGS=("-Xmx600m" "${JAVA_ARGS[@]}")
+ fi
+else
+ JAVA_ARGS=("-Xmx600m" "${JAVA_ARGS[@]}")
+fi
+
+exec "${JAVA_BIN}" "${JAVA_ARGS[@]}" -classpath "lib/*" net.yacy.yacy -gui "${USER_DATA_HOME}"
+EOF
+perl -0pi -e "s/%PAYLOAD_DIR%/${PAYLOAD_DIR_NAME//\//\\/}/" "${LAUNCHER}"
+chmod +x "${LAUNCHER}"
+
+FINAL_APP="${OUTPUT_DIR}/${APP_NAME}.app"
+rm -rf "${FINAL_APP}"
+mv "${APP_BUNDLE_SOURCE}" "${FINAL_APP}"
+
+DMG_NAME="${RELEASE_STUB}.dmg"
+DMG_PATH="${OUTPUT_DIR}/${DMG_NAME}"
+rm -f "${DMG_PATH}"
+echo "Creating DMG at ${DMG_PATH}"
+hdiutil create -volname "${VOLUME_NAME}" -srcfolder "${FINAL_APP}" -ov -format UDZO "${DMG_PATH}" >/dev/null
+
+echo "macOS app created:"
+echo " App bundle: ${FINAL_APP}"
+echo " DMG: ${DMG_PATH}"