UltiMaker-Cura.spec.jinja 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. # -*- mode: python ; coding: utf-8 -*-
  2. import os
  3. from pathlib import Path
  4. from PyInstaller.utils.hooks import collect_all
  5. datas = {{ datas }}
  6. binaries = {{ binaries }}
  7. hiddenimports = {{ hiddenimports }}
  8. {% for value in collect_all %}tmp_ret = collect_all('{{ value }}')
  9. datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
  10. {% endfor %}
  11. # Add dynamic libs in the venv bin/Script Path. This is needed because we might copy some additional libs
  12. # e.q.: OpenSSL 1.1.1l in that directory with a separate:
  13. # `conan install openssl@1.1.1l -g deploy && cp openssl/bin/*.so cura_inst/bin`
  14. binaries.extend([(str(bin), ".") for bin in Path(r"{{ venv_script_path }}").glob("*.so*")])
  15. binaries.extend([(str(bin), ".") for bin in Path(r"{{ venv_script_path }}").glob("*.dll")])
  16. binaries.extend([(str(bin), ".") for bin in Path(r"{{ venv_script_path }}").glob("*.dylib")])
  17. block_cipher = None
  18. a = Analysis(
  19. [{{ entrypoint }}],
  20. pathex=[],
  21. binaries=binaries,
  22. datas=datas,
  23. hiddenimports=hiddenimports,
  24. hookspath=[],
  25. hooksconfig={},
  26. runtime_hooks=[],
  27. excludes=[],
  28. win_no_prefer_redirects=False,
  29. win_private_assemblies=False,
  30. cipher=block_cipher,
  31. noarchive=False
  32. )
  33. pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
  34. exe = EXE(
  35. pyz,
  36. a.scripts,
  37. [],
  38. exclude_binaries=True,
  39. name=r'{{ name }}',
  40. debug=False,
  41. bootloader_ignore_signals=False,
  42. strip={{ strip }},
  43. upx={{ upx }},
  44. console=False,
  45. disable_windowed_traceback=False,
  46. argv_emulation=False,
  47. target_arch={{ target_arch }},
  48. codesign_identity=os.getenv('CODESIGN_IDENTITY', None),
  49. entitlements_file={{ entitlements_file }},
  50. icon={{ icon }}
  51. )
  52. coll = COLLECT(
  53. exe,
  54. a.binaries,
  55. a.zipfiles,
  56. a.datas,
  57. strip=False,
  58. upx=True,
  59. upx_exclude=[],
  60. name=r'{{ name }}'
  61. )
  62. {% if macos == true %}
  63. # PyInstaller seems to copy everything in the resource folder for the MacOS, this causes issues with codesigning and notarizing
  64. # The folder structure should adhere to the one specified in Table 2-5
  65. # https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1
  66. # The class below is basically ducktyping the BUNDLE class of PyInstaller and using our own `assemble` method for more fine-grain and specific
  67. # control. Some code of the method below is copied from:
  68. # https://github.com/pyinstaller/pyinstaller/blob/22d1d2a5378228744cc95f14904dae1664df32c4/PyInstaller/building/osx.py#L115
  69. #-----------------------------------------------------------------------------
  70. # Copyright (c) 2005-2022, PyInstaller Development Team.
  71. #
  72. # Distributed under the terms of the GNU General Public License (version 2
  73. # or later) with exception for distributing the bootloader.
  74. #
  75. # The full license is in the file COPYING.txt, distributed with this software.
  76. #
  77. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  78. #-----------------------------------------------------------------------------
  79. import plistlib
  80. import shutil
  81. import PyInstaller.utils.osx as osxutils
  82. from pathlib import Path
  83. from PyInstaller.building.osx import BUNDLE
  84. from PyInstaller.building.utils import (_check_path_overlap, _rmtree, add_suffix_to_extension, checkCache)
  85. from PyInstaller.building.datastruct import logger
  86. from PyInstaller.building.icon import normalize_icon_type
  87. class UMBUNDLE(BUNDLE):
  88. def assemble(self):
  89. from PyInstaller.config import CONF
  90. if _check_path_overlap(self.name) and os.path.isdir(self.name):
  91. _rmtree(self.name)
  92. logger.info("Building BUNDLE %s", self.tocbasename)
  93. # Create a minimal Mac bundle structure.
  94. macos_path = Path(self.name, "Contents", "MacOS")
  95. resources_path = Path(self.name, "Contents", "Resources")
  96. frameworks_path = Path(self.name, "Contents", "Frameworks")
  97. os.makedirs(macos_path)
  98. os.makedirs(resources_path)
  99. os.makedirs(frameworks_path)
  100. # Makes sure the icon exists and attempts to convert to the proper format if applicable
  101. self.icon = normalize_icon_type(self.icon, ("icns",), "icns", CONF["workpath"])
  102. # Ensure icon path is absolute
  103. self.icon = os.path.abspath(self.icon)
  104. # Copy icns icon to Resources directory.
  105. shutil.copy(self.icon, os.path.join(self.name, 'Contents', 'Resources'))
  106. # Key/values for a minimal Info.plist file
  107. info_plist_dict = {
  108. "CFBundleDisplayName": self.appname,
  109. "CFBundleName": self.appname,
  110. # Required by 'codesign' utility.
  111. # The value for CFBundleIdentifier is used as the default unique name of your program for Code Signing
  112. # purposes. It even identifies the APP for access to restricted OS X areas like Keychain.
  113. #
  114. # The identifier used for signing must be globally unique. The usual form for this identifier is a
  115. # hierarchical name in reverse DNS notation, starting with the toplevel domain, followed by the company
  116. # name, followed by the department within the company, and ending with the product name. Usually in the
  117. # form: com.mycompany.department.appname
  118. # CLI option --osx-bundle-identifier sets this value.
  119. "CFBundleIdentifier": self.bundle_identifier,
  120. "CFBundleExecutable": os.path.basename(self.exename),
  121. "CFBundleIconFile": os.path.basename(self.icon),
  122. "CFBundleInfoDictionaryVersion": "6.0",
  123. "CFBundlePackageType": "APPL",
  124. "CFBundleVersionString": self.version,
  125. "CFBundleShortVersionString": self.version,
  126. }
  127. # Set some default values. But they still can be overwritten by the user.
  128. if self.console:
  129. # Setting EXE console=True implies LSBackgroundOnly=True.
  130. info_plist_dict['LSBackgroundOnly'] = True
  131. else:
  132. # Let's use high resolution by default.
  133. info_plist_dict['NSHighResolutionCapable'] = True
  134. # Merge info_plist settings from spec file
  135. if isinstance(self.info_plist, dict) and self.info_plist:
  136. info_plist_dict.update(self.info_plist)
  137. plist_filename = os.path.join(self.name, "Contents", "Info.plist")
  138. with open(plist_filename, "wb") as plist_fh:
  139. plistlib.dump(info_plist_dict, plist_fh)
  140. links = []
  141. _QT_BASE_PATH = {'PySide2', 'PySide6', 'PyQt5', 'PyQt6', 'PySide6'}
  142. for inm, fnm, typ in self.toc:
  143. # Adjust name for extensions, if applicable
  144. inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ)
  145. inm = Path(inm)
  146. fnm = Path(fnm)
  147. # Copy files from cache. This ensures that are used files with relative paths to dynamic library
  148. # dependencies (@executable_path)
  149. if typ in ('EXTENSION', 'BINARY') or (typ == 'DATA' and inm.suffix == '.so'):
  150. if any(['.' in p for p in inm.parent.parts]):
  151. inm = Path(inm.name)
  152. fnm = Path(checkCache(
  153. str(fnm),
  154. strip = self.strip,
  155. upx = self.upx,
  156. upx_exclude = self.upx_exclude,
  157. dist_nm = str(inm),
  158. target_arch = self.target_arch,
  159. codesign_identity = self.codesign_identity,
  160. entitlements_file = self.entitlements_file,
  161. strict_arch_validation = (typ == 'EXTENSION'),
  162. ))
  163. frame_dst = frameworks_path.joinpath(inm)
  164. if not frame_dst.exists():
  165. if frame_dst.is_dir():
  166. os.makedirs(frame_dst, exist_ok = True)
  167. else:
  168. os.makedirs(frame_dst.parent, exist_ok = True)
  169. shutil.copy(fnm, frame_dst, follow_symlinks = True)
  170. macos_dst = macos_path.joinpath(inm)
  171. if not macos_dst.exists():
  172. if macos_dst.is_dir():
  173. os.makedirs(macos_dst, exist_ok = True)
  174. else:
  175. os.makedirs(macos_dst.parent, exist_ok = True)
  176. # Create relative symlink to the framework
  177. symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Frameworks").joinpath(
  178. frame_dst.relative_to(frameworks_path))
  179. try:
  180. macos_dst.symlink_to(symlink_to)
  181. except FileExistsError:
  182. pass
  183. else:
  184. if typ == 'DATA':
  185. if any(['.' in p for p in inm.parent.parts]) or inm.suffix == '.so':
  186. # Skip info dist egg and some not needed folders in tcl and tk, since they all contain dots in their files
  187. logger.warning(f"Skipping DATA file {inm}")
  188. continue
  189. res_dst = resources_path.joinpath(inm)
  190. if not res_dst.exists():
  191. if res_dst.is_dir():
  192. os.makedirs(res_dst, exist_ok = True)
  193. else:
  194. os.makedirs(res_dst.parent, exist_ok = True)
  195. shutil.copy(fnm, res_dst, follow_symlinks = True)
  196. macos_dst = macos_path.joinpath(inm)
  197. if not macos_dst.exists():
  198. if macos_dst.is_dir():
  199. os.makedirs(macos_dst, exist_ok = True)
  200. else:
  201. os.makedirs(macos_dst.parent, exist_ok = True)
  202. # Create relative symlink to the resource
  203. symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Resources").joinpath(
  204. res_dst.relative_to(resources_path))
  205. try:
  206. macos_dst.symlink_to(symlink_to)
  207. except FileExistsError:
  208. pass
  209. else:
  210. macos_dst = macos_path.joinpath(inm)
  211. if not macos_dst.exists():
  212. if macos_dst.is_dir():
  213. os.makedirs(macos_dst, exist_ok = True)
  214. else:
  215. os.makedirs(macos_dst.parent, exist_ok = True)
  216. shutil.copy(fnm, macos_dst, follow_symlinks = True)
  217. # Sign the bundle
  218. logger.info('Signing the BUNDLE...')
  219. try:
  220. osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep = True)
  221. except Exception as e:
  222. logger.warning(f"Error while signing the bundle: {e}")
  223. logger.warning("You will need to sign the bundle manually!")
  224. logger.info(f"Building BUNDLE {self.tocbasename} completed successfully.")
  225. app = UMBUNDLE(
  226. coll,
  227. name='{{ display_name }}.app',
  228. icon={{ icon }},
  229. bundle_identifier={{ osx_bundle_identifier }} + "_" + '{{ display_name }}'.replace(" ", "_") + "_" {{ short_version }},
  230. version={{ version }},
  231. info_plist={
  232. 'CFBundleDisplayName': '{{ display_name }}',
  233. 'NSPrincipalClass': 'NSApplication',
  234. 'CFBundleDevelopmentRegion': 'English',
  235. 'CFBundleExecutable': '{{ name }}',
  236. 'CFBundleInfoDictionaryVersion': '6.0',
  237. 'CFBundlePackageType': 'APPL',
  238. 'CFBundleVersionString': {{ version }},
  239. 'CFBundleShortVersionString': {{ short_version }},
  240. 'CFBundleURLTypes': [{
  241. 'CFBundleURLName': '{{ display_name }}',
  242. 'CFBundleURLSchemes': ['cura', 'prusaslicer'],
  243. }],
  244. 'CFBundleDocumentTypes': [{
  245. 'CFBundleTypeRole': 'Viewer',
  246. 'CFBundleTypeExtensions': ['*'],
  247. 'CFBundleTypeName': 'Model Files',
  248. }]
  249. },
  250. ){% endif %}