fix_app_qt_folder_names_for_codesign.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. # -*- coding: utf-8 -*-
  2. # Due to the dots present in the various qt paths code signing doesn't work on MacOS
  3. # Running this script over the packaged .app fill fixes that problem
  4. #
  5. # usage: python3 fix_app_qt_folder_names_for_codesign.py dist/dist/Ultimaker-Cura.app
  6. #
  7. # source: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-OSX-Code-Signing-Qt
  8. import os
  9. import shutil
  10. import sys
  11. from pathlib import Path
  12. from typing import Generator, List, Optional
  13. from macholib.MachO import MachO
  14. def create_symlink(folder: Path) -> None:
  15. """Create the appropriate symlink in the MacOS folder
  16. pointing to the Resources folder.
  17. """
  18. sibbling = Path(str(folder).replace("MacOS", ""))
  19. # PyQt5/Qt/qml/QtQml/Models.2
  20. root = str(sibbling).partition("Contents")[2].lstrip("/")
  21. # ../../../../
  22. backward = "../" * (root.count("/") + 1)
  23. # ../../../../Resources/PyQt5/Qt/qml/QtQml/Models.2
  24. good_path = f"{backward}Resources/{root}"
  25. folder.symlink_to(good_path)
  26. def fix_dll(dll: Path) -> None:
  27. """Fix the DLL lookup paths to use relative ones for Qt dependencies.
  28. Inspiration: PyInstaller/depend/dylib.py:mac_set_relative_dylib_deps()
  29. Currently one header is pointing to (we are in the Resources folder):
  30. @loader_path/../../../../QtCore (it is referencing to the old MacOS folder)
  31. It will be converted to:
  32. @loader_path/../../../../../../MacOS/QtCore
  33. """
  34. def match_func(pth: str) -> Optional[str]:
  35. """Callback function for MachO.rewriteLoadCommands() that is
  36. called on every lookup path setted in the DLL headers.
  37. By returning None for system libraries, it changes nothing.
  38. Else we return a relative path pointing to the good file
  39. in the MacOS folder.
  40. """
  41. basename = os.path.basename(pth)
  42. if not basename.startswith("Qt"):
  43. return None
  44. return f"@loader_path{good_path}/{basename}"
  45. # Resources/PyQt5/Qt/qml/QtQuick/Controls.2/Fusion
  46. root = str(dll.parent).partition("Contents")[2][1:]
  47. # /../../../../../../..
  48. backward = "/.." * (root.count("/") + 1)
  49. # /../../../../../../../MacOS
  50. good_path = f"{backward}/MacOS"
  51. # Rewrite Mach headers with corrected @loader_path
  52. dll = MachO(dll)
  53. dll.rewriteLoadCommands(match_func)
  54. with open(dll.filename, "rb+") as f:
  55. for header in dll.headers:
  56. f.seek(0)
  57. dll.write(f)
  58. f.seek(0, 2)
  59. f.flush()
  60. def find_problematic_folders(folder: Path) -> Generator[Path, None, None]:
  61. """Recursively yields problematic folders (containing a dot in their name)."""
  62. for path in folder.iterdir():
  63. if not path.is_dir() or path.is_symlink():
  64. # Skip simlinks as they are allowed (even with a dot)
  65. continue
  66. if "." in path.name:
  67. yield path
  68. else:
  69. yield from find_problematic_folders(path)
  70. def move_contents_to_resources(folder: Path) -> Generator[Path, None, None]:
  71. """Recursively move any non symlink file from a problematic folder
  72. to the sibbling one in Resources.
  73. """
  74. for path in folder.iterdir():
  75. if path.is_symlink():
  76. continue
  77. if path.name == "qml":
  78. yield from move_contents_to_resources(path)
  79. else:
  80. sibbling = Path(str(path).replace("MacOS", "Resources"))
  81. sibbling.parent.mkdir(parents=True, exist_ok=True)
  82. shutil.move(path, sibbling)
  83. yield sibbling
  84. def main(args: List[str]) -> int:
  85. """
  86. Fix the application to allow codesign (NXDRIVE-1301).
  87. Take one or more .app as arguments: "Nuxeo Drive.app".
  88. To overall process will:
  89. - move problematic folders from MacOS to Resources
  90. - fix the DLLs lookup paths
  91. - create the appropriate symbolic link
  92. """
  93. for app in args:
  94. name = os.path.basename(app)
  95. print(f">>> [{name}] Fixing Qt folder names")
  96. path = Path(app) / "Contents" / "MacOS"
  97. for folder in find_problematic_folders(path):
  98. for file in move_contents_to_resources(folder):
  99. try:
  100. fix_dll(file)
  101. except (ValueError, IsADirectoryError):
  102. continue
  103. shutil.rmtree(folder)
  104. create_symlink(folder)
  105. print(f" !! Fixed {folder}")
  106. print(f">>> [{name}] Application fixed.")
  107. if __name__ == "__main__":
  108. sys.exit(main(sys.argv[1:]))