-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Recipe OSX Code Signing Qt
If your application is using Qt and its QML feature, this will likely break the code signing. The root cause if that folders containing a dot in their name is not allowed in the Contents/MacOS
folder. And Qt has folders like QtQuick.2
to make the difference between QtQuick versions.
The revelant documentation says:
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.
This is an issue on the Qt side, but you can still have a compliant directory tree by using the script below. It takes one or more .app as arguments. The overall process will:
- move problematic folders from
MacOS
toResources
- fix the DLLs lookup paths
- create the appropriate symbolic link
Content of the fix_app_qt_folder_names_for_codesign.py file:
# -*- coding: utf-8 -*-
import os
import shutil
import sys
from pathlib import Path
from typing import Generator, List, Optional
from macholib.MachO import MachO
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 "." in path.name:
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.name == "qml":
yield from move_contents_to_resources(path)
else:
sibbling = Path(str(path).replace("MacOS", "Resources"))
sibbling.parent.mkdir(parents=True, exist_ok=True)
shutil.move(path, sibbling)
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):
try:
fix_dll(file)
except (ValueError, IsADirectoryError):
continue
shutil.rmtree(folder)
create_symlink(folder)
print(f" !! Fixed {folder}")
print(f">>> [{name}] Application fixed.")
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Usage: python3 fix_app_qt_folder_names_for_codesign.py myapp.app