#-----------------------------------------------------------------------------
|
# 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, checkCache, 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
|
from PyInstaller.depend.utils import is_path_to_egg
|
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 ZlibArchive that contains all pure Python modules.
|
"""
|
def __init__(self, *tocs, **kwargs):
|
"""
|
tocs
|
One or more TOC (Table of Contents) lists, usually an `Analysis.pure` and an `Analysis.zipped_data`.
|
|
If the passed TOC has an attribute `_code_cache`, it is expected to be a dictionary of module code objects
|
from ModuleGraph.
|
|
kwargs
|
Possible keyword arguments:
|
|
name
|
A filename for the .pyz. Normally not needed, as the generated name will do fine.
|
cipher
|
The block cipher that will be used to encrypt Python bytecode.
|
"""
|
|
from PyInstaller.config import CONF
|
|
super().__init__()
|
|
name = kwargs.get('name', None)
|
cipher = kwargs.get('cipher', 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()
|
|
# Bundle the crypto key.
|
self.cipher = cipher
|
if cipher:
|
key_file = ('pyimod00_crypto_key', os.path.join(CONF['workpath'], 'pyimod00_crypto_key.py'), 'PYMODULE')
|
# Insert the key as the first module in the list. The key module contains just variables and does not depend
|
# on other modules.
|
bootstrap_dependencies.insert(0, key_file)
|
|
# Compile the python modules that are part of bootstrap dependencies, so that they can be collected into the
|
# CArchive 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.
|
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 PYMODULE entries (python code objects) and DATA entries (data collected from zipped eggs).
|
assert typecode in ('PYMODULE', 'DATA'), f"Invalid entry passed to PYZ: {entry}!"
|
# Module required during bootstrap; skip to avoid collecting a duplicate.
|
if typecode == 'PYMODULE' and 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 typecode == 'PYMODULE' and 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, cipher=self.cipher)
|
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'
|
}
|
|
def __init__(
|
self,
|
toc,
|
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.
|
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.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
|
}
|
|
self.__postinit__()
|
|
_GUTS = ( # input parameters
|
('name', _check_guts_eq),
|
('cdict', _check_guts_eq),
|
('toc', _check_guts_toc), # list unchanged and no newer files
|
('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'.
|
if typecode not in ('OPTION', 'DEPENDENCY') and not os.path.exists(src_name):
|
# If file is contained within python egg, it will be added with the egg.
|
if not is_path_to_egg(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 `checkCache` helper.
|
src_name = checkCache(
|
src_name,
|
strip=self.strip_binaries,
|
upx=self.upx_binaries,
|
upx_exclude=self.upx_exclude,
|
dist_nm=dest_name,
|
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 == '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
|
# TODO: are DATA and ZIPFILE valid here?
|
archive_toc.append((dest_name, src_name, self.cdict.get(typecode, False), self.xformdict[typecode]))
|
|
# Bootloader has to know the name of Python library. Pass python libname to CArchive.
|
pylib_name = os.path.basename(bindepend.get_python_library_path())
|
|
# 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=pylib_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).
|
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.
|
embed_manifest
|
Windows only. Setting to True (the default) embeds the manifest into the executable. Setting to False
|
generates an external .exe.manifest file. Applicable only in onedir mode (exclude_binaries=True); in
|
onefile mode (exclude_binaries=False), the manifest is always embedded in the executable, regardless
|
of this option.
|
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).
|
"""
|
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.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.embed_manifest = kwargs.get('embed_manifest', True)
|
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)
|
# 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)
|
|
if CONF['hasUPX']:
|
self.upx = kwargs.get('upx', False)
|
else:
|
self.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
|
|
# 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 the icon path is relative, make it relative to the .spec file.
|
def makeabs(path):
|
if os.path.isabs(path):
|
return path
|
else:
|
return os.path.join(CONF['specpath'], path)
|
|
if self.icon and self.icon != "NONE":
|
if isinstance(self.icon, list):
|
self.icon = [makeabs(ic) for ic in self.icon]
|
else:
|
self.icon = [makeabs(self.icon)]
|
|
if is_win:
|
if not self.exclude_binaries:
|
# onefile mode forces embed_manifest=True
|
if not self.embed_manifest:
|
logger.warning("Ignoring embed_manifest=False setting in onefile mode!")
|
self.embed_manifest = True
|
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)
|
filename = os.path.join(CONF['workpath'], CONF['specnm'] + ".exe.manifest")
|
self.manifest = winmanifest.create_manifest(
|
filename, self.manifest, self.console, self.uac_admin, self.uac_uiaccess
|
)
|
|
manifest_filename = os.path.basename(self.name) + ".manifest"
|
|
# If external manifest file is requested (supported only in onedir mode), add the file to the TOC in order
|
# for it to be collected as an external manifest file. Otherwise, the assembly pipeline will embed the
|
# manifest into the executable later on.
|
if not self.embed_manifest:
|
self.toc.append((manifest_filename, filename, 'BINARY'))
|
|
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
|
if not os.path.isabs(self.versrsrc):
|
self.versrsrc = os.path.join(CONF['specpath'], 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}")
|
|
# Normalize TOC
|
self.toc = normalize_toc(self.toc)
|
|
self.pkg = PKG(
|
self.toc,
|
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),
|
('embed_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 ius 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
|
# no calculated/analysed values
|
('exefiles', _check_guts_toc),
|
)
|
|
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
|
|
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):
|
from PyInstaller.config import CONF
|
|
# On Windows, we must never create a file with a .exe suffix that we then have to (re)write to (see #6467).
|
# Any intermediate/temporary file must have an alternative suffix.
|
build_name = self.name + '.notanexecutable' if is_win or is_cygwin else 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))
|
exe = self.exefiles[0][1] # pathname of bootloader
|
if not os.path.exists(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._copyfile(exe, build_name)
|
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).
|
winresource.RemoveAllResources(build_name)
|
# Embed icon.
|
if self.icon != "NONE":
|
logger.info("Copying icon to EXE")
|
icon.CopyIcons(build_name, self.icon)
|
# Embed version info.
|
if self.versrsrc:
|
logger.info("Copying version information to EXE")
|
versioninfo.write_version_info_to_executable(build_name, self.versrsrc)
|
# Embed other resources.
|
logger.info("Copying %d resources to EXE", len(self.resources))
|
for res in self.resources:
|
res = res.split(",")
|
for i in range(1, len(res)):
|
try:
|
res[i] = int(res[i])
|
except ValueError:
|
pass
|
resfile = res[0]
|
if not os.path.isabs(resfile):
|
resfile = os.path.join(CONF['specpath'], resfile)
|
restype = resname = reslang = None
|
if len(res) > 1:
|
restype = res[1]
|
if len(res) > 2:
|
resname = res[2]
|
if len(res) > 3:
|
reslang = res[3]
|
try:
|
winresource.UpdateResourcesFromResFile(
|
build_name, resfile, [restype or "*"], [resname or "*"], [reslang or "*"]
|
)
|
except winresource.pywintypes.error as exc:
|
if exc.args[0] != winresource.ERROR_BAD_EXE_FORMAT:
|
logger.error(
|
"Error while updating resources in %s from resource file %s!",
|
build_name,
|
resfile,
|
exc_info=1
|
)
|
continue
|
|
# Handle the case where the file contains no resources, and is intended as a single resource to be
|
# added to the exe.
|
if not restype or not resname:
|
logger.error("Resource type and/or name not specified!")
|
continue
|
if "*" in (restype, resname):
|
logger.error(
|
"No wildcards allowed for resource type and name when the source file does not contain "
|
"any resources!"
|
)
|
continue
|
try:
|
winresource.UpdateResourcesFromDataFile(build_name, resfile, restype, [resname], [reslang or 0])
|
except winresource.pywintypes.error:
|
logger.error(
|
"Error while updating resource %s %s in %s from data file %s!",
|
restype,
|
resname,
|
build_name,
|
resfile,
|
exc_info=1
|
)
|
# Embed the manifest into the executable.
|
if self.embed_manifest:
|
logger.info("Embedding manifest in EXE")
|
self.manifest.update_resources(build_name, [1])
|
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)
|
self._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, universal_newlines=True)
|
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)
|
with open(build_name, 'ab') as outf:
|
with open(append_file, 'rb') as inf:
|
shutil.copyfileobj(inf, outf, length=64 * 1024)
|
|
# 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)
|
with open(build_name, 'ab') as outf:
|
with open(append_file, 'rb') as inf:
|
shutil.copyfileobj(inf, outf, length=64 * 1024)
|
|
# 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()))
|
winutils.set_exe_build_timestamp(build_name, build_timestamp)
|
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(bindepend.get_python_library_path())
|
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
|
os.chmod(build_name, 0o755)
|
# Get mtime for storing into the guts
|
self.mtm = miscutils.mtime(build_name)
|
if build_name != self.name:
|
os.rename(build_name, self.name)
|
logger.info("Building EXE from %s completed successfully.", self.tocbasename)
|
|
def _copyfile(self, infile, outfile):
|
with open(infile, 'rb') as infh:
|
with open(outfile, 'wb') as outfh:
|
shutil.copyfileobj(infh, outfh, length=64 * 1024)
|
|
|
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
|
|
if CONF['hasUPX']:
|
self.upx_binaries = kwargs.get('upx', False)
|
else:
|
self.upx_binaries = 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')))
|
|
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'.
|
if typecode != 'DEPENDENCY' and not os.path.exists(src_name):
|
# If file is contained within python egg, it will be added with the egg.
|
if not is_path_to_egg(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
|
# 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
|
dest_path = os.path.join(self.name, dest_name) # Absolute destination path
|
dest_dir = os.path.dirname(dest_path)
|
if not os.path.exists(dest_dir):
|
os.makedirs(dest_dir)
|
elif not os.path.isdir(dest_dir):
|
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 = checkCache(
|
src_name,
|
strip=self.strip_binaries,
|
upx=self.upx_binaries,
|
upx_exclude=self.upx_exclude,
|
dist_nm=dest_name,
|
target_arch=self.target_arch,
|
codesign_identity=self.codesign_identity,
|
entitlements_file=self.entitlements_file,
|
strict_arch_validation=(typecode == 'EXTENSION'),
|
)
|
if 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})"
|
)
|
shutil.copy2(src_name, dest_path) # Use copy2 to (attempt to) preserve metadata
|
if typecode in ('EXTENSION', 'BINARY'):
|
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 = {}
|
|
# 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
|
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>"""
|