cmake: Modernize (and fix) deployment on macOS
authorManuel Nickschas <sputnick@quassel-irc.org>
Sun, 3 Jan 2021 13:48:36 +0000 (14:48 +0100)
committerManuel Nickschas <sputnick@quassel-irc.org>
Mon, 4 Jan 2021 07:16:42 +0000 (08:16 +0100)
For the past decade or so, we have used a bunch of self-written python
and bash scripts for creating packages for macOS. These have not aged
well, and recently several workarounds had to be hacked in to keep
the machinery somewhat working at all. Still, invoking the scripts at
build time rather than install time caused a race condition where
sometimes not all the packages would be created in CI. To break the
camel's back, deploying dependencies no longer worked correctly and
broke the packages completely in 0.14-rc1.

Fix this by modernizing the whole deployment process and related
parts of the build system, replacing the custom scripts by relying
on Qt's and CMake's own tooling instead. Some workarounds still need
to be added to that to make everything work correctly (neither tool
can deal correctly with QtWebEngine, for some reason, and CMake's
Info.plist template lacks functionality), but the main part of the
work is now delegated to official tooling, everything properly
happend at install time avoiding race conditions in the build process,
and we can remove a bunch of decade-old and hardly maintained custom
code.

Also adapt the CI configuration to use -DBUNDLE instead of the
old -DDEPLOY, and remove the explicit setting of qmake's path too,
as it is no longer needed now.

13 files changed:
.github/workflows/main.yml
CMakeLists.txt
ChangeLog
cmake/FinalizeBundle.cmake.in [new file with mode: 0644]
cmake/MacOSXBundleInfo.plist.in [moved from scripts/build/Info.plist with 81% similarity]
cmake/QuasselMacros.cmake
data/qt.conf [new file with mode: 0644]
scripts/build/macosx_DeployApp.py [deleted file]
scripts/build/macosx_makePackage.sh [deleted file]
scripts/build/macosx_makebundle.py [deleted file]
scripts/build/macosx_qt.py [deleted file]
src/CMakeLists.txt
src/main/CMakeLists.txt

index c0dae8e..2bcfc19 100644 (file)
@@ -142,24 +142,25 @@ jobs:
         mkdir build
         cd build && cmake $GITHUB_WORKSPACE \
                           -GNinja \
+                          -DWANT_CORE=ON \
+                          -DWANT_QTCLIENT=ON \
+                          -DWANT_MONO=ON \
                           -DCMAKE_PREFIX_PATH=/usr/local/opt/qt/lib/cmake \
+                          -DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/bundles \
                           -DCMAKE_BUILD_TYPE=Release \
                           -DBUILD_TESTING=ON \
                           -DFATAL_WARNINGS=OFF \
-                          -DDEPLOY=ON \
-                          -DENABLE_SHARED=OFF
+                          -DENABLE_SHARED=OFF \
+                          -DBUNDLE=ON \
 
     - name: Build
-      run: |
-        # Deploy scripts require qmake in the path
-        export PATH=$PATH:/usr/local/opt/qt5/bin
-        cd build && ninja
+      run: cd build && ninja
 
     - name: Run tests
       run: cd build && ctest
 
     - name: Install
-      run: cd build && DESTDIR=$GITHUB_WORKSPACE/image ninja install
+      run: cd build && ninja install
 
     - name: Print ccache stats
       run: ccache -s
@@ -168,7 +169,7 @@ jobs:
       uses: actions/upload-artifact@v2
       with:
         name: macOS
-        path: ${{ github.workspace }}/build/*.dmg
+        path: ${{ github.workspace }}/bundles/*.dmg
 
 # ------------------------------------------------------------------------------------------------------------------------------------------
   build-windows:
index cebb61b..dfdaaeb 100644 (file)
@@ -93,6 +93,10 @@ if (APPLE)
     find_library(CARBON_LIBRARY Carbon)
     mark_as_advanced(CARBON_LIBRARY)
     link_libraries(${CARBON_LIBRARY})
+
+    # Whether to enable the creation of bundles and DMG images
+    cmake_dependent_option(BUNDLE "Create bundles and DMG images" OFF "APPLE" OFF)
+    add_feature_info(BUNDLE BUNDLE "Create bundles and DMG images")
 endif()
 
 # Always embed on Windows or OSX; never embed when enabling KDE integration
@@ -101,14 +105,13 @@ if (WIN32 OR APPLE)
     set(EMBED_DEFAULT ON)
 endif()
 cmake_dependent_option(EMBED_DATA "Embed icons and translations into the binaries instead of installing them" ${EMBED_DEFAULT}
-                                   "NOT WIN32;NOT WITH_KDE" ${EMBED_DEFAULT})
+                                  "NOT WIN32;NOT WITH_KDE" ${EMBED_DEFAULT})
 if (NOT EMBED_DEFAULT)
     add_feature_info(EMBED_DATA EMBED_DATA "Embed icons and translations in the binaries instead of installing them")
 endif()
 
-# The following options are not for end-user consumption, so don't list them in the feature summary
+# The following option is not for end-user consumption, so don't list it in the feature summary
 option(FATAL_WARNINGS "Make compile warnings fatal (most useful for CI builds)" OFF)
-cmake_dependent_option(DEPLOY "Add required libs to bundle resources and create a dmg" OFF "APPLE" OFF)
 
 # List of authenticators and the cmake flags to build them
 # (currently that's just LDAP, but more can be added here).
index c395764..223e940 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -47,6 +47,7 @@ NOTE:  Database schema format change, no downgrade possible!
 * Support building shared libraries via the ENABLE_SHARED CMake option (defaults to on)
 * Introduce support for (and a small selection of) unit tests via the BUILD_TESTING CMake option
 * Use Github Actions as CI system, replacing Travis and Appveyor
+* Revamp bundle/DMG creation on macOS
 * Many smaller fixes
 * Improve documentation and UI help
 * Update translations
diff --git a/cmake/FinalizeBundle.cmake.in b/cmake/FinalizeBundle.cmake.in
new file mode 100644 (file)
index 0000000..bce8c07
--- /dev/null
@@ -0,0 +1,63 @@
+include(BundleUtilities)
+
+# After the relevant targets, support files, as well as plugins have already been installed into the bundle structure,
+# the bundle must still be made standalone by copying the required frameworks and making them position-independent.
+# This is generally called "fixing up" the bundle.
+#
+# Principally there are two ways to do that: Qt's official macdeployqt tool, and CMake's BundleUtilities.
+#
+# Some frameworks, in particular QtWebEngineCore, come with nested bundles. In order for them to work correctly, the Frameworks directory
+# from the main bundle must be symlinked into the nested bundle, otherwise dependencies cannot be resolved.
+# Neither macdeployqt (shockingly) nor BundleUtilities can handle this properly. The former simply ignores nested bundles and thus
+# does not fix them up at all. The latter scans for additional binaries and tries fixing them up, but there is no way to inject
+# creation of the Frameworks symlink between the copy and the fixup steps.
+#
+# The working solution implemented here is to first run macdeployqt (which also handles some Qt-specific quirks), then symlink Frameworks
+# into the nested bundles (if any), then use BundleUtilities to perform the remaining fixups and verify the bundle.
+
+# Since we're in the install phase, DESTDIR might be set
+set(BUNDLE_PATH "$ENV{DESTDIR}@BUNDLE_PATH@")
+set(DMG_PATH    "$ENV{DESTDIR}@DMG_PATH@")
+
+# First, use Qt's official tool, macdeployqt, for deploying the needed Qt Frameworks into the bundle
+message(STATUS "Deploying Qt Frameworks in bundle \"${BUNDLE_PATH}\"")
+execute_process(
+    # Don't deploy plugins - we've already installed the selection relevant for our target!
+    COMMAND @MACDEPLOYQT_EXECUTABLE@ "${BUNDLE_PATH}" -verbose=1 -no-plugins
+    RESULT_VARIABLE result
+)
+if(NOT result EQUAL 0)
+    message(FATAL_ERROR "Deploying Qt Frameworks failed.")
+endif()
+
+# Scan for nested bundles and symlink the main bundle's Frameworks directory into them
+message(STATUS "Checking for nested bundles")
+execute_process(
+    COMMAND find "${BUNDLE_PATH}" -mindepth 1 -type d -name "*.app"
+    OUTPUT_VARIABLE nested_bundles
+    OUTPUT_STRIP_TRAILING_WHITESPACE
+)
+if(nested_bundles)
+    string(REPLACE "\n" ";" nested_bundles ${nested_bundles})
+    foreach(nested_bundle IN LISTS nested_bundles)
+        message(STATUS "Symlinking Frameworks into nested bundle \"${nested_bundle}\"")
+        file(RELATIVE_PATH path "${nested_bundle}/Contents" "${BUNDLE_PATH}/Contents/Frameworks")
+        execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink "${path}" "${nested_bundle}/Contents/Frameworks")
+    endforeach()
+else()
+    message("Checking for nested bundles - none found")
+endif()
+
+# Now fixup the whole thing using CMake's own tooling, which (unlike macdeployqt) will take care of any additional internal executables
+message(STATUS "Fixing up bundle...")
+fixup_bundle("${BUNDLE_PATH}" "" "")
+
+# Create the DMG image
+message(STATUS "Creating DMG image...")
+execute_process(
+    COMMAND hdiutil create "${DMG_PATH}" -srcfolder "${BUNDLE_PATH}" -format "UDBZ" -fs "HFS+" -volname "Quassel IRC"
+    RESULT_VARIABLE result
+)
+if(NOT result EQUAL 0)
+    message(FATAL_ERROR "Creating DMG image failed.")
+endif()
similarity index 81%
rename from scripts/build/Info.plist
rename to cmake/MacOSXBundleInfo.plist.in
index c773a3f..7670078 100644 (file)
@@ -5,11 +5,11 @@
        <key>CFBundleDevelopmentRegion</key>
        <string>English</string>
        <key>CFBundleExecutable</key>
-       <string>%(BUNDLE_NAME)s</string>
+       <string>@BUNDLE_NAME@</string>
        <key>CFBundleGetInfoString</key>
        <string>Quassel IRC Client</string>
        <key>CFBundleIconFile</key>
-       <string>%(ICON_FILE)s</string>
+       <string>quassel.icns</string>
        <key>CFBundleIdentifier</key>
        <string>org.quassel-irc.client</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleShortVersionString</key>
-       <string>%(BUNDLE_VERSION)s</string>
+       <string>@QUASSEL_MAJOR@.@QUASSEL_MINOR@.@QUASSEL_PATCH@</string>
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
-       <string>%(BUNDLE_VERSION)s</string>
+       <string>@QUASSEL_MAJOR@.@QUASSEL_MINOR@.@QUASSEL_PATCH@</string>
+       <key>LSMinimumSystemVersion</key>
+       <string>@QMAKE_MACOSX_DEPLOYMENT_TARGET@</string>
        <key>LSRequiresCarbon</key>
        <true/>
-       <key>NSPrincipalClass</key>
-       <string>NSApplication</string>
        <key>NSHighResolutionCapable</key>
        <true/>
        <key>NSHumanReadableCopyright</key>
        <string>© 2005-2020, Quassel IRC Team</string>
+       <key>NSPrincipalClass</key>
+       <string>NSApplication</string>
        <key>NSSupportsAutomaticGraphicsSwitching</key>
        <true/>
-       <key>LSMinimumSystemVersion</key>
-       <string>%(QT_MACOSX_DEPLOYMENT_TARGET)s</string>
 </dict>
 </plist>
index f6d4ea6..077e59a 100644 (file)
@@ -90,6 +90,143 @@ function(quassel_add_module _module)
     set(TARGET ${target} PARENT_SCOPE)
 endfunction()
 
+###################################################################################################
+# Adds an executable target for Quassel.
+#
+# quassel_add_executable(<target> COMPONENT <Core|Client|Mono> [SOURCES src1 src2...] [LIBRARIES lib1 lib2...])
+#
+# This function supports the creation of either of the three hardcoded executable targets: Core, Client, and Mono.
+# Given sources and libraries are added to the target.
+#
+# On macOS, the creation of bundles and corresponding DMG files is supported and can be enabled by setting the
+# BUNDLE option to ON.
+#
+function(quassel_add_executable _target)
+    set(options)
+    set(oneValueArgs COMPONENT)
+    set(multiValueArgs SOURCES LIBRARIES)
+    cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+    # Set up some hard-coded data based on the component to be built
+    if(ARG_COMPONENT STREQUAL "Core")
+        set(DEFINE BUILD_CORE)
+        set(WIN32 FALSE)
+        set(BUNDLE_NAME "Quassel Core")
+    elseif(ARG_COMPONENT STREQUAL "Client")
+        set(DEFINE BUILD_QTUI)
+        set(WIN32 TRUE)
+        set(BUNDLE_NAME "Quassel Client")
+    elseif(ARG_COMPONENT STREQUAL "Mono")
+        set(DEFINE BUILD_MONO)
+        set(WIN32 TRUE)
+        set(BUNDLE_NAME "Quassel")
+    else()
+        message(FATAL_ERROR "quassel_executable requires a COMPONENT argument with one of the values 'Core', 'Client' or 'Mono'")
+    endif()
+
+    add_executable(${_target} ${ARG_SOURCES})
+    set_property(TARGET ${_target} APPEND PROPERTY COMPILE_DEFINITIONS ${DEFINE})
+    set_target_properties(${_target} PROPERTIES
+        RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}
+        WIN32_EXECUTABLE ${WIN32}  # Ignored on non-Windows platforms
+    )
+    target_link_libraries(${_target} PUBLIC ${ARG_LIBRARIES})  # Link publicly, so plugin detection for bundles work
+
+    # Prepare bundle creation on macOS
+    if(APPLE AND BUNDLE)
+        set(BUNDLE_PATH "${CMAKE_INSTALL_PREFIX}/${BUNDLE_NAME}.app")
+        set(DMG_PATH "${CMAKE_INSTALL_PREFIX}/Quassel${ARG_COMPONENT}_MacOSX-x86_64_${QUASSEL_VERSION_STRING}.dmg")
+
+        # Generate an appropriate Info.plist
+        set(BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info_${ARG_COMPONENT}.plist")
+        configure_file(${CMAKE_SOURCE_DIR}/cmake/MacOSXBundleInfo.plist.in ${BUNDLE_INFO_PLIST} @ONLY)
+
+        # Set some bundle-specific properties
+        set_target_properties(${_target} PROPERTIES
+            MACOSX_BUNDLE TRUE
+            MACOSX_BUNDLE_INFO_PLIST "${BUNDLE_INFO_PLIST}"
+            OUTPUT_NAME "${BUNDLE_NAME}"
+        )
+    endif()
+
+    # Install main target; this will also create an initial bundle skeleton if appropriate
+    install(TARGETS ${_target}
+        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+        BUNDLE DESTINATION ${CMAKE_INSTALL_PREFIX}  # Ignored when not creating a bundle
+        COMPONENT ${ARG_COMPONENT}
+    )
+
+    # Once the bundle skeleton has been created and the main executable installed, finalize bundle creation and build DMGs
+    if(APPLE AND BUNDLE)
+        # We cannot rely on Qt's macdeployqt for deploying plugins, because it will unconditionally install a bunch of unneeded ones,
+        # dragging in unwanted dependencies.
+        # Instead, transitively determine all Qt modules that the Quassel executable links against, and deploy only the plugins belonging
+        # to those modules.
+        # macdeployqt will take care of fixing up dependencies afterwards.
+        function(find_transitive_link_deps target var)
+            if(TARGET ${target})
+                get_target_property(libs ${target} LINK_LIBRARIES)
+                if(libs)
+                    foreach(lib IN LISTS libs)
+                        if(NOT lib IN_LIST ${var})
+                            list(APPEND ${var} ${lib})
+                            find_transitive_link_deps(${lib} ${var})
+                        endif()
+                    endforeach()
+                endif()
+                set(${var} ${${var}} PARENT_SCOPE)
+            endif()
+        endfunction()
+
+        find_transitive_link_deps(${_target} link_deps)
+        # TODO CMake 3.6: use list(FILTER...)
+        foreach(dep IN LISTS link_deps)
+            if(${dep} MATCHES "^Qt5::.*")
+                list(APPEND qt_deps ${dep})
+            endif()
+        endforeach()
+
+        foreach(module IN LISTS qt_deps)
+            string(REPLACE "::" "" module ${module})
+            foreach(plugin ${${module}_PLUGINS})
+                install(
+                    FILES $<TARGET_PROPERTY:${plugin},LOCATION>
+                    DESTINATION ${BUNDLE_PATH}/Contents/PlugIns/$<TARGET_PROPERTY:${plugin},QT_PLUGIN_TYPE>
+                    COMPONENT ${ARG_COMPONENT}
+                )
+            endforeach()
+        endforeach()
+
+        # Generate iconset and deploy it as well as a qt.conf enabling plugins
+        add_dependencies(${_target} MacOsIcons)
+        install(
+            FILES ${CMAKE_SOURCE_DIR}/data/qt.conf ${CMAKE_BINARY_DIR}/pics/quassel.icns
+            DESTINATION ${BUNDLE_PATH}/Contents/Resources
+            COMPONENT ${ARG_COMPONENT}
+        )
+
+        # Determine the location of macdeployqt. Not available directly via CMake, so look for it in qmake's bindir...
+        get_target_property(QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION)
+        get_filename_component(qt_bin_dir ${QMAKE_EXECUTABLE} DIRECTORY)
+        find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS ${qt_bin_dir} REQUIRED)
+
+        # Generate and invoke post-install script, finalizing the bundle and creating a DMG image
+        #set(BUNDLE_PATH $ENV{DESTDIR}/${BUNDLE_PATH})
+        configure_file(${CMAKE_SOURCE_DIR}/cmake/FinalizeBundle.cmake.in ${CMAKE_BINARY_DIR}/FinalizeBundle_${ARG_COMPONENT}.cmake @ONLY)
+        install(CODE "
+                execute_process(
+                    COMMAND ${CMAKE_COMMAND} -P ${CMAKE_BINARY_DIR}/FinalizeBundle_${ARG_COMPONENT}.cmake
+                    RESULT_VARIABLE result
+                )
+                if(NOT result EQUAL 0)
+                    message(FATAL_ERROR \"Finalizing bundle failed.\")
+                endif()
+            "
+            COMPONENT ${ARG_COMPONENT}
+        )
+    endif()
+endfunction()
+
 ###################################################################################################
 # Provides a library that contains data files as a Qt resource (.qrc).
 #
diff --git a/data/qt.conf b/data/qt.conf
new file mode 100644 (file)
index 0000000..02408fe
--- /dev/null
@@ -0,0 +1,4 @@
+[Paths]
+Plugins = PlugIns
+Imports = Resources/qml
+Qml2Imports = Resources/qml
diff --git a/scripts/build/macosx_DeployApp.py b/scripts/build/macosx_DeployApp.py
deleted file mode 100755 (executable)
index edcb13c..0000000
+++ /dev/null
@@ -1,232 +0,0 @@
-#!/usr/bin/python
-# -*- coding: iso-8859-1 -*-
-
-################################################################################
-#                                                                              #
-# 2008 June 27th by Marcus 'EgS' Eggenberger <egs@quassel-irc.org>             #
-#                                                                              #
-# The author disclaims copyright to this source code.                          #
-# This Python Script is in the PUBLIC DOMAIN.                                  #
-#                                                                              #
-################################################################################
-
-# ==============================
-#  Imports
-# ==============================
-import sys
-import os
-import os.path
-
-from subprocess import Popen, PIPE
-
-# Handling Qt properties
-import macosx_qt
-
-# ==============================
-#  Constants
-# ==============================
-QT_CONFIG = """[Paths]
- Plugins = plugins
-"""
-
-QT_CONFIG_NOBUNDLE = """[Paths]
- Prefix = ../
- Plugins = plugins
-"""
-
-
-class InstallQt(object):
-    def __init__(self, appdir, bundle=True, requestedPlugins=[], skipInstallQtConf=False):
-        self.appDir = appdir
-        self.bundle = bundle
-        self.frameworkDir = self.appDir + "/Frameworks"
-        self.pluginDir = self.appDir + "/plugins"
-        self.executableDir = self.appDir
-        if bundle:
-            self.executableDir += "/MacOS"
-
-        self.installedFrameworks = set()
-
-        self.findFrameworkPath()
-
-        executables = [self.executableDir + "/" + executable for executable in os.listdir(self.executableDir)]
-        for executable in executables:
-            self.resolveDependancies(executable)
-
-        self.findPluginsPath()
-        self.installPlugins(requestedPlugins)
-        if not skipInstallQtConf:
-            self.installQtConf()
-
-    def findFrameworkPath(self):
-        self.sourceFrameworkPath = macosx_qt.qtProperty('QT_INSTALL_LIBS')
-
-    def findPluginsPath(self):
-        self.sourcePluginsPath = macosx_qt.qtProperty('QT_INSTALL_PLUGINS')
-
-    def findPlugin(self, pluginname):
-        qmakeProcess = Popen('find %s -name %s' % (self.sourcePluginsPath, pluginname), shell=True, stdout=PIPE, stderr=PIPE)
-        result = qmakeProcess.stdout.read().strip()
-        qmakeProcess.stdout.close()
-        qmakeProcess.wait()
-        if not result:
-            raise OSError
-        return result
-
-    def installPlugins(self, requestedPlugins):
-        try:
-            os.mkdir(self.pluginDir)
-        except:
-            pass
-
-        for plugin in requestedPlugins:
-            if not plugin.isalnum():
-                print "Skipping library '%s'..." % plugin
-                continue
-
-            pluginName = "lib%s.dylib" % plugin
-            pluginSource = ''
-            try:
-                pluginSource = self.findPlugin(pluginName)
-            except OSError:
-                print "WARNING: Requested library does not exist: '%s'" % plugin
-                continue
-
-            pluginSubDir = os.path.dirname(pluginSource)
-            pluginSubDir = pluginSubDir.replace(self.sourcePluginsPath, '').strip('/')
-            try:
-                os.mkdir("%s/%s" % (self.pluginDir, pluginSubDir))
-            except OSError:
-                pass
-
-            os.system('cp "%s" "%s/%s"' % (pluginSource, self.pluginDir, pluginSubDir))
-
-            self.resolveDependancies("%s/%s/%s" % (self.pluginDir, pluginSubDir, pluginName))
-
-    def installQtConf(self):
-        qtConfName = self.appDir + "/qt.conf"
-        qtConfContent = QT_CONFIG_NOBUNDLE
-        if self.bundle:
-            qtConfContent = QT_CONFIG
-            qtConfName = self.appDir + "/Resources/qt.conf"
-
-        qtConf = open(qtConfName, 'w')
-        qtConf.write(qtConfContent)
-        qtConf.close()
-
-    def resolveDependancies(self, obj):
-        # obj must be either an application binary or a framework library
-        # print "resolving deps for:", obj
-        for framework, lib in self.determineDependancies(obj):
-            self.installFramework(framework)
-            self.changeDylPath(obj, framework, lib)
-
-    def installFramework(self, framework):
-        # skip if framework is already installed.
-        if framework in self.installedFrameworks:
-            return
-
-        self.installedFrameworks.add(framework)
-
-        # if the Framework-Folder is a Symlink we are in a Helper-Process ".app" (e.g. in QtWebEngine),
-        # in this case skip copying/installing on existing folders
-        skipExisting = False;
-        if os.path.islink(self.frameworkDir):
-            skipExisting = True;
-
-        # ensure that the framework directory exists
-        try:
-            os.mkdir(self.frameworkDir)
-        except:
-            pass
-
-        if not framework.startswith('/'):
-            framework = "%s/%s" % (self.sourceFrameworkPath, framework)
-
-        frameworkname = framework.split('/')[-1]
-        localframework = self.frameworkDir + "/" + frameworkname
-
-        # Framework already installed in previous run ... see above
-        if skipExisting and os.path.isdir(localframework):
-            return
-
-        # Copy Framework
-        os.system('cp -R "%s" "%s"' % (framework, self.frameworkDir))
-
-        # De-Myllify
-        os.system('find "%s" -name *debug* -exec rm -f {} \;' % localframework)
-        os.system('find "%s" -name Headers -exec rm -rf {} \; 2>/dev/null' % localframework)
-
-        # Install new Lib ID and Change Path to Frameworks for the Dynamic linker
-        for lib in os.listdir(localframework + "/Versions/Current"):
-            lib = "%s/Versions/Current/%s" % (localframework, lib)
-            otoolProcess = Popen('otool -D "%s"' % lib, shell=True, stdout=PIPE, stderr=PIPE)
-            try:
-                libname = [line for line in otoolProcess.stdout][1].strip()
-            except:
-                libname = ''
-            otoolProcess.stdout.close()
-            if otoolProcess.wait() == 1:  # we found some Resource dir or similar -> skip
-                continue
-            frameworkpath, libpath = libname.split(frameworkname)
-            if self.bundle:
-                newlibname = "@executable_path/../%s%s" % (frameworkname, libpath)
-            else:
-                newlibname = "@executable_path/%s%s" % (frameworkname, libpath)
-            # print 'install_name_tool -id "%s" "%s"' % (newlibname, lib)
-            os.system('chmod +w "%s"' % (lib))
-            os.system('install_name_tool -id "%s" "%s"' % (newlibname, lib))
-
-            self.resolveDependancies(lib)
-
-    def determineDependancies(self, app):
-        otoolPipe = Popen('otool -L "%s"' % app, shell=True, stdout=PIPE).stdout
-        otoolOutput = [line for line in otoolPipe]
-        otoolPipe.close()
-        libs = [line.split()[0] for line in otoolOutput[1:] if ("Qt" in line or "phonon" in line) and "@executable_path" not in line]
-        frameworks = [lib[:lib.find(".framework") + len(".framework")] for lib in libs]
-        frameworks = [framework[framework.rfind('/') + 1:] for framework in frameworks]
-        return zip(frameworks, libs)
-
-    def changeDylPath(self, obj, framework, lib):
-        newlibname = framework + lib.split(framework)[1]
-        if self.bundle:
-            newlibname = "@executable_path/../Frameworks/%s" % newlibname
-        else:
-            newlibname = "@executable_path/Frameworks/%s" % newlibname
-
-        # print 'install_name_tool -change "%s" "%s" "%s"' % (lib, newlibname, obj)
-        os.system('chmod +w "%s"' % (lib))
-        os.system('chmod +w "%s"' % (obj))
-        os.system('install_name_tool -change "%s" "%s" "%s"' % (lib, newlibname, obj))
-
-if __name__ == "__main__":
-    if len(sys.argv) < 2:
-        print "Wrong Argument Count (Syntax: %s [--nobundle] [--plugins=plugin1,plugin2,...] $TARGET_APP)" % sys.argv[0]
-        sys.exit(1)
-    else:
-        bundle = True
-        plugins = []
-        offset = 1
-
-        while offset < len(sys.argv) and sys.argv[offset].startswith("--"):
-            if sys.argv[offset] == "--nobundle":
-                bundle = False
-
-            if sys.argv[offset].startswith("--plugins="):
-                plugins = sys.argv[offset].split('=')[1].split(',')
-
-            offset += 1
-
-        targetDir = sys.argv[offset]
-        if bundle:
-            targetDir += "/Contents"
-
-        InstallQt(targetDir, bundle, plugins)
-
-        if bundle:
-            webenginetarget = targetDir + '/Frameworks/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app/Contents'
-
-            if os.path.isdir(webenginetarget):
-                os.system('ln -s ../../../../../../ "%s"/Frameworks' % webenginetarget)
-                InstallQt(webenginetarget, bundle, [], True)
diff --git a/scripts/build/macosx_makePackage.sh b/scripts/build/macosx_makePackage.sh
deleted file mode 100755 (executable)
index e9342dd..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-#!/bin/bash
-# Don't consider packaging a success if any commands fail
-# See http://redsymbol.net/articles/unofficial-bash-strict-mode/
-set -euo pipefail
-
-myname=$0
-if [ -s "$myname" ] && [ -x "$myname" ]; then
-    # $myname is already a valid file name
-
-    mypath=$myname
-else
-    case "$myname" in
-    /*) exit 1;;             # absolute path - do not search PATH
-    *)
-        # Search all directories from the PATH variable. Take
-        # care to interpret leading and trailing ":" as meaning
-        # the current directory; the same is true for "::" within
-        # the PATH.
-
-        # Replace leading : with . in PATH, store in p
-        p=${PATH/#:/.:}
-        # Replace trailing : with .
-        p=${p//%:/:.}
-        # Replace :: with :.:
-        p=${p//::/:.:}
-        # Temporary input field separator, see FAQ #1
-        OFS=$IFS IFS=:
-        # Split the path on colons and loop through each of them
-        for dir in $p; do
-                [ -f "$dir/$myname" ] || continue # no file
-                [ -x "$dir/$myname" ] || continue # not executable
-                mypath=$dir/$myname
-                break           # only return first matching file
-        done
-        # Restore old input field separator
-        IFS=$OFS
-        ;;
-    esac
-fi
-
-if [ ! -f "$mypath" ]; then
-    echo >&2 "cannot find full path name: $myname"
-    exit 1
-fi
-
-SCRIPTDIR=$(dirname $mypath)
-QUASSEL_VERSION=$(git describe)
-BUILDTYPE=$1
-
-# check the working dir
-# Default to "." using Bash default-value syntax
-WORKINGDIR="${2:-.}"
-WORKINGDIR="${WORKINGDIR}/"
-PACKAGETMPDIR="${WORKINGDIR}PACKAGE_TMP_DIR_${BUILDTYPE}"
-QUASSEL_DMG="Quassel${BUILDTYPE}_MacOSX-x86_64_${QUASSEL_VERSION}.dmg"
-
-# Default to null string
-if [[ -z ${3:-} ]]; then
-       ADDITIONAL_PLUGINS=""
-else
-       # Options provided, append to list
-       ADDITIONAL_PLUGINS=",$3"
-fi
-
-echo "ADDITIONAL_PLUGINS: ${ADDITIONAL_PLUGINS}"
-
-mkdir $PACKAGETMPDIR
-case $BUILDTYPE in
-"Client")
-       cp -r ${WORKINGDIR}Quassel\ Client.app ${PACKAGETMPDIR}/
-       ${SCRIPTDIR}/macosx_DeployApp.py --plugins=qcocoa,qgenericbearer,qcorewlanbearer,qmacstyle${ADDITIONAL_PLUGINS} "${PACKAGETMPDIR}/Quassel Client.app"
-       ;;
-"Core")
-       cp ${WORKINGDIR}quasselcore ${PACKAGETMPDIR}/
-       ${SCRIPTDIR}/macosx_DeployApp.py --nobundle --plugins=qsqlite,qsqlpsql${ADDITIONAL_PLUGINS} ${PACKAGETMPDIR}
-       ;;
-"Mono")
-       cp -r ${WORKINGDIR}Quassel.app ${PACKAGETMPDIR}/
-       ${SCRIPTDIR}/macosx_DeployApp.py --plugins=qsqlite,qsqlpsql,qcocoa,qgenericbearer,qcorewlanbearer,qmacstyle${ADDITIONAL_PLUGINS} "${PACKAGETMPDIR}/Quassel.app"
-       ;;
-*)
-       echo >&2 "Valid parameters are \"Client\", \"Core\", or \"Mono\"."
-       rmdir ${PACKAGETMPDIR}
-       exit 1
-       ;;
-esac
-
-echo "Creating macOS disk image with hdiutil: 'Quassel ${BUILDTYPE} - ${QUASSEL_VERSION}'"
-
-# Modern macOS versions support APFS, however default to HFS+ for now in order
-# to ensure old macOS versions can parse the package and display the warning
-# about being out of date.  This mirrors the approach taken by Qt's macdeployqt
-# tool.  In the future if this isn't needed, just remove "-fs HFS+" to revert
-# to default.
-#
-# See https://doc.qt.io/qt-5/macos-deployment.html
-
-# hdiutil seems to have a bit of a reputation for failing to create disk images
-# for various reasons.
-#
-# If you've come here to see why on earth your macOS build is failing despite
-# making changes entirely unrelated to macOS, you have my sympathy.
-#
-# There are two main approaches:
-#
-# 1.  Let hdiutil calculate a size automatically
-#
-# 2.  Separately calculate the size with a margin of error, then specify this
-#     to hdiutil during disk image creation.
-#
-# Both seem to have caused issues, but in recent tests, option #1 seemed more
-# reliable.
-#
-# Option 1:
-
-hdiutil create -srcfolder ${PACKAGETMPDIR} -format UDBZ -fs HFS+ -volname "Quassel ${BUILDTYPE} - ${QUASSEL_VERSION}" "${WORKINGDIR}${QUASSEL_DMG}" >/dev/null
-
-# If hdiutil changes over time and fails often, you can try the other option.
-#
-# Option 2:
-#
-#PACKAGESIZE_MARGIN="1.1"
-#PACKAGESIZE=$(echo "$(du -ms ${PACKAGETMPDIR} | cut -f1) * $PACKAGESIZE_MARGIN" | bc)
-#echo "PACKAGESIZE: $PACKAGESIZE MB"
-#hdiutil create -srcfolder ${PACKAGETMPDIR} -format UDBZ -fs HFS+ -size ${PACKAGESIZE}M -volname "Quassel ${BUILDTYPE} - ${QUASSEL_VERSION}" "${WORKINGDIR}${QUASSEL_DMG}" >/dev/null
-
-
-# Regardless of choice, clean up the packaging temporary directory
-rm -rf ${PACKAGETMPDIR}
diff --git a/scripts/build/macosx_makebundle.py b/scripts/build/macosx_makebundle.py
deleted file mode 100755 (executable)
index 85742a0..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/python
-# -*- coding: iso-8859-1 -*-
-
-################################################################################
-#                                                                              #
-# 2008 June 27th by Marcus 'EgS' Eggenberger <egs@quassel-irc.org>             #
-#                                                                              #
-# The author disclaims copyright to this source code.                          #
-# This Python Script is in the PUBLIC DOMAIN.                                  #
-#                                                                              #
-################################################################################
-
-# ==============================
-#  Imports
-# ==============================
-import os
-import os.path
-import sys
-import commands
-
-# Handling Qt properties
-import macosx_qt
-
-# ==============================
-#  Constants
-# ==============================
-if len(sys.argv) < 2:
-    sys.exit(1)
-
-SOURCE_DIR = sys.argv[1]
-
-if len(sys.argv) < 4:
-    BUNDLE_NAME = "Quassel Client"
-    EXE_NAME = "quasselclient"
-else:
-    EXE_NAME = sys.argv[3]
-    BUNDLE_NAME = sys.argv[2]
-
-# make the dir of the exe the target dir
-if(os.path.dirname(EXE_NAME)):
-    CONTENTS_DIR = os.path.dirname(EXE_NAME) + "/"
-CONTENTS_DIR += BUNDLE_NAME + ".app/Contents/"
-
-BUNDLE_VERSION = commands.getoutput("git --git-dir=" + SOURCE_DIR + "/.git/ describe")
-ICONSET_FOLDER = "pics/iconset/"
-
-
-def createBundle():
-    try:
-        os.makedirs(CONTENTS_DIR + "MacOS")
-        os.makedirs(CONTENTS_DIR + "Resources")
-    except:
-        pass
-
-
-def copyFiles(exeFile, iconset):
-    os.system("cp %s %sMacOs/%s" % (exeFile, CONTENTS_DIR.replace(' ', '\ '), BUNDLE_NAME.replace(' ', '\ ')))
-    os.system("cp -r %s/%s %s/Resources/quassel.iconset/" % (SOURCE_DIR, iconset, CONTENTS_DIR.replace(' ', '\ ')))
-
-
-def createPlist(bundleName, bundleVersion):
-    templateFile = file(SOURCE_DIR + "/scripts/build/Info.plist", 'r')
-    template = templateFile.read()
-    templateFile.close()
-
-    # Get the minimum macOS deployment version
-    QT_MACOSX_DEPLOYMENT_TARGET = macosx_qt.qtMakespec('QMAKE_MACOSX_DEPLOYMENT_TARGET')
-    # Keep in sync with QMAKE_MACOSX_DEPLOYMENT_TARGET
-    # See https://doc.qt.io/qt-5/macos.html
-    if QT_MACOSX_DEPLOYMENT_TARGET is None:
-        # Something went wrong
-        sys.exit("Could not determine 'QMAKE_MACOSX_DEPLOYMENT_TARGET', check build scripts")
-    print("Qt macOS deployment target (minimum version): %s" % QT_MACOSX_DEPLOYMENT_TARGET)
-
-    plistFile = file(CONTENTS_DIR + "Info.plist", 'w')
-    plistFile.write(template % {"BUNDLE_NAME": bundleName,
-                                "ICON_FILE": "quassel.icns",
-                                "BUNDLE_VERSION": bundleVersion,
-                                "QT_MACOSX_DEPLOYMENT_TARGET": QT_MACOSX_DEPLOYMENT_TARGET})
-    plistFile.close()
-
-def convertIconset():
-    os.system("iconutil -c icns %s/Resources/quassel.iconset" % CONTENTS_DIR.replace(' ', '\ '))
-    os.system("rm -R %s/Resources/quassel.iconset" % CONTENTS_DIR.replace(' ', '\ '))
-
-if __name__ == "__main__":
-    createBundle()
-    createPlist(BUNDLE_NAME, BUNDLE_VERSION)
-    copyFiles(EXE_NAME, ICONSET_FOLDER)
-    convertIconset()
diff --git a/scripts/build/macosx_qt.py b/scripts/build/macosx_qt.py
deleted file mode 100755 (executable)
index 3955364..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/usr/bin/python
-# -*- coding: iso-8859-1 -*-
-
-################################################################################
-#                                                                              #
-# 2008 June 27th by Marcus 'EgS' Eggenberger <egs@quassel-irc.org>             #
-#                                                                              #
-# The author disclaims copyright to this source code.                          #
-# This Python Script is in the PUBLIC DOMAIN.                                  #
-#                                                                              #
-################################################################################
-
-# ==============================
-#  Imports
-# ==============================
-import os
-from subprocess import Popen, PIPE
-
-# ==============================
-#  Global Functions
-# ==============================
-def qtProperty(qtProperty):
-    """
-    Query persistent property of Qt via qmake
-    """
-    VALID_PROPERTIES = ['QT_INSTALL_PREFIX',
-                        'QT_INSTALL_DATA',
-                        'QT_INSTALL_DOCS',
-                        'QT_INSTALL_HEADERS',
-                        'QT_INSTALL_LIBS',
-                        'QT_INSTALL_BINS',
-                        'QT_INSTALL_PLUGINS',
-                        'QT_INSTALL_IMPORTS',
-                        'QT_INSTALL_TRANSLATIONS',
-                        'QT_INSTALL_CONFIGURATION',
-                        'QT_INSTALL_EXAMPLES',
-                        'QT_INSTALL_DEMOS',
-                        'QMAKE_MKSPECS',
-                        'QMAKE_VERSION',
-                        'QT_VERSION'
-                        ]
-    if qtProperty not in VALID_PROPERTIES:
-        return None
-
-    qmakeProcess = Popen('qmake -query %s' % qtProperty, shell=True, stdout=PIPE, stderr=PIPE)
-    result = qmakeProcess.stdout.read().strip()
-    qmakeProcess.stdout.close()
-    qmakeProcess.wait()
-    return result
-
-def qtMakespec(qtMakespec):
-    """
-    Query a Makespec value of Qt via qmake
-    """
-
-    VALID_PROPERTIES = ['QMAKE_MACOSX_DEPLOYMENT_TARGET',
-                        ]
-    if qtMakespec not in VALID_PROPERTIES:
-        return None
-
-    # QMAKE_MACOSX_DEPLOYMENT_TARGET sadly cannot be queried in the traditional way
-    #
-    # Inspired by https://code.qt.io/cgit/pyside/pyside-setup.git/tree/qtinfo.py?h=5.6
-    # Simplified, no caching, etc, as we're just looking for the macOS version.
-    # If a cleaner solution is desired, look into license compatibility in
-    # order to simply copy the above code.
-
-    current_dir = os.getcwd()
-    qmakeFakeProjectFile = os.path.join(current_dir, "qmake_empty_project.txt")
-    qmakeStashFile = os.path.join(current_dir, ".qmake.stash")
-    # Make an empty file
-    open(qmakeFakeProjectFile, 'a').close()
-
-    qmakeProcess = Popen('qmake -E %s' % qmakeFakeProjectFile, shell=True, stdout=PIPE, stderr=PIPE)
-    result = qmakeProcess.stdout.read().strip()
-    qmakeProcess.stdout.close()
-    qmakeProcess.wait()
-
-    # Clean up temporary files
-    try:
-        os.remove(qmakeFakeProjectFile)
-    except OSError:
-        pass
-    try:
-        os.remove(qmakeStashFile)
-    except OSError:
-        pass
-
-    # Result should be like this:
-    # PROPERTY = VALUE\n
-    result_list = result.splitlines()
-    # Clear result so if nothing matches, nothing is returned
-    result = None
-    # Search keys
-    for line in result_list:
-        if not '=' in line:
-            # Ignore lines without '='
-            continue
-
-        # Find property = value
-        parts = line.split('=', 1)
-        prop = parts[0].strip()
-        value = parts[1].strip()
-        if (prop == qtMakespec):
-            result = value
-            break
-
-    return result
index 130144b..042b1c0 100644 (file)
@@ -1,5 +1,4 @@
 add_subdirectory(common)
-add_subdirectory(main)
 if (BUILD_CORE)
     add_subdirectory(core)
 endif()
@@ -12,3 +11,5 @@ endif()
 if (BUILD_TESTING)
     add_subdirectory(test)
 endif()
+
+add_subdirectory(main)
index b5aa963..a3ad83d 100644 (file)
@@ -1,13 +1,3 @@
-# Convenience function to avoid boilerplate
-function(setup_executable _target _define)
-    set_target_properties(${_target} PROPERTIES
-        COMPILE_FLAGS ${_define}
-        RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}
-    )
-    target_link_libraries(${_target} PRIVATE ${ARGN})
-    install(TARGETS ${_target} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
-endfunction()
-
 if (HAVE_UMASK)
     set_property(SOURCE main.cpp APPEND PROPERTY COMPILE_DEFINITIONS HAVE_UMASK)
 endif()
@@ -33,54 +23,23 @@ if(WIN32)
     endif()
 endif()
 
-
 # Build the executables
 if (WANT_CORE)
-    add_executable(quasselcore main.cpp ${WIN_RC})
-    setup_executable(quasselcore -DBUILD_CORE Qt5::Core Quassel::Core)
+    quassel_add_executable(quasselcore COMPONENT Core SOURCES main.cpp ${WIN_RC} LIBRARIES Qt5::Core Quassel::Core)
 endif()
 
 if (WANT_QTCLIENT)
-    add_executable(quasselclient WIN32 main.cpp ${WIN_RC})
-    setup_executable(quasselclient -DBUILD_QTUI Qt5::Core Qt5::Gui Quassel::QtUi)
+    set(libs Qt5::Core Qt5::Gui Quassel::QtUi)
     if (WITH_KDE)
-        target_link_libraries(quasselclient PRIVATE KF5::CoreAddons)
+        list(APPEND libs KF5::CoreAddons)
     endif()
+    quassel_add_executable(quasselclient COMPONENT Client SOURCES main.cpp ${WIN_RC} LIBRARIES ${libs})
 endif()
 
 if (WANT_MONO)
-    add_executable(quassel WIN32 main.cpp monoapplication.cpp ${WIN_RC})
-    setup_executable(quassel -DBUILD_MONO Qt5::Core Qt5::Gui Quassel::Core Quassel::QtUi)
+    set(libs Qt5::Core Qt5::Gui Quassel::Core Quassel::QtUi)
     if (WITH_KDE)
-        target_link_libraries(quassel PRIVATE KF5::CoreAddons)
-    endif()
-endif()
-
-# Build bundles for MacOSX
-if (APPLE)
-    if (WANT_QTCLIENT)
-        add_custom_command(TARGET quasselclient POST_BUILD
-                           COMMAND ${CMAKE_SOURCE_DIR}/scripts/build/macosx_makebundle.py
-                                   ${CMAKE_SOURCE_DIR} "Quassel Client" ${CMAKE_BINARY_DIR}/quasselclient)
-    endif()
-    if (WANT_MONO)
-        add_custom_command(TARGET quassel POST_BUILD
-                           COMMAND ${CMAKE_SOURCE_DIR}/scripts/build/macosx_makebundle.py
-                                   ${CMAKE_SOURCE_DIR} "Quassel" ${CMAKE_BINARY_DIR}/quassel)
-    endif()
-
-    if (DEPLOY)
-        if (WANT_QTCLIENT)
-            add_custom_command(TARGET quasselclient POST_BUILD WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-                               COMMAND ${CMAKE_SOURCE_DIR}/scripts/build/macosx_makePackage.sh Client ${CMAKE_BINARY_DIR} qsvgicon)
-        endif()
-        if (WANT_CORE)
-            add_custom_command(TARGET quasselcore POST_BUILD WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-                               COMMAND ${CMAKE_SOURCE_DIR}/scripts/build/macosx_makePackage.sh Core ${CMAKE_BINARY_DIR})
-        endif()
-        if (WANT_MONO)
-            add_custom_command(TARGET quassel POST_BUILD WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-                               COMMAND ${CMAKE_SOURCE_DIR}/scripts/build/macosx_makePackage.sh Mono ${CMAKE_BINARY_DIR} qsvgicon)
-        endif()
+        list(APPEND libs KF5::CoreAddons)
     endif()
+    quassel_add_executable(quassel COMPONENT Mono SOURCES main.cpp monoapplication.cpp ${WIN_RC} LIBRARIES ${libs})
 endif()