1271 lines
63 KiB
Python
1271 lines
63 KiB
Python
|
#-----------------------------------------------------------------------------
|
||
|
# 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)
|
||
|
#-----------------------------------------------------------------------------
|
||
|
"""
|
||
|
This module contains classes that are available for the .spec files.
|
||
|
|
||
|
Spec file is generated by PyInstaller. The generated code from .spec file
|
||
|
is a way how PyInstaller does the dependency analysis and creates executable.
|
||
|
"""
|
||
|
|
||
|
import os
|
||
|
import subprocess
|
||
|
import time
|
||
|
import shutil
|
||
|
from operator import itemgetter
|
||
|
|
||
|
from PyInstaller import HOMEPATH, PLATFORM
|
||
|
from PyInstaller import log as logging
|
||
|
from PyInstaller.archive.writers import CArchiveWriter, ZlibArchiveWriter
|
||
|
from PyInstaller.building.datastruct import Target, _check_guts_eq, normalize_pyz_toc, normalize_toc
|
||
|
from PyInstaller.building.utils import (
|
||
|
_check_guts_toc, _make_clean_directory, _rmtree, process_collected_binary, get_code_object, strip_paths_in_code,
|
||
|
compile_pymodule
|
||
|
)
|
||
|
from PyInstaller.building.splash import Splash # argument type validation in EXE
|
||
|
from PyInstaller.compat import is_cygwin, is_darwin, is_linux, is_win, strict_collect_mode
|
||
|
from PyInstaller.depend import bindepend
|
||
|
from PyInstaller.depend.analysis import get_bootstrap_modules
|
||
|
import PyInstaller.utils.misc as miscutils
|
||
|
|
||
|
logger = logging.getLogger(__name__)
|
||
|
|
||
|
if is_win:
|
||
|
from PyInstaller.utils.win32 import (icon, versioninfo, winmanifest, winresource, winutils)
|
||
|
|
||
|
if is_darwin:
|
||
|
import PyInstaller.utils.osx as osxutils
|
||
|
|
||
|
|
||
|
class PYZ(Target):
|
||
|
"""
|
||
|
Creates a zlib-based PYZ archive that contains byte-compiled pure Python modules.
|
||
|
"""
|
||
|
def __init__(self, *tocs, **kwargs):
|
||
|
"""
|
||
|
tocs
|
||
|
One or more TOC (Table of Contents) lists, usually an `Analysis.pure`.
|
||
|
|
||
|
kwargs
|
||
|
Possible keyword arguments:
|
||
|
|
||
|
name
|
||
|
A filename for the .pyz. Normally not needed, as the generated name will do fine.
|
||
|
"""
|
||
|
if kwargs.get("cipher"):
|
||
|
from PyInstaller.exceptions import RemovedCipherFeatureError
|
||
|
raise RemovedCipherFeatureError(
|
||
|
"Please remove the 'cipher' arguments to PYZ() and Analysis() in your spec file."
|
||
|
)
|
||
|
|
||
|
from PyInstaller.config import CONF
|
||
|
|
||
|
super().__init__()
|
||
|
|
||
|
name = kwargs.get('name', None)
|
||
|
|
||
|
self.name = name
|
||
|
if name is None:
|
||
|
self.name = os.path.splitext(self.tocfilename)[0] + '.pyz'
|
||
|
|
||
|
# PyInstaller bootstrapping modules.
|
||
|
bootstrap_dependencies = get_bootstrap_modules()
|
||
|
|
||
|
# Compile the python modules that are part of bootstrap dependencies, so that they can be collected into the
|
||
|
# CArchive/PKG and imported by the bootstrap script.
|
||
|
self.dependencies = []
|
||
|
workpath = os.path.join(CONF['workpath'], 'localpycs')
|
||
|
for name, src_path, typecode in bootstrap_dependencies:
|
||
|
if typecode == 'PYMODULE':
|
||
|
# Compile pymodule and include the compiled .pyc file.
|
||
|
pyc_path = compile_pymodule(name, src_path, workpath, code_cache=None)
|
||
|
self.dependencies.append((name, pyc_path, typecode))
|
||
|
else:
|
||
|
# Include as is (extensions).
|
||
|
self.dependencies.append((name, src_path, typecode))
|
||
|
|
||
|
# Merge input TOC(s) and their code object dictionaries (if available). Skip the bootstrap modules, which will
|
||
|
# be passed on to CArchive/PKG.
|
||
|
bootstrap_module_names = set(name for name, _, typecode in self.dependencies if typecode == 'PYMODULE')
|
||
|
self.toc = []
|
||
|
self.code_dict = {}
|
||
|
for toc in tocs:
|
||
|
# Check if code cache association exists for the given TOC list
|
||
|
code_cache = CONF['code_cache'].get(id(toc))
|
||
|
if code_cache is not None:
|
||
|
self.code_dict.update(code_cache)
|
||
|
|
||
|
for entry in toc:
|
||
|
name, _, typecode = entry
|
||
|
# PYZ expects only PYMODULE entries (python code objects).
|
||
|
assert typecode == 'PYMODULE', f"Invalid entry passed to PYZ: {entry}!"
|
||
|
# Module required during bootstrap; skip to avoid collecting a duplicate.
|
||
|
if name in bootstrap_module_names:
|
||
|
continue
|
||
|
self.toc.append(entry)
|
||
|
|
||
|
# Normalize TOC
|
||
|
self.toc = normalize_pyz_toc(self.toc)
|
||
|
|
||
|
# Alphabetically sort the TOC to enable reproducible builds.
|
||
|
self.toc.sort()
|
||
|
|
||
|
self.__postinit__()
|
||
|
|
||
|
_GUTS = (
|
||
|
# input parameters
|
||
|
('name', _check_guts_eq),
|
||
|
('toc', _check_guts_toc),
|
||
|
# no calculated/analysed values
|
||
|
)
|
||
|
|
||
|
def assemble(self):
|
||
|
logger.info("Building PYZ (ZlibArchive) %s", self.name)
|
||
|
|
||
|
# Ensure code objects are available for all modules we are about to collect.
|
||
|
# NOTE: `self.toc` is already sorted by names.
|
||
|
archive_toc = []
|
||
|
for entry in self.toc:
|
||
|
name, src_path, typecode = entry
|
||
|
if name not in self.code_dict:
|
||
|
# The code object is not available from the ModuleGraph's cache; re-create it.
|
||
|
try:
|
||
|
self.code_dict[name] = get_code_object(name, src_path)
|
||
|
except SyntaxError:
|
||
|
# The module was likely written for different Python version; exclude it
|
||
|
continue
|
||
|
archive_toc.append(entry)
|
||
|
|
||
|
# Remove leading parts of paths in code objects.
|
||
|
self.code_dict = {name: strip_paths_in_code(code) for name, code in self.code_dict.items()}
|
||
|
|
||
|
# Create the archive
|
||
|
ZlibArchiveWriter(self.name, archive_toc, code_dict=self.code_dict)
|
||
|
logger.info("Building PYZ (ZlibArchive) %s completed successfully.", self.name)
|
||
|
|
||
|
|
||
|
class PKG(Target):
|
||
|
"""
|
||
|
Creates a CArchive. CArchive is the data structure that is embedded into the executable. This data structure allows
|
||
|
to include various read-only data in a single-file deployment.
|
||
|
"""
|
||
|
xformdict = {
|
||
|
'PYMODULE': 'm',
|
||
|
'PYSOURCE': 's',
|
||
|
'EXTENSION': 'b',
|
||
|
'PYZ': 'z',
|
||
|
'PKG': 'a',
|
||
|
'DATA': 'x',
|
||
|
'BINARY': 'b',
|
||
|
'ZIPFILE': 'Z',
|
||
|
'EXECUTABLE': 'b',
|
||
|
'DEPENDENCY': 'd',
|
||
|
'SPLASH': 'l',
|
||
|
'SYMLINK': 'n',
|
||
|
}
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
toc,
|
||
|
python_lib_name,
|
||
|
name=None,
|
||
|
cdict=None,
|
||
|
exclude_binaries=False,
|
||
|
strip_binaries=False,
|
||
|
upx_binaries=False,
|
||
|
upx_exclude=None,
|
||
|
target_arch=None,
|
||
|
codesign_identity=None,
|
||
|
entitlements_file=None
|
||
|
):
|
||
|
"""
|
||
|
toc
|
||
|
A TOC (Table of Contents) list.
|
||
|
python_lib_name
|
||
|
Name of the python shared library to store in PKG. Required by bootloader.
|
||
|
name
|
||
|
An optional filename for the PKG.
|
||
|
cdict
|
||
|
Dictionary that specifies compression by typecode. For Example, PYZ is left uncompressed so that it
|
||
|
can be accessed inside the PKG. The default uses sensible values. If zlib is not available, no
|
||
|
compression is used.
|
||
|
exclude_binaries
|
||
|
If True, EXTENSIONs and BINARYs will be left out of the PKG, and forwarded to its container (usually
|
||
|
a COLLECT).
|
||
|
strip_binaries
|
||
|
If True, use 'strip' command to reduce the size of binary files.
|
||
|
upx_binaries
|
||
|
"""
|
||
|
super().__init__()
|
||
|
|
||
|
self.toc = normalize_toc(toc) # Ensure guts contain normalized TOC
|
||
|
self.python_lib_name = python_lib_name
|
||
|
self.cdict = cdict
|
||
|
self.name = name
|
||
|
if name is None:
|
||
|
self.name = os.path.splitext(self.tocfilename)[0] + '.pkg'
|
||
|
self.exclude_binaries = exclude_binaries
|
||
|
self.strip_binaries = strip_binaries
|
||
|
self.upx_binaries = upx_binaries
|
||
|
self.upx_exclude = upx_exclude or []
|
||
|
self.target_arch = target_arch
|
||
|
self.codesign_identity = codesign_identity
|
||
|
self.entitlements_file = entitlements_file
|
||
|
|
||
|
# This dict tells PyInstaller what items embedded in the executable should be compressed.
|
||
|
if self.cdict is None:
|
||
|
self.cdict = {
|
||
|
'EXTENSION': COMPRESSED,
|
||
|
'DATA': COMPRESSED,
|
||
|
'BINARY': COMPRESSED,
|
||
|
'EXECUTABLE': COMPRESSED,
|
||
|
'PYSOURCE': COMPRESSED,
|
||
|
'PYMODULE': COMPRESSED,
|
||
|
'SPLASH': COMPRESSED,
|
||
|
# Do not compress PYZ as a whole, as it contains individually-compressed modules.
|
||
|
'PYZ': UNCOMPRESSED,
|
||
|
# Do not compress target names in symbolic links.
|
||
|
'SYMLINK': UNCOMPRESSED,
|
||
|
}
|
||
|
|
||
|
self.__postinit__()
|
||
|
|
||
|
_GUTS = ( # input parameters
|
||
|
('name', _check_guts_eq),
|
||
|
('cdict', _check_guts_eq),
|
||
|
('toc', _check_guts_toc), # list unchanged and no newer files
|
||
|
('python_lib_name', _check_guts_eq),
|
||
|
('exclude_binaries', _check_guts_eq),
|
||
|
('strip_binaries', _check_guts_eq),
|
||
|
('upx_binaries', _check_guts_eq),
|
||
|
('upx_exclude', _check_guts_eq),
|
||
|
('target_arch', _check_guts_eq),
|
||
|
('codesign_identity', _check_guts_eq),
|
||
|
('entitlements_file', _check_guts_eq),
|
||
|
# no calculated/analysed values
|
||
|
)
|
||
|
|
||
|
def assemble(self):
|
||
|
logger.info("Building PKG (CArchive) %s", os.path.basename(self.name))
|
||
|
|
||
|
bootstrap_toc = [] # TOC containing bootstrap scripts and modules, which must not be sorted.
|
||
|
archive_toc = [] # TOC containing all other elements. Sorted to enable reproducible builds.
|
||
|
|
||
|
for dest_name, src_name, typecode in self.toc:
|
||
|
# Ensure that the source file exists, if necessary. Skip the check for OPTION entries, where 'src_name' is
|
||
|
# None. Also skip DEPENDENCY entries due to special contents of 'dest_name' and/or 'src_name'. Same for the
|
||
|
# SYMLINK entries, where 'src_name' is relative target name for symbolic link.
|
||
|
if typecode not in {'OPTION', 'DEPENDENCY', 'SYMLINK'} and not os.path.exists(src_name):
|
||
|
if strict_collect_mode:
|
||
|
raise ValueError(f"Non-existent resource {src_name}, meant to be collected as {dest_name}!")
|
||
|
else:
|
||
|
logger.warning(
|
||
|
"Ignoring non-existent resource %s, meant to be collected as %s", src_name, dest_name
|
||
|
)
|
||
|
continue
|
||
|
if typecode in ('BINARY', 'EXTENSION'):
|
||
|
if self.exclude_binaries:
|
||
|
# This is onedir-specific codepath - the EXE and consequently PKG should not be passed the Analysis'
|
||
|
# `datas` and `binaries` TOCs (unless the user messes up the .spec file). However, EXTENSION entries
|
||
|
# might still slip in via `PYZ.dependencies`, which are merged by EXE into its TOC and passed on to
|
||
|
# PKG here. Such entries need to be passed to the parent container (the COLLECT) via
|
||
|
# `PKG.dependencies`.
|
||
|
#
|
||
|
# This codepath formerly performed such pass-through only for EXTENSION entries, but in order to
|
||
|
# keep code simple, we now also do it for BINARY entries. In a sane world, we do not expect to
|
||
|
# encounter them here; but if they do happen to pass through here and we pass them on, the
|
||
|
# container's TOC de-duplication should take care of them (same as with EXTENSION ones, really).
|
||
|
self.dependencies.append((dest_name, src_name, typecode))
|
||
|
else:
|
||
|
# This is onefile-specific codepath. The binaries (both EXTENSION and BINARY entries) need to be
|
||
|
# processed using `process_collected_binary` helper.
|
||
|
src_name = process_collected_binary(
|
||
|
src_name,
|
||
|
dest_name,
|
||
|
use_strip=self.strip_binaries,
|
||
|
use_upx=self.upx_binaries,
|
||
|
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'),
|
||
|
)
|
||
|
archive_toc.append((dest_name, src_name, self.cdict.get(typecode, False), self.xformdict[typecode]))
|
||
|
elif typecode in ('DATA', 'ZIPFILE'):
|
||
|
# Same logic as above for BINARY and EXTENSION; if `exclude_binaries` is set, we are in onedir mode;
|
||
|
# we should exclude DATA (and ZIPFILE) entries and instead pass them on via PKG's `dependencies`. This
|
||
|
# prevents a onedir application from becoming a broken onefile one if user accidentally passes datas
|
||
|
# and binaries TOCs to EXE instead of COLLECT.
|
||
|
if self.exclude_binaries:
|
||
|
self.dependencies.append((dest_name, src_name, typecode))
|
||
|
else:
|
||
|
if typecode == 'DATA' and os.access(src_name, os.X_OK):
|
||
|
# DATA with executable bit set (e.g., shell script); turn into binary so that executable bit is
|
||
|
# restored on the extracted file.
|
||
|
carchive_typecode = 'b'
|
||
|
else:
|
||
|
carchive_typecode = self.xformdict[typecode]
|
||
|
archive_toc.append((dest_name, src_name, self.cdict.get(typecode, False), carchive_typecode))
|
||
|
elif typecode == 'OPTION':
|
||
|
archive_toc.append((dest_name, '', False, 'o'))
|
||
|
elif typecode in ('PYSOURCE', 'PYMODULE'):
|
||
|
# Collect python script and modules in a TOC that will not be sorted.
|
||
|
bootstrap_toc.append((dest_name, src_name, self.cdict.get(typecode, False), self.xformdict[typecode]))
|
||
|
else:
|
||
|
# PYZ, PKG, DEPENDENCY, SPLASH, SYMLINK
|
||
|
archive_toc.append((dest_name, src_name, self.cdict.get(typecode, False), self.xformdict[typecode]))
|
||
|
|
||
|
# Sort content alphabetically by type and name to enable reproducible builds.
|
||
|
archive_toc.sort(key=itemgetter(3, 0))
|
||
|
# Do *not* sort modules and scripts, as their order is important.
|
||
|
# TODO: Think about having all modules first and then all scripts.
|
||
|
CArchiveWriter(self.name, bootstrap_toc + archive_toc, pylib_name=self.python_lib_name)
|
||
|
|
||
|
logger.info("Building PKG (CArchive) %s completed successfully.", os.path.basename(self.name))
|
||
|
|
||
|
|
||
|
class EXE(Target):
|
||
|
"""
|
||
|
Creates the final executable of the frozen app. This bundles all necessary files together.
|
||
|
"""
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
"""
|
||
|
args
|
||
|
One or more arguments that are either an instance of `Target` or an iterable representing TOC list.
|
||
|
kwargs
|
||
|
Possible keyword arguments:
|
||
|
|
||
|
bootloader_ignore_signals
|
||
|
Non-Windows only. If True, the bootloader process will ignore all ignorable signals. If False (default),
|
||
|
it will forward all signals to the child process. Useful in situations where for example a supervisor
|
||
|
process signals both the bootloader and the child (e.g., via a process group) to avoid signalling the
|
||
|
child twice.
|
||
|
console
|
||
|
On Windows or Mac OS governs whether to use the console executable or the windowed executable. Always
|
||
|
True on Linux/Unix (always console executable - it does not matter there).
|
||
|
hide_console
|
||
|
Windows only. In console-enabled executable, hide or minimize the console window if the program owns the
|
||
|
console window (i.e., was not launched from existing console window). Depending on the setting, the
|
||
|
console is hidden/mininized either early in the bootloader execution ('hide-early', 'minimize-early') or
|
||
|
late in the bootloader execution ('hide-late', 'minimize-late'). The early option takes place as soon as
|
||
|
the PKG archive is found. In onefile builds, the late option takes place after application has unpacked
|
||
|
itself and before it launches the child process. In onedir builds, the late option takes place before
|
||
|
starting the embedded python interpreter.
|
||
|
disable_windowed_traceback
|
||
|
Disable traceback dump of unhandled exception in windowed (noconsole) mode (Windows and macOS only),
|
||
|
and instead display a message that this feature is disabled.
|
||
|
debug
|
||
|
Setting to True gives you progress messages from the executable (for console=False there will be
|
||
|
annoying MessageBoxes on Windows).
|
||
|
name
|
||
|
The filename for the executable. On Windows suffix '.exe' is appended.
|
||
|
exclude_binaries
|
||
|
Forwarded to the PKG the EXE builds.
|
||
|
icon
|
||
|
Windows and Mac OS only. icon='myicon.ico' to use an icon file or icon='notepad.exe,0' to grab an icon
|
||
|
resource. Defaults to use PyInstaller's console or windowed icon. Use icon=`NONE` to not add any icon.
|
||
|
version
|
||
|
Windows only. version='myversion.txt'. Use grab_version.py to get a version resource from an executable
|
||
|
and then edit the output to create your own. (The syntax of version resources is so arcane that I would
|
||
|
not attempt to write one from scratch).
|
||
|
uac_admin
|
||
|
Windows only. Setting to True creates a Manifest with will request elevation upon application start.
|
||
|
uac_uiaccess
|
||
|
Windows only. Setting to True allows an elevated application to work with Remote Desktop.
|
||
|
argv_emulation
|
||
|
macOS only. Enables argv emulation in macOS .app bundles (i.e., windowed bootloader). If enabled, the
|
||
|
initial open document/URL Apple Events are intercepted by bootloader and converted into sys.argv.
|
||
|
target_arch
|
||
|
macOS only. Used to explicitly specify the target architecture; either single-arch ('x86_64' or 'arm64')
|
||
|
or 'universal2'. Used in checks that the collected binaries contain the requires arch slice(s) and/or
|
||
|
to convert fat binaries into thin ones as necessary. If not specified (default), a single-arch build
|
||
|
corresponding to running architecture is assumed.
|
||
|
codesign_identity
|
||
|
macOS only. Use the provided identity to sign collected binaries and the generated executable. If
|
||
|
signing identity is not provided, ad-hoc signing is performed.
|
||
|
entitlements_file
|
||
|
macOS only. Optional path to entitlements file to use with code signing of collected binaries
|
||
|
(--entitlements option to codesign utility).
|
||
|
contents_directory
|
||
|
Onedir mode only. Specifies the name of the directory where all files par the executable will be placed.
|
||
|
Setting the name to '.' (or '' or None) re-enables old onedir layout without contents directory.
|
||
|
"""
|
||
|
from PyInstaller.config import CONF
|
||
|
|
||
|
super().__init__()
|
||
|
|
||
|
# Available options for EXE in .spec files.
|
||
|
self.exclude_binaries = kwargs.get('exclude_binaries', False)
|
||
|
self.bootloader_ignore_signals = kwargs.get('bootloader_ignore_signals', False)
|
||
|
self.console = kwargs.get('console', True)
|
||
|
self.hide_console = kwargs.get('hide_console', None)
|
||
|
self.disable_windowed_traceback = kwargs.get('disable_windowed_traceback', False)
|
||
|
self.debug = kwargs.get('debug', False)
|
||
|
self.name = kwargs.get('name', None)
|
||
|
self.icon = kwargs.get('icon', None)
|
||
|
self.versrsrc = kwargs.get('version', None)
|
||
|
self.manifest = kwargs.get('manifest', None)
|
||
|
self.resources = kwargs.get('resources', [])
|
||
|
self.strip = kwargs.get('strip', False)
|
||
|
self.upx_exclude = kwargs.get("upx_exclude", [])
|
||
|
self.runtime_tmpdir = kwargs.get('runtime_tmpdir', None)
|
||
|
self.contents_directory = kwargs.get("contents_directory", "_internal")
|
||
|
# If ``append_pkg`` is false, the archive will not be appended to the exe, but copied beside it.
|
||
|
self.append_pkg = kwargs.get('append_pkg', True)
|
||
|
|
||
|
# On Windows allows the exe to request admin privileges.
|
||
|
self.uac_admin = kwargs.get('uac_admin', False)
|
||
|
self.uac_uiaccess = kwargs.get('uac_uiaccess', False)
|
||
|
|
||
|
# macOS argv emulation
|
||
|
self.argv_emulation = kwargs.get('argv_emulation', False)
|
||
|
|
||
|
# Target architecture (macOS only)
|
||
|
self.target_arch = kwargs.get('target_arch', None)
|
||
|
if is_darwin:
|
||
|
if self.target_arch is None:
|
||
|
import platform
|
||
|
self.target_arch = platform.machine()
|
||
|
else:
|
||
|
assert self.target_arch in {'x86_64', 'arm64', 'universal2'}, \
|
||
|
f"Unsupported target arch: {self.target_arch}"
|
||
|
logger.info("EXE target arch: %s", self.target_arch)
|
||
|
else:
|
||
|
self.target_arch = None # explicitly disable
|
||
|
|
||
|
# Code signing identity (macOS only)
|
||
|
self.codesign_identity = kwargs.get('codesign_identity', None)
|
||
|
if is_darwin:
|
||
|
logger.info("Code signing identity: %s", self.codesign_identity)
|
||
|
else:
|
||
|
self.codesign_identity = None # explicitly disable
|
||
|
# Code signing entitlements
|
||
|
self.entitlements_file = kwargs.get('entitlements_file', None)
|
||
|
|
||
|
# UPX needs to be both available and enabled for the target.
|
||
|
self.upx = CONF['upx_available'] and kwargs.get('upx', False)
|
||
|
|
||
|
# Catch and clear options that are unsupported on specific platforms.
|
||
|
if self.versrsrc and not is_win:
|
||
|
logger.warning('Ignoring version information; supported only on Windows!')
|
||
|
self.versrsrc = None
|
||
|
if self.manifest and not is_win:
|
||
|
logger.warning('Ignoring manifest; supported only on Windows!')
|
||
|
self.manifest = None
|
||
|
if self.resources and not is_win:
|
||
|
logger.warning('Ignoring resources; supported only on Windows!')
|
||
|
self.resources = []
|
||
|
if self.icon and not (is_win or is_darwin):
|
||
|
logger.warning('Ignoring icon; supported only on Windows and macOS!')
|
||
|
self.icon = None
|
||
|
if self.hide_console and not is_win:
|
||
|
logger.warning('Ignoring hide_console; supported only on Windows!')
|
||
|
self.hide_console = None
|
||
|
|
||
|
if self.contents_directory in ("", "."):
|
||
|
self.contents_directory = None # Re-enable old onedir layout without contents directory.
|
||
|
elif self.contents_directory == ".." or "/" in self.contents_directory or "\\" in self.contents_directory:
|
||
|
raise SystemExit(
|
||
|
f'Invalid value "{self.contents_directory}" passed to `--contents-directory` or `contents_directory`. '
|
||
|
'Exactly one directory level is required (or just "." to disable the contents directory).'
|
||
|
)
|
||
|
|
||
|
if not kwargs.get('embed_manifest', True):
|
||
|
from PyInstaller.exceptions import RemovedExternalManifestError
|
||
|
raise RemovedExternalManifestError(
|
||
|
"Please remove the 'embed_manifest' argument to EXE() in your spec file."
|
||
|
)
|
||
|
|
||
|
# Old .spec format included in 'name' the path where to put created app. New format includes only exename.
|
||
|
#
|
||
|
# Ignore fullpath in the 'name' and prepend DISTPATH or WORKPATH.
|
||
|
# DISTPATH - onefile
|
||
|
# WORKPATH - onedir
|
||
|
if self.exclude_binaries:
|
||
|
# onedir mode - create executable in WORKPATH.
|
||
|
self.name = os.path.join(CONF['workpath'], os.path.basename(self.name))
|
||
|
else:
|
||
|
# onefile mode - create executable in DISTPATH.
|
||
|
self.name = os.path.join(CONF['distpath'], os.path.basename(self.name))
|
||
|
|
||
|
# Old .spec format included on Windows in 'name' .exe suffix.
|
||
|
if is_win or is_cygwin:
|
||
|
# Append .exe suffix if it is not already there.
|
||
|
if not self.name.endswith('.exe'):
|
||
|
self.name += '.exe'
|
||
|
base_name = os.path.splitext(os.path.basename(self.name))[0]
|
||
|
else:
|
||
|
base_name = os.path.basename(self.name)
|
||
|
# Create the CArchive PKG in WORKPATH. When instancing PKG(), set name so that guts check can test whether the
|
||
|
# file already exists.
|
||
|
self.pkgname = os.path.join(CONF['workpath'], base_name + '.pkg')
|
||
|
|
||
|
self.toc = []
|
||
|
|
||
|
for arg in args:
|
||
|
# Valid arguments: PYZ object, Splash object, and TOC-list iterables
|
||
|
if isinstance(arg, (PYZ, Splash)):
|
||
|
# Add object as an entry to the TOC, and merge its dependencies TOC
|
||
|
if isinstance(arg, PYZ):
|
||
|
self.toc.append((os.path.basename(arg.name), arg.name, "PYZ"))
|
||
|
else:
|
||
|
self.toc.append((os.path.basename(arg.name), arg.name, "SPLASH"))
|
||
|
self.toc.extend(arg.dependencies)
|
||
|
elif miscutils.is_iterable(arg):
|
||
|
# TOC-like iterable
|
||
|
self.toc.extend(arg)
|
||
|
else:
|
||
|
raise TypeError(f"Invalid argument type for EXE: {type(arg)!r}")
|
||
|
|
||
|
if self.runtime_tmpdir is not None:
|
||
|
self.toc.append(("pyi-runtime-tmpdir " + self.runtime_tmpdir, "", "OPTION"))
|
||
|
|
||
|
if self.bootloader_ignore_signals:
|
||
|
# no value; presence means "true"
|
||
|
self.toc.append(("pyi-bootloader-ignore-signals", "", "OPTION"))
|
||
|
|
||
|
if self.disable_windowed_traceback:
|
||
|
# no value; presence means "true"
|
||
|
self.toc.append(("pyi-disable-windowed-traceback", "", "OPTION"))
|
||
|
|
||
|
if self.argv_emulation:
|
||
|
# no value; presence means "true"
|
||
|
self.toc.append(("pyi-macos-argv-emulation", "", "OPTION"))
|
||
|
|
||
|
if self.contents_directory:
|
||
|
self.toc.append(("pyi-contents-directory " + self.contents_directory, "", "OPTION"))
|
||
|
|
||
|
if self.hide_console:
|
||
|
# Validate the value
|
||
|
_HIDE_CONSOLE_VALUES = {'hide-early', 'minimize-early', 'hide-late', 'minimize-late'}
|
||
|
self.hide_console = self.hide_console.lower()
|
||
|
if self.hide_console not in _HIDE_CONSOLE_VALUES:
|
||
|
raise ValueError(
|
||
|
f"Invalid hide_console value: {self.hide_console}! Allowed values: {_HIDE_CONSOLE_VALUES}"
|
||
|
)
|
||
|
self.toc.append((f"pyi-hide-console {self.hide_console}", "", "OPTION"))
|
||
|
|
||
|
# If the icon path is relative, make it relative to the .spec file.
|
||
|
if self.icon and self.icon != "NONE":
|
||
|
if isinstance(self.icon, list):
|
||
|
self.icon = [self._makeabs(ic) for ic in self.icon]
|
||
|
else:
|
||
|
self.icon = [self._makeabs(self.icon)]
|
||
|
|
||
|
if is_win:
|
||
|
if not self.icon:
|
||
|
# --icon not specified; use default from bootloader folder
|
||
|
if self.console:
|
||
|
ico = 'icon-console.ico'
|
||
|
else:
|
||
|
ico = 'icon-windowed.ico'
|
||
|
self.icon = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'bootloader', 'images', ico)
|
||
|
|
||
|
# Prepare manifest for the executable by creating minimal manifest or modifying the supplied one.
|
||
|
if self.manifest:
|
||
|
# Determine if we were given a filename or an XML string.
|
||
|
if "<" in self.manifest:
|
||
|
self.manifest = self.manifest.encode("utf-8")
|
||
|
else:
|
||
|
self.manifest = self._makeabs(self.manifest)
|
||
|
with open(self.manifest, "rb") as fp:
|
||
|
self.manifest = fp.read()
|
||
|
self.manifest = winmanifest.create_application_manifest(self.manifest, self.uac_admin, self.uac_uiaccess)
|
||
|
|
||
|
if self.versrsrc:
|
||
|
if isinstance(self.versrsrc, versioninfo.VSVersionInfo):
|
||
|
# We were passed a valid versioninfo.VSVersionInfo structure
|
||
|
pass
|
||
|
elif isinstance(self.versrsrc, (str, bytes, os.PathLike)):
|
||
|
# File path; either absolute, or relative to the spec file
|
||
|
self.versrsrc = self._makeabs(self.versrsrc)
|
||
|
logger.debug("Loading version info from file: %r", self.versrsrc)
|
||
|
self.versrsrc = versioninfo.load_version_info_from_text_file(self.versrsrc)
|
||
|
else:
|
||
|
raise TypeError(f"Unsupported type for version info argument: {type(self.versrsrc)!r}")
|
||
|
|
||
|
# Identify python shared library. This is needed both for PKG (where we need to store the name so that
|
||
|
# bootloader can look it up), and for macOS-specific processing of the generated executable (adjusting the SDK
|
||
|
# version).
|
||
|
#
|
||
|
# NOTE: we already performed an equivalent search (using the same `get_python_library_path` helper) during the
|
||
|
# analysis stage to ensure that the python shared library is collected. Unfortunately, with the way data passing
|
||
|
# works in onedir builds, we cannot look up the value in the TOC at this stage, and we need to search again.
|
||
|
self.python_lib = bindepend.get_python_library_path()
|
||
|
if self.python_lib is None:
|
||
|
from PyInstaller.exceptions import PythonLibraryNotFoundError
|
||
|
raise PythonLibraryNotFoundError()
|
||
|
|
||
|
# Normalize TOC
|
||
|
self.toc = normalize_toc(self.toc)
|
||
|
|
||
|
self.pkg = PKG(
|
||
|
toc=self.toc,
|
||
|
python_lib_name=os.path.basename(self.python_lib),
|
||
|
name=self.pkgname,
|
||
|
cdict=kwargs.get('cdict', None),
|
||
|
exclude_binaries=self.exclude_binaries,
|
||
|
strip_binaries=self.strip,
|
||
|
upx_binaries=self.upx,
|
||
|
upx_exclude=self.upx_exclude,
|
||
|
target_arch=self.target_arch,
|
||
|
codesign_identity=self.codesign_identity,
|
||
|
entitlements_file=self.entitlements_file
|
||
|
)
|
||
|
self.dependencies = self.pkg.dependencies
|
||
|
|
||
|
# Get the path of the bootloader and store it in a TOC, so it can be checked for being changed.
|
||
|
exe = self._bootloader_file('run', '.exe' if is_win or is_cygwin else '')
|
||
|
self.exefiles = [(os.path.basename(exe), exe, 'EXECUTABLE')]
|
||
|
|
||
|
self.__postinit__()
|
||
|
|
||
|
_GUTS = (
|
||
|
# input parameters
|
||
|
('name', _check_guts_eq),
|
||
|
('console', _check_guts_eq),
|
||
|
('debug', _check_guts_eq),
|
||
|
('exclude_binaries', _check_guts_eq),
|
||
|
('icon', _check_guts_eq),
|
||
|
('versrsrc', _check_guts_eq),
|
||
|
('uac_admin', _check_guts_eq),
|
||
|
('uac_uiaccess', _check_guts_eq),
|
||
|
('manifest', _check_guts_eq),
|
||
|
('append_pkg', _check_guts_eq),
|
||
|
('argv_emulation', _check_guts_eq),
|
||
|
('target_arch', _check_guts_eq),
|
||
|
('codesign_identity', _check_guts_eq),
|
||
|
('entitlements_file', _check_guts_eq),
|
||
|
# for the case the directory is shared between platforms:
|
||
|
('pkgname', _check_guts_eq),
|
||
|
('toc', _check_guts_eq),
|
||
|
('resources', _check_guts_eq),
|
||
|
('strip', _check_guts_eq),
|
||
|
('upx', _check_guts_eq),
|
||
|
('mtm', None), # checked below
|
||
|
# derived values
|
||
|
('exefiles', _check_guts_toc),
|
||
|
('python_lib', _check_guts_eq),
|
||
|
)
|
||
|
|
||
|
def _check_guts(self, data, last_build):
|
||
|
if not os.path.exists(self.name):
|
||
|
logger.info("Rebuilding %s because %s missing", self.tocbasename, os.path.basename(self.name))
|
||
|
return True
|
||
|
if not self.append_pkg and not os.path.exists(self.pkgname):
|
||
|
logger.info("Rebuilding because %s missing", os.path.basename(self.pkgname))
|
||
|
return True
|
||
|
|
||
|
if Target._check_guts(self, data, last_build):
|
||
|
return True
|
||
|
|
||
|
mtm = data['mtm']
|
||
|
if mtm != miscutils.mtime(self.name):
|
||
|
logger.info("Rebuilding %s because mtimes don't match", self.tocbasename)
|
||
|
return True
|
||
|
if mtm < miscutils.mtime(self.pkg.tocfilename):
|
||
|
logger.info("Rebuilding %s because pkg is more recent", self.tocbasename)
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
@staticmethod
|
||
|
def _makeabs(path):
|
||
|
"""
|
||
|
Helper for anchoring relative paths to spec file location.
|
||
|
"""
|
||
|
from PyInstaller.config import CONF
|
||
|
if os.path.isabs(path):
|
||
|
return path
|
||
|
else:
|
||
|
return os.path.join(CONF['specpath'], path)
|
||
|
|
||
|
def _bootloader_file(self, exe, extension=None):
|
||
|
"""
|
||
|
Pick up the right bootloader file - debug, console, windowed.
|
||
|
"""
|
||
|
# Having console/windowed bootloader makes sense only on Windows and Mac OS.
|
||
|
if is_win or is_darwin:
|
||
|
if not self.console:
|
||
|
exe = exe + 'w'
|
||
|
# There are two types of bootloaders:
|
||
|
# run - release, no verbose messages in console.
|
||
|
# run_d - contains verbose messages in console.
|
||
|
if self.debug:
|
||
|
exe = exe + '_d'
|
||
|
if extension:
|
||
|
exe = exe + extension
|
||
|
bootloader_file = os.path.join(HOMEPATH, 'PyInstaller', 'bootloader', PLATFORM, exe)
|
||
|
logger.info('Bootloader %s' % bootloader_file)
|
||
|
return bootloader_file
|
||
|
|
||
|
def assemble(self):
|
||
|
# On Windows, we used to append .notanexecutable to the intermediate/temporary file name to (attempt to)
|
||
|
# prevent interference from anti-virus programs with the build process (see #6467). This is now disabled
|
||
|
# as we wrap all processing steps that modify the executable in the `_retry_operation` helper; however,
|
||
|
# we keep around the `build_name` variable instead of directly using `self.name`, just in case we need
|
||
|
# to re-enable it...
|
||
|
build_name = self.name
|
||
|
|
||
|
logger.info("Building EXE from %s", self.tocbasename)
|
||
|
if os.path.exists(self.name):
|
||
|
if os.path.isdir(self.name):
|
||
|
_rmtree(self.name) # will prompt for confirmation if --noconfirm is not given
|
||
|
else:
|
||
|
os.remove(self.name)
|
||
|
if not os.path.exists(os.path.dirname(self.name)):
|
||
|
os.makedirs(os.path.dirname(self.name))
|
||
|
bootloader_exe = self.exefiles[0][1] # pathname of bootloader
|
||
|
if not os.path.exists(bootloader_exe):
|
||
|
raise SystemExit(_MISSING_BOOTLOADER_ERRORMSG)
|
||
|
|
||
|
# Step 1: copy the bootloader file, and perform any operations that need to be done prior to appending the PKG.
|
||
|
logger.info("Copying bootloader EXE to %s", build_name)
|
||
|
self._retry_operation(shutil.copyfile, bootloader_exe, build_name)
|
||
|
self._retry_operation(os.chmod, build_name, 0o755)
|
||
|
|
||
|
if is_win:
|
||
|
# First, remove all resources from the file. This ensures that no manifest is embedded, even if bootloader
|
||
|
# was compiled with a toolchain that forcibly embeds a default manifest (e.g., mingw toolchain from msys2).
|
||
|
self._retry_operation(winresource.remove_all_resources, build_name)
|
||
|
# Embed icon.
|
||
|
if self.icon != "NONE":
|
||
|
logger.info("Copying icon to EXE")
|
||
|
self._retry_operation(icon.CopyIcons, build_name, self.icon)
|
||
|
# Embed version info.
|
||
|
if self.versrsrc:
|
||
|
logger.info("Copying version information to EXE")
|
||
|
self._retry_operation(versioninfo.write_version_info_to_executable, build_name, self.versrsrc)
|
||
|
# Embed/copy other resources.
|
||
|
logger.info("Copying %d resources to EXE", len(self.resources))
|
||
|
for resource in self.resources:
|
||
|
self._retry_operation(self._copy_windows_resource, build_name, resource)
|
||
|
# Embed the manifest into the executable.
|
||
|
logger.info("Embedding manifest in EXE")
|
||
|
self._retry_operation(winmanifest.write_manifest_to_executable, build_name, self.manifest)
|
||
|
elif is_darwin:
|
||
|
# Convert bootloader to the target arch
|
||
|
logger.info("Converting EXE to target arch (%s)", self.target_arch)
|
||
|
osxutils.binary_to_target_arch(build_name, self.target_arch, display_name='Bootloader EXE')
|
||
|
|
||
|
# Step 2: append the PKG, if necessary
|
||
|
if self.append_pkg:
|
||
|
append_file = self.pkg.name # Append PKG
|
||
|
append_type = 'PKG archive' # For debug messages
|
||
|
else:
|
||
|
# In onefile mode, copy the stand-alone PKG next to the executable. In onedir, this will be done by the
|
||
|
# COLLECT() target.
|
||
|
if not self.exclude_binaries:
|
||
|
pkg_dst = os.path.join(os.path.dirname(build_name), os.path.basename(self.pkgname))
|
||
|
logger.info("Copying stand-alone PKG archive from %s to %s", self.pkg.name, pkg_dst)
|
||
|
shutil.copyfile(self.pkg.name, pkg_dst)
|
||
|
else:
|
||
|
logger.info("Stand-alone PKG archive will be handled by COLLECT")
|
||
|
|
||
|
# The bootloader requires package side-loading to be explicitly enabled, which is done by embedding custom
|
||
|
# signature to the executable. This extra signature ensures that the sideload-enabled executable is at least
|
||
|
# slightly different from the stock bootloader executables, which should prevent antivirus programs from
|
||
|
# flagging our stock bootloaders due to sideload-enabled applications in the wild.
|
||
|
|
||
|
# Write to temporary file
|
||
|
pkgsig_file = self.pkg.name + '.sig'
|
||
|
with open(pkgsig_file, "wb") as f:
|
||
|
# 8-byte MAGIC; slightly changed PKG MAGIC pattern
|
||
|
f.write(b'MEI\015\013\012\013\016')
|
||
|
|
||
|
append_file = pkgsig_file # Append PKG-SIG
|
||
|
append_type = 'PKG sideload signature' # For debug messages
|
||
|
|
||
|
if is_linux:
|
||
|
# Linux: append data into custom ELF section using objcopy.
|
||
|
logger.info("Appending %s to custom ELF section in EXE", append_type)
|
||
|
cmd = ['objcopy', '--add-section', f'pydata={append_file}', build_name]
|
||
|
p = subprocess.run(cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, encoding='utf-8')
|
||
|
if p.returncode:
|
||
|
raise SystemError(f"objcopy Failure: {p.returncode} {p.stdout}")
|
||
|
|
||
|
elif is_darwin:
|
||
|
# macOS: remove signature, append data, and fix-up headers so that the appended data appears to be part of
|
||
|
# the executable (which is required by strict validation during code-signing).
|
||
|
|
||
|
# Strip signatures from all arch slices. Strictly speaking, we need to remove signature (if present) from
|
||
|
# the last slice, because we will be appending data to it. When building universal2 bootloaders natively on
|
||
|
# macOS, only arm64 slices have a (dummy) signature. However, when cross-compiling with osxcross, we seem to
|
||
|
# get dummy signatures on both x86_64 and arm64 slices. While the former should not have any impact, it does
|
||
|
# seem to cause issues with further binary signing using real identity. Therefore, we remove all signatures
|
||
|
# and re-sign the binary using dummy signature once the data is appended.
|
||
|
logger.info("Removing signature(s) from EXE")
|
||
|
osxutils.remove_signature_from_binary(build_name)
|
||
|
|
||
|
# Append the data
|
||
|
logger.info("Appending %s to EXE", append_type)
|
||
|
self._append_data_to_exe(build_name, append_file)
|
||
|
|
||
|
# Fix Mach-O headers
|
||
|
logger.info("Fixing EXE headers for code signing")
|
||
|
osxutils.fix_exe_for_code_signing(build_name)
|
||
|
else:
|
||
|
# Fall back to just appending data at the end of the file
|
||
|
logger.info("Appending %s to EXE", append_type)
|
||
|
self._retry_operation(self._append_data_to_exe, build_name, append_file)
|
||
|
|
||
|
# Step 3: post-processing
|
||
|
if is_win:
|
||
|
# Set checksum to appease antiviral software. Also set build timestamp to current time to increase entropy
|
||
|
# (but honor SOURCE_DATE_EPOCH environment variable for reproducible builds).
|
||
|
logger.info("Fixing EXE headers")
|
||
|
build_timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
|
||
|
self._retry_operation(winutils.set_exe_build_timestamp, build_name, build_timestamp)
|
||
|
self._retry_operation(winutils.update_exe_pe_checksum, build_name)
|
||
|
elif is_darwin:
|
||
|
# If the version of macOS SDK used to build bootloader exceeds that of macOS SDK used to built Python
|
||
|
# library (and, by extension, bundled Tcl/Tk libraries), force the version declared by the frozen executable
|
||
|
# to match that of the Python library.
|
||
|
# Having macOS attempt to enable new features (based on SDK version) for frozen application has no benefit
|
||
|
# if the Python library does not support them as well.
|
||
|
# On the other hand, there seem to be UI issues in tkinter due to failed or partial enablement of dark mode
|
||
|
# (i.e., the bootloader executable being built against SDK 10.14 or later, which causes macOS to enable dark
|
||
|
# mode, and Tk libraries being built against an earlier SDK version that does not support the dark mode).
|
||
|
# With python.org Intel macOS installers, this manifests as black Tk windows and UI elements (see issue
|
||
|
# #5827), while in Anaconda python, it may result in white text on bright background.
|
||
|
pylib_version = osxutils.get_macos_sdk_version(self.python_lib)
|
||
|
exe_version = osxutils.get_macos_sdk_version(build_name)
|
||
|
if pylib_version < exe_version:
|
||
|
logger.info(
|
||
|
"Rewriting the executable's macOS SDK version (%d.%d.%d) to match the SDK version of the Python "
|
||
|
"library (%d.%d.%d) in order to avoid inconsistent behavior and potential UI issues in the "
|
||
|
"frozen application.", *exe_version, *pylib_version
|
||
|
)
|
||
|
osxutils.set_macos_sdk_version(build_name, *pylib_version)
|
||
|
|
||
|
# Re-sign the binary (either ad-hoc or using real identity, if provided).
|
||
|
logger.info("Re-signing the EXE")
|
||
|
osxutils.sign_binary(build_name, self.codesign_identity, self.entitlements_file)
|
||
|
|
||
|
# Ensure executable flag is set
|
||
|
self._retry_operation(os.chmod, build_name, 0o755)
|
||
|
# Get mtime for storing into the guts
|
||
|
self.mtm = self._retry_operation(miscutils.mtime, build_name)
|
||
|
if build_name != self.name:
|
||
|
self._retry_operation(os.rename, build_name, self.name)
|
||
|
logger.info("Building EXE from %s completed successfully.", self.tocbasename)
|
||
|
|
||
|
def _copy_windows_resource(self, build_name, resource_spec):
|
||
|
import pefile
|
||
|
|
||
|
# Helper for optionally converting integer strings to values; resource types and IDs/names can be specified as
|
||
|
# either numeric values or custom strings...
|
||
|
def _to_int(value):
|
||
|
try:
|
||
|
return int(value)
|
||
|
except Exception:
|
||
|
return value
|
||
|
|
||
|
logger.debug("Processing resource: %r", resource_spec)
|
||
|
resource = resource_spec.split(",") # filename,[type],[name],[language]
|
||
|
|
||
|
if len(resource) < 1 or len(resource) > 4:
|
||
|
raise ValueError(
|
||
|
f"Invalid Windows resource specifier {resource_spec!r}! "
|
||
|
f"Must be in format 'filename,[type],[name],[language]'!"
|
||
|
)
|
||
|
|
||
|
# Anchor resource file to spec file location, if necessary.
|
||
|
src_filename = self._makeabs(resource[0])
|
||
|
|
||
|
# Ensure file exists.
|
||
|
if not os.path.isfile(src_filename):
|
||
|
raise ValueError("Resource file {src_filename!r} does not exist!")
|
||
|
|
||
|
# Check if src_filename points to a PE file or an arbitrary (data) file.
|
||
|
try:
|
||
|
with pefile.PE(src_filename, fast_load=True):
|
||
|
is_pe_file = True
|
||
|
except Exception:
|
||
|
is_pe_file = False
|
||
|
|
||
|
if is_pe_file:
|
||
|
# If resource file is PE file, copy all resources from it, subject to specified type, name, and language.
|
||
|
logger.debug("Resource file %r is a PE file...", src_filename)
|
||
|
|
||
|
# Resource type, name, and language serve as filters. If not specified, use "*".
|
||
|
resource_type = _to_int(resource[1]) if len(resource) >= 2 else "*"
|
||
|
resource_name = _to_int(resource[2]) if len(resource) >= 3 else "*"
|
||
|
resource_lang = _to_int(resource[3]) if len(resource) >= 4 else "*"
|
||
|
|
||
|
try:
|
||
|
winresource.copy_resources_from_pe_file(
|
||
|
build_name,
|
||
|
src_filename,
|
||
|
[resource_type],
|
||
|
[resource_name],
|
||
|
[resource_lang],
|
||
|
)
|
||
|
except Exception as e:
|
||
|
raise IOError(f"Failed to copy resources from PE file {src_filename!r}") from e
|
||
|
else:
|
||
|
logger.debug("Resource file %r is an arbitrary data file...", src_filename)
|
||
|
|
||
|
# For arbitrary data file, resource type and name need to be provided.
|
||
|
if len(resource) < 3:
|
||
|
raise ValueError(
|
||
|
f"Invalid Windows resource specifier {resource_spec!r}! "
|
||
|
f"For arbitrary data file, the format is 'filename,type,name,[language]'!"
|
||
|
)
|
||
|
|
||
|
resource_type = _to_int(resource[1])
|
||
|
resource_name = _to_int(resource[2])
|
||
|
resource_lang = _to_int(resource[3]) if len(resource) >= 4 else 0 # LANG_NEUTRAL
|
||
|
|
||
|
# Prohibit wildcards for resource type and name.
|
||
|
if resource_type == "*":
|
||
|
raise ValueError(
|
||
|
f"Invalid Windows resource specifier {resource_spec!r}! "
|
||
|
f"For arbitrary data file, resource type cannot be a wildcard (*)!"
|
||
|
)
|
||
|
if resource_name == "*":
|
||
|
raise ValueError(
|
||
|
f"Invalid Windows resource specifier {resource_spec!r}! "
|
||
|
f"For arbitrary data file, resource ma,e cannot be a wildcard (*)!"
|
||
|
)
|
||
|
|
||
|
try:
|
||
|
with open(src_filename, 'rb') as fp:
|
||
|
data = fp.read()
|
||
|
|
||
|
winresource.add_or_update_resource(
|
||
|
build_name,
|
||
|
data,
|
||
|
resource_type,
|
||
|
[resource_name],
|
||
|
[resource_lang],
|
||
|
)
|
||
|
except Exception as e:
|
||
|
raise IOError(f"Failed to embed data file {src_filename!r} as Windows resource") from e
|
||
|
|
||
|
def _append_data_to_exe(self, build_name, append_file):
|
||
|
with open(build_name, 'ab') as outf:
|
||
|
with open(append_file, 'rb') as inf:
|
||
|
shutil.copyfileobj(inf, outf, length=64 * 1024)
|
||
|
|
||
|
@staticmethod
|
||
|
def _retry_operation(func, *args, max_attempts=20):
|
||
|
"""
|
||
|
Attempt to execute the given function `max_attempts` number of times while catching exceptions that are usually
|
||
|
associated with Windows anti-virus programs temporarily locking the access to the executable.
|
||
|
"""
|
||
|
def _is_allowed_exception(e):
|
||
|
"""
|
||
|
Helper to determine whether the given exception is eligible for retry or not.
|
||
|
"""
|
||
|
if isinstance(e, PermissionError):
|
||
|
# Always retry on all instances of PermissionError
|
||
|
return True
|
||
|
elif is_win:
|
||
|
from PyInstaller.compat import pywintypes
|
||
|
|
||
|
# Windows-specific errno and winerror codes.
|
||
|
# https://learn.microsoft.com/en-us/cpp/c-runtime-library/errno-constants
|
||
|
_ALLOWED_ERRNO = {
|
||
|
13, # EACCES (would typically be a PermissionError instead)
|
||
|
22, # EINVAL (reported to be caused by Crowdstrike; see #7840)
|
||
|
}
|
||
|
# https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
|
||
|
_ALLOWED_WINERROR = {
|
||
|
5, # ERROR_ACCESS_DENIED (reported in #7825)
|
||
|
32, # ERROR_SHARING_VIOLATION (exclusive lock via `CreateFileW` flags, or via `_locked`).
|
||
|
110, # ERROR_OPEN_FAILED (reported in #8138)
|
||
|
}
|
||
|
if isinstance(e, OSError):
|
||
|
# For OSError exceptions other than PermissionError, validate errno.
|
||
|
if e.errno in _ALLOWED_ERRNO:
|
||
|
return True
|
||
|
# OSError typically translates `winerror` into `errno` equivalent; but try to match the original
|
||
|
# values as a fall back, just in case. `OSError.winerror` attribute exists only on Windows.
|
||
|
if e.winerror in _ALLOWED_WINERROR:
|
||
|
return True
|
||
|
elif isinstance(e, pywintypes.error):
|
||
|
# pywintypes.error is raised by helper functions that use win32 C API bound via pywin32-ctypes.
|
||
|
if e.winerror in _ALLOWED_WINERROR:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
func_name = func.__name__
|
||
|
for attempt in range(max_attempts):
|
||
|
try:
|
||
|
return func(*args)
|
||
|
except Exception as e:
|
||
|
# Check if exception is eligible for retry; if not, also check its immediate cause (in case the
|
||
|
# exception was thrown from an eligible exception).
|
||
|
if not _is_allowed_exception(e) and not _is_allowed_exception(e.__context__):
|
||
|
raise
|
||
|
|
||
|
# Retry after sleep (unless this was our last attempt)
|
||
|
if attempt < max_attempts - 1:
|
||
|
sleep_duration = 1 / (max_attempts - 1 - attempt)
|
||
|
logger.warning(
|
||
|
f"Execution of {func_name!r} failed on attempt #{attempt + 1} / {max_attempts}: {e!r}. "
|
||
|
f"Retrying in {sleep_duration:.2f} second(s)..."
|
||
|
)
|
||
|
time.sleep(sleep_duration)
|
||
|
else:
|
||
|
logger.warning(
|
||
|
f"Execution of {func_name!r} failed on attempt #{attempt + 1} / {max_attempts}: {e!r}."
|
||
|
)
|
||
|
raise RuntimeError(f"Execution of {func_name!r} failed - no more attempts left!") from e
|
||
|
|
||
|
|
||
|
class COLLECT(Target):
|
||
|
"""
|
||
|
In one-dir mode creates the output folder with all necessary files.
|
||
|
"""
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
"""
|
||
|
args
|
||
|
One or more arguments that are either an instance of `Target` or an iterable representing TOC list.
|
||
|
kwargs
|
||
|
Possible keyword arguments:
|
||
|
|
||
|
name
|
||
|
The name of the directory to be built.
|
||
|
"""
|
||
|
from PyInstaller.config import CONF
|
||
|
|
||
|
super().__init__()
|
||
|
|
||
|
self.strip_binaries = kwargs.get('strip', False)
|
||
|
self.upx_exclude = kwargs.get("upx_exclude", [])
|
||
|
self.console = True
|
||
|
self.target_arch = None
|
||
|
self.codesign_identity = None
|
||
|
self.entitlements_file = None
|
||
|
|
||
|
# UPX needs to be both available and enabled for the taget.
|
||
|
self.upx_binaries = CONF['upx_available'] and kwargs.get('upx', False)
|
||
|
|
||
|
# The `name` should be the output directory name, without the parent path (the directory is created in the
|
||
|
# DISTPATH). Old .spec formats included parent path, so strip it away.
|
||
|
self.name = os.path.join(CONF['distpath'], os.path.basename(kwargs.get('name')))
|
||
|
|
||
|
for arg in args:
|
||
|
if isinstance(arg, EXE):
|
||
|
self.contents_directory = arg.contents_directory
|
||
|
break
|
||
|
else:
|
||
|
raise ValueError("No EXE() instance was passed to COLLECT()")
|
||
|
|
||
|
self.toc = []
|
||
|
for arg in args:
|
||
|
# Valid arguments: EXE 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.console = arg.console
|
||
|
self.target_arch = arg.target_arch
|
||
|
self.codesign_identity = arg.codesign_identity
|
||
|
self.entitlements_file = arg.entitlements_file
|
||
|
# Search for the executable's external manifest, and collect it if available
|
||
|
for dest_name, src_name, typecode in arg.toc:
|
||
|
if dest_name == os.path.basename(arg.name) + ".manifest":
|
||
|
self.toc.append((dest_name, src_name, typecode))
|
||
|
# If PKG is not appended to the executable, we need to collect it.
|
||
|
if not arg.append_pkg:
|
||
|
self.toc.append((os.path.basename(arg.pkgname), arg.pkgname, 'PKG'))
|
||
|
elif miscutils.is_iterable(arg):
|
||
|
# TOC-like iterable
|
||
|
self.toc.extend(arg)
|
||
|
else:
|
||
|
raise TypeError(f"Invalid argument type for COLLECT: {type(arg)!r}")
|
||
|
|
||
|
# Normalize TOC
|
||
|
self.toc = normalize_toc(self.toc)
|
||
|
|
||
|
self.__postinit__()
|
||
|
|
||
|
_GUTS = (
|
||
|
# COLLECT always builds, we just want the TOC to be written out.
|
||
|
('toc', None),
|
||
|
)
|
||
|
|
||
|
def _check_guts(self, data, last_build):
|
||
|
# COLLECT always needs to be executed, in order to clean the output directory.
|
||
|
return True
|
||
|
|
||
|
def assemble(self):
|
||
|
_make_clean_directory(self.name)
|
||
|
logger.info("Building COLLECT %s", self.tocbasename)
|
||
|
for dest_name, src_name, typecode in self.toc:
|
||
|
# Ensure that the source file exists, if necessary. Skip the check for DEPENDENCY entries due to special
|
||
|
# contents of 'dest_name' and/or 'src_name'. Same for the SYMLINK entries, where 'src_name' is relative
|
||
|
# target name for symbolic link.
|
||
|
if typecode not in {'DEPENDENCY', 'SYMLINK'} and not os.path.exists(src_name):
|
||
|
# If file is contained within python egg, it will be added with the egg.
|
||
|
if strict_collect_mode:
|
||
|
raise ValueError(f"Non-existent resource {src_name}, meant to be collected as {dest_name}!")
|
||
|
else:
|
||
|
logger.warning(
|
||
|
"Ignoring non-existent resource %s, meant to be collected as %s", src_name, dest_name
|
||
|
)
|
||
|
continue
|
||
|
# Disallow collection outside of the dist directory.
|
||
|
if os.pardir in os.path.normpath(dest_name).split(os.sep) or os.path.isabs(dest_name):
|
||
|
raise SystemExit(
|
||
|
'Security-Alert: attempting to store file outside of the dist directory: %r. Aborting.' % dest_name
|
||
|
)
|
||
|
# Create parent directory structure, if necessary
|
||
|
if typecode in ("EXECUTABLE", "PKG"):
|
||
|
dest_path = os.path.join(self.name, dest_name)
|
||
|
else:
|
||
|
dest_path = os.path.join(self.name, self.contents_directory or "", dest_name)
|
||
|
dest_dir = os.path.dirname(dest_path)
|
||
|
try:
|
||
|
os.makedirs(dest_dir, exist_ok=True)
|
||
|
except FileExistsError:
|
||
|
raise SystemExit(
|
||
|
f"Pyinstaller needs to create a directory at {dest_dir!r}, "
|
||
|
"but there already exists a file at that path!"
|
||
|
)
|
||
|
if typecode in ('EXTENSION', 'BINARY'):
|
||
|
src_name = process_collected_binary(
|
||
|
src_name,
|
||
|
dest_name,
|
||
|
use_strip=self.strip_binaries,
|
||
|
use_upx=self.upx_binaries,
|
||
|
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
|
||
|
elif typecode != 'DEPENDENCY':
|
||
|
# 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 COLLECT: {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)
|
||
|
logger.info("Building COLLECT %s completed successfully.", self.tocbasename)
|
||
|
|
||
|
|
||
|
class MERGE:
|
||
|
"""
|
||
|
Given Analysis objects for multiple executables, replace occurrences of data and binary files with references to the
|
||
|
first executable in which they occur. The actual data and binary files are then collected only once, thereby
|
||
|
reducing the disk space used by multiple executables. Every executable (even onedir ones!) obtained from a
|
||
|
MERGE-processed Analysis gains onefile semantics, because it needs to extract its referenced dependencies from other
|
||
|
executables into temporary directory before they can run.
|
||
|
"""
|
||
|
def __init__(self, *args):
|
||
|
"""
|
||
|
args
|
||
|
Dependencies as a list of (analysis, identifier, path_to_exe) tuples. `analysis` is an instance of
|
||
|
`Analysis`, `identifier` is the basename of the entry-point script (without .py suffix), and `path_to_exe`
|
||
|
is path to the corresponding executable, relative to the `dist` directory (without .exe suffix in the
|
||
|
filename component). For onefile executables, `path_to_exe` is usually just executable's base name
|
||
|
(e.g., `myexecutable`). For onedir executables, `path_to_exe` usually comprises both the application's
|
||
|
directory name and executable name (e.g., `myapp/myexecutable`).
|
||
|
"""
|
||
|
self._dependencies = {}
|
||
|
self._symlinks = set()
|
||
|
|
||
|
# Process all given (analysis, identifier, path_to_exe) tuples
|
||
|
for analysis, identifier, path_to_exe in args:
|
||
|
# Process analysis.binaries and analysis.datas TOCs. self._process_toc() call returns two TOCs; the first
|
||
|
# contains entries that remain within this analysis, while the second contains entries that reference
|
||
|
# an entry in another executable.
|
||
|
binaries, binaries_refs = self._process_toc(analysis.binaries, path_to_exe)
|
||
|
datas, datas_refs = self._process_toc(analysis.datas, path_to_exe)
|
||
|
# Update `analysis.binaries`, `analysis.datas`, and `analysis.dependencies`.
|
||
|
# The entries that are found in preceding executable(s) are removed from `binaries` and `datas`, and their
|
||
|
# DEPENDENCY entry counterparts are added to `dependencies`. We cannot simply update the entries in
|
||
|
# `binaries` and `datas`, because at least in theory, we need to support both onefile and onedir mode. And
|
||
|
# while in onefile, `a.datas`, `a.binaries`, and `a.dependencies` are passed to `EXE` (and its `PKG`), with
|
||
|
# onedir, `a.datas` and `a.binaries` need to be passed to `COLLECT` (as they were before the MERGE), while
|
||
|
# `a.dependencies` needs to be passed to `EXE`. This split requires DEPENDENCY entries to be in a separate
|
||
|
# TOC.
|
||
|
analysis.binaries = normalize_toc(binaries)
|
||
|
analysis.datas = normalize_toc(datas)
|
||
|
analysis.dependencies += binaries_refs + datas_refs
|
||
|
|
||
|
def _process_toc(self, toc, path_to_exe):
|
||
|
# NOTE: unfortunately, these need to keep two separate lists. See the comment in the calling code on why this
|
||
|
# is so.
|
||
|
toc_keep = []
|
||
|
toc_refs = []
|
||
|
for entry in toc:
|
||
|
dest_name, src_name, typecode = entry
|
||
|
|
||
|
# Special handling and bookkeeping for symbolic links. We need to account both for dest_name and src_name,
|
||
|
# because src_name might be the same in different contexts. For example, when collecting Qt .framework
|
||
|
# bundles on macOS, there are multiple relative symbolic links `Current -> A` (one in each .framework).
|
||
|
if typecode == 'SYMLINK':
|
||
|
key = dest_name, src_name
|
||
|
if key not in self._symlinks:
|
||
|
# First occurrence; keep the entry in "for-keep" TOC, same as we would for binaries and datas.
|
||
|
logger.debug("Keeping symbolic link %r entry in original TOC.", entry)
|
||
|
self._symlinks.add(key)
|
||
|
toc_keep.append(entry)
|
||
|
else:
|
||
|
# Subsequent occurrence; keep the SYMLINK entry intact, but add it to the references TOC instead of
|
||
|
# "for-keep" TOC, so it ends up in `a.dependencies`.
|
||
|
logger.debug("Moving symbolic link %r entry to references TOC.", entry)
|
||
|
toc_refs.append(entry)
|
||
|
del key # Block-local variable
|
||
|
continue
|
||
|
|
||
|
if src_name not in self._dependencies:
|
||
|
logger.debug("Adding dependency %s located in %s", src_name, path_to_exe)
|
||
|
self._dependencies[src_name] = path_to_exe
|
||
|
# Add entry to list of kept TOC entries
|
||
|
toc_keep.append(entry)
|
||
|
else:
|
||
|
# Construct relative dependency path; i.e., the relative path from this executable (or rather, its
|
||
|
# parent directory) to the executable that contains the dependency.
|
||
|
dep_path = os.path.relpath(self._dependencies[src_name], os.path.dirname(path_to_exe))
|
||
|
# Ignore references that point to the origin package. This can happen if the same resource is listed
|
||
|
# multiple times in TOCs (e.g., once as binary and once as data).
|
||
|
if dep_path.endswith(path_to_exe):
|
||
|
logger.debug(
|
||
|
"Ignoring self-reference of %s for %s, located in %s - duplicated TOC entry?", src_name,
|
||
|
path_to_exe, dep_path
|
||
|
)
|
||
|
# The entry is a duplicate, and should be ignored (i.e., do not add it to either of output TOCs).
|
||
|
continue
|
||
|
logger.debug("Referencing %s to be a dependency for %s, located in %s", src_name, path_to_exe, dep_path)
|
||
|
# Create new DEPENDENCY entry; under destination path (first element), we store the original destination
|
||
|
# path, while source path contains the relative reference path.
|
||
|
toc_refs.append((dest_name, dep_path, "DEPENDENCY"))
|
||
|
|
||
|
return toc_keep, toc_refs
|
||
|
|
||
|
|
||
|
UNCOMPRESSED = False
|
||
|
COMPRESSED = True
|
||
|
|
||
|
_MISSING_BOOTLOADER_ERRORMSG = """Fatal error: PyInstaller does not include a pre-compiled bootloader for your
|
||
|
platform. For more details and instructions how to build the bootloader see
|
||
|
<https://pyinstaller.readthedocs.io/en/stable/bootloader-building.html>"""
|