1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006 |
- #-----------------------------------------------------------------------------
- # Copyright (c) 2005-2021, 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 time
- import pprint
- 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 TOC, Target, _check_guts_eq
- from PyInstaller.building.utils import (
- _check_guts_toc, _make_clean_directory, _rmtree, add_suffix_to_extension, checkCache, get_code_object,
- strip_paths_in_code
- )
- from PyInstaller.compat import (exec_command_all, is_cygwin, is_darwin, is_linux, is_win)
- from PyInstaller.depend import bindepend
- from PyInstaller.depend.analysis import get_bootstrap_modules
- from PyInstaller.depend.utils import is_path_to_egg
- from PyInstaller.utils import misc
- 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.
- """
- typ = 'PYZ'
- def __init__(self, *tocs, **kwargs):
- """
- tocs
- One or more TOCs (Tables of Contents), normally an Analysis.pure.
- If this TOC has an attribute `_code_cache`, this is expected to be a dict 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
- Target.__init__(self)
- name = kwargs.get('name', None)
- cipher = kwargs.get('cipher', None)
- self.toc = TOC()
- # If available, use code objects directly from ModuleGraph to speed up PyInstaller.
- self.code_dict = {}
- for t in tocs:
- self.toc.extend(t)
- self.code_dict.update(getattr(t, '_code_cache', {}))
- self.name = name
- if name is None:
- self.name = os.path.splitext(self.tocfilename)[0] + '.pyz'
- # PyInstaller bootstrapping modules.
- self.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.pyc'), 'PYMODULE')
- # Insert the key as the first module in the list. The key module contains just variables and does not depend
- # on other modules.
- self.dependencies.insert(0, key_file)
- # Compile the top-level modules so that they end up in the CArchive and can be imported by the bootstrap script.
- self.dependencies = misc.compile_py_files(self.dependencies, CONF['workpath'])
- self.__postinit__()
- _GUTS = ( # input parameters
- ('name', _check_guts_eq),
- ('toc', _check_guts_toc), # todo: pyc=1
- # no calculated/analysed values
- )
- def _check_guts(self, data, last_build):
- if Target._check_guts(self, data, last_build):
- return True
- return False
- def assemble(self):
- logger.info("Building PYZ (ZlibArchive) %s", self.name)
- # Do not bundle PyInstaller bootstrap modules into PYZ archive.
- toc = self.toc - self.dependencies
- for entry in toc[:]:
- if not entry[0] in self.code_dict and entry[2] == 'PYMODULE':
- # For some reason the code-object that modulegraph created is unavailable. Re-create it.
- try:
- self.code_dict[entry[0]] = get_code_object(entry[0], entry[1])
- except SyntaxError:
- # Exclude the module in case this is code meant for a newer Python version.
- toc.remove(entry)
- # Sort content alphabetically to support reproducible builds.
- toc.sort()
- # Remove leading parts of paths in code objects.
- self.code_dict = {key: strip_paths_in_code(code) for key, code in self.code_dict.items()}
- ZlibArchiveWriter(self.name, 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.
- """
- typ = 'PKG'
- 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=0,
- strip_binaries=False,
- upx_binaries=False,
- upx_exclude=None,
- target_arch=None,
- codesign_identity=None,
- entitlements_file=None
- ):
- """
- toc
- A TOC (Table of Contents)
- 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
- """
- Target.__init__(self)
- self.toc = 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. Single modules are compressed when creating PYZ archive.
- '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 _check_guts(self, data, last_build):
- if Target._check_guts(self, data, last_build):
- return True
- return False
- def assemble(self):
- logger.info("Building PKG (CArchive) %s", os.path.basename(self.name))
- trash = []
- mytoc = []
- srctoc = []
- seen_inms = {}
- seen_fnms = {}
- seen_fnms_typ = {}
- # 'inm' - relative filename inside a CArchive
- # 'fnm' - absolute filename as it is on the file system.
- for inm, fnm, typ in self.toc:
- # Adjust name for extensions, if applicable
- inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ)
- # Ensure filename 'fnm' is not None or empty string. Otherwise, it will fail when 'typ' is OPTION.
- if fnm and not os.path.isfile(fnm) and is_path_to_egg(fnm):
- # File is contained within python egg; it is added with the egg.
- continue
- if typ in ('BINARY', 'EXTENSION', 'DEPENDENCY'):
- if self.exclude_binaries and typ == 'EXTENSION':
- self.dependencies.append((inm, fnm, typ))
- elif not self.exclude_binaries or typ == 'DEPENDENCY':
- if typ == 'BINARY':
- # Avoid importing the same binary extension twice. This might happen if they come from different
- # sources (eg. once from binary dependence, and once from direct import).
- if inm in seen_inms:
- logger.warning('Two binaries added with the same internal name.')
- logger.warning(pprint.pformat((inm, fnm, typ)))
- logger.warning('was placed previously at')
- logger.warning(pprint.pformat((inm, seen_inms[inm], seen_fnms_typ[seen_inms[inm]])))
- logger.warning('Skipping %s.' % fnm)
- continue
- # Warn if the same binary extension was included with multiple internal names
- if fnm in seen_fnms:
- logger.warning('One binary added with two internal names.')
- logger.warning(pprint.pformat((inm, fnm, typ)))
- logger.warning('was placed previously at')
- logger.warning(pprint.pformat((seen_fnms[fnm], fnm, seen_fnms_typ[fnm])))
- seen_inms[inm] = fnm
- seen_fnms[fnm] = inm
- seen_fnms_typ[fnm] = typ
- fnm = checkCache(
- fnm,
- strip=self.strip_binaries,
- upx=self.upx_binaries,
- upx_exclude=self.upx_exclude,
- dist_nm=inm,
- target_arch=self.target_arch,
- codesign_identity=self.codesign_identity,
- entitlements_file=self.entitlements_file
- )
- mytoc.append((inm, fnm, self.cdict.get(typ, 0), self.xformdict.get(typ, 'b')))
- elif typ == 'OPTION':
- mytoc.append((inm, '', 0, 'o'))
- elif typ in ('PYSOURCE', 'PYMODULE'):
- # collect sourcefiles and module in a toc of it's own which will not be sorted.
- srctoc.append((inm, fnm, self.cdict[typ], self.xformdict[typ]))
- else:
- mytoc.append((inm, fnm, self.cdict.get(typ, 0), self.xformdict.get(typ, 'b')))
- # 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 support reproducible builds.
- mytoc.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, srctoc + mytoc, pylib_name=pylib_name)
- for item in trash:
- os.remove(item)
- 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.
- """
- typ = 'EXECUTABLE'
- def __init__(self, *args, **kwargs):
- """
- args
- One or more arguments that are either TOCs Targets.
- 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.
- 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
- Target.__init__(self)
- # 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)
- # 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
- # 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 = TOC()
- for arg in args:
- if isinstance(arg, TOC):
- self.toc.extend(arg)
- elif isinstance(arg, Target):
- self.toc.append((os.path.basename(arg.name), arg.name, arg.typ))
- self.toc.extend(arg.dependencies)
- else:
- self.toc.extend(arg)
- 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 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 not isinstance(self.versrsrc, versioninfo.VSVersionInfo) and not os.path.isabs(self.versrsrc):
- # relative version-info path is relative to spec file
- self.versrsrc = os.path.join(CONF['specpath'], self.versrsrc)
- 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 = TOC([(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),
- ('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 1
- if not self.append_pkg and not os.path.exists(self.pkgname):
- logger.info("Rebuilding because %s missing", os.path.basename(self.pkgname))
- return 1
- if Target._check_guts(self, data, last_build):
- return True
- if (data['versrsrc'] or data['resources']) and not is_win:
- # todo: really ignore :-)
- logger.warning('ignoring version, manifest and resources, platform not capable')
- if data['icon'] and not (is_win or is_darwin):
- logger.warning('ignoring icon, platform not capable')
- mtm = data['mtm']
- if mtm != misc.mtime(self.name):
- logger.info("Rebuilding %s because mtimes don't match", self.tocbasename)
- return True
- if mtm < misc.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
- 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", self.name)
- self._copyfile(exe, self.name)
- os.chmod(self.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(self.name)
- # Embed icon.
- if self.icon != "NONE":
- logger.info("Copying icon to EXE")
- icon.CopyIcons(self.name, self.icon)
- # Embed version info.
- if self.versrsrc:
- logger.info("Copying version information to EXE")
- versioninfo.SetVersion(self.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(
- self.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!",
- self.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(self.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,
- self.name,
- resfile,
- exc_info=1
- )
- # Embed the manifest into the executable.
- if self.embed_manifest:
- logger.info("Emedding manifest in EXE")
- self.manifest.update_resources(self.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(self.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(self.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)
- retcode, stdout, stderr = exec_command_all('objcopy', '--add-section', 'pydata=%s' % append_file, self.name)
- logger.debug("objcopy returned %i", retcode)
- if stdout:
- logger.debug(stdout)
- if stderr:
- logger.debug(stderr)
- if retcode != 0:
- raise SystemError("objcopy Failure: %s" % stderr)
- 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(self.name)
- # Append the data
- logger.info("Appending %s to EXE", append_type)
- with open(self.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(self.name)
- else:
- # Fall back to just appending data at the end of the file
- logger.info("Appending %s to EXE", append_type)
- with open(self.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).
- build_timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
- winutils.fixup_exe_headers(self.name, build_timestamp)
- 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(self.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(self.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(self.name, self.codesign_identity, self.entitlements_file)
- # Ensure executable flag is set
- os.chmod(self.name, 0o755)
- # Get mtime for storing into the guts
- self.mtm = misc.mtime(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, **kws):
- """
- args
- One or more arguments that are either TOCs Targets.
- kws
- Possible keyword arguments:
- name
- The name of the directory to be built.
- """
- from PyInstaller.config import CONF
- Target.__init__(self)
- self.strip_binaries = kws.get('strip', False)
- self.upx_exclude = kws.get("upx_exclude", [])
- self.console = True
- self.target_arch = None
- self.codesign_identity = None
- self.entitlements_file = None
- if CONF['hasUPX']:
- self.upx_binaries = kws.get('upx', False)
- else:
- self.upx_binaries = False
- self.name = kws.get('name')
- # Old .spec format included in 'name' the path where to collect files for the created app. app. New format
- # includes only directory name.
- #
- # The 'name' directory is created in DISTPATH and necessary files are then collected to this directory.
- self.name = os.path.join(CONF['distpath'], os.path.basename(self.name))
- self.toc = TOC()
- for arg in args:
- if isinstance(arg, TOC):
- self.toc.extend(arg)
- elif isinstance(arg, Target):
- self.toc.append((os.path.basename(arg.name), arg.name, arg.typ))
- if isinstance(arg, EXE):
- self.console = arg.console
- self.target_arch = arg.target_arch
- self.codesign_identity = arg.codesign_identity
- self.entitlements_file = arg.entitlements_file
- for tocnm, fnm, typ in arg.toc:
- if tocnm == os.path.basename(arg.name) + ".manifest":
- self.toc.append((tocnm, fnm, typ))
- if not arg.append_pkg:
- self.toc.append((os.path.basename(arg.pkgname), arg.pkgname, 'PKG'))
- self.toc.extend(arg.dependencies)
- else:
- self.toc.extend(arg)
- self.__postinit__()
- _GUTS = (
- # COLLECT always builds, just want the toc to be written out
- ('toc', None),
- )
- def _check_guts(self, data, last_build):
- # COLLECT always needs to be executed, since it will clean the output directory anyway to make sure there is no
- # existing cruft accumulating
- return 1
- def assemble(self):
- _make_clean_directory(self.name)
- logger.info("Building COLLECT %s", self.tocbasename)
- for inm, fnm, typ in self.toc:
- # Adjust name for extensions, if applicable
- inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ)
- if not os.path.exists(fnm) or not os.path.isfile(fnm) and is_path_to_egg(fnm):
- # File is contained within python egg; it is added with the egg.
- continue
- if os.pardir in os.path.normpath(inm).split(os.sep) or os.path.isabs(inm):
- raise SystemExit('Security-Alert: try to store file outside of dist-directory. Aborting. %r' % inm)
- tofnm = os.path.join(self.name, inm)
- todir = os.path.dirname(tofnm)
- if not os.path.exists(todir):
- os.makedirs(todir)
- elif not os.path.isdir(todir):
- raise SystemExit(
- "Pyinstaller needs to make a directory at %r, but there already exists a file at that path!" % todir
- )
- if typ in ('EXTENSION', 'BINARY'):
- fnm = checkCache(
- fnm,
- strip=self.strip_binaries,
- upx=self.upx_binaries,
- upx_exclude=self.upx_exclude,
- dist_nm=inm,
- target_arch=self.target_arch,
- codesign_identity=self.codesign_identity,
- entitlements_file=self.entitlements_file
- )
- if typ != 'DEPENDENCY':
- if os.path.isdir(fnm):
- # Because shutil.copy2() is the default copy function for shutil.copytree, this will also copy file
- # metadata.
- shutil.copytree(fnm, tofnm)
- else:
- shutil.copy(fnm, tofnm)
- try:
- shutil.copystat(fnm, tofnm)
- except OSError:
- logger.warning("failed to copy flags of %s", fnm)
- if typ in ('EXTENSION', 'BINARY'):
- os.chmod(tofnm, 0o755)
- logger.info("Building COLLECT %s completed successfully.", self.tocbasename)
- class MERGE(object):
- """
- Merge repeated dependencies from other executables into the first executable. Data and binary files are then
- present only once and some disk space is thus reduced.
- """
- def __init__(self, *args):
- """
- Repeated dependencies are then present only once in the first executable in the 'args' list. Other
- executables depend on the first one. Other executables have to extract necessary files from the first
- executable.
- args dependencies in a list of (Analysis, id, filename) tuples.
- Replace id with the correct filename.
- """
- # The first Analysis object with all dependencies.
- # Any item from the first executable cannot be removed.
- self._main = None
- self._dependencies = {}
- self._id_to_path = {}
- for _, i, p in args:
- self._id_to_path[os.path.normcase(i)] = p
- # Get the longest common path
- common_prefix = os.path.commonprefix([os.path.normcase(os.path.abspath(a.scripts[-1][1])) for a, _, _ in args])
- self._common_prefix = os.path.dirname(common_prefix)
- if self._common_prefix[-1] != os.sep:
- self._common_prefix += os.sep
- logger.info("Common prefix: %s", self._common_prefix)
- self._merge_dependencies(args)
- def _merge_dependencies(self, args):
- """
- Filter shared dependencies to be only in first executable.
- """
- for analysis, _, _ in args:
- path = os.path.normcase(os.path.abspath(analysis.scripts[-1][1]))
- path = path.replace(self._common_prefix, "", 1)
- path = os.path.splitext(path)[0]
- if os.path.normcase(path) in self._id_to_path:
- path = self._id_to_path[os.path.normcase(path)]
- self._set_dependencies(analysis, path)
- def _set_dependencies(self, analysis, path):
- """
- Synchronize the Analysis result with the needed dependencies.
- """
- for toc in (analysis.binaries, analysis.datas):
- for i, tpl in enumerate(toc):
- if not tpl[1] in self._dependencies:
- logger.debug("Adding dependency %s located in %s", tpl[1], path)
- self._dependencies[tpl[1]] = path
- else:
- dep_path = self._get_relative_path(path, self._dependencies[tpl[1]])
- # 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):
- logger.debug(
- "Ignoring self-reference of %s for %s, located in %s - duplicated TOC entry?", tpl[1], path,
- dep_path
- )
- # Clear the entry as it is a duplicate.
- toc[i] = (None, None, None)
- continue
- logger.debug("Referencing %s to be a dependency for %s, located in %s", tpl[1], path, dep_path)
- # Determine the path relative to dep_path (i.e, within the target directory) from the 'name'
- # component of the TOC tuple. If entry is EXTENSION, then the relative path needs to be
- # reconstructed from the name components.
- if tpl[2] == 'EXTENSION':
- # Split on os.path.sep first, to handle additional path prefix (e.g., lib-dynload)
- ext_components = tpl[0].split(os.path.sep)
- ext_components = ext_components[:-1] + ext_components[-1].split('.')[:-1]
- if ext_components:
- rel_path = os.path.join(*ext_components)
- else:
- rel_path = ''
- else:
- rel_path = os.path.dirname(tpl[0])
- # Take filename from 'path' (second component of TOC tuple); this way, we don't need to worry about
- # suffix of extensions.
- filename = os.path.basename(tpl[1])
- # Construct the full file path relative to dep_path...
- filename = os.path.join(rel_path, filename)
- # ...and use it in new DEPENDENCY entry
- analysis.dependencies.append((":".join((dep_path, filename)), tpl[1], "DEPENDENCY"))
- toc[i] = (None, None, None)
- # Clean the list
- toc[:] = [tpl for tpl in toc if tpl != (None, None, None)]
- # TODO: use pathlib.Path.relative_to() instead.
- def _get_relative_path(self, startpath, topath):
- start = startpath.split(os.sep)[:-1]
- start = ['..'] * len(start)
- if start:
- start.append(topath)
- return os.sep.join(start)
- else:
- return topath
- UNCOMPRESSED = 0
- COMPRESSED = 1
- _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>"""
|