English version: Recipe OSX Code Signing Qt.

Si votre application utilise Qt et sa fonctionnalité QML, la signature de code ne fonctionnera pas. En cause les dossiers contenant un point dans leur nom qui ne sont pas autorisés dans le dossier Contents/MacOS. Et le fait d'utiliser QML implique des dossiers du genre QtQuick.2 pour faire la différence entre les différentes versions de QtQuick, par exemple.

La documentation révélatrice stipule :

Note that a location where code is expected to reside cannot generally contain directories full of nested code, because those directories tend to be interpreted as bundles. So this occasional practice is not recommended and not officially supported. If you do do this, do not use periods in the directory names. The code signing machinery interprets directories with periods in their names as code bundles and will reject them if they don't conform to the expected code bundle layout.

Il s'agit là bien d'un problème du côté de Qt, mais vous pouvez toujours avoir une arborescence de répertoires conforme en utilisant le script ci-dessous. Il prend en paramètre une ou plusieurs .app. L'ensemble du processus est le suivant :

  1. déplacement des dossiers problématiques depuis MacOS vers Resources ;
  2. correction des chemins de recherche des DLLs ;
  3. création des liens symboliques appropriés.

Voici le contenu du fichier fix_app_qt_folder_names_for_codesign.py :

# coding: utf-8
import os
import shutil
import sys
from macholib.MachO import MachO
from pathlib import Path
from typing import Generator, List, Optional


def create_symlink(folder: Path) -> None:
    """Create the appropriate symlink in the MacOS folder
    pointing to the Resources folder.
    """
    sibbling = Path(str(folder).replace("MacOS", ""))

    # PyQt5/Qt/qml/QtQml/Models.2
    root = str(sibbling).partition("Contents")[2].lstrip("/")
    # ../../../../
    backward = "../" * (root.count("/") + 1)
    # ../../../../Resources/PyQt5/Qt/qml/QtQml/Models.2
    good_path = f"{backward}Resources/{root}"

    folder.symlink_to(good_path)


def fix_dll(dll: Path) -> None:
    """Fix the DLL lookup paths to use relative ones for Qt dependencies.
    Inspiration: PyInstaller/depend/dylib.py:mac_set_relative_dylib_deps()
    Currently one header is pointing to (we are in the Resources folder):
        @loader_path/../../../../QtCore (it is referencing to the old MacOS folder)
    It will be converted to:
        @loader_path/../../../../../../MacOS/QtCore
    """

    def match_func(pth: str) -> Optional[str]:
        """Callback function for MachO.rewriteLoadCommands() that is
        called on every lookup path setted in the DLL headers.
        By returning None for system libraries, it changes nothing.
        Else we return a relative path pointing to the good file
        in the MacOS folder.
        """
        basename = os.path.basename(pth)
        if not basename.startswith("Qt"):
            return None
        return f"@loader_path{good_path}/{basename}"

    # Resources/PyQt5/Qt/qml/QtQuick/Controls.2/Fusion
    root = str(dll.parent).partition("Contents")[2][1:]
    # /../../../../../../..
    backward = "/.." * (root.count("/") + 1)
    # /../../../../../../../MacOS
    good_path = f"{backward}/MacOS"

    # Rewrite Mach headers with corrected @loader_path
    dll = MachO(dll)
    dll.rewriteLoadCommands(match_func)
    with open(dll.filename, "rb+") as f:
        for header in dll.headers:
            f.seek(0)
            dll.write(f)
        f.seek(0, 2)
        f.flush()


def find_problematic_folders(folder: Path) -> Generator[Path, None, None]:
    """Recursively yields problematic folders (containing a dot in their name)."""
    for path in folder.iterdir():
        if not path.is_dir() or path.is_symlink():
            # Skip simlinks as they are allowed (even with a dot)
            continue
        if path.name == "qml":
            yield path
        else:
            yield from find_problematic_folders(path)


def move_contents_to_resources(folder: Path) -> Generator[Path, None, None]:
    """Recursively move any non symlink file from a problematic folder
    to the sibbling one in Resources.
    """
    for path in folder.iterdir():
        if path.is_symlink():
            continue
        if path.is_dir():
            yield from move_contents_to_resources(path)
        else:
            sibbling = Path(str(path).replace("MacOS", "Resources"))

            # Create the parent if it does not exist yet
            sibbling.parent.mkdir(parents=True, exist_ok=True)

            # Make the move
            shutil.move(path, sibbling)

            # Yield the DLL if it is one
            if sibbling.name.endswith(".dylib"):
                yield sibbling


def main(args: List[str]) -> int:
    """
    Fix the application to allow codesign (NXDRIVE-1301).
    Take one or more .app as arguments: "Nuxeo Drive.app".

    To overall process will:
        - move problematic folders from MacOS to Resources
        - fix the DLLs lookup paths
        - create the appropriate symbolic link
    """
    for app in args:
        name = os.path.basename(app)
        print(f">>> [{name}] Fixing Qt folder names")
        path = Path(app) / "Contents" / "MacOS"
        for folder in find_problematic_folders(path):
            for file in move_contents_to_resources(folder):
                fix_dll(file)
                print(f" !! Fixed {str(file)!r}")
            shutil.rmtree(folder)
            create_symlink(folder)
        print(f">>> [{name}] Application fixed.")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

À utiliser tel que (Python 3.6 minimum est requis) :

$ python fix_app_qt_folder_names_for_codesign.py dist/*.app
>>> [Nuxeo Drive.app] Fixing Qt folder names
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/Qt/labs/calendar/libqtlabscalendarplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/Qt/labs/folderlistmodel/libqmlfolderlistmodelplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/Qt/labs/platform/libqtlabsplatformplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/Qt/labs/qmlmodels/liblabsmodelsplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/Qt/labs/settings/libqmlsettingsplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/Qt/labs/sharedimage/libsharedimageplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/Qt/labs/wavefrontmesh/libqmlwavefrontmeshplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQml/Models.2/libmodelsplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQml/RemoteObjects/libqtqmlremoteobjects.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQml/StateMachine/libqtqmlstatemachine.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Controls/libqtquickcontrolsplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Controls/Styles/Flat/libqtquickextrasflatplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Controls.2/Fusion/libqtquickcontrols2fusionstyleplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Controls.2/Imagine/libqtquickcontrols2imaginestyleplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Controls.2/libqtquickcontrols2plugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Controls.2/Material/libqtquickcontrols2materialstyleplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Controls.2/Universal/libqtquickcontrols2universalstyleplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Dialogs/libdialogplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Dialogs/Private/libdialogsprivateplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Extras/libqtquickextrasplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Layouts/libqquicklayoutsplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/LocalStorage/libqmllocalstorageplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/PrivateWidgets/libwidgetsplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Shapes/libqmlshapesplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Templates.2/libqtquicktemplates2plugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/Window.2/libwindowplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick/XmlListModel/libqmlxmllistmodelplugin.dylib'
 !! Fixed DLL 'dist/Nuxeo Drive.app/Contents/Resources/PyQt5/Qt/qml/QtQuick.2/libqtquick2plugin.dylib'
>>> [Nuxeo Drive.app] Application fixed.

Historique

  • 2019-10-11 : Fix pour macOS 10.14 (Mojave) et utilisation de str.count() plutôt que len(str.split()).