Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(macos): codesign despite Qt5 folder names #217

Merged
merged 1 commit into from
Mar 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/install-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ ls -la /usr/local/lib/libportaudio.dylib
ls -la /usr/local/Cellar/portaudio/*/lib/libportaudio*.dylib
ls -la dist/friture.app/Contents/MacOS/_sounddevice_data/portaudio-binaries

# PyInstaller will try to codesign friture.app, but will fail because of the Qt5 file structure
# (this is visible in the logs)
# see https://github.com/pyinstaller/pyinstaller/wiki/Recipe-OSX-Code-Signing-Qt
# so we fix the folder names and then sign again manually
python3 installer/fix_app_qt_folder_names_for_codesign.py dist/friture.app
codesign -s - --force --all-architectures --timestamp --deep dist/friture.app
codesign -dv dist/friture.app

# prepare a dmg out of friture.app
export ARTIFACT_FILENAME=friture-$(python3 -c 'import friture; print(friture.__version__)')-$(date +'%Y%m%d').dmg
echo $ARTIFACT_FILENAME
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ Notepad++
.vscode
.qt_for_python

.mypy_cache
.mypy_cache

# Macos
.DS_Store
123 changes: 123 additions & 0 deletions installer/fix_app_qt_folder_names_for_codesign.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-

# see https://github.com/pyinstaller/pyinstaller/wiki/Recipe-OSX-Code-Signing-Qt

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:]))