osx.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2023, 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 pathlib
  13. import plistlib
  14. import shutil
  15. import subprocess
  16. from PyInstaller import log as logging
  17. from PyInstaller.building.api import COLLECT, EXE
  18. from PyInstaller.building.datastruct import Target, logger, normalize_toc
  19. from PyInstaller.building.utils import _check_path_overlap, _rmtree, process_collected_binary
  20. from PyInstaller.compat import is_darwin, strict_collect_mode
  21. from PyInstaller.building.icon import normalize_icon_type
  22. import PyInstaller.utils.misc as miscutils
  23. if is_darwin:
  24. import PyInstaller.utils.osx as osxutils
  25. # Character sequence used to replace dot (`.`) in names of directories that are created in `Contents/MacOS` or
  26. # `Contents/Frameworks`, where only .framework bundle directories are allowed to have dot in name.
  27. DOT_REPLACEMENT = '__dot__'
  28. WINDOWED_ONEFILE_DEPRCATION = (
  29. "Onefile mode in combination with macOS .app bundles (windowed mode) don't make sense (a .app bundle can not be a "
  30. "single file) and clashes with macOS's security. Please migrate to onedir mode. This will become an error "
  31. "in v7.0."
  32. )
  33. class BUNDLE(Target):
  34. def __init__(self, *args, **kwargs):
  35. from PyInstaller.config import CONF
  36. for item in args:
  37. if isinstance(item, EXE) and not item.exclude_binaries:
  38. logger.log(logging.DEPRECATION, WINDOWED_ONEFILE_DEPRCATION)
  39. # BUNDLE only has a sense under macOS, it is a noop on other platforms.
  40. if not is_darwin:
  41. return
  42. # Get a path to a .icns icon for the app bundle.
  43. self.icon = kwargs.get('icon')
  44. if not self.icon:
  45. # --icon not specified; use the default in the pyinstaller folder
  46. self.icon = os.path.join(
  47. os.path.dirname(os.path.dirname(__file__)), 'bootloader', 'images', 'icon-windowed.icns'
  48. )
  49. else:
  50. # User gave an --icon=path. If it is relative, make it relative to the spec file location.
  51. if not os.path.isabs(self.icon):
  52. self.icon = os.path.join(CONF['specpath'], self.icon)
  53. super().__init__()
  54. # .app bundle is created in DISTPATH.
  55. self.name = kwargs.get('name', None)
  56. base_name = os.path.basename(self.name)
  57. self.name = os.path.join(CONF['distpath'], base_name)
  58. self.appname = os.path.splitext(base_name)[0]
  59. # Ensure version is a string, even if user accidentally passed an int or a float.
  60. # Having a `CFBundleShortVersionString` entry of non-string type in `Info.plist` causes the .app bundle to
  61. # crash at start (#4466).
  62. self.version = str(kwargs.get("version", "0.0.0"))
  63. self.toc = []
  64. self.strip = False
  65. self.upx = False
  66. self.console = True
  67. self.target_arch = None
  68. self.codesign_identity = None
  69. self.entitlements_file = None
  70. # .app bundle identifier for Code Signing
  71. self.bundle_identifier = kwargs.get('bundle_identifier')
  72. if not self.bundle_identifier:
  73. # Fallback to appname.
  74. self.bundle_identifier = self.appname
  75. self.info_plist = kwargs.get('info_plist', None)
  76. for arg in args:
  77. # Valid arguments: EXE object, COLLECT object, and TOC-like iterables
  78. if isinstance(arg, EXE):
  79. # Add EXE as an entry to the TOC, and merge its dependencies TOC
  80. self.toc.append((os.path.basename(arg.name), arg.name, 'EXECUTABLE'))
  81. self.toc.extend(arg.dependencies)
  82. # Inherit settings
  83. self.strip = arg.strip
  84. self.upx = arg.upx
  85. self.upx_exclude = arg.upx_exclude
  86. self.console = arg.console
  87. self.target_arch = arg.target_arch
  88. self.codesign_identity = arg.codesign_identity
  89. self.entitlements_file = arg.entitlements_file
  90. elif isinstance(arg, COLLECT):
  91. # Merge the TOC
  92. self.toc.extend(arg.toc)
  93. # Inherit settings
  94. self.strip = arg.strip_binaries
  95. self.upx = arg.upx_binaries
  96. self.upx_exclude = arg.upx_exclude
  97. self.console = arg.console
  98. self.target_arch = arg.target_arch
  99. self.codesign_identity = arg.codesign_identity
  100. self.entitlements_file = arg.entitlements_file
  101. elif miscutils.is_iterable(arg):
  102. # TOC-like iterable
  103. self.toc.extend(arg)
  104. else:
  105. raise TypeError(f"Invalid argument type for BUNDLE: {type(arg)!r}")
  106. # Infer the executable name from the first EXECUTABLE entry in the TOC; it might have come from the COLLECT
  107. # (as opposed to the stand-alone EXE).
  108. for dest_name, src_name, typecode in self.toc:
  109. if typecode == "EXECUTABLE":
  110. self.exename = src_name
  111. break
  112. else:
  113. raise ValueError("No EXECUTABLE entry found in the TOC!")
  114. # Normalize TOC
  115. self.toc = normalize_toc(self.toc)
  116. # Alphabetically sort the TOC to ensure that order of processing is predictable and reproducible.
  117. self.toc.sort()
  118. self.__postinit__()
  119. _GUTS = (
  120. # BUNDLE always builds, just want the toc to be written out
  121. ('toc', None),
  122. )
  123. def _check_guts(self, data, last_build):
  124. # BUNDLE always needs to be executed, in order to clean the output directory.
  125. return True
  126. # Helper for determining whether the given file belongs to a .framework bundle or not. If it does, it returns
  127. # the path to the top-level .framework bundle directory; otherwise, returns None. In case of nested .framework
  128. # bundles, the path to the top-most .framework bundle directory is returned.
  129. @staticmethod
  130. def _is_framework_file(dest_path):
  131. # NOTE: reverse the parents list because we are looking for the top-most .framework bundle directory!
  132. for parent in reversed(dest_path.parents):
  133. if parent.name.endswith('.framework'):
  134. return parent
  135. return None
  136. # Helper that computes relative cross-link path between link's location and target, assuming they are both
  137. # rooted in the `Contents` directory of a macOS .app bundle.
  138. @staticmethod
  139. def _compute_relative_crosslink(crosslink_location, crosslink_target):
  140. # We could take symlink_location and symlink_target as they are (relative to parent of the `Contents`
  141. # directory), but that would introduce an unnecessary `../Contents` part. So instead, we take both paths
  142. # relative to the `Contents` directory.
  143. return os.path.join(
  144. *['..' for level in pathlib.PurePath(crosslink_location).relative_to('Contents').parent.parts],
  145. pathlib.PurePath(crosslink_target).relative_to('Contents'),
  146. )
  147. # This method takes the original (input) TOC and processes it into final TOC, based on which the `assemble` method
  148. # performs its file collection. The TOC processing here represents the core of our efforts to generate an .app
  149. # bundle that is compatible with Apple's code-signing requirements.
  150. #
  151. # For in-depth details on the code-signing, see Apple's `Technical Note TN2206: macOS Code Signing In Depth` at
  152. # https://developer.apple.com/library/archive/technotes/tn2206/_index.html
  153. #
  154. # The requirements, framed from PyInstaller's perspective, can be summarized as follows:
  155. #
  156. # 1. The `Contents/MacOS` directory is expected to contain only the program executable and (binary) code (= dylibs
  157. # and nested .framework bundles). Alternatively, the dylibs and .framework bundles can be also placed into
  158. # `Contents/Frameworks` directory (where same rules apply as for `Contents/MacOS`, so the remainder of this
  159. # text refers to the two inter-changeably, unless explicitly noted otherwise). The code in `Contents/MacOS`
  160. # is expected to be signed, and the `codesign` utility will recursively sign all found code when using `--deep`
  161. # option to sign the .app bundle.
  162. #
  163. # 2. All non-code files should be be placed in `Contents/Resources`, so they become sealed (data) resources;
  164. # i.e., their signature data is recorded in `Contents/_CodeSignature/CodeResources`. (As a side note,
  165. # it seems that signature information for data/resources in `Contents/Resources` is kept nder `file` key in
  166. # the `CodeResources` file, while the information for contents in `Contents/MacOS` is kept under `file2` key).
  167. #
  168. # 3. The directories in `Contents/MacOS` may not contain dots (`.`) in their names, except for the nested
  169. # .framework bundle directories. The directories in `Contents/Resources` have no such restrictions.
  170. #
  171. # 4. There may not be any content in the top level of a bundle. In other words, if a bundle has a `Contents`
  172. # or a `Versions` directory at its top level, there may be no other files or directories alongside them. The
  173. # sole exception is that alongside `Versions`, there may be symlinks to files and directories in
  174. # `Versions/Current`. This rule is important for nested .framework bundles that we collect from python packages.
  175. #
  176. # Next, let us consider the consequences of violating each of the above requirements:
  177. #
  178. # 1. Code signing machinery can directly store signature only in Mach-O binaries and nested .framework bundles; if
  179. # a data file is placed in `Contents/MacOS`, the signature is stored in the file's extended attributes. If the
  180. # extended attributes are lost, the program's signature will be broken. Many file transfer techniques (e.g., a
  181. # zip file) do not preserve extended attributes, nor are they preserved when uploading to the Mac App Store.
  182. #
  183. # 2. Putting code (a dylib or a .framework bundle) into `Contents/Resources` causes it to be treated as a resource;
  184. # the outer signature (i.e., of the whole .app bundle) does not know that this nested content is actually a code.
  185. # Consequently, signing the bundle with `codesign --deep` will NOT sign binaries placed in the
  186. # `Contents/Resources`, which may result in missing signatures when .app bundle is verified for notarization.
  187. # This might be worked around by signing each binary separately, and then signing the whole bundle (without the
  188. # `--deep` option), but requires the user to keep track of the offending binaries.
  189. #
  190. # 3. If a directory in `Contents/MacOS` contains a dot in the name, code-signing the bundle fails with
  191. # `bundle format unrecognized, invalid, or unsuitable` due to code signing machinery treating directory as a
  192. # nested .framework bundle directory.
  193. #
  194. # 4. If nested .framework bundle is malformed, the signing of the .app bundle might succeed, but subsequent
  195. # verification will fail, for example with `embedded framework contains modified or invalid version` (as observed
  196. # with .framework bundles shipped by contemporary PyQt/PySide PyPI wheels).
  197. #
  198. # The above requirements are unfortunately often at odds with the structure of python packages:
  199. #
  200. # * In general, python packages are mixed-content directories, where binaries and data files may be expected to
  201. # be found next to each other.
  202. #
  203. # For example, `opencv-python` provides a custom loader script that requires the package to be collected in the
  204. # source-only form by PyInstaller (i.e., the python modules and scripts collected as source .py files). At the
  205. # same time, it expects the .py loader script to be able to find the binary extension next to itself.
  206. #
  207. # Another example of mixed-mode directories are Qt QML components' sub-directories, which contain both the
  208. # component's plugin (a binary) and associated meta files (data files).
  209. #
  210. # * In python world, the directories often contain dots in their names.
  211. #
  212. # Dots are often used for private directories containing binaries that are shipped with a package. For example,
  213. # `numpy/.dylibs`, `scipy/.dylibs`, etc.
  214. #
  215. # Qt QML components may also contain a dot in their name; couple of examples from `PySide2` package:
  216. # `PySide2/Qt/qml/QtQuick.2`, `PySide2/Qt/qml/QtQuick/Controls.2`, `PySide2/Qt/qml/QtQuick/Particles.2`, etc.
  217. #
  218. # The packages' metadata directories also invariably contain dots in the name due to version (for example,
  219. # `numpy-1.24.3.dist-info`).
  220. #
  221. # In the light of all above, PyInstaller attempts to strictly place all files to their mandated location
  222. # (`Contents/MacOS` or `Contents/Frameworks` vs `Contents/Resources`). To preserve the illusion of mixed-content
  223. # directories, the content is cross-linked from one directory to the other. Specifically:
  224. #
  225. # * All entries with DATA typecode are assumed to be data files, and are always placed in corresponding directory
  226. # structure rooted in `Contents/Resources`.
  227. #
  228. # * All entries with BINARY or EXTENSION typecode are always placed in corresponding directory structure rooted in
  229. # `Contents/Frameworks`.
  230. #
  231. # * All entries with EXECUTABLE are placed in `Contents/MacOS` directory.
  232. #
  233. # * For the purposes of relocation, nested .framework bundles are treated as a single BINARY entity; i.e., the
  234. # whole .bundle directory is placed in corresponding directory structure rooted in `Contents/Frameworks` (even
  235. # though some of its contents, such as `Info.plist` file, are actually data files).
  236. #
  237. # * Top-level data files and binaries are always cross-linked to the other directory. For example, given a data file
  238. # `data_file.txt` that was collected into `Contents/Resources`, we create a symbolic link called
  239. # `Contents/MacOS/data_file.txt` that points to `../Resources/data_file.txt`.
  240. #
  241. # * The executable itself, while placed in `Contents/MacOS`, are cross-linked into both `Contents/Framworks` and
  242. # `Contents/Resources`.
  243. #
  244. # * The stand-alone PKG entries (used with onefile builds that side-load the PKG archive) are treated as data files
  245. # and collected into `Contents/Resources`, but cross-linked only into `Contents/MacOS` directory (because they
  246. # must appear to be next to the program executable). This is the only entry type that is cross-linked into the
  247. # `Contents/MacOS` directory and also the only data-like entry type that is not cross-linked into the
  248. # `Contents/Frameworks` directory.
  249. #
  250. # * For files in sub-directories, the cross-linking behavior depends on the type of directory:
  251. #
  252. # * A data-only directory is created in directory structure rooted in `Contents/Resources`, and cross-linked
  253. # into directory structure rooted in `Contents/Frameworks` at directory level (i.e., we link the whole
  254. # directory instead of individual files).
  255. #
  256. # This largely saves us from having to deal with dots in the names of collected metadata directories, which
  257. # are examples of data-only directories.
  258. #
  259. # * A binary-only directory is created in directory structure rooted in `Contents/Frameworks`, and cross-linked
  260. # into `Contents/Resources` at directory level.
  261. #
  262. # * A mixed-content directory is created in both directory structures. Files are placed into corresponding
  263. # directory structure based on their type, and cross-linked into other directory structure at file level.
  264. #
  265. # * This rule is applied recursively; for example, a data-only sub-directory in a mixed-content directory is
  266. # cross-linked at directory level, while adjacent binary and data files are cross-linked at file level.
  267. #
  268. # * To work around the issue with dots in the names of directories in `Contents/Frameworks` (applicable to
  269. # binary-only or mixed-content directories), such directories are created with modified name (the dot replaced
  270. # with a pre-defined pattern). Next to the modified directory, a symbolic link with original name is created,
  271. # pointing to the directory with modified name. With mixed-content directories, this modification is performed
  272. # only on the `Contents/Frameworks` side; the corresponding directory in `Contents/Resources` can be created
  273. # directly, without name modification and symbolic link.
  274. #
  275. # * If a symbolic link needs to be created in a mixed-content directory due to a SYMLINK entry from the original
  276. # TOC (i.e., a "collected" symlink originating from analysis, as opposed to the cross-linking mechanism described
  277. # above), the link is created in both directory structures, each pointing to the resource in its corresponding
  278. # directory structure (with one such resource being an actual file, and the other being a cross-link to the file).
  279. #
  280. # Final remarks:
  281. #
  282. # NOTE: the relocation mechanism is codified by tests in `tests/functional/test_macos_bundle_structure.py`.
  283. #
  284. # NOTE: by placing binaries and nested .framework entries into `Contents/Frameworks` instead of `Contents/MacOS`,
  285. # we have effectively relocated the `sys._MEIPASS` directory from the `Contents/MacOS` (= the parent directory of
  286. # the program executable) into `Contents/Frameworks`. This requires the PyInstaller's bootloader to detect that it
  287. # is running in the app-bundle mode (e.g., by checking if program executable's parent directory is `Contents/NacOS`)
  288. # and adjust the path accordingly.
  289. #
  290. # NOTE: the implemented relocation mechanism depends on the input TOC containing properly classified entries
  291. # w.r.t. BINARY vs DATA. So hooks and .spec files triggering collection of binaries as datas (and vice versa) will
  292. # result in incorrect placement of those files in the generated .app bundle. However, this is *not* the proper place
  293. # to address such issues; if necessary, automatic (re)classification should be added to analysis process, to ensure
  294. # that BUNDLE (as well as other build targets) receive correctly classified TOC.
  295. #
  296. # NOTE: similar to the previous note, the relocation mechanism is also not the proper place to enforce compliant
  297. # structure of the nested .framework bundles. Instead, this is handled by the analysis process, using the
  298. # `PyInstaller.utils.osx.collect_files_from_framework_bundles` helper function. So the input TOC that BUNDLE
  299. # receives should already contain entries that reconstruct compliant nested .framework bundles.
  300. def _process_bundle_toc(self, toc):
  301. bundle_toc = []
  302. # Step 1: inspect the directory layout and classify the directories according to their contents.
  303. directory_types = dict()
  304. _MIXED_DIR_TYPE = 'MIXED-DIR'
  305. _DATA_DIR_TYPE = 'DATA-DIR'
  306. _BINARY_DIR_TYPE = 'BINARY-DIR'
  307. _FRAMEWORK_DIR_TYPE = 'FRAMEWORK-DIR'
  308. _UNKNOWN_DIR_TYPE = 'UNKNOWN-DIR' # used only in this step
  309. _TOP_LEVEL_DIR = pathlib.PurePath('.')
  310. for dest_name, src_name, typecode in toc:
  311. dest_path = pathlib.PurePath(dest_name)
  312. framework_dir = self._is_framework_file(dest_path)
  313. if framework_dir:
  314. # Mark the framework directory as FRAMEWORK-DIR.
  315. directory_types[framework_dir] = _FRAMEWORK_DIR_TYPE
  316. # Treat the framework directory as BINARY file when classifying parent directories.
  317. typecode = 'BINARY'
  318. parent_dirs = framework_dir.parents
  319. else:
  320. parent_dirs = dest_path.parents
  321. # Treat BINARY and EXTENSION as BINARY to simplify further processing.
  322. if typecode == 'EXTENSION':
  323. typecode = 'BINARY'
  324. # Directory type as per this entry's typecode. Symbolic links are "neutral". On the off chance that a
  325. # directory contains only symbolic link(s), we will override the type of "unknown" directories after this
  326. # loop finishes.
  327. if typecode == 'SYMLINK':
  328. entry_type = _UNKNOWN_DIR_TYPE
  329. elif typecode == 'BINARY':
  330. entry_type = _BINARY_DIR_TYPE
  331. else:
  332. entry_type = _DATA_DIR_TYPE
  333. # (Re)classify parent directories
  334. for parent_dir in parent_dirs:
  335. # Skip the top-level `.` dir. This is also the only directory that can contain EXECUTABLE and PKG
  336. # entries, so we do not have to worry about them.
  337. if parent_dir == _TOP_LEVEL_DIR:
  338. continue
  339. directory_type = directory_types.get(parent_dir, entry_type)
  340. # If directory was previously marked as unknown, overwrite its type with the new one.
  341. if directory_type == _UNKNOWN_DIR_TYPE:
  342. directory_type = entry_type
  343. # Update type into mixed-content, if necessary.
  344. if directory_type == _DATA_DIR_TYPE and entry_type == _BINARY_DIR_TYPE:
  345. directory_type = _MIXED_DIR_TYPE
  346. if directory_type == _BINARY_DIR_TYPE and entry_type == _DATA_DIR_TYPE:
  347. directory_type = _MIXED_DIR_TYPE
  348. directory_types[parent_dir] = directory_type
  349. # Reclassify all "unknown" directories into "data-only"; these are directories that contain only one or more
  350. # symbolic links.
  351. directory_types = {
  352. directory: directory_type if directory_type != _UNKNOWN_DIR_TYPE else _DATA_DIR_TYPE
  353. for directory, directory_type in directory_types.items()
  354. }
  355. logger.debug("Directory classification: %r", directory_types)
  356. # Step 2: process the obtained directory structure and create symlink entries for directories that need to be
  357. # cross-linked. Such directories are data-only and binary-only directories (and framework directories) that are
  358. # located either in the top-level directory (have no parent) or in a mixed-content directory.
  359. for directory_path, directory_type in directory_types.items():
  360. # Cross-linking at directory level applies only to data-only and binary-only directories (as well as
  361. # framework directories).
  362. if directory_type == _MIXED_DIR_TYPE:
  363. continue
  364. # The parent needs to be either top-level directory or a mixed-content directory. Otherwise, the parent
  365. # (or one of its ancestors) will get cross-linked, and we do not need the link here.
  366. parent_dir = directory_path.parent
  367. requires_crosslink = parent_dir == _TOP_LEVEL_DIR or directory_types.get(parent_dir) == _MIXED_DIR_TYPE
  368. if not requires_crosslink:
  369. continue
  370. logger.debug("Cross-linking directory %r of type %r", directory_path, directory_type)
  371. # Data-only directories are created in `Contents/Resources`, needs to be cross-linked into `Contents/MacOS`.
  372. # Vice versa for binary-only or framework directories. The directory creation is handled implicitly, when we
  373. # create parent directory structure for collected files.
  374. if directory_type == _DATA_DIR_TYPE:
  375. symlink_src = os.path.join('Contents/Resources', directory_path)
  376. symlink_dest = os.path.join('Contents/Frameworks', directory_path)
  377. else:
  378. symlink_src = os.path.join('Contents/Frameworks', directory_path)
  379. symlink_dest = os.path.join('Contents/Resources', directory_path)
  380. symlink_ref = self._compute_relative_crosslink(symlink_dest, symlink_src)
  381. bundle_toc.append((symlink_dest, symlink_ref, 'SYMLINK'))
  382. # Step 3: first part of the work-around for directories that are located in `Contents/Frameworks` but contain a
  383. # dot in their name. As per `codesign` rules, the only directories in `Contents/Frameworks` that are allowed to
  384. # contain a dot in their name are .framework bundle directories. So we replace the dot with a custom character
  385. # sequence (stored in global `DOT_REPLACEMENT` variable), and create a symbolic with original name pointing to
  386. # the modified name. This is the best we can do with code-sign requirements vs. python community showing their
  387. # packages' dylibs into `.dylib` subdirectories, or Qt storing their Qml components in directories named
  388. # `QtQuick.2`, `QtQuick/Controls.2`, `QtQuick/Particles.2`, `QtQuick/Templates.2`, etc.
  389. #
  390. # In this step, we only prepare symlink entries that link the original directory name (with dot) to the modified
  391. # one (with dot replaced). The parent paths for collected files are modified in later step(s).
  392. for directory_path, directory_type in directory_types.items():
  393. # .framework bundle directories contain a dot in the name, but are allowed that.
  394. if directory_type == _FRAMEWORK_DIR_TYPE:
  395. continue
  396. # Data-only directories are fully located in `Contents/Resources` and cross-linked to `Contents/Frameworks`
  397. # at directory level, so they are also allowed a dot in their name.
  398. if directory_type == _DATA_DIR_TYPE:
  399. continue
  400. # Apply the work-around, if necessary...
  401. if '.' not in directory_path.name:
  402. continue
  403. logger.debug(
  404. "Creating symlink to work around the dot in the name of directory %r (%s)...", str(directory_path),
  405. directory_type
  406. )
  407. # Create a SYMLINK entry, but only for this level. In case of nested directories with dots in names, the
  408. # symlinks for ancestors will be created by corresponding loop iteration.
  409. bundle_toc.append((
  410. os.path.join('Contents/Frameworks', directory_path),
  411. directory_path.name.replace('.', DOT_REPLACEMENT),
  412. 'SYMLINK',
  413. ))
  414. # Step 4: process the entries for collected files, and decide whether they should be placed into
  415. # `Contents/MacOS`, `Contents/Frameworks`, or `Contents/Resources`, and whether they should be cross-linked into
  416. # other directories.
  417. for orig_dest_name, src_name, typecode in toc:
  418. orig_dest_path = pathlib.PurePath(orig_dest_name)
  419. # Special handling for EXECUTABLE and PKG entries
  420. if typecode == 'EXECUTABLE':
  421. # Place into `Contents/MacOS`, ...
  422. file_dest = os.path.join('Contents/MacOS', orig_dest_name)
  423. bundle_toc.append((file_dest, src_name, typecode))
  424. # ... and do nothing else. We explicitly avoid cross-linking the executable to `Contents/Frameworks` and
  425. # `Contents/Resources`, because it should be not necessary (the executable's location should be
  426. # discovered via `sys.executable`) and to prevent issues when executable name collides with name of a
  427. # package from which we collect either binaries or data files (or both); see #7314.
  428. continue
  429. elif typecode == 'PKG':
  430. # Place into `Contents/Resources` ...
  431. file_dest = os.path.join('Contents/Resources', orig_dest_name)
  432. bundle_toc.append((file_dest, src_name, typecode))
  433. # ... and cross-link only into `Contents/MacOS`.
  434. # This is used only in `onefile` mode, where there is actually no other content to distribute among the
  435. # `Contents/Resources` and `Contents/Frameworks` directories, so cross-linking into the latter makes
  436. # little sense.
  437. symlink_dest = os.path.join('Contents/MacOS', orig_dest_name)
  438. symlink_ref = self._compute_relative_crosslink(symlink_dest, file_dest)
  439. bundle_toc.append((symlink_dest, symlink_ref, 'SYMLINK'))
  440. continue
  441. # Standard data vs binary processing...
  442. # Determine file location based on its type.
  443. if self._is_framework_file(orig_dest_path):
  444. # File from a framework bundle; put into `Contents/Frameworks`, but never cross-link the file itself.
  445. # The whole .framework bundle directory will be linked as necessary by the directory cross-linking
  446. # mechanism.
  447. file_base_dir = 'Contents/Frameworks'
  448. crosslink_base_dir = None
  449. elif typecode == 'SYMLINK':
  450. # Symbolic links
  451. parent_dir = orig_dest_path.parent
  452. if parent_dir == _TOP_LEVEL_DIR or directory_types.get(parent_dir) == _MIXED_DIR_TYPE:
  453. # Symbolic links that need to be cross-linked (because they are located in top-level directory or
  454. # in a mixed-content directory) are instead created in both locations, and point to the (relative)
  455. # resource in the same directory; so one of the targets will likely be a file, and the other will
  456. # be a symlink due to cross-linking.
  457. bundle_toc.append((os.path.join('Contents/Frameworks', orig_dest_name), src_name, typecode))
  458. bundle_toc.append((os.path.join('Contents/Resources', orig_dest_name), src_name, typecode))
  459. continue
  460. elif directory_types.get(parent_dir) == _DATA_DIR_TYPE:
  461. # Symbolic link in a data-only directory; relocate to 'Contents/Resources' and do NOT cross-link
  462. # (since the whole directory will be cross-linked).
  463. file_base_dir = 'Contents/Resources'
  464. crosslink_base_dir = None
  465. else:
  466. # Symbolic link in a binary-only directory; similar to data-only directory, except we relocate to
  467. # 'Contents/Frameworks'.
  468. file_base_dir = 'Contents/Frameworks'
  469. crosslink_base_dir = None
  470. elif typecode == 'DATA':
  471. # Data file; relocate to `Contents/Resources` and cross-link it back into `Contents/Frameworks`.
  472. file_base_dir = 'Contents/Resources'
  473. crosslink_base_dir = 'Contents/Frameworks'
  474. else:
  475. # Binary; put into `Contents/Frameworks` and cross-link it into `Contents/Resources`.
  476. file_base_dir = 'Contents/Frameworks'
  477. crosslink_base_dir = 'Contents/Resources'
  478. # Determine if we need to cross-link the file. We need to do this for top-level files (the ones without
  479. # parent directories), and for files whose parent directories are mixed-content directories.
  480. requires_crosslink = False
  481. if crosslink_base_dir is not None:
  482. parent_dir = orig_dest_path.parent
  483. requires_crosslink = parent_dir == _TOP_LEVEL_DIR or directory_types.get(parent_dir) == _MIXED_DIR_TYPE
  484. # The file itself.
  485. file_dest = os.path.join(file_base_dir, orig_dest_name)
  486. bundle_toc.append((file_dest, src_name, typecode))
  487. # Symlink for cross-linking
  488. if requires_crosslink:
  489. symlink_dest = os.path.join(crosslink_base_dir, orig_dest_name)
  490. symlink_ref = self._compute_relative_crosslink(symlink_dest, file_dest)
  491. bundle_toc.append((symlink_dest, symlink_ref, 'SYMLINK'))
  492. # Step 5: sanitize all destination paths in the new TOC, to ensure that paths that are rooted in
  493. # `Contents/Frameworks` do not contain directories with dots in their names. Doing this as a post-processing
  494. # step keeps code simple and clean and ensures that this step is applied to files, symlinks that originate from
  495. # cross-linking files, and symlinks that originate from cross-linking directories. This in turn ensures that
  496. # all directory hierarchies created during the actual file collection have sanitized names, and that collection
  497. # outcome does not depend on the order of entries in the TOC.
  498. sanitized_toc = []
  499. for dest_name, src_name, typecode in bundle_toc:
  500. dest_path = pathlib.PurePath(dest_name)
  501. # Paths rooted in Contents/Resources do not require sanitizing.
  502. if dest_path.parts[0] == 'Contents' and dest_path.parts[1] == 'Resources':
  503. sanitized_toc.append((dest_name, src_name, typecode))
  504. continue
  505. # Special handling for files from .framework bundle directories; sanitize only parent path of the .framework
  506. # directory.
  507. framework_path = self._is_framework_file(dest_path)
  508. if framework_path:
  509. parent_path = framework_path.parent
  510. remaining_path = dest_path.relative_to(parent_path)
  511. else:
  512. parent_path = dest_path.parent
  513. remaining_path = dest_path.name
  514. sanitized_dest_path = pathlib.PurePath(
  515. *parent_path.parts[:2], # Contents/Frameworks
  516. *[part.replace('.', DOT_REPLACEMENT) for part in parent_path.parts[2:]],
  517. remaining_path,
  518. )
  519. sanitized_dest_name = str(sanitized_dest_path)
  520. if sanitized_dest_path != dest_path:
  521. logger.debug("Sanitizing dest path: %r -> %r", dest_name, sanitized_dest_name)
  522. sanitized_toc.append((sanitized_dest_name, src_name, typecode))
  523. bundle_toc = sanitized_toc
  524. # Normalize and sort the TOC for easier inspection
  525. bundle_toc = sorted(normalize_toc(bundle_toc))
  526. return bundle_toc
  527. def assemble(self):
  528. from PyInstaller.config import CONF
  529. if _check_path_overlap(self.name) and os.path.isdir(self.name):
  530. _rmtree(self.name)
  531. logger.info("Building BUNDLE %s", self.tocbasename)
  532. # Create a minimal Mac bundle structure.
  533. os.makedirs(os.path.join(self.name, "Contents", "MacOS"))
  534. os.makedirs(os.path.join(self.name, "Contents", "Resources"))
  535. os.makedirs(os.path.join(self.name, "Contents", "Frameworks"))
  536. # Makes sure the icon exists and attempts to convert to the proper format if applicable
  537. self.icon = normalize_icon_type(self.icon, ("icns",), "icns", CONF["workpath"])
  538. # Ensure icon path is absolute
  539. self.icon = os.path.abspath(self.icon)
  540. # Copy icns icon to Resources directory.
  541. shutil.copyfile(self.icon, os.path.join(self.name, 'Contents', 'Resources', os.path.basename(self.icon)))
  542. # Key/values for a minimal Info.plist file
  543. info_plist_dict = {
  544. "CFBundleDisplayName": self.appname,
  545. "CFBundleName": self.appname,
  546. # Required by 'codesign' utility.
  547. # The value for CFBundleIdentifier is used as the default unique name of your program for Code Signing
  548. # purposes. It even identifies the APP for access to restricted macOS areas like Keychain.
  549. #
  550. # The identifier used for signing must be globally unique. The usual form for this identifier is a
  551. # hierarchical name in reverse DNS notation, starting with the toplevel domain, followed by the company
  552. # name, followed by the department within the company, and ending with the product name. Usually in the
  553. # form: com.mycompany.department.appname
  554. # CLI option --osx-bundle-identifier sets this value.
  555. "CFBundleIdentifier": self.bundle_identifier,
  556. "CFBundleExecutable": os.path.basename(self.exename),
  557. "CFBundleIconFile": os.path.basename(self.icon),
  558. "CFBundleInfoDictionaryVersion": "6.0",
  559. "CFBundlePackageType": "APPL",
  560. "CFBundleShortVersionString": self.version,
  561. }
  562. # Set some default values. But they still can be overwritten by the user.
  563. if self.console:
  564. # Setting EXE console=True implies LSBackgroundOnly=True.
  565. info_plist_dict['LSBackgroundOnly'] = True
  566. else:
  567. # Let's use high resolution by default.
  568. info_plist_dict['NSHighResolutionCapable'] = True
  569. # Merge info_plist settings from spec file
  570. if isinstance(self.info_plist, dict) and self.info_plist:
  571. info_plist_dict.update(self.info_plist)
  572. plist_filename = os.path.join(self.name, "Contents", "Info.plist")
  573. with open(plist_filename, "wb") as plist_fh:
  574. plistlib.dump(info_plist_dict, plist_fh)
  575. # Pre-process the TOC into its final BUNDLE-compatible form.
  576. bundle_toc = self._process_bundle_toc(self.toc)
  577. # Perform the actual collection.
  578. CONTENTS_FRAMEWORKS_PATH = pathlib.PurePath('Contents/Frameworks')
  579. for dest_name, src_name, typecode in bundle_toc:
  580. # Create parent directory structure, if necessary
  581. dest_path = os.path.join(self.name, dest_name) # Absolute destination path
  582. dest_dir = os.path.dirname(dest_path)
  583. try:
  584. os.makedirs(dest_dir, exist_ok=True)
  585. except FileExistsError:
  586. raise SystemExit(
  587. f"ERROR: Pyinstaller needs to create a directory at {dest_dir!r}, "
  588. "but there already exists a file at that path!"
  589. )
  590. # Copy extensions and binaries from cache. This ensures that these files undergo additional binary
  591. # processing - have paths to linked libraries rewritten (relative to `@rpath`) and have rpath set to the
  592. # top-level directory (relative to `@loader_path`, i.e., the file's location). The "top-level" directory
  593. # in this case corresponds to `Contents/MacOS` (where `sys._MEIPASS` also points), so we need to pass
  594. # the cache retrieval function the *original* destination path (which is without preceding
  595. # `Contents/MacOS`).
  596. if typecode in ('EXTENSION', 'BINARY'):
  597. orig_dest_name = str(pathlib.PurePath(dest_name).relative_to(CONTENTS_FRAMEWORKS_PATH))
  598. src_name = process_collected_binary(
  599. src_name,
  600. orig_dest_name,
  601. use_strip=self.strip,
  602. use_upx=self.upx,
  603. upx_exclude=self.upx_exclude,
  604. target_arch=self.target_arch,
  605. codesign_identity=self.codesign_identity,
  606. entitlements_file=self.entitlements_file,
  607. strict_arch_validation=(typecode == 'EXTENSION'),
  608. )
  609. if typecode == 'SYMLINK':
  610. os.symlink(src_name, dest_path) # Create link at dest_path, pointing at (relative) src_name
  611. else:
  612. # BUNDLE does not support MERGE-based multipackage
  613. assert typecode != 'DEPENDENCY', "MERGE DEPENDENCY entries are not supported in BUNDLE!"
  614. # At this point, `src_name` should be a valid file.
  615. if not os.path.isfile(src_name):
  616. raise ValueError(f"Resource {src_name!r} is not a valid file!")
  617. # If strict collection mode is enabled, the destination should not exist yet.
  618. if strict_collect_mode and os.path.exists(dest_path):
  619. raise ValueError(
  620. f"Attempting to collect a duplicated file into BUNDLE: {dest_name} (type: {typecode})"
  621. )
  622. # Use `shutil.copyfile` to copy file with default permissions. We do not attempt to preserve original
  623. # permissions nor metadata, as they might be too restrictive and cause issues either during subsequent
  624. # re-build attempts or when trying to move the application bundle. For binaries (and data files with
  625. # executable bit set), we manually set the executable bits after copying the file.
  626. shutil.copyfile(src_name, dest_path)
  627. if (
  628. typecode in ('EXTENSION', 'BINARY', 'EXECUTABLE')
  629. or (typecode == 'DATA' and os.access(src_name, os.X_OK))
  630. ):
  631. os.chmod(dest_path, 0o755)
  632. # Sign the bundle
  633. logger.info('Signing the BUNDLE...')
  634. try:
  635. osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep=True)
  636. except Exception as e:
  637. # Display a warning or re-raise the error, depending on the environment-variable setting.
  638. if os.environ.get("PYINSTALLER_STRICT_BUNDLE_CODESIGN_ERROR", "0") == "0":
  639. logger.warning("Error while signing the bundle: %s", e)
  640. logger.warning("You will need to sign the bundle manually!")
  641. else:
  642. raise RuntimeError("Failed to codesign the bundle!") from e
  643. logger.info("Building BUNDLE %s completed successfully.", self.tocbasename)
  644. # Optionally verify bundle's signature. This is primarily intended for our CI.
  645. if os.environ.get("PYINSTALLER_VERIFY_BUNDLE_SIGNATURE", "0") != "0":
  646. logger.info("Verifying signature for BUNDLE %s...", self.name)
  647. self.verify_bundle_signature(self.name)
  648. logger.info("BUNDLE verification complete!")
  649. @staticmethod
  650. def verify_bundle_signature(bundle_dir):
  651. # First, verify the bundle signature using codesign.
  652. cmd_args = ['/usr/bin/codesign', '--verify', '--all-architectures', '--deep', '--strict', bundle_dir]
  653. p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf8')
  654. if p.returncode:
  655. raise SystemError(
  656. f"codesign command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}"
  657. )
  658. # Ensure that code-signing information is *NOT* embedded in the files' extended attributes.
  659. #
  660. # This happens when files other than binaries are present in `Contents/MacOS` or `Contents/Frameworks`
  661. # directory; as the signature cannot be embedded within the file itself (contrary to binaries with
  662. # `LC_CODE_SIGNATURE` section in their header), it ends up stores in the file's extended attributes. However,
  663. # if such bundle is transferred using a method that does not support extended attributes (for example, a zip
  664. # file), the signatures on these files are lost, and the signature of the bundle as a whole becomes invalid.
  665. # This is the primary reason why we need to relocate non-binaries into `Contents/Resources` - the signatures
  666. # for files in that directory end up stored in `Contents/_CodeSignature/CodeResources` file.
  667. #
  668. # This check therefore aims to ensure that all files have been properly relocated to their corresponding
  669. # locations w.r.t. the code-signing requirements.
  670. try:
  671. import xattr
  672. except ModuleNotFoundError:
  673. logger.info("xattr package not available; skipping verification of extended attributes!")
  674. return
  675. CODESIGN_ATTRS = (
  676. "com.apple.cs.CodeDirectory",
  677. "com.apple.cs.CodeRequirements",
  678. "com.apple.cs.CodeRequirements-1",
  679. "com.apple.cs.CodeSignature",
  680. )
  681. for entry in pathlib.Path(bundle_dir).rglob("*"):
  682. if not entry.is_file():
  683. continue
  684. file_attrs = xattr.listxattr(entry)
  685. if any([codesign_attr in file_attrs for codesign_attr in CODESIGN_ATTRS]):
  686. raise ValueError(f"Code-sign attributes found in extended attributes of {str(entry)!r}!")