From dce0880576274b21533991bf2297660d6e320a69 Mon Sep 17 00:00:00 2001 From: diya Date: Sun, 13 Sep 2026 14:40:09 +0200 Subject: [PATCH] Add macOS app packaging script --- DEVELOPMENT.md | 7 ++ Makefile | 5 +- scripts/build-install-launch.zsh | 156 +++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100755 scripts/build-install-launch.zsh diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0b2c848..544bdb3 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -14,3 +14,10 @@ Current environment keys: Provider integrations are intentionally protocol-only in the repository foundation phase. + +## Packaging + +Run `make install-app` to build the release executable, package it as +`AI Music Studio.app`, install it in `/Applications`, and launch it. + +Set `APP_ICON=/path/to/icon.icns` when packaging with a custom icon. diff --git a/Makefile b/Makefile index a6e0f26..19ea76c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build dev test +.PHONY: build dev test install-app build: swift build @@ -8,3 +8,6 @@ dev: test: swift test + +install-app: + scripts/build-install-launch.zsh diff --git a/scripts/build-install-launch.zsh b/scripts/build-install-launch.zsh new file mode 100755 index 0000000..b30d0b1 --- /dev/null +++ b/scripts/build-install-launch.zsh @@ -0,0 +1,156 @@ +#!/bin/zsh +# Build, package, install, and launch the AI Music Studio macOS application. + +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +APP_DISPLAY_NAME="${APP_DISPLAY_NAME:-AI Music Studio}" +EXECUTABLE_NAME="${EXECUTABLE_NAME:-music-assistant}" +BUNDLE_IDENTIFIER="${BUNDLE_IDENTIFIER:-com.musicassistant.app}" +APP_BUNDLE_NAME="${APP_DISPLAY_NAME}.app" +BUILD_DIR="${PROJECT_ROOT}/.build/app" +APP_BUNDLE="${BUILD_DIR}/${APP_BUNDLE_NAME}" +INSTALL_DIR="${INSTALL_DIR:-/Applications}" +INSTALL_PATH="${INSTALL_DIR}/${APP_BUNDLE_NAME}" +CONTENTS_DIR="${APP_BUNDLE}/Contents" +MACOS_DIR="${CONTENTS_DIR}/MacOS" +RESOURCES_DIR="${CONTENTS_DIR}/Resources" + +fail() { + print -u2 -- "Error: $1" + exit 1 +} + +step() { + print -- "==> $1" +} + +find_app_icon() { + if [[ -n "${APP_ICON:-}" ]]; then + [[ -f "${APP_ICON}" ]] || fail "App icon was not found at ${APP_ICON}." + print -- "${APP_ICON}" + return + fi + + local candidate + for candidate in \ + "${PROJECT_ROOT}/Resources/AI Music Studio.icns" \ + "${PROJECT_ROOT}/Resources/AppIcon.icns" \ + "${PROJECT_ROOT}/Resources/MusicAssistant.icns"; do + if [[ -f "${candidate}" ]]; then + print -- "${candidate}" + return + fi + done +} + +[[ -f "${PROJECT_ROOT}/Package.swift" ]] || fail "Package.swift was not found in ${PROJECT_ROOT}." +command -v swift >/dev/null 2>&1 || fail "Swift is required. Install Xcode or the Xcode Command Line Tools." +command -v codesign >/dev/null 2>&1 || fail "codesign is required and should be available with Xcode." +command -v ditto >/dev/null 2>&1 || fail "ditto is required and should be available on macOS." +command -v open >/dev/null 2>&1 || fail "open is required and should be available on macOS." + +cd "${PROJECT_ROOT}" + +step "Stopping any running ${APP_DISPLAY_NAME} application" +if pgrep -x "${EXECUTABLE_NAME}" >/dev/null 2>&1; then + pkill -x "${EXECUTABLE_NAME}" || fail "Could not stop the running ${APP_DISPLAY_NAME} application." + for _ in {1..20}; do + pgrep -x "${EXECUTABLE_NAME}" >/dev/null 2>&1 || break + sleep 0.1 + done + pgrep -x "${EXECUTABLE_NAME}" >/dev/null 2>&1 && fail "${APP_DISPLAY_NAME} is still running. Quit it and retry." +fi + +step "Removing stale installed copy" +if [[ -e "${INSTALL_PATH}" ]]; then + rm -rf "${INSTALL_PATH}" || fail "Could not remove ${INSTALL_PATH}. Check permissions and retry." +fi + +step "Cleaning local Swift build artifacts" +swift package clean || fail "Swift Package Manager could not clean the local build artifacts." + +step "Building ${EXECUTABLE_NAME} in release mode" +BIN_PATH="$(swift build -c release --show-bin-path 2>/dev/null | tail -n1)" +SOURCE_EXECUTABLE="${BIN_PATH}/${EXECUTABLE_NAME}" + +built=0 +for attempt in 1 2 3; do + swift build -c release || fail "Release build failed. Review the Swift compiler diagnostics above." + if [[ -x "${SOURCE_EXECUTABLE}" ]]; then + built=1 + break + fi + print -- "==> Executable not present after build (attempt ${attempt}); retrying" + sleep 1 +done +[[ "${built}" -eq 1 ]] || fail "Built executable was not found at ${SOURCE_EXECUTABLE}." + +step "Packaging ${APP_BUNDLE_NAME}" +rm -rf "${APP_BUNDLE}" +mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}" +cp "${SOURCE_EXECUTABLE}" "${MACOS_DIR}/${EXECUTABLE_NAME}" + +ICON_PATH="$(find_app_icon)" +PLIST_ICON_ENTRY="" +if [[ -n "${ICON_PATH}" ]]; then + ICON_FILE_NAME="$(basename "${ICON_PATH}")" + ICON_FILE_BASE="${ICON_FILE_NAME:r}" + cp "${ICON_PATH}" "${RESOURCES_DIR}/${ICON_FILE_NAME}" + PLIST_ICON_ENTRY=$' CFBundleIconFile\n '"${ICON_FILE_BASE}"$'' +else + step "No app icon found; packaging without a custom icon" +fi + +cat > "${CONTENTS_DIR}/Info.plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${APP_DISPLAY_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 +${PLIST_ICON_ENTRY} + CFBundleName + ${APP_DISPLAY_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 14.0 + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + +PLIST + +plutil -lint "${CONTENTS_DIR}/Info.plist" >/dev/null || fail "Generated Info.plist is invalid." +codesign --force --sign - --timestamp=none "${APP_BUNDLE}" || fail "Ad-hoc signing of ${APP_BUNDLE_NAME} failed." +codesign --verify --deep --strict "${APP_BUNDLE}" || fail "Packaged ${APP_BUNDLE_NAME} failed code-signature verification." +[[ -x "${MACOS_DIR}/${EXECUTABLE_NAME}" ]] || fail "Packaged executable is missing." + +step "Installing ${APP_BUNDLE_NAME} in ${INSTALL_DIR}" +ditto "${APP_BUNDLE}" "${INSTALL_PATH}" || fail "Could not install ${APP_BUNDLE_NAME} in ${INSTALL_DIR}. Check permissions and retry." + +INSTALLED_EXECUTABLE="${INSTALL_PATH}/Contents/MacOS/${EXECUTABLE_NAME}" +[[ -x "${INSTALLED_EXECUTABLE}" ]] || fail "Installed executable is missing at ${INSTALLED_EXECUTABLE}." +SOURCE_HASH="$(shasum -a 256 "${MACOS_DIR}/${EXECUTABLE_NAME}" | awk '{print $1}')" +INSTALLED_HASH="$(shasum -a 256 "${INSTALLED_EXECUTABLE}" | awk '{print $1}')" +[[ "${SOURCE_HASH}" == "${INSTALLED_HASH}" ]] || fail "Installed executable does not match the newly built executable." + +step "Launching ${APP_DISPLAY_NAME}" +open -n "${INSTALL_PATH}" || fail "${APP_DISPLAY_NAME} was installed but could not be opened." + +print -- "==> ${APP_DISPLAY_NAME} was rebuilt, installed, and launched successfully."