From 6fb48af598e327ec408e35329b11abd50abbc1f7 Mon Sep 17 00:00:00 2001
From: Jonas Kvinge <jonas@jkvinge.net>
Date: Thu, 18 Mar 2021 00:52:04 +0100
Subject: [PATCH] Fix macOS deployment

---
 .github/workflows/ccpp.yml      |  12 +---
 .travis.yml                     |   4 +-
 3rdparty/macdeployqt/main.cpp   |   5 +-
 3rdparty/macdeployqt/shared.cpp | 112 ++++++++++++++++++++++++++++++++
 3rdparty/macdeployqt/shared.h   |   2 +-
 CMakeLists.txt                  |   2 +-
 cmake/Dmg.cmake                 |   8 ++-
 7 files changed, 127 insertions(+), 18 deletions(-)

diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml
index db506b62c..a30bc913b 100644
--- a/.github/workflows/ccpp.yml
+++ b/.github/workflows/ccpp.yml
@@ -1055,8 +1055,7 @@ jobs:
       - name: Link Sparkle
         shell: bash
         run: |
-          sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework /Library/Frameworks/Sparkle.framework
-          sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework.dSYM /Library/Frameworks/Sparkle.framework.dSYM
+          sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1) /usr/local/opt/sparkle
 
       - name: Create Build Environment
         shell: bash
@@ -1082,9 +1081,6 @@ jobs:
         working-directory: build
         shell: bash
         run: make install
-      - name: Hack to make macdeployqt find plugins
-        shell: bash
-        run: sudo ln -s /usr/local/Cellar/qt/$(ls /usr/local/Cellar/qt/ | tail -n1)/share/qt/plugins /usr/local/plugins
       - name: Create DMG
         working-directory: build
         shell: bash
@@ -1131,8 +1127,7 @@ jobs:
       - name: Link Sparkle
         shell: bash
         run: |
-          sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework /Library/Frameworks/Sparkle.framework
-          sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework.dSYM /Library/Frameworks/Sparkle.framework.dSYM
+          sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1) /usr/local/opt/sparkle
 
       - name: Create Build Environment
         shell: bash
@@ -1158,9 +1153,6 @@ jobs:
         working-directory: build
         shell: bash
         run: make install
-      - name: Hack to make macdeployqt find plugins
-        shell: bash
-        run: sudo ln -s /usr/local/Cellar/qt/$(ls /usr/local/Cellar/qt/ | tail -n1)/share/qt/plugins /usr/local/plugins
       - name: Create DMG
         working-directory: build
         shell: bash
diff --git a/.travis.yml b/.travis.yml
index aad8aaf20..729abc7bc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,8 +19,7 @@ before_install:
   - brew install libcdio libmtp
   - brew install create-dmg
   - brew install --cask sparkle
-  - sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework /Library/Frameworks/Sparkle.framework
-  - sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework.dSYM /Library/Frameworks/Sparkle.framework.dSYM
+  - sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1) /usr/local/opt/sparkle
   - export Qt6_DIR=/usr/local/opt/qt6/lib/cmake
   - export Qt6LinguistTools_DIR=/usr/local/opt/qt6/lib/cmake/Qt6LinguistTools
   - ls /usr/local/lib/gstreamer-1.0
@@ -31,7 +30,6 @@ before_script:
 script:
   - make -j8
   - make install
-  - sudo ln -s /usr/local/Cellar/qt/$(ls /usr/local/Cellar/qt/ | tail -n1)/share/qt/plugins /usr/local/plugins
   - make dmg2
 after_success:
   - ls -lh strawberry*.dmg
diff --git a/3rdparty/macdeployqt/main.cpp b/3rdparty/macdeployqt/main.cpp
index b7fd985a1..01fa07588 100644
--- a/3rdparty/macdeployqt/main.cpp
+++ b/3rdparty/macdeployqt/main.cpp
@@ -278,6 +278,10 @@ int main(int argc, char **argv)
     if (runStripEnabled)
         stripAppBinary(appBundlePath);
 
+    if (!FinalCheck(appBundlePath)) {
+        return 1;
+    }
+
     if (runCodesign)
         codesign(codesignIdentiy, appBundlePath);
 
@@ -288,4 +292,3 @@ int main(int argc, char **argv)
 
     return 0;
 }
-
diff --git a/3rdparty/macdeployqt/shared.cpp b/3rdparty/macdeployqt/shared.cpp
index 8d50ee75b..69ff9cb9f 100644
--- a/3rdparty/macdeployqt/shared.cpp
+++ b/3rdparty/macdeployqt/shared.cpp
@@ -856,6 +856,11 @@ void changeInstallName(const QString &bundlePath, const FrameworkInfo &framework
         if (!canonicalInstallName.isEmpty() && canonicalInstallName != framework.installName) {
             changeInstallName(canonicalInstallName, deployedInstallName, binary);
         }
+        // Homebrew workaround, resolve symlink /usr/local/opt/library to /usr/local/Cellar/library
+        if (framework.installName.startsWith("/usr/local/opt/") && framework.installName.count('/') >= 5) {
+            canonicalInstallName = QFileInfo(framework.installName.section('/', 0, 4)).canonicalFilePath() + "/" + framework.installName.section('/', 5);
+            changeInstallName(canonicalInstallName, deployedInstallName, binary);
+        }
     }
 }
 
@@ -1679,3 +1684,110 @@ void fixupFramework(const QString &frameworkName)
     changeIdentification("@rpath/" + frameworkBinary, frameworkBinary);
     addRPath("@loader_path/../../Contents/Frameworks/", frameworkBinary);
 }
+
+bool FinalCheck(const QString &appBundlePath) {
+
+  bool success = true;
+
+  QDirIterator iter(appBundlePath, QDir::Files | QDir::NoSymLinks, QDirIterator::Subdirectories);
+  while (iter.hasNext()) {
+    iter.next();
+    QString filepath = iter.fileInfo().filePath();
+
+    if (filepath.endsWith(".plist") ||
+        filepath.endsWith(".icns") ||
+        filepath.endsWith(".prl") ||
+        filepath.endsWith(".conf") ||
+        filepath.endsWith(".h") ||
+        filepath.endsWith(".nib") ||
+        filepath.endsWith(".strings") ||
+        filepath.endsWith(".css") ||
+        filepath.endsWith("CodeResources") ||
+        filepath.endsWith("PkgInfo") ||
+        filepath.endsWith(".modulemap")) {
+      continue;
+    }
+
+    //qDebug() << "Final check on" << filepath;
+
+    QProcess otool;
+    otool.start("otool", QStringList() << "-L" << filepath);
+    otool.waitForFinished();
+    if (otool.exitStatus() != QProcess::NormalExit || otool.exitCode() != 0) {
+      LogError() << otool.readAllStandardError();
+      success = false;
+      continue;
+    }
+    QString output = otool.readAllStandardOutput();
+    QStringList output_lines = output.split("\n", Qt::SkipEmptyParts);
+    if (output_lines.size() < 2) {
+      LogError() << "Could not parse otool output:" << output;
+      success = false;
+      continue;
+    }
+    QString first_line = output_lines.first();
+    if (first_line.endsWith(':')) first_line.chop(1);
+    if (first_line == filepath) {
+      output_lines.removeFirst();
+    }
+    else {
+      LogError() << "First line" << first_line << "does not match" << filepath;
+      success = false;
+    }
+    static const QRegularExpression regexp(QStringLiteral("^\\t(.+) \\(compatibility version (\\d+\\.\\d+\\.\\d+), current version (\\d+\\.\\d+\\.\\d+)(, weak)?\\)$"));
+    for (const QString &output_line : output_lines) {
+
+      //qDebug() << "Final check on" << filepath << output_line;
+
+      const auto match = regexp.match(output_line);
+      if (match.hasMatch()) {
+        QString library = match.captured(1);
+        if (QFileInfo(library).fileName() == QFileInfo(filepath).fileName()) { // It's this.
+          continue;
+        }
+        else if (library.startsWith("@executable_path")) {
+          QString real_path = library;
+          real_path = real_path.replace("@executable_path", appBundlePath + "/Contents/MacOS");
+          if (!QFile(real_path).exists()) {
+            LogError() << real_path << "does not exist for" << filepath;
+            success = false;
+          }
+        }
+        else if (library.startsWith("@rpath")) {
+          QString real_path = library;
+          real_path = real_path.replace("@rpath", appBundlePath + "/Contents/Frameworks");
+          if (!QFile(real_path).exists() && !real_path.endsWith("QtSvg")) { // FIXME: Ignore broken svg image plugin.
+            LogError() << real_path << "does not exist for" << filepath;
+            success = false;
+          }
+        }
+        else if (library.startsWith("@loader_path")) {
+          QString loader_path = QFileInfo(filepath).path();
+          QString real_path = library;
+          real_path = real_path.replace("@loader_path", loader_path);
+          if (!QFile(real_path).exists()) {
+            LogError() << real_path << "does not exist for" << filepath;
+            success = false;
+          }
+        }
+        else if (library.startsWith("/System/Library/") || library.startsWith("/usr/lib/")) { // System library
+          continue;
+        }
+        else if (library.endsWith("libgcc_s.1.dylib")) {  // fftw points to it for some reason.
+          continue;
+        }
+        else {
+          LogError() << "File" << filepath << "points to" << library;
+          success = false;
+        }
+      }
+      else {
+        LogError() << "Could not parse otool output line:" << output_line;
+        success = false;
+      }
+    }
+  }
+
+  return success;
+
+}
diff --git a/3rdparty/macdeployqt/shared.h b/3rdparty/macdeployqt/shared.h
index 15ff08430..c6b8c692d 100644
--- a/3rdparty/macdeployqt/shared.h
+++ b/3rdparty/macdeployqt/shared.h
@@ -136,6 +136,6 @@ QSet<QString> codesignBundle(const QString &identity,
 void codesign(const QString &identity, const QString &appBundlePath);
 void createDiskImage(const QString &appBundlePath, const QString &filesystemType);
 void fixupFramework(const QString &appBundlePath);
-
+bool FinalCheck(const QString &appBundlePath);
 
 #endif
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 725f6a398..996401333 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -251,7 +251,7 @@ set(SINGLEAPPLICATION_LIBRARIES singleapplication)
 set(SINGLECOREAPPLICATION_LIBRARIES singlecoreapplication)
 
 if(APPLE)
-  find_library(SPARKLE Sparkle)
+  find_library(SPARKLE Sparkle PATHS "/usr/local/opt/sparkle")
   add_subdirectory(3rdparty/macdeployqt)
   add_subdirectory(3rdparty/SPMediaKeyTap)
   set(SPMEDIAKEYTAP_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/SPMediaKeyTap)
diff --git a/cmake/Dmg.cmake b/cmake/Dmg.cmake
index 2cc75c777..a030c51e7 100644
--- a/cmake/Dmg.cmake
+++ b/cmake/Dmg.cmake
@@ -21,12 +21,16 @@ endif()
 
 if(MACDEPLOYQT_EXECUTABLE AND CREATEDMG_EXECUTABLE AND MACOS_VERSION_PACKAGE)
   add_custom_target(dmg
-    COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader -verbose=3
+    COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Frameworks/
+    COMMAND cp -r /usr/local/opt/sparkle/Sparkle.framework ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Frameworks/
+    COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader
     COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 strawberry-${STRAWBERRY_VERSION_PACKAGE}-${MACOS_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
     WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
   )
   add_custom_target(dmg2
-    COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader -verbose=3
+    COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Frameworks/
+    COMMAND cp -r /usr/local/opt/sparkle/Sparkle.framework ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Frameworks/
+    COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader
     COMMAND ${CREATEDMG_EXECUTABLE} --skip-jenkins --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 strawberry-${STRAWBERRY_VERSION_PACKAGE}-${MACOS_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
     WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
   )