Remove hardcoded libpython binaries and add debug step
All checks were successful
build / build-linux (push) Successful in 16s
All checks were successful
build / build-linux (push) Successful in 16s
This commit is contained in:
736
venv/lib/python3.12/site-packages/PyInstaller/building/osx.py
Executable file
736
venv/lib/python3.12/site-packages/PyInstaller/building/osx.py
Executable file
@@ -0,0 +1,736 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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)
|
||||
|
||||
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'
|
||||
|
||||
_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'
|
||||
|
||||
# (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.
|
||||
if parent_dir == _TOP_LEVEL_DIR:
|
||||
continue
|
||||
|
||||
directory_type = _BINARY_DIR_TYPE if typecode == 'BINARY' else _DATA_DIR_TYPE # default
|
||||
directory_type = directory_types.get(parent_dir, directory_type)
|
||||
|
||||
if directory_type == _DATA_DIR_TYPE and typecode == 'BINARY':
|
||||
directory_type = _MIXED_DIR_TYPE
|
||||
if directory_type == _BINARY_DIR_TYPE and typecode == 'DATA':
|
||||
directory_type = _MIXED_DIR_TYPE
|
||||
|
||||
directory_types[parent_dir] = directory_type
|
||||
|
||||
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 == '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
|
||||
|
||||
# Special handling for SYMLINK entries in original TOC; if we need to cross-link a symlink entry, we create
|
||||
# it in both locations, and have each 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).
|
||||
if typecode == 'SYMLINK' and requires_crosslink:
|
||||
bundle_toc.append((os.path.join(file_base_dir, orig_dest_name), src_name, typecode))
|
||||
bundle_toc.append((os.path.join(crosslink_base_dir, orig_dest_name), src_name, typecode))
|
||||
continue
|
||||
|
||||
# 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}!")
|
||||
Reference in New Issue
Block a user