diff --git a/build_scripts/OSX/deploy-macos.sh b/build_scripts/OSX/deploy-macos.sh index 2b95abd8e..af7c09bbb 100755 --- a/build_scripts/OSX/deploy-macos.sh +++ b/build_scripts/OSX/deploy-macos.sh @@ -22,13 +22,13 @@ cd "$(dirname "$0")/../.." || exit 1 BUILD_DIR="${BUILD_DIR:-Build-cmake}" APP_NAME="RetroShare" -# 1. Vérifications initiales du dossier de build +# 1. Initial checks on the build directory if [ ! -d "$BUILD_DIR" ]; then echo "ERROR: Build directory '$BUILD_DIR' not found. Please compile the project first!" exit 1 fi -# On cherche l'application compilée, mais on exclut les dossiers de déploiement précédents +# Find the compiled app, excluding previous deployment directories APP_BUNDLE=$(find "$BUILD_DIR" -maxdepth 3 -type d -name "retroshare-gui.app" ! -path "*/RetroShare-*" | head -n 1) if [ -z "$APP_BUNDLE" ]; then @@ -40,7 +40,7 @@ echo "========================================================================== echo " RETROSHARE MACOS DEPLOYMENT GENERATOR" echo "================================================================================" -# 2. Détermination dynamique des variables de version +# 2. Dynamically determine the version variables echo ">>> Extracting versioning info from Git and environment..." DATE_STR=$(date +%Y%m%d) @@ -57,25 +57,51 @@ else GIT_SUFFIX="-${DATE_STR}" fi -# Detect the Qt version for the archive name (qmake reports it). Homebrew's qt@5 -# is keg-only so its qmake is usually not in PATH; fall back to it, then to a -# generic label if nothing is found. +# Detect the Qt version for the archive name from the ACTUAL built binary, NOT +# from whichever qmake is first in PATH. With both qt (Qt6) and qt@5 installed, +# `qmake` resolves to one of them regardless of what the app was built against, +# mislabelling the archive (a Qt6 build packaged as "Qt-5..."). Read the QtCore +# the app links - the same signal used for plugin bundling further below - and +# query the matching Homebrew keg's qmake. +GUI_BIN="$APP_BUNDLE/Contents/MacOS/retroshare-gui" +if otool -L "$GUI_BIN" 2>/dev/null | grep -q "QtCore.framework/Versions/5"; then + QT_MAJOR=5 + QMAKE_BIN="$(brew --prefix qt@5 2>/dev/null)/bin/qmake" +else + QT_MAJOR=6 + QMAKE_BIN="$(brew --prefix qt 2>/dev/null)/bin/qmake" +fi +echo " Qt major linked by the app (otool): Qt${QT_MAJOR}" + QT_VERSION="" -if command -v qmake &> /dev/null; then - QT_VERSION=$(qmake -query QT_VERSION 2>/dev/null) +[ -x "$QMAKE_BIN" ] || QMAKE_BIN="$(command -v qmake)" +if [ -x "$QMAKE_BIN" ]; then + v=$("$QMAKE_BIN" -query QT_VERSION 2>/dev/null) + [[ "$v" == "${QT_MAJOR}."* ]] && QT_VERSION="$v" fi +# Fallback: at least keep the major version right even if qmake is unavailable. if [[ ! "$QT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - QT5_QMAKE="$(brew --prefix qt@5 2>/dev/null)/bin/qmake" - [ -x "$QT5_QMAKE" ] && QT_VERSION=$("$QT5_QMAKE" -query QT_VERSION 2>/dev/null) -fi -if [[ ! "$QT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - QT_VERSION="UnknownQt" + QT_VERSION="${QT_MAJOR}.x" fi MACVERSION=$(sw_vers -productVersion 2>/dev/null || echo "UnknownOS") ARCH=$(uname -m) -ARCHIVE_BASE="RetroShare-${VERSION}-macOS-${ARCH}${GIT_SUFFIX}-Qt-${QT_VERSION}" +# Minimum macOS version to run = the deployment target baked into the built +# binary (the macOS analog of the AppImage's glibc floor / min glibc to run). +# Read it from the GUI binary's load commands (LC_BUILD_VERSION `minos`, or the +# older LC_VERSION_MIN_MACOSX `version`); fall back to the build host's version. +MINOS=$(otool -l "$GUI_BIN" 2>/dev/null \ + | grep -A3 -E 'LC_BUILD_VERSION|LC_VERSION_MIN_MACOSX' \ + | grep -oE '(minos|version) [0-9]+(\.[0-9]+)+' \ + | grep -oE '[0-9]+(\.[0-9]+)+' | head -1) +[ -n "$MINOS" ] || MINOS="$MACVERSION" + +# Naming mirrors the Linux AppImage and Windows schemes: +# RetroShare-v---g-Qt--macos--.dmg +# GIT_SUFFIX already carries "---g" (or just the date when +# exactly on a tag). +ARCHIVE_BASE="RetroShare-v${VERSION}${GIT_SUFFIX}-Qt-${QT_VERSION}-macos-${MINOS}-${ARCH}" DEPLOY_DIR="${BUILD_DIR}/${ARCHIVE_BASE}" echo " Target Package Name: ${ARCHIVE_BASE}" @@ -85,7 +111,7 @@ echo ">>> Cleaning up previous deployment directories..." rm -rf "${BUILD_DIR}/RetroShare-"*"-macOS-"* mkdir -p "$DEPLOY_DIR" -# 3. Copie de l'application dans notre dossier de déploiement +# 3. Copy the app into our deployment directory echo ">>> Copying retroshare-gui.app..." cp -R "$APP_BUNDLE" "$DEPLOY_DIR/" TARGET_APP="$DEPLOY_DIR/$(basename "$APP_BUNDLE")" @@ -99,7 +125,7 @@ mkdir -p "$APP_RESOURCES/sounds" mkdir -p "$APP_RESOURCES/translations" mkdir -p "$APP_RESOURCES/webui" -# 4. Copie des exécutables annexes (service, friendserver) s'ils existent (non-bloquant) +# 4. Copy the extra executables (service, friendserver) if present (non-blocking) echo ">>> Copying additional executables if they exist..." for exe in retroshare-service retroshare-friendserver; do found_exe=$(find "$BUILD_DIR" -type f -name "$exe" -perm -0111 | grep -v "CMakeFiles" | head -n 1) @@ -111,13 +137,13 @@ for exe in retroshare-service retroshare-friendserver; do fi done -# Copie des plugins +# Copy the plugins echo ">>> Copying RetroShare plugins..." if [ -d "$BUILD_DIR/plugins" ]; then mkdir -p "$APP_RESOURCES/extensions6" for plugin in $(find "$BUILD_DIR/plugins" -type f \( -name "*.dylib" -o -name "*.so" \)); do filename=$(basename "$plugin") - # RetroShare sur macOS refuse de charger les plugins s'ils ne finissent pas par .dylib ! + # RetroShare on macOS refuses to load plugins unless they end in .dylib! newname="${filename%.*}.dylib" cp "$plugin" "$APP_RESOURCES/$newname" cp "$plugin" "$APP_RESOURCES/extensions6/$newname" @@ -127,7 +153,7 @@ else echo " INFO: Plugins directory not found. Skipping plugins." fi -# 5. Copie des ressources statiques +# 5. Copy the static assets echo ">>> Copying RetroShare static assets..." if [ -d "retroshare-gui/src/sounds" ]; then @@ -148,7 +174,7 @@ if [ -d "retroshare-webui/src" ]; then echo " WebUI copied to app resources." fi -# 6. Mise à jour du Info.plist +# 6. Update Info.plist echo ">>> Updating Info.plist..." PLIST_PATH="$TARGET_APP/Contents/Info.plist" if command -v /usr/libexec/PlistBuddy &> /dev/null; then @@ -162,40 +188,138 @@ else echo " WARNING: PlistBuddy not found, skipping Info.plist update." fi -# 7. Déploiement Qt via macdeployqt +# 7. Qt deployment via macdeployqt echo ">>> Running macdeployqt..." -if command -v macdeployqt &> /dev/null; then - # Forcer la copie des plugins d'images (SVG, etc.) au cas où macdeployqt les oublie - MACDEPLOY_BIN=$(command -v macdeployqt) - QT_BASE=$(dirname "$(dirname "$MACDEPLOY_BIN")") - QT_PLUGINS_DIR="$QT_BASE/share/qt/plugins" - if [ ! -d "$QT_PLUGINS_DIR" ]; then - QT_PLUGINS_DIR="$QT_BASE/plugins" - fi +# Use the SAME Qt the app was built against — NOT whatever macdeployqt happens to +# be first in PATH. With both qt (Qt6) and qt@5 installed, PATH often points at +# qt@5, which copies Qt5 plugins into a Qt6 app; the Qt5 cocoa plugin then can't +# load ("Could not find the Qt platform plugin cocoa"). Detect the major version +# from the app's QtCore link and pick the matching keg + its macdeployqt/plugins. +if otool -L "$TARGET_APP/Contents/MacOS/retroshare-gui" 2>/dev/null | grep -q "QtCore.framework/Versions/5"; then + QT_PREFIX="$(brew --prefix qt@5 2>/dev/null)" +else + QT_PREFIX="$(brew --prefix qt 2>/dev/null)" +fi +MACDEPLOY_BIN="$QT_PREFIX/bin/macdeployqt" +[ -x "$MACDEPLOY_BIN" ] || MACDEPLOY_BIN="$(command -v macdeployqt)" +QT_PLUGINS_DIR="$QT_PREFIX/share/qt/plugins" +[ -d "$QT_PLUGINS_DIR" ] || QT_PLUGINS_DIR="$QT_PREFIX/plugins" - if [ -d "$QT_PLUGINS_DIR/imageformats" ]; then - echo ">>> Forcing copy of Qt imageformats plugins (SVG support) from $QT_PLUGINS_DIR..." - mkdir -p "$TARGET_APP/Contents/PlugIns/imageformats" - cp "$QT_PLUGINS_DIR/imageformats/"*.dylib "$TARGET_APP/Contents/PlugIns/imageformats/" 2>/dev/null || true - fi +if [ -x "$MACDEPLOY_BIN" ]; then + echo ">>> Using Qt toolchain: $QT_PREFIX (macdeployqt: $MACDEPLOY_BIN)" - # Par défaut macdeployqt ignore les fichiers .so, il faut le forcer avec -executable + # Force-copy the essential Qt plugin categories. Without "platforms" (the + # mandatory cocoa plugin) the app aborts at launch with "Could not find the Qt + # platform plugin cocoa" — macdeployqt has been seen to skip it here. styles/tls + # are needed for native look and HTTPS; imageformats = SVG icons. + echo ">>> Forcing copy of essential Qt plugins from $QT_PLUGINS_DIR..." + for cat in platforms styles imageformats iconengines tls networkinformation; do + if [ -d "$QT_PLUGINS_DIR/$cat" ]; then + mkdir -p "$TARGET_APP/Contents/PlugIns/$cat" + cp "$QT_PLUGINS_DIR/$cat/"*.dylib "$TARGET_APP/Contents/PlugIns/$cat/" 2>/dev/null || true + echo " + $cat" + fi + done + + # Tell Qt where the bundled plugins live (macdeployqt normally writes this). + mkdir -p "$TARGET_APP/Contents/Resources" + [ -f "$TARGET_APP/Contents/Resources/qt.conf" ] || printf '[Paths]\nPlugins = PlugIns\n' > "$TARGET_APP/Contents/Resources/qt.conf" + + # By default macdeployqt ignores .so files; force them with -executable EXTRA_EXECS="" - for plugin in "$TARGET_APP"/Contents/Resources/*.so "$TARGET_APP"/Contents/Resources/*.dylib "$TARGET_APP"/Contents/Resources/extensions6/*.so "$TARGET_APP"/Contents/Resources/extensions6/*.dylib "$TARGET_APP"/Contents/PlugIns/imageformats/*.dylib; do + for plugin in "$TARGET_APP"/Contents/Resources/*.so "$TARGET_APP"/Contents/Resources/*.dylib "$TARGET_APP"/Contents/Resources/extensions6/*.so "$TARGET_APP"/Contents/Resources/extensions6/*.dylib "$TARGET_APP"/Contents/PlugIns/*/*.dylib; do if [ -f "$plugin" ]; then EXTRA_EXECS="$EXTRA_EXECS -executable=$plugin" fi done - # macdeployqt copie les frameworks et ajuste les rpaths - macdeployqt "$TARGET_APP" -always-overwrite $EXTRA_EXECS - + # macdeployqt copies the frameworks and fixes the rpaths + "$MACDEPLOY_BIN" "$TARGET_APP" -always-overwrite $EXTRA_EXECS + + # -------------------------------------------------------------------------- + # Bundle the transitive third-party Homebrew dependencies that macdeployqt + # misses (the ffmpeg chain: libavcodec -> libswresample, etc.). + # + # macdeployqt copies each plugin's DIRECT dependencies (e.g. VOIP -> + # libavcodec, libavutil, libspeex) but does NOT recurse into them: libavcodec + # keeps an absolute reference to /opt/homebrew/.../libswresample.6.dylib. + # On the build Mac that file exists (Homebrew) so the bundle "works"; on a + # clean Mac it is missing and the plugin fails to load. + # + # DELIBERATELY NARROW SCOPE: only the flat third-party dylibs in Frameworks/ + # and the RetroShare plugins in Resources/ are processed. Qt is NOT touched: + # neither the .frameworks (binary with no extension) nor Contents/PlugIns/ — + # Qt is already handled by macdeployqt, and sweeping it here would re-copy all + # of Qt in bulk. References are rewritten to @executable_path/../Frameworks, + # which points to Contents/Frameworks for ANY binary in the bundle. + # -------------------------------------------------------------------------- + echo ">>> Bundling transitive third-party (ffmpeg) dependencies missed by macdeployqt..." + FRAMEWORKS_DIR="$TARGET_APP/Contents/Frameworks" + mkdir -p "$FRAMEWORKS_DIR" + + # Mach-O files to make self-contained: flat third-party dylibs in Frameworks/ + # (ffmpeg, speex, openssl...) + RetroShare plugins in Resources/. No PlugIns/ + # and no Qt .frameworks (their binary has no .dylib extension, so never listed). + list_macho() { + { + find "$FRAMEWORKS_DIR" -maxdepth 1 -type f -name "*.dylib" + find "$TARGET_APP/Contents/Resources" -maxdepth 2 -type f -name "*.dylib" + } | sort -u + } + # Homebrew/MacPorts dependencies of a Mach-O, EXCLUDING its own install id + # (first line of otool -L = LC_ID_DYLIB, not a dependency) and all of Qt. + homebrew_deps() { + local id + id=$(otool -D "$1" 2>/dev/null | sed -n '2p') + otool -L "$1" 2>/dev/null | awk 'NR>1{print $1}' \ + | grep -E '^(/opt/homebrew|/usr/local|/opt/local)/' \ + | grep -vE '/[Qq]t@?[0-9]*/' \ + | grep -vxF "${id:-@@none@@}" \ + || true + } + + # 1) Pull in the missing libs until no new one appears. + changed=1 + while [ "$changed" -eq 1 ]; do + changed=0 + while IFS= read -r macho; do + for dep in $(homebrew_deps "$macho"); do + base=$(basename "$dep") + if [ ! -f "$FRAMEWORKS_DIR/$base" ]; then + echo " + bundling $base (needed by $(basename "$macho"))" + cp -L "$dep" "$FRAMEWORKS_DIR/$base" + chmod u+w "$FRAMEWORKS_DIR/$base" + install_name_tool -id "@rpath/$base" "$FRAMEWORKS_DIR/$base" 2>/dev/null + changed=1 + fi + done + done < <(list_macho) + done + + # 2) Rewrite every remaining Homebrew reference to point inside the bundle. + while IFS= read -r macho; do + for dep in $(homebrew_deps "$macho"); do + install_name_tool -change "$dep" \ + "@executable_path/../Frameworks/$(basename "$dep")" "$macho" 2>/dev/null + done + done < <(list_macho) + + # 3) Verify: NO non-bundled third-party reference must remain. + LEAKS=$(while IFS= read -r m; do homebrew_deps "$m"; done < <(list_macho) | sort -u) + if [ -n "$LEAKS" ]; then + echo " WARNING: residual non-bundled references remain (bundle NOT self-contained):" + echo "$LEAKS" | sed 's/^/ /' + else + echo " OK: third-party libs self-contained (no /opt/homebrew or /usr/local references left)." + fi + + echo ">>> Ad-hoc code signing (required for Apple Silicon)..." if command -v codesign &> /dev/null; then - # On signe d'abord toutes les bibliothèques, plugins (.so/.dylib) et frameworks individuellement + # First sign every library, plugin (.so/.dylib) and framework individually find "$TARGET_APP" -type f \( -name "*.dylib" -o -name "*.so" \) -exec codesign --force --sign - {} \; 2>/dev/null || true find "$TARGET_APP" -type d -name "*.framework" -exec codesign --force --sign - {} \; 2>/dev/null || true - # Puis on signe l'application entière + # Then sign the whole application codesign --force --deep --sign - "$TARGET_APP" fi