diff options
| author | Michael Peter Christen <mc@yacy.net> | 2025-11-19 13:47:42 +0100 |
|---|---|---|
| committer | Michael Peter Christen <mc@yacy.net> | 2025-11-19 13:47:42 +0100 |
| commit | 610400d2519f0e80da4b0a3acc9474f09c8ec1d2 (patch) | |
| tree | c5671f6fca6d6dcf504a9f0385967a189a2eaa96 | |
| parent | 8d3c443ea571d3b71096f7749d3e6dd2c56ee5a2 (diff) | |
added mac app packager
| -rwxr-xr-x | package_macos_app.sh | 356 |
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}" |
