| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337 |
- #-----------------------------------------------------------------------------
- # 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 sys
- import time
- import pathlib
- 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, 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, is_nogil, is_aix
- 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,
- # Never optimize bootstrap dependencies!
- optimize=0,
- 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 in {'PYMODULE', 'PYMODULE-1', 'PYMODULE-2'}, 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: PEP-420 namespace packages (marked by src_path being set to '-') do not have code objects.
- # NOTE: `self.toc` is already sorted by names.
- archive_toc = []
- for entry in self.toc:
- name, src_path, typecode = entry
- if src_path not in {'-', None} and name not in self.code_dict:
- # The code object is not available from the ModuleGraph's cache; re-create it.
- optim_level = {'PYMODULE': 0, 'PYMODULE-1': 1, 'PYMODULE-2': 2}[typecode]
- try:
- self.code_dict[name] = get_code_object(name, src_path, optimize=optim_level)
- except SyntaxError:
- # The module was likely written for different Python version; exclude it
- continue
- archive_toc.append(entry)
- # 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 entries are already byte-compiled, so we do not need to encode optimization level in the low-level
- # typecodes. PYSOURCE entries are byte-compiled by the underlying writer, so we need to pass the optimization
- # level via low-level typecodes.
- 'PYMODULE': 'm',
- 'PYMODULE-1': 'm',
- 'PYMODULE-2': 'm',
- 'PYSOURCE': 's',
- 'PYSOURCE-1': 's1',
- 'PYSOURCE-2': 's2',
- '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))
- pkg_file = pathlib.Path(self.name).resolve() # Used to detect attempts at PKG feeding itself
- 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'}:
- if 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
- # Detect attempt at collecting PKG into itself, as it results in and endless feeding loop and exhaustion
- # of all available storage space.
- if pathlib.Path(src_name).resolve() == pkg_file:
- raise ValueError(f"Trying to collect PKG file {src_name} into itself!")
- 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', 'PYSOURCE-1', 'PYSOURCE-2', 'PYMODULE', 'PYMODULE-1', 'PYMODULE-2'}:
- # 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]))
- elif typecode == 'PYZ':
- # Override PYZ name in the PKG archive into PYZ.pyz, regardless of what the original name was. The
- # bootloader looks for PYZ via the typecode and implicitly expects a single entry, so the name does
- # not matter. However, having a fixed name matters if we want reproducibility in scenarios where
- # multiple builds are performed within the same process (for example, on our CI).
- archive_toc.append(('PYZ.pyz', src_name, self.cdict.get(typecode, False), self.xformdict[typecode]))
- else:
- # 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 macOS 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 macOS 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'ERROR: 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 is_nogil:
- # Signal to bootloader that python was built with Py_GIL_DISABLED, in order to select correct `PyConfig`
- # structure layout at run-time.
- self.toc.append(("pyi-python-flag Py_GIL_DISABLED", "", "OPTION"))
- 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()
- # On AIX, the python shared library might in fact be an ar archive with shared object inside it, and needs to
- # be `dlopen`'ed with full name (for example, `libpython3.9.a(libpython3.9.so)`. So if the library's suffix is
- # .a, adjust the name accordingly, assuming fixed format for the shared object name. NOTE: the information about
- # shared object name is in fact available from `ldd` but not propagated from our binary dependency analysis. If
- # we ever need to determine the shared object's name dynamically, we could write a simple ar parser, based on
- # information from `https://www.ibm.com/docs/en/aix/7.3?topic=formats-ar-file-format-big`.
- if is_aix:
- _, ext = os.path.splitext(self.python_lib)
- if ext == '.a':
- _py_major, _py_minor = sys.version_info[:2]
- self.python_lib += f"(libpython{_py_major}.{_py_minor}.so)"
- # 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 macOS.
- 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)
- # Fix Mach-O image UUID(s) in executable to ensure uniqueness across different builds.
- # NOTE: even if PKG is side-loaded, use the hash of its contents to generate the new UUID.
- # NOTE: this step is performed *before* PKG is appended and sizes are fixed in the executable's headers;
- # this ensures that we are operating only on original header size instead of enlarged one (which could
- # be significantly larger in large onefile builds).
- logger.info("Modifying Mach-O image UUID(s) in EXE")
- osxutils.update_exe_identifier(build_name, self.pkg.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(f"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)
- # Alphabetically sort the TOC to ensure that order of processing is predictable and reproducible.
- self.toc.sort()
- 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(
- 'ERROR: 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"ERROR: 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':
- # On Windows, ensure that symlink target path (stored in src_name) is using Windows-style back slash
- # separators.
- if is_win and os.path.sep == '/':
- src_name = src_name.replace(os.path.sep, '\\')
- 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
- # In fact, we need to accout for both dest_name and src_name with regular entries as well; previous
- # approach that considered only src_name ended tripped up when same file was collected in different
- # locations (i.e., same src_name but different dest_names).
- key = dest_name, src_name
- if key not in self._dependencies:
- logger.debug("Adding dependency %r located in %s", key, path_to_exe)
- self._dependencies[key] = 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[key], 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 %r for %s, located in %s - duplicated TOC entry?", key, 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 %r to be a dependency for %s, located in %s", key, 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>"""
|