osx.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2022, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. import os
  12. import plistlib
  13. import shutil
  14. from PyInstaller.building.api import COLLECT, EXE
  15. from PyInstaller.building.datastruct import TOC, Target, logger
  16. from PyInstaller.building.utils import (_check_path_overlap, _rmtree, add_suffix_to_extension, checkCache)
  17. from PyInstaller.compat import is_darwin
  18. from PyInstaller.building.icon import normalize_icon_type
  19. if is_darwin:
  20. import PyInstaller.utils.osx as osxutils
  21. class BUNDLE(Target):
  22. def __init__(self, *args, **kws):
  23. from PyInstaller.config import CONF
  24. # BUNDLE only has a sense under Mac OS, it's a noop on other platforms
  25. if not is_darwin:
  26. return
  27. # Get a path to a .icns icon for the app bundle.
  28. self.icon = kws.get('icon')
  29. if not self.icon:
  30. # --icon not specified; use the default in the pyinstaller folder
  31. self.icon = os.path.join(
  32. os.path.dirname(os.path.dirname(__file__)), 'bootloader', 'images', 'icon-windowed.icns'
  33. )
  34. else:
  35. # User gave an --icon=path. If it is relative, make it relative to the spec file location.
  36. if not os.path.isabs(self.icon):
  37. self.icon = os.path.join(CONF['specpath'], self.icon)
  38. Target.__init__(self)
  39. # .app bundle is created in DISTPATH.
  40. self.name = kws.get('name', None)
  41. base_name = os.path.basename(self.name)
  42. self.name = os.path.join(CONF['distpath'], base_name)
  43. self.appname = os.path.splitext(base_name)[0]
  44. self.version = kws.get("version", "0.0.0")
  45. self.toc = TOC()
  46. self.strip = False
  47. self.upx = False
  48. self.console = True
  49. self.target_arch = None
  50. self.codesign_identity = None
  51. self.entitlements_file = None
  52. # .app bundle identifier for Code Signing
  53. self.bundle_identifier = kws.get('bundle_identifier')
  54. if not self.bundle_identifier:
  55. # Fallback to appname.
  56. self.bundle_identifier = self.appname
  57. self.info_plist = kws.get('info_plist', None)
  58. for arg in args:
  59. if isinstance(arg, EXE):
  60. self.toc.append((os.path.basename(arg.name), arg.name, arg.typ))
  61. self.toc.extend(arg.dependencies)
  62. self.strip = arg.strip
  63. self.upx = arg.upx
  64. self.upx_exclude = arg.upx_exclude
  65. self.console = arg.console
  66. self.target_arch = arg.target_arch
  67. self.codesign_identity = arg.codesign_identity
  68. self.entitlements_file = arg.entitlements_file
  69. elif isinstance(arg, TOC):
  70. self.toc.extend(arg)
  71. # TOC doesn't have a strip or upx attribute, so there is no way for us to tell which cache we should
  72. # draw from.
  73. elif isinstance(arg, COLLECT):
  74. self.toc.extend(arg.toc)
  75. self.strip = arg.strip_binaries
  76. self.upx = arg.upx_binaries
  77. self.upx_exclude = arg.upx_exclude
  78. self.console = arg.console
  79. self.target_arch = arg.target_arch
  80. self.codesign_identity = arg.codesign_identity
  81. self.entitlements_file = arg.entitlements_file
  82. else:
  83. logger.info("unsupported entry %s", arg.__class__.__name__)
  84. # Now, find values for app filepath (name), app name (appname), and name of the actual executable (exename) from
  85. # the first EXECUTABLE item in toc, which might have come from a COLLECT too (not from an EXE).
  86. for inm, name, typ in self.toc:
  87. if typ == "EXECUTABLE":
  88. self.exename = name
  89. break
  90. self.__postinit__()
  91. _GUTS = (
  92. # BUNDLE always builds, just want the toc to be written out
  93. ('toc', None),
  94. )
  95. def _check_guts(self, data, last_build):
  96. # BUNDLE always needs to be executed, since it will clean the output directory anyway to make sure there is no
  97. # existing cruft accumulating.
  98. return 1
  99. def assemble(self):
  100. from PyInstaller.config import CONF
  101. if _check_path_overlap(self.name) and os.path.isdir(self.name):
  102. _rmtree(self.name)
  103. logger.info("Building BUNDLE %s", self.tocbasename)
  104. # Create a minimal Mac bundle structure.
  105. os.makedirs(os.path.join(self.name, "Contents", "MacOS"))
  106. os.makedirs(os.path.join(self.name, "Contents", "Resources"))
  107. os.makedirs(os.path.join(self.name, "Contents", "Frameworks"))
  108. # Makes sure the icon exists and attempts to convert to the proper format if applicable
  109. self.icon = normalize_icon_type(self.icon, ("icns",), "icns", CONF["workpath"])
  110. # Ensure icon path is absolute
  111. self.icon = os.path.abspath(self.icon)
  112. # Copy icns icon to Resources directory.
  113. shutil.copy(self.icon, os.path.join(self.name, 'Contents', 'Resources'))
  114. # Key/values for a minimal Info.plist file
  115. info_plist_dict = {
  116. "CFBundleDisplayName": self.appname,
  117. "CFBundleName": self.appname,
  118. # Required by 'codesign' utility.
  119. # The value for CFBundleIdentifier is used as the default unique name of your program for Code Signing
  120. # purposes. It even identifies the APP for access to restricted OS X areas like Keychain.
  121. #
  122. # The identifier used for signing must be globally unique. The usual form for this identifier is a
  123. # hierarchical name in reverse DNS notation, starting with the toplevel domain, followed by the company
  124. # name, followed by the department within the company, and ending with the product name. Usually in the
  125. # form: com.mycompany.department.appname
  126. # CLI option --osx-bundle-identifier sets this value.
  127. "CFBundleIdentifier": self.bundle_identifier,
  128. "CFBundleExecutable": os.path.basename(self.exename),
  129. "CFBundleIconFile": os.path.basename(self.icon),
  130. "CFBundleInfoDictionaryVersion": "6.0",
  131. "CFBundlePackageType": "APPL",
  132. "CFBundleShortVersionString": self.version,
  133. }
  134. # Set some default values. But they still can be overwritten by the user.
  135. if self.console:
  136. # Setting EXE console=True implies LSBackgroundOnly=True.
  137. info_plist_dict['LSBackgroundOnly'] = True
  138. else:
  139. # Let's use high resolution by default.
  140. info_plist_dict['NSHighResolutionCapable'] = True
  141. # Merge info_plist settings from spec file
  142. if isinstance(self.info_plist, dict) and self.info_plist:
  143. info_plist_dict.update(self.info_plist)
  144. plist_filename = os.path.join(self.name, "Contents", "Info.plist")
  145. with open(plist_filename, "wb") as plist_fh:
  146. plistlib.dump(info_plist_dict, plist_fh)
  147. links = []
  148. _QT_BASE_PATH = {'PySide2', 'PySide6', 'PyQt5', 'PySide6'}
  149. for inm, fnm, typ in self.toc:
  150. # Adjust name for extensions, if applicable
  151. inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ)
  152. # Copy files from cache. This ensures that are used files with relative paths to dynamic library
  153. # dependencies (@executable_path)
  154. base_path = inm.split('/', 1)[0]
  155. if typ in ('EXTENSION', 'BINARY'):
  156. fnm = checkCache(
  157. fnm,
  158. strip=self.strip,
  159. upx=self.upx,
  160. upx_exclude=self.upx_exclude,
  161. dist_nm=inm,
  162. target_arch=self.target_arch,
  163. codesign_identity=self.codesign_identity,
  164. entitlements_file=self.entitlements_file,
  165. strict_arch_validation=(typ == 'EXTENSION'),
  166. )
  167. # Add most data files to a list for symlinking later.
  168. if typ == 'DATA' and base_path not in _QT_BASE_PATH:
  169. links.append((inm, fnm))
  170. else:
  171. tofnm = os.path.join(self.name, "Contents", "MacOS", inm)
  172. todir = os.path.dirname(tofnm)
  173. if not os.path.exists(todir):
  174. os.makedirs(todir)
  175. if os.path.isdir(fnm):
  176. # Because shutil.copy2() is the default copy function for shutil.copytree, this will also copy file
  177. # metadata.
  178. shutil.copytree(fnm, tofnm)
  179. else:
  180. shutil.copy(fnm, tofnm)
  181. logger.info('Moving BUNDLE data files to Resource directory')
  182. # Mac OS Code Signing does not work when .app bundle contains data files in dir ./Contents/MacOS.
  183. # Put all data files in ./Resources and create symlinks in ./MacOS.
  184. bin_dir = os.path.join(self.name, 'Contents', 'MacOS')
  185. res_dir = os.path.join(self.name, 'Contents', 'Resources')
  186. for inm, fnm in links:
  187. tofnm = os.path.join(res_dir, inm)
  188. todir = os.path.dirname(tofnm)
  189. if not os.path.exists(todir):
  190. os.makedirs(todir)
  191. if os.path.isdir(fnm):
  192. # Because shutil.copy2() is the default copy function for shutil.copytree, this will also copy file
  193. # metadata.
  194. shutil.copytree(fnm, tofnm)
  195. else:
  196. shutil.copy(fnm, tofnm)
  197. base_path = os.path.split(inm)[0]
  198. if base_path:
  199. if not os.path.exists(os.path.join(bin_dir, inm)):
  200. path = ''
  201. for part in iter(base_path.split(os.path.sep)):
  202. # Build path from previous path and the next part of the base path
  203. path = os.path.join(path, part)
  204. try:
  205. relative_source_path = os.path.relpath(
  206. os.path.join(res_dir, path),
  207. os.path.split(os.path.join(bin_dir, path))[0]
  208. )
  209. dest_path = os.path.join(bin_dir, path)
  210. os.symlink(relative_source_path, dest_path)
  211. break
  212. except FileExistsError:
  213. pass
  214. if not os.path.exists(os.path.join(bin_dir, inm)):
  215. relative_source_path = os.path.relpath(
  216. os.path.join(res_dir, inm),
  217. os.path.split(os.path.join(bin_dir, inm))[0]
  218. )
  219. dest_path = os.path.join(bin_dir, inm)
  220. os.symlink(relative_source_path, dest_path)
  221. else: # If path is empty, e.g., a top-level file, try to just symlink the file.
  222. os.symlink(
  223. os.path.relpath(os.path.join(res_dir, inm),
  224. os.path.split(os.path.join(bin_dir, inm))[0]), os.path.join(bin_dir, inm)
  225. )
  226. # Sign the bundle
  227. logger.info('Signing the BUNDLE...')
  228. try:
  229. osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep=True)
  230. except Exception as e:
  231. logger.warning("Error while signing the bundle: %s", e)
  232. logger.warning("You will need to sign the bundle manually!")
  233. logger.info("Building BUNDLE %s completed successfully.", self.tocbasename)