| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775 |
- #-----------------------------------------------------------------------------
- # Copyright (c) 2005-2023, PyInstaller Development Team.
- #
- # Distributed under the terms of the GNU General Public License (version 2
- # or later) with exception for distributing the bootloader.
- #
- # The full license is in the file COPYING.txt, distributed with this software.
- #
- # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
- #-----------------------------------------------------------------------------
- import os
- import pathlib
- import plistlib
- import shutil
- import subprocess
- from PyInstaller import log as logging
- from PyInstaller.building.api import COLLECT, EXE
- from PyInstaller.building.datastruct import Target, logger, normalize_toc
- from PyInstaller.building.utils import _check_path_overlap, _rmtree, process_collected_binary
- from PyInstaller.compat import is_darwin, strict_collect_mode
- from PyInstaller.building.icon import normalize_icon_type
- import PyInstaller.utils.misc as miscutils
- if is_darwin:
- import PyInstaller.utils.osx as osxutils
- # Character sequence used to replace dot (`.`) in names of directories that are created in `Contents/MacOS` or
- # `Contents/Frameworks`, where only .framework bundle directories are allowed to have dot in name.
- DOT_REPLACEMENT = '__dot__'
- WINDOWED_ONEFILE_DEPRCATION = (
- "Onefile mode in combination with macOS .app bundles (windowed mode) don't make sense (a .app bundle can not be a "
- "single file) and clashes with macOS's security. Please migrate to onedir mode. This will become an error "
- "in v7.0."
- )
- class BUNDLE(Target):
- def __init__(self, *args, **kwargs):
- from PyInstaller.config import CONF
- for item in args:
- if isinstance(item, EXE) and not item.exclude_binaries:
- logger.log(logging.DEPRECATION, WINDOWED_ONEFILE_DEPRCATION)
- # BUNDLE only has a sense under macOS, it is a noop on other platforms.
- if not is_darwin:
- return
- # Get a path to a .icns icon for the app bundle.
- self.icon = kwargs.get('icon')
- if not self.icon:
- # --icon not specified; use the default in the pyinstaller folder
- self.icon = os.path.join(
- os.path.dirname(os.path.dirname(__file__)), 'bootloader', 'images', 'icon-windowed.icns'
- )
- else:
- # User gave an --icon=path. If it is relative, make it relative to the spec file location.
- if not os.path.isabs(self.icon):
- self.icon = os.path.join(CONF['specpath'], self.icon)
- super().__init__()
- # .app bundle is created in DISTPATH.
- self.name = kwargs.get('name', None)
- base_name = os.path.basename(self.name)
- self.name = os.path.join(CONF['distpath'], base_name)
- self.appname = os.path.splitext(base_name)[0]
- # Ensure version is a string, even if user accidentally passed an int or a float.
- # Having a `CFBundleShortVersionString` entry of non-string type in `Info.plist` causes the .app bundle to
- # crash at start (#4466).
- self.version = str(kwargs.get("version", "0.0.0"))
- self.toc = []
- self.strip = False
- self.upx = False
- self.console = True
- self.target_arch = None
- self.codesign_identity = None
- self.entitlements_file = None
- # .app bundle identifier for Code Signing
- self.bundle_identifier = kwargs.get('bundle_identifier')
- if not self.bundle_identifier:
- # Fallback to appname.
- self.bundle_identifier = self.appname
- self.info_plist = kwargs.get('info_plist', None)
- for arg in args:
- # Valid arguments: EXE object, COLLECT object, and TOC-like iterables
- if isinstance(arg, EXE):
- # Add EXE as an entry to the TOC, and merge its dependencies TOC
- self.toc.append((os.path.basename(arg.name), arg.name, 'EXECUTABLE'))
- self.toc.extend(arg.dependencies)
- # Inherit settings
- self.strip = arg.strip
- self.upx = arg.upx
- self.upx_exclude = arg.upx_exclude
- self.console = arg.console
- self.target_arch = arg.target_arch
- self.codesign_identity = arg.codesign_identity
- self.entitlements_file = arg.entitlements_file
- elif isinstance(arg, COLLECT):
- # Merge the TOC
- self.toc.extend(arg.toc)
- # Inherit settings
- self.strip = arg.strip_binaries
- self.upx = arg.upx_binaries
- self.upx_exclude = arg.upx_exclude
- self.console = arg.console
- self.target_arch = arg.target_arch
- self.codesign_identity = arg.codesign_identity
- self.entitlements_file = arg.entitlements_file
- elif miscutils.is_iterable(arg):
- # TOC-like iterable
- self.toc.extend(arg)
- else:
- raise TypeError(f"Invalid argument type for BUNDLE: {type(arg)!r}")
- # Infer the executable name from the first EXECUTABLE entry in the TOC; it might have come from the COLLECT
- # (as opposed to the stand-alone EXE).
- for dest_name, src_name, typecode in self.toc:
- if typecode == "EXECUTABLE":
- self.exename = src_name
- break
- else:
- raise ValueError("No EXECUTABLE entry found in the TOC!")
- # Normalize TOC
- self.toc = normalize_toc(self.toc)
- # Alphabetically sort the TOC to ensure that order of processing is predictable and reproducible.
- self.toc.sort()
- self.__postinit__()
- _GUTS = (
- # BUNDLE always builds, just want the toc to be written out
- ('toc', None),
- )
- def _check_guts(self, data, last_build):
- # BUNDLE always needs to be executed, in order to clean the output directory.
- return True
- # Helper for determining whether the given file belongs to a .framework bundle or not. If it does, it returns
- # the path to the top-level .framework bundle directory; otherwise, returns None. In case of nested .framework
- # bundles, the path to the top-most .framework bundle directory is returned.
- @staticmethod
- def _is_framework_file(dest_path):
- # NOTE: reverse the parents list because we are looking for the top-most .framework bundle directory!
- for parent in reversed(dest_path.parents):
- if parent.name.endswith('.framework'):
- return parent
- return None
- # Helper that computes relative cross-link path between link's location and target, assuming they are both
- # rooted in the `Contents` directory of a macOS .app bundle.
- @staticmethod
- def _compute_relative_crosslink(crosslink_location, crosslink_target):
- # We could take symlink_location and symlink_target as they are (relative to parent of the `Contents`
- # directory), but that would introduce an unnecessary `../Contents` part. So instead, we take both paths
- # relative to the `Contents` directory.
- return os.path.join(
- *['..' for level in pathlib.PurePath(crosslink_location).relative_to('Contents').parent.parts],
- pathlib.PurePath(crosslink_target).relative_to('Contents'),
- )
- # This method takes the original (input) TOC and processes it into final TOC, based on which the `assemble` method
- # performs its file collection. The TOC processing here represents the core of our efforts to generate an .app
- # bundle that is compatible with Apple's code-signing requirements.
- #
- # For in-depth details on the code-signing, see Apple's `Technical Note TN2206: macOS Code Signing In Depth` at
- # https://developer.apple.com/library/archive/technotes/tn2206/_index.html
- #
- # The requirements, framed from PyInstaller's perspective, can be summarized as follows:
- #
- # 1. The `Contents/MacOS` directory is expected to contain only the program executable and (binary) code (= dylibs
- # and nested .framework bundles). Alternatively, the dylibs and .framework bundles can be also placed into
- # `Contents/Frameworks` directory (where same rules apply as for `Contents/MacOS`, so the remainder of this
- # text refers to the two inter-changeably, unless explicitly noted otherwise). The code in `Contents/MacOS`
- # is expected to be signed, and the `codesign` utility will recursively sign all found code when using `--deep`
- # option to sign the .app bundle.
- #
- # 2. All non-code files should be be placed in `Contents/Resources`, so they become sealed (data) resources;
- # i.e., their signature data is recorded in `Contents/_CodeSignature/CodeResources`. (As a side note,
- # it seems that signature information for data/resources in `Contents/Resources` is kept nder `file` key in
- # the `CodeResources` file, while the information for contents in `Contents/MacOS` is kept under `file2` key).
- #
- # 3. The directories in `Contents/MacOS` may not contain dots (`.`) in their names, except for the nested
- # .framework bundle directories. The directories in `Contents/Resources` have no such restrictions.
- #
- # 4. There may not be any content in the top level of a bundle. In other words, if a bundle has a `Contents`
- # or a `Versions` directory at its top level, there may be no other files or directories alongside them. The
- # sole exception is that alongside `Versions`, there may be symlinks to files and directories in
- # `Versions/Current`. This rule is important for nested .framework bundles that we collect from python packages.
- #
- # Next, let us consider the consequences of violating each of the above requirements:
- #
- # 1. Code signing machinery can directly store signature only in Mach-O binaries and nested .framework bundles; if
- # a data file is placed in `Contents/MacOS`, the signature is stored in the file's extended attributes. If the
- # extended attributes are lost, the program's signature will be broken. Many file transfer techniques (e.g., a
- # zip file) do not preserve extended attributes, nor are they preserved when uploading to the Mac App Store.
- #
- # 2. Putting code (a dylib or a .framework bundle) into `Contents/Resources` causes it to be treated as a resource;
- # the outer signature (i.e., of the whole .app bundle) does not know that this nested content is actually a code.
- # Consequently, signing the bundle with `codesign --deep` will NOT sign binaries placed in the
- # `Contents/Resources`, which may result in missing signatures when .app bundle is verified for notarization.
- # This might be worked around by signing each binary separately, and then signing the whole bundle (without the
- # `--deep` option), but requires the user to keep track of the offending binaries.
- #
- # 3. If a directory in `Contents/MacOS` contains a dot in the name, code-signing the bundle fails with
- # `bundle format unrecognized, invalid, or unsuitable` due to code signing machinery treating directory as a
- # nested .framework bundle directory.
- #
- # 4. If nested .framework bundle is malformed, the signing of the .app bundle might succeed, but subsequent
- # verification will fail, for example with `embedded framework contains modified or invalid version` (as observed
- # with .framework bundles shipped by contemporary PyQt/PySide PyPI wheels).
- #
- # The above requirements are unfortunately often at odds with the structure of python packages:
- #
- # * In general, python packages are mixed-content directories, where binaries and data files may be expected to
- # be found next to each other.
- #
- # For example, `opencv-python` provides a custom loader script that requires the package to be collected in the
- # source-only form by PyInstaller (i.e., the python modules and scripts collected as source .py files). At the
- # same time, it expects the .py loader script to be able to find the binary extension next to itself.
- #
- # Another example of mixed-mode directories are Qt QML components' sub-directories, which contain both the
- # component's plugin (a binary) and associated meta files (data files).
- #
- # * In python world, the directories often contain dots in their names.
- #
- # Dots are often used for private directories containing binaries that are shipped with a package. For example,
- # `numpy/.dylibs`, `scipy/.dylibs`, etc.
- #
- # Qt QML components may also contain a dot in their name; couple of examples from `PySide2` package:
- # `PySide2/Qt/qml/QtQuick.2`, `PySide2/Qt/qml/QtQuick/Controls.2`, `PySide2/Qt/qml/QtQuick/Particles.2`, etc.
- #
- # The packages' metadata directories also invariably contain dots in the name due to version (for example,
- # `numpy-1.24.3.dist-info`).
- #
- # In the light of all above, PyInstaller attempts to strictly place all files to their mandated location
- # (`Contents/MacOS` or `Contents/Frameworks` vs `Contents/Resources`). To preserve the illusion of mixed-content
- # directories, the content is cross-linked from one directory to the other. Specifically:
- #
- # * All entries with DATA typecode are assumed to be data files, and are always placed in corresponding directory
- # structure rooted in `Contents/Resources`.
- #
- # * All entries with BINARY or EXTENSION typecode are always placed in corresponding directory structure rooted in
- # `Contents/Frameworks`.
- #
- # * All entries with EXECUTABLE are placed in `Contents/MacOS` directory.
- #
- # * For the purposes of relocation, nested .framework bundles are treated as a single BINARY entity; i.e., the
- # whole .bundle directory is placed in corresponding directory structure rooted in `Contents/Frameworks` (even
- # though some of its contents, such as `Info.plist` file, are actually data files).
- #
- # * Top-level data files and binaries are always cross-linked to the other directory. For example, given a data file
- # `data_file.txt` that was collected into `Contents/Resources`, we create a symbolic link called
- # `Contents/MacOS/data_file.txt` that points to `../Resources/data_file.txt`.
- #
- # * The executable itself, while placed in `Contents/MacOS`, are cross-linked into both `Contents/Framworks` and
- # `Contents/Resources`.
- #
- # * The stand-alone PKG entries (used with onefile builds that side-load the PKG archive) are treated as data files
- # and collected into `Contents/Resources`, but cross-linked only into `Contents/MacOS` directory (because they
- # must appear to be next to the program executable). This is the only entry type that is cross-linked into the
- # `Contents/MacOS` directory and also the only data-like entry type that is not cross-linked into the
- # `Contents/Frameworks` directory.
- #
- # * For files in sub-directories, the cross-linking behavior depends on the type of directory:
- #
- # * A data-only directory is created in directory structure rooted in `Contents/Resources`, and cross-linked
- # into directory structure rooted in `Contents/Frameworks` at directory level (i.e., we link the whole
- # directory instead of individual files).
- #
- # This largely saves us from having to deal with dots in the names of collected metadata directories, which
- # are examples of data-only directories.
- #
- # * A binary-only directory is created in directory structure rooted in `Contents/Frameworks`, and cross-linked
- # into `Contents/Resources` at directory level.
- #
- # * A mixed-content directory is created in both directory structures. Files are placed into corresponding
- # directory structure based on their type, and cross-linked into other directory structure at file level.
- #
- # * This rule is applied recursively; for example, a data-only sub-directory in a mixed-content directory is
- # cross-linked at directory level, while adjacent binary and data files are cross-linked at file level.
- #
- # * To work around the issue with dots in the names of directories in `Contents/Frameworks` (applicable to
- # binary-only or mixed-content directories), such directories are created with modified name (the dot replaced
- # with a pre-defined pattern). Next to the modified directory, a symbolic link with original name is created,
- # pointing to the directory with modified name. With mixed-content directories, this modification is performed
- # only on the `Contents/Frameworks` side; the corresponding directory in `Contents/Resources` can be created
- # directly, without name modification and symbolic link.
- #
- # * If a symbolic link needs to be created in a mixed-content directory due to a SYMLINK entry from the original
- # TOC (i.e., a "collected" symlink originating from analysis, as opposed to the cross-linking mechanism described
- # above), the link is created in both directory structures, each pointing to the resource in its corresponding
- # directory structure (with one such resource being an actual file, and the other being a cross-link to the file).
- #
- # Final remarks:
- #
- # NOTE: the relocation mechanism is codified by tests in `tests/functional/test_macos_bundle_structure.py`.
- #
- # NOTE: by placing binaries and nested .framework entries into `Contents/Frameworks` instead of `Contents/MacOS`,
- # we have effectively relocated the `sys._MEIPASS` directory from the `Contents/MacOS` (= the parent directory of
- # the program executable) into `Contents/Frameworks`. This requires the PyInstaller's bootloader to detect that it
- # is running in the app-bundle mode (e.g., by checking if program executable's parent directory is `Contents/NacOS`)
- # and adjust the path accordingly.
- #
- # NOTE: the implemented relocation mechanism depends on the input TOC containing properly classified entries
- # w.r.t. BINARY vs DATA. So hooks and .spec files triggering collection of binaries as datas (and vice versa) will
- # result in incorrect placement of those files in the generated .app bundle. However, this is *not* the proper place
- # to address such issues; if necessary, automatic (re)classification should be added to analysis process, to ensure
- # that BUNDLE (as well as other build targets) receive correctly classified TOC.
- #
- # NOTE: similar to the previous note, the relocation mechanism is also not the proper place to enforce compliant
- # structure of the nested .framework bundles. Instead, this is handled by the analysis process, using the
- # `PyInstaller.utils.osx.collect_files_from_framework_bundles` helper function. So the input TOC that BUNDLE
- # receives should already contain entries that reconstruct compliant nested .framework bundles.
- def _process_bundle_toc(self, toc):
- bundle_toc = []
- # Step 1: inspect the directory layout and classify the directories according to their contents.
- directory_types = dict()
- _MIXED_DIR_TYPE = 'MIXED-DIR'
- _DATA_DIR_TYPE = 'DATA-DIR'
- _BINARY_DIR_TYPE = 'BINARY-DIR'
- _FRAMEWORK_DIR_TYPE = 'FRAMEWORK-DIR'
- _UNKNOWN_DIR_TYPE = 'UNKNOWN-DIR' # used only in this step
- _TOP_LEVEL_DIR = pathlib.PurePath('.')
- for dest_name, src_name, typecode in toc:
- dest_path = pathlib.PurePath(dest_name)
- framework_dir = self._is_framework_file(dest_path)
- if framework_dir:
- # Mark the framework directory as FRAMEWORK-DIR.
- directory_types[framework_dir] = _FRAMEWORK_DIR_TYPE
- # Treat the framework directory as BINARY file when classifying parent directories.
- typecode = 'BINARY'
- parent_dirs = framework_dir.parents
- else:
- parent_dirs = dest_path.parents
- # Treat BINARY and EXTENSION as BINARY to simplify further processing.
- if typecode == 'EXTENSION':
- typecode = 'BINARY'
- # Directory type as per this entry's typecode. Symbolic links are "neutral". On the off chance that a
- # directory contains only symbolic link(s), we will override the type of "unknown" directories after this
- # loop finishes.
- if typecode == 'SYMLINK':
- entry_type = _UNKNOWN_DIR_TYPE
- elif typecode == 'BINARY':
- entry_type = _BINARY_DIR_TYPE
- else:
- entry_type = _DATA_DIR_TYPE
- # (Re)classify parent directories
- for parent_dir in parent_dirs:
- # Skip the top-level `.` dir. This is also the only directory that can contain EXECUTABLE and PKG
- # entries, so we do not have to worry about them.
- if parent_dir == _TOP_LEVEL_DIR:
- continue
- directory_type = directory_types.get(parent_dir, entry_type)
- # If directory was previously marked as unknown, overwrite its type with the new one.
- if directory_type == _UNKNOWN_DIR_TYPE:
- directory_type = entry_type
- # Update type into mixed-content, if necessary.
- if directory_type == _DATA_DIR_TYPE and entry_type == _BINARY_DIR_TYPE:
- directory_type = _MIXED_DIR_TYPE
- if directory_type == _BINARY_DIR_TYPE and entry_type == _DATA_DIR_TYPE:
- directory_type = _MIXED_DIR_TYPE
- directory_types[parent_dir] = directory_type
- # Reclassify all "unknown" directories into "data-only"; these are directories that contain only one or more
- # symbolic links.
- directory_types = {
- directory: directory_type if directory_type != _UNKNOWN_DIR_TYPE else _DATA_DIR_TYPE
- for directory, directory_type in directory_types.items()
- }
- logger.debug("Directory classification: %r", directory_types)
- # Step 2: process the obtained directory structure and create symlink entries for directories that need to be
- # cross-linked. Such directories are data-only and binary-only directories (and framework directories) that are
- # located either in the top-level directory (have no parent) or in a mixed-content directory.
- for directory_path, directory_type in directory_types.items():
- # Cross-linking at directory level applies only to data-only and binary-only directories (as well as
- # framework directories).
- if directory_type == _MIXED_DIR_TYPE:
- continue
- # The parent needs to be either top-level directory or a mixed-content directory. Otherwise, the parent
- # (or one of its ancestors) will get cross-linked, and we do not need the link here.
- parent_dir = directory_path.parent
- requires_crosslink = parent_dir == _TOP_LEVEL_DIR or directory_types.get(parent_dir) == _MIXED_DIR_TYPE
- if not requires_crosslink:
- continue
- logger.debug("Cross-linking directory %r of type %r", directory_path, directory_type)
- # Data-only directories are created in `Contents/Resources`, needs to be cross-linked into `Contents/MacOS`.
- # Vice versa for binary-only or framework directories. The directory creation is handled implicitly, when we
- # create parent directory structure for collected files.
- if directory_type == _DATA_DIR_TYPE:
- symlink_src = os.path.join('Contents/Resources', directory_path)
- symlink_dest = os.path.join('Contents/Frameworks', directory_path)
- else:
- symlink_src = os.path.join('Contents/Frameworks', directory_path)
- symlink_dest = os.path.join('Contents/Resources', directory_path)
- symlink_ref = self._compute_relative_crosslink(symlink_dest, symlink_src)
- bundle_toc.append((symlink_dest, symlink_ref, 'SYMLINK'))
- # Step 3: first part of the work-around for directories that are located in `Contents/Frameworks` but contain a
- # dot in their name. As per `codesign` rules, the only directories in `Contents/Frameworks` that are allowed to
- # contain a dot in their name are .framework bundle directories. So we replace the dot with a custom character
- # sequence (stored in global `DOT_REPLACEMENT` variable), and create a symbolic with original name pointing to
- # the modified name. This is the best we can do with code-sign requirements vs. python community showing their
- # packages' dylibs into `.dylib` subdirectories, or Qt storing their Qml components in directories named
- # `QtQuick.2`, `QtQuick/Controls.2`, `QtQuick/Particles.2`, `QtQuick/Templates.2`, etc.
- #
- # In this step, we only prepare symlink entries that link the original directory name (with dot) to the modified
- # one (with dot replaced). The parent paths for collected files are modified in later step(s).
- for directory_path, directory_type in directory_types.items():
- # .framework bundle directories contain a dot in the name, but are allowed that.
- if directory_type == _FRAMEWORK_DIR_TYPE:
- continue
- # Data-only directories are fully located in `Contents/Resources` and cross-linked to `Contents/Frameworks`
- # at directory level, so they are also allowed a dot in their name.
- if directory_type == _DATA_DIR_TYPE:
- continue
- # Apply the work-around, if necessary...
- if '.' not in directory_path.name:
- continue
- logger.debug(
- "Creating symlink to work around the dot in the name of directory %r (%s)...", str(directory_path),
- directory_type
- )
- # Create a SYMLINK entry, but only for this level. In case of nested directories with dots in names, the
- # symlinks for ancestors will be created by corresponding loop iteration.
- bundle_toc.append((
- os.path.join('Contents/Frameworks', directory_path),
- directory_path.name.replace('.', DOT_REPLACEMENT),
- 'SYMLINK',
- ))
- # Step 4: process the entries for collected files, and decide whether they should be placed into
- # `Contents/MacOS`, `Contents/Frameworks`, or `Contents/Resources`, and whether they should be cross-linked into
- # other directories.
- for orig_dest_name, src_name, typecode in toc:
- orig_dest_path = pathlib.PurePath(orig_dest_name)
- # Special handling for EXECUTABLE and PKG entries
- if typecode == 'EXECUTABLE':
- # Place into `Contents/MacOS`, ...
- file_dest = os.path.join('Contents/MacOS', orig_dest_name)
- bundle_toc.append((file_dest, src_name, typecode))
- # ... and do nothing else. We explicitly avoid cross-linking the executable to `Contents/Frameworks` and
- # `Contents/Resources`, because it should be not necessary (the executable's location should be
- # discovered via `sys.executable`) and to prevent issues when executable name collides with name of a
- # package from which we collect either binaries or data files (or both); see #7314.
- continue
- elif typecode == 'PKG':
- # Place into `Contents/Resources` ...
- file_dest = os.path.join('Contents/Resources', orig_dest_name)
- bundle_toc.append((file_dest, src_name, typecode))
- # ... and cross-link only into `Contents/MacOS`.
- # This is used only in `onefile` mode, where there is actually no other content to distribute among the
- # `Contents/Resources` and `Contents/Frameworks` directories, so cross-linking into the latter makes
- # little sense.
- symlink_dest = os.path.join('Contents/MacOS', orig_dest_name)
- symlink_ref = self._compute_relative_crosslink(symlink_dest, file_dest)
- bundle_toc.append((symlink_dest, symlink_ref, 'SYMLINK'))
- continue
- # Standard data vs binary processing...
- # Determine file location based on its type.
- if self._is_framework_file(orig_dest_path):
- # File from a framework bundle; put into `Contents/Frameworks`, but never cross-link the file itself.
- # The whole .framework bundle directory will be linked as necessary by the directory cross-linking
- # mechanism.
- file_base_dir = 'Contents/Frameworks'
- crosslink_base_dir = None
- elif typecode == 'SYMLINK':
- # Symbolic links
- parent_dir = orig_dest_path.parent
- if parent_dir == _TOP_LEVEL_DIR or directory_types.get(parent_dir) == _MIXED_DIR_TYPE:
- # Symbolic links that need to be cross-linked (because they are located in top-level directory or
- # in a mixed-content directory) are instead created in both locations, and point to the (relative)
- # resource in the same directory; so one of the targets will likely be a file, and the other will
- # be a symlink due to cross-linking.
- bundle_toc.append((os.path.join('Contents/Frameworks', orig_dest_name), src_name, typecode))
- bundle_toc.append((os.path.join('Contents/Resources', orig_dest_name), src_name, typecode))
- continue
- elif directory_types.get(parent_dir) == _DATA_DIR_TYPE:
- # Symbolic link in a data-only directory; relocate to 'Contents/Resources' and do NOT cross-link
- # (since the whole directory will be cross-linked).
- file_base_dir = 'Contents/Resources'
- crosslink_base_dir = None
- else:
- # Symbolic link in a binary-only directory; similar to data-only directory, except we relocate to
- # 'Contents/Frameworks'.
- file_base_dir = 'Contents/Frameworks'
- crosslink_base_dir = None
- elif typecode == 'DATA':
- # Data file; relocate to `Contents/Resources` and cross-link it back into `Contents/Frameworks`.
- file_base_dir = 'Contents/Resources'
- crosslink_base_dir = 'Contents/Frameworks'
- else:
- # Binary; put into `Contents/Frameworks` and cross-link it into `Contents/Resources`.
- file_base_dir = 'Contents/Frameworks'
- crosslink_base_dir = 'Contents/Resources'
- # Determine if we need to cross-link the file. We need to do this for top-level files (the ones without
- # parent directories), and for files whose parent directories are mixed-content directories.
- requires_crosslink = False
- if crosslink_base_dir is not None:
- parent_dir = orig_dest_path.parent
- requires_crosslink = parent_dir == _TOP_LEVEL_DIR or directory_types.get(parent_dir) == _MIXED_DIR_TYPE
- # The file itself.
- file_dest = os.path.join(file_base_dir, orig_dest_name)
- bundle_toc.append((file_dest, src_name, typecode))
- # Symlink for cross-linking
- if requires_crosslink:
- symlink_dest = os.path.join(crosslink_base_dir, orig_dest_name)
- symlink_ref = self._compute_relative_crosslink(symlink_dest, file_dest)
- bundle_toc.append((symlink_dest, symlink_ref, 'SYMLINK'))
- # Step 5: sanitize all destination paths in the new TOC, to ensure that paths that are rooted in
- # `Contents/Frameworks` do not contain directories with dots in their names. Doing this as a post-processing
- # step keeps code simple and clean and ensures that this step is applied to files, symlinks that originate from
- # cross-linking files, and symlinks that originate from cross-linking directories. This in turn ensures that
- # all directory hierarchies created during the actual file collection have sanitized names, and that collection
- # outcome does not depend on the order of entries in the TOC.
- sanitized_toc = []
- for dest_name, src_name, typecode in bundle_toc:
- dest_path = pathlib.PurePath(dest_name)
- # Paths rooted in Contents/Resources do not require sanitizing.
- if dest_path.parts[0] == 'Contents' and dest_path.parts[1] == 'Resources':
- sanitized_toc.append((dest_name, src_name, typecode))
- continue
- # Special handling for files from .framework bundle directories; sanitize only parent path of the .framework
- # directory.
- framework_path = self._is_framework_file(dest_path)
- if framework_path:
- parent_path = framework_path.parent
- remaining_path = dest_path.relative_to(parent_path)
- else:
- parent_path = dest_path.parent
- remaining_path = dest_path.name
- sanitized_dest_path = pathlib.PurePath(
- *parent_path.parts[:2], # Contents/Frameworks
- *[part.replace('.', DOT_REPLACEMENT) for part in parent_path.parts[2:]],
- remaining_path,
- )
- sanitized_dest_name = str(sanitized_dest_path)
- if sanitized_dest_path != dest_path:
- logger.debug("Sanitizing dest path: %r -> %r", dest_name, sanitized_dest_name)
- sanitized_toc.append((sanitized_dest_name, src_name, typecode))
- bundle_toc = sanitized_toc
- # Normalize and sort the TOC for easier inspection
- bundle_toc = sorted(normalize_toc(bundle_toc))
- return bundle_toc
- def assemble(self):
- from PyInstaller.config import CONF
- if _check_path_overlap(self.name) and os.path.isdir(self.name):
- _rmtree(self.name)
- logger.info("Building BUNDLE %s", self.tocbasename)
- # Create a minimal Mac bundle structure.
- os.makedirs(os.path.join(self.name, "Contents", "MacOS"))
- os.makedirs(os.path.join(self.name, "Contents", "Resources"))
- os.makedirs(os.path.join(self.name, "Contents", "Frameworks"))
- # Makes sure the icon exists and attempts to convert to the proper format if applicable
- self.icon = normalize_icon_type(self.icon, ("icns",), "icns", CONF["workpath"])
- # Ensure icon path is absolute
- self.icon = os.path.abspath(self.icon)
- # Copy icns icon to Resources directory.
- shutil.copyfile(self.icon, os.path.join(self.name, 'Contents', 'Resources', os.path.basename(self.icon)))
- # Key/values for a minimal Info.plist file
- info_plist_dict = {
- "CFBundleDisplayName": self.appname,
- "CFBundleName": self.appname,
- # Required by 'codesign' utility.
- # The value for CFBundleIdentifier is used as the default unique name of your program for Code Signing
- # purposes. It even identifies the APP for access to restricted macOS areas like Keychain.
- #
- # The identifier used for signing must be globally unique. The usual form for this identifier is a
- # hierarchical name in reverse DNS notation, starting with the toplevel domain, followed by the company
- # name, followed by the department within the company, and ending with the product name. Usually in the
- # form: com.mycompany.department.appname
- # CLI option --osx-bundle-identifier sets this value.
- "CFBundleIdentifier": self.bundle_identifier,
- "CFBundleExecutable": os.path.basename(self.exename),
- "CFBundleIconFile": os.path.basename(self.icon),
- "CFBundleInfoDictionaryVersion": "6.0",
- "CFBundlePackageType": "APPL",
- "CFBundleShortVersionString": self.version,
- }
- # Set some default values. But they still can be overwritten by the user.
- if self.console:
- # Setting EXE console=True implies LSBackgroundOnly=True.
- info_plist_dict['LSBackgroundOnly'] = True
- else:
- # Let's use high resolution by default.
- info_plist_dict['NSHighResolutionCapable'] = True
- # Merge info_plist settings from spec file
- if isinstance(self.info_plist, dict) and self.info_plist:
- info_plist_dict.update(self.info_plist)
- plist_filename = os.path.join(self.name, "Contents", "Info.plist")
- with open(plist_filename, "wb") as plist_fh:
- plistlib.dump(info_plist_dict, plist_fh)
- # Pre-process the TOC into its final BUNDLE-compatible form.
- bundle_toc = self._process_bundle_toc(self.toc)
- # Perform the actual collection.
- CONTENTS_FRAMEWORKS_PATH = pathlib.PurePath('Contents/Frameworks')
- for dest_name, src_name, typecode in bundle_toc:
- # Create parent directory structure, if necessary
- dest_path = os.path.join(self.name, dest_name) # Absolute destination path
- dest_dir = os.path.dirname(dest_path)
- try:
- os.makedirs(dest_dir, exist_ok=True)
- except FileExistsError:
- raise SystemExit(
- f"ERROR: Pyinstaller needs to create a directory at {dest_dir!r}, "
- "but there already exists a file at that path!"
- )
- # Copy extensions and binaries from cache. This ensures that these files undergo additional binary
- # processing - have paths to linked libraries rewritten (relative to `@rpath`) and have rpath set to the
- # top-level directory (relative to `@loader_path`, i.e., the file's location). The "top-level" directory
- # in this case corresponds to `Contents/MacOS` (where `sys._MEIPASS` also points), so we need to pass
- # the cache retrieval function the *original* destination path (which is without preceding
- # `Contents/MacOS`).
- if typecode in ('EXTENSION', 'BINARY'):
- orig_dest_name = str(pathlib.PurePath(dest_name).relative_to(CONTENTS_FRAMEWORKS_PATH))
- src_name = process_collected_binary(
- src_name,
- orig_dest_name,
- use_strip=self.strip,
- use_upx=self.upx,
- upx_exclude=self.upx_exclude,
- target_arch=self.target_arch,
- codesign_identity=self.codesign_identity,
- entitlements_file=self.entitlements_file,
- strict_arch_validation=(typecode == 'EXTENSION'),
- )
- if typecode == 'SYMLINK':
- os.symlink(src_name, dest_path) # Create link at dest_path, pointing at (relative) src_name
- else:
- # BUNDLE does not support MERGE-based multipackage
- assert typecode != 'DEPENDENCY', "MERGE DEPENDENCY entries are not supported in BUNDLE!"
- # At this point, `src_name` should be a valid file.
- if not os.path.isfile(src_name):
- raise ValueError(f"Resource {src_name!r} is not a valid file!")
- # If strict collection mode is enabled, the destination should not exist yet.
- if strict_collect_mode and os.path.exists(dest_path):
- raise ValueError(
- f"Attempting to collect a duplicated file into BUNDLE: {dest_name} (type: {typecode})"
- )
- # Use `shutil.copyfile` to copy file with default permissions. We do not attempt to preserve original
- # permissions nor metadata, as they might be too restrictive and cause issues either during subsequent
- # re-build attempts or when trying to move the application bundle. For binaries (and data files with
- # executable bit set), we manually set the executable bits after copying the file.
- shutil.copyfile(src_name, dest_path)
- if (
- typecode in ('EXTENSION', 'BINARY', 'EXECUTABLE')
- or (typecode == 'DATA' and os.access(src_name, os.X_OK))
- ):
- os.chmod(dest_path, 0o755)
- # Sign the bundle
- logger.info('Signing the BUNDLE...')
- try:
- osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep=True)
- except Exception as e:
- # Display a warning or re-raise the error, depending on the environment-variable setting.
- if os.environ.get("PYINSTALLER_STRICT_BUNDLE_CODESIGN_ERROR", "0") == "0":
- logger.warning("Error while signing the bundle: %s", e)
- logger.warning("You will need to sign the bundle manually!")
- else:
- raise RuntimeError("Failed to codesign the bundle!") from e
- logger.info("Building BUNDLE %s completed successfully.", self.tocbasename)
- # Optionally verify bundle's signature. This is primarily intended for our CI.
- if os.environ.get("PYINSTALLER_VERIFY_BUNDLE_SIGNATURE", "0") != "0":
- logger.info("Verifying signature for BUNDLE %s...", self.name)
- self.verify_bundle_signature(self.name)
- logger.info("BUNDLE verification complete!")
- @staticmethod
- def verify_bundle_signature(bundle_dir):
- # First, verify the bundle signature using codesign.
- cmd_args = ['/usr/bin/codesign', '--verify', '--all-architectures', '--deep', '--strict', bundle_dir]
- p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf8')
- if p.returncode:
- raise SystemError(
- f"codesign command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}"
- )
- # Ensure that code-signing information is *NOT* embedded in the files' extended attributes.
- #
- # This happens when files other than binaries are present in `Contents/MacOS` or `Contents/Frameworks`
- # directory; as the signature cannot be embedded within the file itself (contrary to binaries with
- # `LC_CODE_SIGNATURE` section in their header), it ends up stores in the file's extended attributes. However,
- # if such bundle is transferred using a method that does not support extended attributes (for example, a zip
- # file), the signatures on these files are lost, and the signature of the bundle as a whole becomes invalid.
- # This is the primary reason why we need to relocate non-binaries into `Contents/Resources` - the signatures
- # for files in that directory end up stored in `Contents/_CodeSignature/CodeResources` file.
- #
- # This check therefore aims to ensure that all files have been properly relocated to their corresponding
- # locations w.r.t. the code-signing requirements.
- try:
- import xattr
- except ModuleNotFoundError:
- logger.info("xattr package not available; skipping verification of extended attributes!")
- return
- CODESIGN_ATTRS = (
- "com.apple.cs.CodeDirectory",
- "com.apple.cs.CodeRequirements",
- "com.apple.cs.CodeRequirements-1",
- "com.apple.cs.CodeSignature",
- )
- for entry in pathlib.Path(bundle_dir).rglob("*"):
- if not entry.is_file():
- continue
- file_attrs = xattr.listxattr(entry)
- if any([codesign_attr in file_attrs for codesign_attr in CODESIGN_ATTRS]):
- raise ValueError(f"Code-sign attributes found in extended attributes of {str(entry)!r}!")
|