From bd59c193016ae02269d14374fb67e232041ac051 Mon Sep 17 00:00:00 2001 From: David Helkowski Date: Thu, 22 Jan 2026 19:28:00 +0900 Subject: [PATCH] Implement Mac App Store build support by introducing the BUILD_FOR_MAC_APP_STORE option. This change disables Sparkle and localhost OAuth redirect server for MAS builds, updates CMake configuration, and modifies build scripts accordingly. Additionally, the macOS bundle identifier is now configurable via CMake. --- CMakeLists.txt | 38 ++- build_tools/macos/build_app.sh | 13 +- .../export_compliance/EXPORT_COMPLIANCE.txt | 26 ++ .../macos/export_compliance/make_pdf.sh | 103 +++++++ dist/CMakeLists.txt | 33 ++- dist/macos/Info.mas.plist.in | 253 ++++++++++++++++++ dist/macos/Info.plist.in | 6 +- dist/macos/entitlements.mas.plist | 20 ++ src/config.h.in | 2 + src/core/localredirectserver.cpp | 6 + src/core/oauthenticator.cpp | 8 + 11 files changed, 479 insertions(+), 29 deletions(-) create mode 100644 build_tools/macos/export_compliance/EXPORT_COMPLIANCE.txt create mode 100755 build_tools/macos/export_compliance/make_pdf.sh create mode 100644 dist/macos/Info.mas.plist.in create mode 100644 dist/macos/entitlements.mas.plist diff --git a/CMakeLists.txt b/CMakeLists.txt index 009671407..bc594050e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,14 @@ if(APPLE) enable_language(OBJC OBJCXX) endif() +if(APPLE) + option(BUILD_FOR_MAC_APP_STORE "Build for Mac App Store (MAS): disables Sparkle + any localhost port-listener OAuth redirect server, and uses MAS-focused defaults." OFF) +else() + set(BUILD_FOR_MAC_APP_STORE OFF) +endif() + +set(MACOS_BUNDLE_ID "com.dryark.strawberry" CACHE STRING "macOS bundle identifier (CFBundleIdentifier)") + if(POLICY CMP0054) cmake_policy(SET CMP0054 NEW) endif() @@ -32,18 +40,24 @@ if(LINUX) endif() if(APPLE) - # Find Sparkle early so cmake/Dmg.cmake (deploy target) can bundle it into the app. - # Sparkle is optional; if not found, update functionality is disabled. - find_library(SPARKLE Sparkle - PATHS - /Library/Frameworks - /System/Library/Frameworks - /opt/homebrew/Frameworks - /opt/homebrew/opt/sparkle-framework/Frameworks - /usr/local/Frameworks - /usr/local/opt/sparkle-framework/Frameworks - PATH_SUFFIXES Frameworks - ) + if(BUILD_FOR_MAC_APP_STORE) + # MAS builds: Sparkle (and QtSparkle) must be disabled. + set(ENABLE_SPARKLE OFF CACHE BOOL "Sparkle integration" FORCE) + set(ENABLE_QTSPARKLE OFF CACHE BOOL "QtSparkle integration" FORCE) + else() + # Find Sparkle early so cmake/Dmg.cmake (deploy target) can bundle it into the app. + # Sparkle is optional; if not found, update functionality is disabled. + find_library(SPARKLE Sparkle + PATHS + /Library/Frameworks + /System/Library/Frameworks + /opt/homebrew/Frameworks + /opt/homebrew/opt/sparkle-framework/Frameworks + /usr/local/Frameworks + /usr/local/opt/sparkle-framework/Frameworks + PATH_SUFFIXES Frameworks + ) + endif() include(cmake/Dmg.cmake) endif() diff --git a/build_tools/macos/build_app.sh b/build_tools/macos/build_app.sh index 254dcdcbf..ebf9d9663 100755 --- a/build_tools/macos/build_app.sh +++ b/build_tools/macos/build_app.sh @@ -91,7 +91,7 @@ run_with_heartbeat() { usage() { cat <<'EOF' Usage: - ./build_tools/macos/build_app.sh [--debug|--release] [--deploy] [--dmg] [--clean] [--build-dir ] + ./build_tools/macos/build_app.sh [--debug|--release] [--mas] [--deploy] [--dmg] [--clean] [--build-dir ] What it does: - Configures and builds Strawberry with CMake + Ninja @@ -100,6 +100,7 @@ What it does: Options: --release Release build (default) --debug Debug build + --mas Build for Mac App Store (BUILD_FOR_MAC_APP_STORE=ON). Disables Sparkle/QtSparkle and any localhost OAuth redirect listener. --deploy Run: cmake --build --target deploy --dmg Run: cmake --build --target dmg (implies --deploy) --clean Delete the build dir before configuring @@ -109,6 +110,7 @@ EOF } config="Release" +do_mas=0 do_deploy=0 do_dmg=0 do_clean=0 @@ -118,6 +120,7 @@ while [[ $# -gt 0 ]]; do case "$1" in --release) config="Release"; shift ;; --debug) config="Debug"; shift ;; + --mas) do_mas=1; shift ;; --deploy) do_deploy=1; shift ;; --dmg) do_dmg=1; do_deploy=1; shift ;; --clean) do_clean=1; shift ;; @@ -165,6 +168,9 @@ fi echo "==> [$(ts)] Repo: ${repo_root}" echo "==> [$(ts)] Build dir: ${build_dir}" echo "==> [$(ts)] Config: ${config}" +if [[ "$do_mas" -eq 1 ]]; then + echo "==> [$(ts)] MAS: enabled (BUILD_FOR_MAC_APP_STORE=ON)" +fi if [[ "$do_clean" -eq 1 ]]; then echo "==> [$(ts)] Cleaning build dir" @@ -213,6 +219,11 @@ cmake_prefix_path="${qt_prefix};${brew_prefix}" cmake_extra_args=() +# Mac App Store build mode +if [[ "$do_mas" -eq 1 ]]; then + cmake_extra_args+=("-DBUILD_FOR_MAC_APP_STORE=ON") +fi + # Optional: override Sparkle update feed / key for your own published builds. # Example: # export SPARKLE_FEED_URL="https://example.com/appcast.xml" diff --git a/build_tools/macos/export_compliance/EXPORT_COMPLIANCE.txt b/build_tools/macos/export_compliance/EXPORT_COMPLIANCE.txt new file mode 100644 index 000000000..69da47119 --- /dev/null +++ b/build_tools/macos/export_compliance/EXPORT_COMPLIANCE.txt @@ -0,0 +1,26 @@ +Encryption Export Compliance Statement (EAR) + +App Name: Strawberry +Bundle ID: @BUNDLE_ID@ +Version: @VERSION@ +Developer: @DEVELOPER@ +Date: @DATE@ +Contact: @CONTACT@ + +Statement +--------- +This app uses encryption only for standard network communications security (for example, TLS/HTTPS connections to web services). + +The app does not implement proprietary or non-standard encryption algorithms, and it does not ship its own cryptographic library in place of Apple’s operating system encryption. + +The app uses only encryption provided by Apple’s operating system (e.g., Apple-provided TLS stacks accessed through system frameworks used by the app and its dependencies). + +The app is not a VPN client/server, does not provide end-to-end encrypted messaging, and does not provide user-controlled key management or custom cryptographic functionality beyond standard transport security. + +Accordingly, the app’s use of encryption is believed to qualify as exempt under U.S. export regulations for mass-market software using standard OS-provided encryption. + +Reference +--------- +Apple: Complying with Encryption Export Regulations +https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations + diff --git a/build_tools/macos/export_compliance/make_pdf.sh b/build_tools/macos/export_compliance/make_pdf.sh new file mode 100755 index 000000000..ec562d717 --- /dev/null +++ b/build_tools/macos/export_compliance/make_pdf.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + ./build_tools/macos/export_compliance/make_pdf.sh \ + --bundle-id com.dryark.strawberry \ + --version 1.2.3 \ + --developer "Dry Ark LLC" \ + --contact "support@example.com" + +Outputs (in the same folder as this script): + - EXPORT_COMPLIANCE.filled.txt + - EXPORT_COMPLIANCE.pdf + +Notes: + - Uses macOS built-in /usr/bin/textutil + /usr/sbin/cupsfilter to generate the PDF. +EOF +} + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +template="${script_dir}/EXPORT_COMPLIANCE.txt" +filled="${script_dir}/EXPORT_COMPLIANCE.filled.txt" +pdf="${script_dir}/EXPORT_COMPLIANCE.pdf" + +bundle_id="" +version="" +developer="" +contact="" +date_str="$(date +%Y-%m-%d)" + +while [[ $# -gt 0 ]]; do + case "$1" in + --bundle-id) bundle_id="${2:-}"; shift 2 ;; + --version) version="${2:-}"; shift 2 ;; + --developer) developer="${2:-}"; shift 2 ;; + --contact) contact="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown arg: $1" >&2; usage; exit 2 ;; + esac +done + +if [[ -z "$bundle_id" || -z "$version" || -z "$developer" || -z "$contact" ]]; then + echo "Error: missing required args." >&2 + usage + exit 2 +fi + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Error: This script is for macOS only (uses textutil)." >&2 + exit 1 +fi + +if [[ ! -f "$template" ]]; then + echo "Error: missing template file: $template" >&2 + exit 1 +fi + +if [[ ! -x /usr/bin/textutil ]]; then + echo "Error: /usr/bin/textutil not found. This should exist on macOS." >&2 + exit 1 +fi +if [[ ! -x /usr/sbin/cupsfilter ]]; then + echo "Error: /usr/sbin/cupsfilter not found. This should exist on macOS." >&2 + exit 1 +fi + +escape_sed_repl() { + # Escape characters that are special in sed replacement strings: \ & and delimiter | + # bash 3.2 compatible + echo "$1" | sed -e 's/[\\&|]/\\&/g' +} + +bundle_id_esc="$(escape_sed_repl "$bundle_id")" +version_esc="$(escape_sed_repl "$version")" +developer_esc="$(escape_sed_repl "$developer")" +contact_esc="$(escape_sed_repl "$contact")" +date_esc="$(escape_sed_repl "$date_str")" + +sed \ + -e "s|@BUNDLE_ID@|${bundle_id_esc}|g" \ + -e "s|@VERSION@|${version_esc}|g" \ + -e "s|@DEVELOPER@|${developer_esc}|g" \ + -e "s|@CONTACT@|${contact_esc}|g" \ + -e "s|@DATE@|${date_esc}|g" \ + "$template" > "$filled" + +rm -f "$pdf" >/dev/null 2>&1 || true + +# textutil does not support direct PDF output on modern macOS; convert to HTML first, then print-to-PDF via cupsfilter. +tmp_html="${script_dir}/EXPORT_COMPLIANCE.tmp.html" +rm -f "$tmp_html" >/dev/null 2>&1 || true +/usr/bin/textutil -convert html "$filled" -output "$tmp_html" >/dev/null + +# Convert HTML to PDF. cupsfilter writes to stdout by default. +/usr/sbin/cupsfilter -i text/html -m application/pdf "$tmp_html" > "$pdf" +rm -f "$tmp_html" >/dev/null 2>&1 || true + +echo "Wrote:" +echo " $filled" +echo " $pdf" + diff --git a/dist/CMakeLists.txt b/dist/CMakeLists.txt index 4fce209f5..9de58ca04 100644 --- a/dist/CMakeLists.txt +++ b/dist/CMakeLists.txt @@ -10,21 +10,26 @@ if(APPLE) set(LSMinimumSystemVersion 12.0) endif() - # Sparkle (macOS updates) - # These values are embedded into Info.plist and control where the app checks for updates. - # Downstream builders can override on the CMake command line: - # -DSPARKLE_FEED_URL="https://example.com/appcast.xml" - # -DSPARKLE_PUBLIC_ED25519_KEY="base64==" - # - # Defaults preserve upstream behavior, but are intentionally configurable for third-party builds. - if(NOT DEFINED SPARKLE_FEED_URL) - set(SPARKLE_FEED_URL "https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@") - endif() - if(NOT DEFINED SPARKLE_PUBLIC_ED25519_KEY) - set(SPARKLE_PUBLIC_ED25519_KEY "/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=") - endif() + if(BUILD_FOR_MAC_APP_STORE) + # MAS builds must not embed Sparkle update configuration in Info.plist. + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.mas.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist) + else() + # Sparkle (macOS updates) + # These values are embedded into Info.plist and control where the app checks for updates. + # Downstream builders can override on the CMake command line: + # -DSPARKLE_FEED_URL="https://example.com/appcast.xml" + # -DSPARKLE_PUBLIC_ED25519_KEY="base64==" + # + # Defaults preserve upstream behavior, but are intentionally configurable for third-party builds. + if(NOT DEFINED SPARKLE_FEED_URL) + set(SPARKLE_FEED_URL "https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@") + endif() + if(NOT DEFINED SPARKLE_PUBLIC_ED25519_KEY) + set(SPARKLE_PUBLIC_ED25519_KEY "/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=") + endif() - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist) + endif() endif() if(WIN32) diff --git a/dist/macos/Info.mas.plist.in b/dist/macos/Info.mas.plist.in new file mode 100644 index 000000000..1a94bae23 --- /dev/null +++ b/dist/macos/Info.mas.plist.in @@ -0,0 +1,253 @@ + + + + + NSPrincipalClass + NSApplication + CFBundleDevelopmentRegion + English + CFBundleExecutable + strawberry + CFBundleGetInfoString + Strawberry ${STRAWBERRY_VERSION_DISPLAY} + CFBundleIconFile + strawberry.icns + CFBundleIdentifier + @MACOS_BUNDLE_ID@ + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${STRAWBERRY_VERSION_DISPLAY} + CFBundleName + Strawberry + CFBundlePackageType + APPL + CFBundleShortVersionString + ${STRAWBERRY_VERSION_DISPLAY} + CFBundleVersion + ${STRAWBERRY_VERSION_PACKAGE} + CSResourcesFileMapped + + LSRequiresCarbon + + LSApplicationCategoryType + public.app-category.music + LSMinimumSystemVersion + @LSMinimumSystemVersion@ + ITSAppUsesNonExemptEncryption + + + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + @MACOS_BUNDLE_ID@ + CFBundleURLSchemes + + tidal + + + + + CFBundleDocumentTypes + + + CFBundleTypeOSTypes + + **** + fold + disk + + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + xspf + + CFBundleTypeIconFile + Generic.icns + CFBundleTypeMIMETypes + + application/xspf+xml + + CFBundleTypeName + XSPF Playlist + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + wav + + CFBundleTypeMIMETypes + + audio/x-wav + + CFBundleTypeName + WAVE Audio File + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + pls + + CFBundleTypeIconFile + pls.icns + CFBundleTypeName + Shoutcast playlist + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + m3u + + CFBundleTypeIconFile + m3u.icns + CFBundleTypeMIMETypes + + audio/x-mpegurl + + CFBundleTypeName + Playlist file + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + aac + + CFBundleTypeIconFile + mpeg4.icns + CFBundleTypeName + AAC file + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + ogg + ogx + ogm + + CFBundleTypeIconFile + ogg.icns + CFBundleTypeMIMETypes + + audio/ogg + + CFBundleTypeName + Ogg Vorbis File + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + oga + + CFBundleTypeIconFile + ogg.icns + CFBundleTypeMIMETypes + + audio/ogg + + CFBundleTypeName + Ogg Audio File + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + wma + + CFBundleTypeIconFile + wma.icns + CFBundleTypeName + WIndows Media Audio + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + mp3 + + CFBundleTypeIconFile + mp3.icns + CFBundleTypeMIMETypes + + audio/mpeg + + CFBundleTypeName + MPEG Audio Layer 3 + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + 3gp + + CFBundleTypeIconFile + generic.icns + CFBundleTypeName + 3GPP File + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + m4a + + CFBundleTypeIconFile + mpeg4.icns + CFBundleTypeName + MPEG-4 Audio File + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + mpc + + CFBundleTypeIconFile + generic.icns + CFBundleTypeName + Musepack Audio File + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + flac + + CFBundleTypeIconFile + generic.icns + CFBundleTypeMIMETypes + + audio/flac + + CFBundleTypeName + FLAC Audio File + CFBundleTypeRole + Viewer + + + + + diff --git a/dist/macos/Info.plist.in b/dist/macos/Info.plist.in index 607cfe952..ec8453023 100644 --- a/dist/macos/Info.plist.in +++ b/dist/macos/Info.plist.in @@ -13,7 +13,7 @@ CFBundleIconFile strawberry.icns CFBundleIdentifier - org.strawberrymusicplayer.strawberry + @MACOS_BUNDLE_ID@ CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString @@ -34,6 +34,8 @@ public.app-category.music LSMinimumSystemVersion @LSMinimumSystemVersion@ + ITSAppUsesNonExemptEncryption + SUFeedURL @SPARKLE_FEED_URL@ SUPublicEDKey @@ -44,7 +46,7 @@ CFBundleTypeRole Viewer CFBundleURLName - org.strawberrymusicplayer.strawberry + @MACOS_BUNDLE_ID@ CFBundleURLSchemes tidal diff --git a/dist/macos/entitlements.mas.plist b/dist/macos/entitlements.mas.plist new file mode 100644 index 000000000..bf227347b --- /dev/null +++ b/dist/macos/entitlements.mas.plist @@ -0,0 +1,20 @@ + + + + + + com.apple.security.app-sandbox + + + + com.apple.security.network.client + + + + com.apple.security.files.user-selected.read-write + + + + + + diff --git a/src/config.h.in b/src/config.h.in index 84b474d01..afcc42f7a 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -7,6 +7,8 @@ #cmakedefine USE_INSTALL_PREFIX +#cmakedefine BUILD_FOR_MAC_APP_STORE + #cmakedefine HAVE_BACKTRACE #cmakedefine HAVE_ALSA #cmakedefine HAVE_PULSE diff --git a/src/core/localredirectserver.cpp b/src/core/localredirectserver.cpp index db4664649..44108668a 100644 --- a/src/core/localredirectserver.cpp +++ b/src/core/localredirectserver.cpp @@ -52,6 +52,12 @@ LocalRedirectServer::~LocalRedirectServer() { bool LocalRedirectServer::Listen() { +#ifdef BUILD_FOR_MAC_APP_STORE + success_ = false; + error_ = "Local redirect server is disabled in Mac App Store builds."_L1; + return false; +#endif + if (!listen(QHostAddress::LocalHost, static_cast(port_))) { success_ = false; error_ = errorString(); diff --git a/src/core/oauthenticator.cpp b/src/core/oauthenticator.cpp index e60127b9d..2fc5c5a21 100644 --- a/src/core/oauthenticator.cpp +++ b/src/core/oauthenticator.cpp @@ -236,6 +236,14 @@ void OAuthenticator::Authenticate() { return; } + // Mac App Store builds: do not start any localhost listening redirect server. +#ifdef BUILD_FOR_MAC_APP_STORE + if (use_local_redirect_server_) { + Q_EMIT AuthenticationFinished(false, tr("This authentication flow is disabled in Mac App Store builds.")); + return; + } +#endif + QUrl redirect_url(redirect_url_); if (use_local_redirect_server_) {