def get_bootstrap_modules(): """ Get TOC with the bootstrapping modules and their dependencies. :return: TOC with modules """ # Import 'struct' modules to get real paths to module file names. mod_struct = __import__('struct') # Basic modules necessary for the bootstrap process. loader_mods = TOC() loaderpath = os.path.join(HOMEPATH, 'PyInstaller', 'loader') # On some platforms (Windows, Debian/Ubuntu) '_struct' and zlib modules are # built-in modules (linked statically) and thus does not have attribute __file__. # 'struct' module is required for reading Python bytecode from executable. # 'zlib' is required to decompress this bytecode. for mod_name in ['_struct', 'zlib']: mod = __import__(mod_name) # C extension. if hasattr(mod, '__file__'): loader_mods.append((mod_name, os.path.abspath(mod.__file__), 'EXTENSION')) # NOTE:These modules should be kept simple without any complicated dependencies. loader_mods +=[ ('struct', os.path.abspath(mod_struct.__file__), 'PYMODULE'), ('pyimod01_os_path', os.path.join(loaderpath, 'pyimod01_os_path.pyc'), 'PYMODULE'), ('pyimod02_archive', os.path.join(loaderpath, 'pyimod02_archive.pyc'), 'PYMODULE'), ('pyimod03_importers', os.path.join(loaderpath, 'pyimod03_importers.pyc'), 'PYMODULE'), ('pyiboot01_bootstrap', os.path.join(loaderpath, 'pyiboot01_bootstrap.py'), 'PYSOURCE'), ] return loader_mods
def add_suffix_to_extensions(toc): """ Returns a new TOC with proper library suffix for EXTENSION items. """ # TODO: Fix this recursive import from PyInstaller.building.datastruct import TOC new_toc = TOC() for inm, fnm, typ in toc: if typ == 'EXTENSION': # Change the dotted name into a relative path. This places C # extensions in the Python-standard location. inm = inm.replace('.', os.sep) # In some rare cases extension might already contain a suffix. # Skip it in this case. if os.path.splitext(inm)[1] not in EXTENSION_SUFFIXES: # Determine the base name of the file. base_name = os.path.basename(inm) assert '.' not in base_name # Use this file's existing extension. For extensions such as # ``libzmq.cp36-win_amd64.pyd``, we can't use # ``os.path.splitext``, which would give only the ```.pyd`` part # of the extension. inm = inm + os.path.basename(fnm)[len(base_name):] elif typ == 'DEPENDENCY': # Use the suffix from the filename. # TODO Verify what extensions are by DEPENDENCIES. binext = os.path.splitext(fnm)[1] if not os.path.splitext(inm)[1] == binext: inm = inm + binext new_toc.append((inm, fnm, typ)) return new_toc
def test_append_other_case_pymodule(): # python modules should not use C-I comparisons. Both 'encodings' and # 'EnCodIngs' should be added. toc = TOC(ELEMS1) elem = ('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'PYMODULE') toc.append(elem) expected = list(ELEMS1) expected.append(elem) assert toc == expected
def test_append_other_case_mixed(): # If a binary file is added with the same filename as an existing pymodule, # it should not be added. toc = TOC(ELEMS1) elem = ('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'BINARY') toc.append(elem) expected = list(ELEMS1) assert toc == expected
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 keywork arguments: name The name of the directory to be built. """ from ..config import CONF Target.__init__(self) self.strip_binaries = kws.get('strip', False) 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): 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): if _check_path_overlap(self.name) and os.path.isdir(self.name): _rmtree(self.name) logger.info("Building COLLECT %s", self.tocbasename) os.makedirs(self.name) toc = add_suffix_to_extensions(self.toc) for inm, fnm, typ in toc: 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) 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) if typ in ('EXTENSION', 'BINARY'): fnm = checkCache(fnm, strip=self.strip_binaries, upx=(self.upx_binaries and (is_win or is_cygwin)), dist_nm=inm) if typ != 'DEPENDENCY': shutil.copy(fnm, tofnm) try: shutil.copystat(fnm, tofnm) except OSError: logger.warn("failed to copy flags of %s", fnm) if typ in ('EXTENSION', 'BINARY'): os.chmod(tofnm, 0o755)
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 keywork arguments: console On Windows or OSX governs whether to use the console executable or the windowed executable. Always True on Linux/Unix (always console executable - it does not matter there). debug Setting to True gives you progress mesages 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 or OSX only. icon='myicon.ico' to use an icon file or icon='notepad.exe,0' to grab an icon resource. 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 wouldn't attempt to write one from scratch). uac_admin Windows only. Setting to True creates a Manifest with will request elevation upon application restart uac_uiaccess Windows only. Setting to True allows an elevated application to work with Remote Desktop """ from ..config import CONF Target.__init__(self) # Available options for EXE in .spec files. self.exclude_binaries = kwargs.get('exclude_binaries', False) self.console = kwargs.get('console', True) 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) # 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) 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) self.pkgname = 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 is_win: 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" self.toc.append((manifest_filename, filename, 'BINARY')) if not self.exclude_binaries: # Onefile mode: manifest file is explicitly loaded. # Store name of manifest file as bootloader option. Allows # the exe to be renamed. self.toc.append(("pyi-windows-manifest-filename " + manifest_filename, "", "OPTION")) self.pkg = PKG(self.toc, cdict=kwargs.get('cdict', None), exclude_binaries=self.exclude_binaries, strip_binaries=self.strip, upx_binaries=self.upx, ) 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), ('append_pkg', _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.warn('ignoring version, manifest and resources, platform not capable') if data['icon'] and not (is_win or is_darwin): logger.warn('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 bootolader makes sense only on Windows and # Mac OS X. 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): logger.info("Building EXE from %s", self.tocbasename) trash = [] if not os.path.exists(os.path.dirname(self.name)): os.makedirs(os.path.dirname(self.name)) outf = open(self.name, 'wb') exe = self.exefiles[0][1] # pathname of bootloader if not os.path.exists(exe): raise SystemExit(_MISSING_BOOTLOADER_ERRORMSG) if is_win and (self.icon or self.versrsrc or self.resources): tmpnm = tempfile.mktemp() shutil.copy2(exe, tmpnm) os.chmod(tmpnm, 0o755) if self.icon: icon.CopyIcons(tmpnm, self.icon) if self.versrsrc: versioninfo.SetVersion(tmpnm, self.versrsrc) 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] 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(tmpnm, resfile, [restype or "*"], [resname or "*"], [reslang or "*"]) except winresource.pywintypes.error as exc: if exc.args[0] != winresource.ERROR_BAD_EXE_FORMAT: logger.exception(exc) continue 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 source file does not " "contain resources") continue try: winresource.UpdateResourcesFromDataFile(tmpnm, resfile, restype, [resname], [reslang or 0]) except winresource.pywintypes.error as exc: logger.exception(exc) trash.append(tmpnm) exe = tmpnm exe = checkCache(exe, strip=self.strip, upx=self.upx) self.copy(exe, outf) if self.append_pkg: logger.info("Appending archive to EXE %s", self.name) self.copy(self.pkg.name, outf) else: logger.info("Copying archive to %s", self.pkgname) shutil.copy2(self.pkg.name, self.pkgname) outf.close() if is_darwin: # Fix Mach-O header for codesigning on OS X. logger.info("Fixing EXE for code signing %s", self.name) import PyInstaller.utils.osx as osxutils osxutils.fix_exe_for_code_signing(self.name) pass os.chmod(self.name, 0o755) # get mtime for storing into the guts self.mtm = misc.mtime(self.name) for item in trash: os.remove(item) def copy(self, fnm, outf): inf = open(fnm, 'rb') while 1: data = inf.read(64 * 1024) if not data: break outf.write(data)
def test_append_keep_filename(): # name in TOC should be the same as the one added toc = TOC() entry = ('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'BINARY') toc.append(entry) assert toc[0][0] == entry[0]
def test_append_existing(): toc = TOC(ELEMS1) toc.append(ELEMS1[-1]) expected = list(ELEMS1) assert toc == expected
def test_append(): toc = TOC(ELEMS1) toc.append(('li-la-lu', '/home/myself/li-la-su', 'SOMETHING')) expected = list(ELEMS1) expected.append(('li-la-lu', '/home/myself/li-la-su', 'SOMETHING')) assert toc == expected
def test_append_other_case_binary(): # binary files should use C-I comparisons. 'LiBrEADlInE.so.6' should not be added. toc = TOC(ELEMS1) toc.append(('LiBrEADlInE.so.6', '/lib64/libreadline.so.6', 'BINARY')) expected = list(ELEMS1) assert toc == expected
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 keywork 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 e.g. a supervisor process signals both the bootloader and child (e.g. via a process group) to avoid signalling the child twice. console On Windows or OSX governs whether to use the console executable or the windowed executable. Always True on Linux/Unix (always console executable - it does not matter there). debug Setting to True gives you progress mesages 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 or OSX only. icon='myicon.ico' to use an icon file or icon='notepad.exe,0' to grab an icon resource. 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 wouldn't attempt to write one from scratch). uac_admin Windows only. Setting to True creates a Manifest with will request elevation upon application restart uac_uiaccess Windows only. Setting to True allows an elevated application to work with Remote Desktop """ from ..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.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.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) 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) self.pkgname = 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 is_win: 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" self.toc.append((manifest_filename, filename, 'BINARY')) if not self.exclude_binaries: # Onefile mode: manifest file is explicitly loaded. # Store name of manifest file as bootloader option. Allows # the exe to be renamed. self.toc.append( ("pyi-windows-manifest-filename " + manifest_filename, "", "OPTION")) self.pkg = PKG( self.toc, cdict=kwargs.get('cdict', None), exclude_binaries=self.exclude_binaries, strip_binaries=self.strip, upx_binaries=self.upx, ) 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), ('append_pkg', _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 bootolader makes sense only on Windows and # Mac OS X. 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): logger.info("Building EXE from %s", self.tocbasename) trash = [] if os.path.exists(self.name): 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) if is_win and (self.icon or self.versrsrc or self.resources): tmpnm = tempfile.mktemp() self._copyfile(exe, tmpnm) os.chmod(tmpnm, 0o755) if self.icon: icon.CopyIcons(tmpnm, self.icon) if self.versrsrc: versioninfo.SetVersion(tmpnm, self.versrsrc) 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] 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( tmpnm, 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", tmpnm, 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 source file does not " "contain resources") continue try: winresource.UpdateResourcesFromDataFile( tmpnm, 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, tmpnm, resfile, exc_info=1) trash.append(tmpnm) exe = tmpnm # NOTE: Do not look up for bootloader file in the cache because it might # get corrupted by UPX when UPX is available. See #1863 for details. if not self.append_pkg: logger.info("Copying bootloader exe to %s", self.name) self._copyfile(exe, self.name) logger.info("Copying archive to %s", self.pkgname) self._copyfile(self.pkg.name, self.pkgname) elif is_linux: self._copyfile(exe, self.name) logger.info("Appending archive to ELF section in EXE %s", self.name) retcode, stdout, stderr = exec_command_all( 'objcopy', '--add-section', 'pydata=%s' % self.pkg.name, 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) else: # Fall back to just append on end of file logger.info("Appending archive to EXE %s", self.name) with open(self.name, 'wb') as outf: # write the bootloader data with open(exe, 'rb') as infh: shutil.copyfileobj(infh, outf, length=64 * 1024) # write the archive data with open(self.pkg.name, 'rb') as infh: shutil.copyfileobj(infh, outf, length=64 * 1024) if is_darwin: # Fix Mach-O header for codesigning on OS X. logger.info("Fixing EXE for code signing %s", self.name) import PyInstaller.utils.osx as osxutils osxutils.fix_exe_for_code_signing(self.name) os.chmod(self.name, 0o755) # get mtime for storing into the guts self.mtm = misc.mtime(self.name) for item in trash: os.remove(item) 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 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): 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 not self.append_pkg: # 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") elif is_linux: # Linux: append PKG into ELF section using objcopy logger.info("Appending PKG archive to ELF section in EXE") retcode, stdout, stderr = exec_command_all( 'objcopy', '--add-section', 'pydata=%s' % self.pkg.name, 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 PKG, and fix-up headers so that PKG appears to be part of the executable. # 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 PKG data logger.info("Appending PKG archive to EXE") with open(self.name, 'ab') as outf: with open(self.pkg.name, 'rb') as inf: shutil.copyfileobj(inf, outf, length=64 * 1024) # Fix Mach-O header for code signing logger.info("Fixing EXE headers for code signing") osxutils.fix_exe_for_code_signing(self.name) else: # Fall back to just appending PKG at the end of the file logger.info("Appending PKG archive to EXE") with open(self.name, 'ab') as outf: with open(self.pkg.name, 'rb') as inf: shutil.copyfileobj(inf, outf, length=64 * 1024) # Step 3: post-processing if is_win: # Set checksum to appease antiviral software. set_exe_checksum(self.name) elif is_darwin: # If the version of macOS SDK used to build bootloader exceeds that of macOS SDK used to built Python # library (and, by extension, bundled Tcl/Tk libraries), force the version declared by the frozen executable # to match that of the Python library. # Having macOS attempt to enable new features (based on SDK version) for frozen application has no benefit # if the Python library does not support them as well. # On the other hand, there seem to be UI issues in tkinter due to failed or partial enablement of dark mode # (i.e., the bootloader executable being built against SDK 10.14 or later, which causes macOS to enable dark # mode, and Tk libraries being built against an earlier SDK version that does not support the dark mode). # With python.org Intel macOS installers, this manifests as black Tk windows and UI elements (see issue # #5827), while in Anaconda python, it may result in white text on bright background. pylib_version = osxutils.get_macos_sdk_version(bindepend.get_python_library_path()) exe_version = osxutils.get_macos_sdk_version(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 Analysis(Target): """ Class does analysis of the user's main Python scripts. An Analysis has five outputs, all TOCs (Table of Contents) accessed as attributes of the analysis. scripts The scripts you gave Analysis as input, with any runtime hook scripts prepended. pure The pure Python modules. binaries The extensionmodules and their dependencies. The secondary dependecies are filtered. On Windows files from C:\\Windows are excluded by default. On Linux/Unix only system libraries from /lib or /usr/lib are excluded. datas Data-file dependencies. These are data-file that are found to be needed by modules. They can be anything: plugins, font files, images, translations, etc. zipfiles The zipfiles dependencies (usually .egg files). """ _old_scripts = { absnormpath(os.path.join(HOMEPATH, "support", "_mountzlib.py")), absnormpath(os.path.join(HOMEPATH, "support", "useUnicode.py")), absnormpath(os.path.join(HOMEPATH, "support", "useTK.py")), absnormpath(os.path.join(HOMEPATH, "support", "unpackTK.py")), absnormpath(os.path.join(HOMEPATH, "support", "removeTK.py")) } def __init__(self, scripts, pathex=None, binaries=None, datas=None, hiddenimports=None, hookspath=None, hooksconfig=None, excludes=None, runtime_hooks=None, cipher=None, win_no_prefer_redirects=False, win_private_assemblies=False, noarchive=False): """ scripts A list of scripts specified as file names. pathex An optional list of paths to be searched before sys.path. binaries An optional list of additional binaries (dlls, etc.) to include. datas An optional list of additional data files to include. hiddenimport An optional list of additional (hidden) modules to include. hookspath An optional list of additional paths to search for hooks. (hook-modules). hooksconfig An optional dict of config settings for hooks. (hook-modules). excludes An optional list of module or package names (their Python names, not path names) that will be ignored (as though they were not found). runtime_hooks An optional list of scripts to use as users' runtime hooks. Specified as file names. cipher Add optional instance of the pyz_crypto.PyiBlockCipher class (with a provided key). win_no_prefer_redirects If True, prefers not to follow version redirects when searching for Windows SxS Assemblies. win_private_assemblies If True, changes all bundled Windows SxS Assemblies into Private Assemblies to enforce assembly versions. noarchive If True, don't place source files in a archive, but keep them as individual files. """ super(Analysis, self).__init__() from PyInstaller.config import CONF self.inputs = [] spec_dir = os.path.dirname(CONF['spec']) for script in scripts: # If path is relative, it is relative to the location of .spec file. if not os.path.isabs(script): script = os.path.join(spec_dir, script) if absnormpath(script) in self._old_scripts: logger.warning('Ignoring obsolete auto-added script %s', script) continue # Normalize script path. script = os.path.normpath(script) if not os.path.exists(script): raise SystemExit("script '%s' not found" % script) self.inputs.append(script) # Django hook requires this variable to find the script manage.py. CONF['main_script'] = self.inputs[0] self.pathex = self._extend_pathex(pathex, self.inputs) # Set global config variable 'pathex' to make it available for # PyInstaller.utils.hooks and import hooks. Path extensions for module # search. CONF['pathex'] = self.pathex # Extend sys.path so PyInstaller could find all necessary modules. logger.info('Extending PYTHONPATH with paths\n' + pprint.pformat(self.pathex)) sys.path.extend(self.pathex) # Set global variable to hold assembly binding redirects CONF['binding_redirects'] = [] self.hiddenimports = hiddenimports or [] # Include modules detected when parsing options, like 'codecs' and encodings. self.hiddenimports.extend(CONF['hiddenimports']) self.hookspath = [] # Append directories in `hookspath` (`--additional-hooks-dir`) to # take precedence over those from the entry points. if hookspath: self.hookspath.extend(hookspath) # Add hook directories from PyInstaller entry points. self.hookspath += discover_hook_directories() self.hooksconfig = {} if hooksconfig: self.hooksconfig.update(hooksconfig) # Custom runtime hook files that should be included and started before # any existing PyInstaller runtime hooks. self.custom_runtime_hooks = runtime_hooks or [] if cipher: logger.info('Will encrypt Python bytecode with key: %s', cipher.key) # Create a Python module which contains the decryption key which will # be used at runtime by pyi_crypto.PyiBlockCipher. pyi_crypto_key_path = os.path.join(CONF['workpath'], 'pyimod00_crypto_key.py') with open(pyi_crypto_key_path, 'w', encoding='utf-8') as f: f.write('# -*- coding: utf-8 -*-\n' 'key = %r\n' % cipher.key) self.hiddenimports.append('tinyaes') self.excludes = excludes or [] self.scripts = TOC() self.pure = TOC() self.binaries = TOC() self.zipfiles = TOC() self.zipped_data = TOC() self.datas = TOC() self.dependencies = TOC() self.binding_redirects = CONF['binding_redirects'] = [] self.win_no_prefer_redirects = win_no_prefer_redirects self.win_private_assemblies = win_private_assemblies self._python_version = sys.version self.noarchive = noarchive self.__postinit__() # TODO create function to convert datas/binaries from 'hook format' to TOC. # Initialise 'binaries' and 'datas' with lists specified in .spec file. if binaries: logger.info("Appending 'binaries' from .spec") for name, pth in format_binaries_and_datas(binaries, workingdir=spec_dir): self.binaries.append((name, pth, 'BINARY')) if datas: logger.info("Appending 'datas' from .spec") for name, pth in format_binaries_and_datas(datas, workingdir=spec_dir): self.datas.append((name, pth, 'DATA')) _GUTS = ( # input parameters ('inputs', _check_guts_eq), # parameter `scripts` ('pathex', _check_guts_eq), ('hiddenimports', _check_guts_eq), ('hookspath', _check_guts_eq), ('hooksconfig', _check_guts_eq), ('excludes', _check_guts_eq), ('custom_runtime_hooks', _check_guts_eq), ('win_no_prefer_redirects', _check_guts_eq), ('win_private_assemblies', _check_guts_eq), ('noarchive', _check_guts_eq), #'cipher': no need to check as it is implied by an # additional hidden import #calculated/analysed values ('_python_version', _check_guts_eq), ('scripts', _check_guts_toc_mtime), ('pure', lambda *args: _check_guts_toc_mtime(*args, **{'pyc': 1})), ('binaries', _check_guts_toc_mtime), ('zipfiles', _check_guts_toc_mtime), ('zipped_data', None), # TODO check this, too ('datas', _check_guts_toc_mtime), # TODO: Need to add "dependencies"? # cached binding redirects - loaded into CONF for PYZ/COLLECT to find. ('binding_redirects', None), ) def _extend_pathex(self, spec_pathex, scripts): """ Normalize additional paths where PyInstaller will look for modules and add paths with scripts to the list of paths. :param spec_pathex: Additional paths defined defined in .spec file. :param scripts: Scripts to create executable from. :return: list of updated paths """ # Based on main supplied script - add top-level modules directory to PYTHONPATH. # Sometimes the main app script is not top-level module but submodule like 'mymodule.mainscript.py'. # In that case PyInstaller will not be able find modules in the directory containing 'mymodule'. # Add this directory to PYTHONPATH so PyInstaller could find it. pathex = [] # Add scripts paths first. for script in scripts: logger.debug('script: %s' % script) script_toplevel_dir = get_path_to_toplevel_modules(script) if script_toplevel_dir: pathex.append(script_toplevel_dir) # Append paths from .spec. if spec_pathex is not None: pathex.extend(spec_pathex) # Normalize paths in pathex and make them absolute. return [absnormpath(p) for p in pathex] def _check_guts(self, data, last_build): if Target._check_guts(self, data, last_build): return True for fnm in self.inputs: if mtime(fnm) > last_build: logger.info("Building because %s changed", fnm) return True # Now we know that none of the input parameters and none of # the input files has changed. So take the values calculated # resp. analysed in the last run and store them in `self`. self.scripts = TOC(data['scripts']) self.pure = TOC(data['pure']) self.binaries = TOC(data['binaries']) self.zipfiles = TOC(data['zipfiles']) self.zipped_data = TOC(data['zipped_data']) self.datas = TOC(data['datas']) # Store previously found binding redirects in CONF for later use by PKG/COLLECT from PyInstaller.config import CONF self.binding_redirects = CONF['binding_redirects'] = data[ 'binding_redirects'] return False def assemble(self): """ This method is the MAIN method for finding all necessary files to be bundled. """ from PyInstaller.config import CONF for m in self.excludes: logger.debug("Excluding module '%s'" % m) self.graph = initialize_modgraph(excludes=self.excludes, user_hook_dirs=self.hookspath) # TODO Find a better place where to put 'base_library.zip' and when to created it. # For Python 3 it is necessary to create file 'base_library.zip' # containing core Python modules. In Python 3 some built-in modules # are written in pure Python. base_library.zip is a way how to have # those modules as "built-in". libzip_filename = os.path.join(CONF['workpath'], 'base_library.zip') create_py3_base_library(libzip_filename, graph=self.graph) # Bundle base_library.zip as data file. # Data format of TOC item: ('relative_path_in_dist_dir', 'absolute_path_on_disk', 'DATA') self.datas.append( (os.path.basename(libzip_filename), libzip_filename, 'DATA')) # Expand sys.path of module graph. # The attribute is the set of paths to use for imports: sys.path, # plus our loader, plus other paths from e.g. --path option). self.graph.path = self.pathex + self.graph.path self.graph.set_setuptools_nspackages() logger.info("running Analysis %s", self.tocbasename) # Get paths to Python and, in Windows, the manifest. python = compat.python_executable if not is_win: # Linux/MacOS: get a real, non-link path to the running Python executable. while os.path.islink(python): python = os.path.join(os.path.dirname(python), os.readlink(python)) depmanifest = None else: # Windows: Create a manifest to embed into built .exe, containing the same # dependencies as python.exe. depmanifest = winmanifest.Manifest( type_="win32", name=CONF['specnm'], processorArchitecture=winmanifest.processor_architecture(), version=(1, 0, 0, 0)) depmanifest.filename = os.path.join( CONF['workpath'], CONF['specnm'] + ".exe.manifest") # We record "binaries" separately from the modulegraph, as there # is no way to record those dependencies in the graph. These include # the python executable and any binaries added by hooks later. # "binaries" are not the same as "extensions" which are .so or .dylib # that are found and recorded as extension nodes in the graph. # Reset seen variable before running bindepend. We use bindepend only for # the python executable. bindepend.seen.clear() # Add binary and assembly dependencies of Python.exe. # This also ensures that its assembly depencies under Windows get added to the # built .exe's manifest. Python 2.7 extension modules have no assembly # dependencies, and rely on the app-global dependencies set by the .exe. self.binaries.extend( bindepend.Dependencies([('', python, '')], manifest=depmanifest, redirects=self.binding_redirects)[1:]) if is_win: depmanifest.writeprettyxml() ### Module graph. # # Construct the module graph of import relationships between modules # required by this user's application. For each entry point (top-level # user-defined Python script), all imports originating from this entry # point are recursively parsed into a subgraph of the module graph. This # subgraph is then connected to this graph's root node, ensuring # imported module nodes will be reachable from the root node -- which is # is (arbitrarily) chosen to be the first entry point's node. # List to hold graph nodes of scripts and runtime hooks in use order. priority_scripts = [] # Assume that if the script does not exist, Modulegraph will raise error. # Save the graph nodes of each in sequence. for script in self.inputs: logger.info("Analyzing %s", script) priority_scripts.append(self.graph.add_script(script)) # Analyze the script's hidden imports (named on the command line) self.graph.add_hiddenimports(self.hiddenimports) ### Post-graph hooks. self.graph.process_post_graph_hooks(self) # Update 'binaries' TOC and 'datas' TOC. deps_proc = DependencyProcessor(self.graph, self.graph._additional_files_cache) self.binaries.extend(deps_proc.make_binaries_toc()) self.datas.extend(deps_proc.make_datas_toc()) self.zipped_data.extend(deps_proc.make_zipped_data_toc()) # Note: zipped eggs are collected below ### Look for dlls that are imported by Python 'ctypes' module. # First get code objects of all modules that import 'ctypes'. logger.info('Looking for ctypes DLLs') # dict like: {'module1': code_obj, 'module2': code_obj} ctypes_code_objs = self.graph.get_code_using("ctypes") for name, co in ctypes_code_objs.items(): # Get dlls that might be needed by ctypes. logger.debug('Scanning %s for shared libraries or dlls', name) try: ctypes_binaries = scan_code_for_ctypes(co) self.binaries.extend(set(ctypes_binaries)) except Exception as ex: raise RuntimeError(f"Failed to scan the module '{name}'. " f"This is a bug. Please report it.") from ex self.datas.extend((dest, source, "DATA") for (dest, source) in format_binaries_and_datas( self.graph.metadata_required())) # Analyze run-time hooks. # Run-time hooks has to be executed before user scripts. Add them # to the beginning of 'priority_scripts'. priority_scripts = self.graph.analyze_runtime_hooks( self.custom_runtime_hooks) + priority_scripts # 'priority_scripts' is now a list of the graph nodes of custom runtime # hooks, then regular runtime hooks, then the PyI loader scripts. # Further on, we will make sure they end up at the front of self.scripts ### Extract the nodes of the graph as TOCs for further processing. # Initialize the scripts list with priority scripts in the proper order. self.scripts = self.graph.nodes_to_toc(priority_scripts) # Extend the binaries list with all the Extensions modulegraph has found. self.binaries = self.graph.make_binaries_toc(self.binaries) # Fill the "pure" list with pure Python modules. assert len(self.pure) == 0 self.pure = self.graph.make_pure_toc() # And get references to module code objects constructed by ModuleGraph # to avoid writing .pyc/pyo files to hdd. self.pure._code_cache = self.graph.get_code_objects() # Add remaining binary dependencies - analyze Python C-extensions and what # DLLs they depend on. logger.info('Looking for dynamic libraries') self.binaries.extend( bindepend.Dependencies(self.binaries, redirects=self.binding_redirects)) ### Include zipped Python eggs. logger.info('Looking for eggs') self.zipfiles.extend(deps_proc.make_zipfiles_toc()) # Verify that Python dynamic library can be found. # Without dynamic Python library PyInstaller cannot continue. self._check_python_library(self.binaries) if is_win: # Remove duplicate redirects self.binding_redirects[:] = list(set(self.binding_redirects)) logger.info("Found binding redirects: \n%s", self.binding_redirects) # Filter binaries to adjust path of extensions that come from # python's lib-dynload directory. Prefix them with lib-dynload # so that we'll collect them into subdirectory instead of # directly into _MEIPASS for idx, tpl in enumerate(self.binaries): name, path, typecode = tpl if typecode == 'EXTENSION' \ and not os.path.dirname(os.path.normpath(name)) \ and os.path.basename(os.path.dirname(path)) == 'lib-dynload': name = os.path.join('lib-dynload', name) self.binaries[idx] = (name, path, typecode) # Place Python source in data files for the noarchive case. if self.noarchive: # Create a new TOC of ``(dest path for .pyc, source for .py, type)``. new_toc = TOC() for name, path, typecode in self.pure: assert typecode == 'PYMODULE' # Transform a python module name into a file name. name = name.replace('.', os.sep) # Special case: modules have an implied filename to add. if os.path.splitext(os.path.basename(path))[0] == '__init__': name += os.sep + '__init__' # Append the extension for the compiled result. # In python 3.5 (PEP-488) .pyo files were replaced by # .opt-1.pyc and .opt-2.pyc. However, it seems that for # bytecode-only module distribution, we always need to # use the .pyc extension. name += '.pyc' new_toc.append((name, path, typecode)) # Put the result of byte-compiling this TOC in datas. Mark all entries as data. for name, path, typecode in compile_py_files( new_toc, CONF['workpath']): self.datas.append((name, path, 'DATA')) # Store no source in the archive. self.pure = TOC() # Write warnings about missing modules. self._write_warnings() # Write debug information about hte graph self._write_graph_debug() def _write_warnings(self): """ Write warnings about missing modules. Get them from the graph and use the graph to figure out who tried to import them. """ def dependency_description(name, depInfo): if not depInfo or depInfo == 'direct': imptype = 0 else: imptype = (depInfo.conditional + 2 * depInfo.function + 4 * depInfo.tryexcept) return '%s (%s)' % (name, IMPORT_TYPES[imptype]) from PyInstaller.config import CONF miss_toc = self.graph.make_missing_toc() with open(CONF['warnfile'], 'w', encoding='utf-8') as wf: wf.write(WARNFILE_HEADER) for (n, p, status) in miss_toc: importers = self.graph.get_importers(n) print(status, 'module named', n, '- imported by', ', '.join( dependency_description(name, data) for name, data in importers), file=wf) logger.info("Warnings written to %s", CONF['warnfile']) def _write_graph_debug(self): """Write a xref (in html) and with `--log-level DEBUG` a dot-drawing of the graph. """ from PyInstaller.config import CONF with open(CONF['xref-file'], 'w', encoding='utf-8') as fh: self.graph.create_xref(fh) logger.info("Graph cross-reference written to %s", CONF['xref-file']) if logger.getEffectiveLevel() > logging.DEBUG: return # The `DOT language's <https://www.graphviz.org/doc/info/lang.html>`_ # default character encoding (see the end of the linked page) is UTF-8. with open(CONF['dot-file'], 'w', encoding='utf-8') as fh: self.graph.graphreport(fh) logger.info("Graph drawing written to %s", CONF['dot-file']) def _check_python_library(self, binaries): """ Verify presence of the Python dynamic library in the binary dependencies. Python library is an essential piece that has to be always included. """ # First check that libpython is in resolved binary dependencies. for (nm, filename, typ) in binaries: if typ == 'BINARY' and nm in PYDYLIB_NAMES: # Just print its filename and return. logger.info('Using Python library %s', filename) # Checking was successful - end of function. return # Python lib not in dependencies - try to find it. logger.info( 'Python library not in binary dependencies. Doing additional searching...' ) python_lib = bindepend.get_python_library_path() logger.debug('Adding Python library to binary dependencies') binaries.append((os.path.basename(python_lib), python_lib, 'BINARY')) logger.info('Using Python library %s', python_lib) def exclude_system_libraries(self, list_of_exceptions=[]): """ This method may be optionally called from the spec file to exclude any system libraries from the list of binaries other than those containing the shell-style wildcards in list_of_exceptions. Those that match '*python*' or are stored under 'lib-dynload' are always treated as exceptions and not excluded. """ self.binaries = \ [i for i in self.binaries if _should_include_system_binary(i, list_of_exceptions)]
class Splash(Target): """ Bundles the required resources for the splash screen into a file, which will be included in the CArchive. A Splash has two outputs, one is itself and one is sored in splash.binaries. Both need to be passed to other build targets in order to enable the splash screen. """ typ = 'SPLASH' def __init__(self, image_file, binaries, datas, **kwargs): """ :param str image_file: A path-like object to the image to be used. Only the PNG file format is supported. .. note:: If a different file format is supplied and PIL (Pillow) is installed, the file will be converted automatically. .. note:: *Windows*: Due to the implementation, the color Magenta/ RGB(255, 0, 255) must not be used in the image or text. .. note:: If PIL (Pillow) is installed and the image is bigger than max_img_size, the image will be resized to fit into the specified area. :param TOC binaries: The TOC of binaries the Analysis build target found. This TOC includes all extensionmodules and their dependencies. This is required to figure out, if the users program uses tkinter. :param TOC datas: The TOC of data the Analysis build target found. This TOC includes all data-file dependencies of the modules. This is required to check if all splash screen requirements can be bundled. :keyword text_pos: An optional 2x integer tuple that represents the origin of the text on the splash screen image. The origin of the text is its lower left corner. A unit in the respective coordinate system is a pixel of the image, its origin lies in the top left corner of the image. This parameter also acts like a switch for the text feature. If omitted, no text will be displayed on the splash screen. This text will be used to show textual progress in onefile mode. :type text_pos: Tuple[int, int] :keyword text_size: The desired size of the font. If the size argument is a positive number, it is interpreted as a size in points. If size is a negative number, its absolute value is interpreted as a size in pixels. Default: ``12`` :type text_size: int :keyword text_font: An optional name of a font for the text. This font must be installed on the user system, otherwise the system default font is used. If this parameter is omitted, the default font is also used. :keyword text_color: An optional color for the text. Either RGB HTML notation or color names are supported. Default: black (Windows: Due to a implementation issue the color magenta/ rgb(255, 0, 255) is forbidden) :type text_color: str :keyword text_default: The default text which will be displayed before the extraction starts. Default: "Initializing" :type text_default: str :keyword full_tk: By default Splash bundles only the necessary files for the splash screen (some tk components). This options enables adding full tk and making it a requirement, meaning all tk files will be unpacked before the splash screen can be started. This is useful during development of the splash screen script. Default: ``False`` :type full_tk: bool :keyword minify_script: The splash screen is created by executing an Tcl/Tk script. This option enables minimizing the script, meaning removing all non essential parts from the script. Default: True :keyword rundir: The folder name in which tcl/tk will be extracted at runtime. There should be no matching folder in your application to avoid conflicts. Default: ``__splash`` :type rundir: str :keyword name: An optional alternative filename for the .res file. If not specified, a name is generated. :type name: str :keyword script_name: An optional alternative filename for the Tcl script, that will be generated. If not specified, a name is generated. :type script_name: str :keyword max_img_size: Maximum size of the splash screen image as a tuple. If the supplied image exceeds this limit, it will be resized to fit the maximum width (to keep the original aspect ratio). This option can be disabled by setting it to None. Default: (760, 480) :type max_img_size: Tuple[int, int] """ from ..config import CONF Target.__init__(self) # Splash screen is not supported on macOS. It operates in a secondary thread and macOS disallows UI operations # in any thread other than main. if is_darwin: raise SystemExit("Splash screen is not supported on macOS.") # Make image path relative to .spec file if not os.path.isabs(image_file): image_file = os.path.join(CONF['specpath'], image_file) image_file = os.path.normpath(image_file) if not os.path.exists(image_file): raise ValueError("Image file '%s' not found" % image_file) # Copy all arguments self.image_file = image_file self.full_tk = kwargs.get("full_tk", False) self.name = kwargs.get("name", None) self.script_name = kwargs.get("script_name", None) self.minify_script = kwargs.get("minify_script", True) self.rundir = kwargs.get("rundir", None) self.max_img_size = kwargs.get("max_img_size", (760, 480)) # text options self.text_pos = kwargs.get("text_pos", None) self.text_size = kwargs.get("text_size", 12) self.text_font = kwargs.get("text_font", "TkDefaultFont") self.text_color = kwargs.get("text_color", "black") self.text_default = kwargs.get("text_default", "Initializing") # Save the generated file separately so that it is not necessary to generate the data again and again root = os.path.splitext(self.tocfilename)[0] if self.name is None: self.name = root + '.res' if self.script_name is None: self.script_name = root + '_script.tcl' if self.rundir is None: self.rundir = self._find_rundir(binaries + datas) # Internal variables try: # Do not import _tkinter at the toplevel, because on some systems _tkinter will fail to load, since it is # not installed. This would cause a runtime error in PyInstaller, since this module is imported from # build_main.py, instead we just want to inform the user that the splash screen feature is not supported on # his platform import _tkinter self._tkinter_module = _tkinter self._tkinter_file = self._tkinter_module.__file__ except ModuleNotFoundError: raise SystemExit( "You platform does not support the splash screen feature, since tkinter is not installed. Please " "install tkinter and try again." ) # Calculated / analysed values self.uses_tkinter = self._uses_tkinter(binaries) self.script = self.generate_script() self.tcl_lib, self.tk_lib = find_tcl_tk_shared_libs(self._tkinter_file) if is_darwin: # Outdated Tcl/Tk 8.5 system framework is not supported. Depending on macOS version, the library path will # come up empty (hidden system libraries on Big Sur), or will be # [/System]/Library/Frameworks/Tcl.framework/Tcl if self.tcl_lib[1] is None or 'Library/Frameworks/Tcl.framework' in self.tcl_lib[1]: raise SystemExit("The splash screen feature does not support macOS system framework version of Tcl/Tk.") # Check if tcl/tk was found assert all(self.tcl_lib) assert all(self.tk_lib) logger.debug("Use Tcl Library from %s and Tk From %s" % (self.tcl_lib, self.tk_lib)) self.splash_requirements = set([self.tcl_lib[0], self.tk_lib[0]] + splash_requirements) logger.info("Collect tcl/tk binaries for the splash screen") tcltk_tree = collect_tcl_tk_files(self._tkinter_file) if self.full_tk: # The user wants a full copy of tk, so make all tk files a requirement. self.splash_requirements.update(toc[0] for toc in tcltk_tree) self.binaries = TOC() if not self.uses_tkinter: # The user's script does not use tkinter, so we need to provide a TOC of all necessary files add the shared # libraries to the binaries. self.binaries.append((self.tcl_lib[0], self.tcl_lib[1], 'BINARY')) self.binaries.append((self.tk_lib[0], self.tk_lib[1], 'BINARY')) # Only add the intersection of the required and the collected resources, or add all entries if full_tk is # true. self.binaries.extend(toc for toc in tcltk_tree if toc[0] in self.splash_requirements) # Check if all requirements were found. fnames = [toc[0] for toc in (binaries + datas + self.binaries)] def _filter(_item): if _item not in fnames: # Item is not bundled, so warn the user about it. This actually may happen on some tkinter installations # that are missing the license.terms file. logger.warning( "The local Tcl/Tk installation is missing the file %s. The behavior of the splash screen is " "therefore undefined and may be unsupported." % _item ) return False return True # Remove all files which were not found. self.splash_requirements = set(filter(_filter, self.splash_requirements)) # Test if the tcl/tk version is supported by the bootloader. self.test_tk_version() logger.debug("Splash Requirements: %s" % self.splash_requirements) self.__postinit__() _GUTS = ( # input parameters ('image_file', _check_guts_eq), ('name', _check_guts_eq), ('script_name', _check_guts_eq), ('text_pos', _check_guts_eq), ('text_size', _check_guts_eq), ('text_font', _check_guts_eq), ('text_color', _check_guts_eq), ('text_default', _check_guts_eq), ('full_tk', _check_guts_eq), ('minify_script', _check_guts_eq), ('rundir', _check_guts_eq), ('max_img_size', _check_guts_eq), # calculated/analysed values ('uses_tkinter', _check_guts_eq), ('script', _check_guts_eq), ('tcl_lib', _check_guts_eq), ('tk_lib', _check_guts_eq), ('splash_requirements', _check_guts_eq), ('binaries', _check_guts_toc), # internal value # Check if the tkinter installation changed. This is theoretically possible if someone uses two different python # installations of the same version. ('_tkinter_file', _check_guts_eq), ) def _check_guts(self, data, last_build): if Target._check_guts(self, data, last_build): return True # Check if the image has been modified. if misc.mtime(self.image_file) > last_build: logger.info("Building %s because file %s changed", self.tocbasename, self.image_file) return True return False def assemble(self): logger.info("Building Splash %s" % self.name) # Function to resize a given image to fit into the area defined by max_img_size. def _resize_image(_image, _orig_size): if PILImage: _w, _h = _orig_size _ratio_w = self.max_img_size[0] / _w if _ratio_w < 1: # Image width exceeds limit _h = int(_h * _ratio_w) _w = self.max_img_size[0] _ratio_h = self.max_img_size[1] / _h if _ratio_h < 1: # Image height exceeds limit _w = int(_w * _ratio_h) _h = self.max_img_size[1] # If a file is given it will be open if isinstance(_image, PILImage.Image): _img = _image else: _img = PILImage.open(_image) _img_resized = _img.resize((_w, _h)) # Save image into a stream _image_stream = io.BytesIO() _img_resized.save(_image_stream, format='PNG') _img.close() _img_resized.close() _image_data = _image_stream.getvalue() logger.info( "Resized image %s from dimensions %s to (%d, %d)" % (self.image_file, str(_orig_size), _w, _h) ) return _image_data else: raise ValueError( "The splash image dimensions (w: %d, h: %d) exceed max_img_size (w: %d, h:%d), but the image " "cannot be resized due to missing PIL.Image! Either install the Pillow package, adjust the " "max_img_size, or use an image of compatible dimensions." % (_orig_size[0], _orig_size[1], self.max_img_size[0], self.max_img_size[1]) ) # Open image file image_file = open(self.image_file, 'rb') # Check header of the file to identify it if image_file.read(8) == b'\x89PNG\r\n\x1a\n': # self.image_file is a PNG file image_file.seek(16) img_size = (struct.unpack("!I", image_file.read(4))[0], struct.unpack("!I", image_file.read(4))[0]) if img_size > self.max_img_size: # The image exceeds the maximum image size, so resize it image = _resize_image(self.image_file, img_size) else: image = os.path.abspath(self.image_file) elif PILImage: # Pillow is installed, meaning the image can be converted automatically img = PILImage.open(self.image_file, mode='r') if img.size > self.max_img_size: image = _resize_image(img, img.size) else: image_data = io.BytesIO() img.save(image_data, format='PNG') img.close() image = image_data.getvalue() logger.info("Converted image %s to PNG format" % self.image_file) else: raise ValueError( "The image %s needs to be converted to a PNG file, but PIL.Image is not available! Either install the " "Pillow package, or use a PNG image for you splash screen." % self.image_file ) image_file.close() SplashWriter( self.name, self.splash_requirements, self.tcl_lib[0], # tcl86t.dll self.tk_lib[0], # tk86t.dll TK_ROOTNAME, self.rundir, image, self.script ) def test_tk_version(self): tcl_version = float(self._tkinter_module.TCL_VERSION) tk_version = float(self._tkinter_module.TK_VERSION) # Test if tcl/tk version is supported if tcl_version < 8.6 or tk_version < 8.6: logger.warning( "The installed Tcl/Tk (%s/%s) version might not work with the splash screen feature of the bootloader. " "The bootloader is tested against Tcl/Tk 8.6" % (self._tkinter_module.TCL_VERSION, self._tkinter_module.TK_VERSION) ) # This should be impossible, since tcl/tk is released together with the same version number, but just in case if tcl_version != tk_version: logger.warning( "The installed version of Tcl (%s) and Tk (%s) do not match. PyInstaller is tested against matching " "versions" % (self._tkinter_module.TCL_VERSION, self._tkinter_module.TK_VERSION) ) # Test if tcl is threaded. # If the variable tcl_platform(threaded) exist, the tcl interpreter was compiled with thread support. threaded = bool(exec_statement( """ from tkinter import Tcl, TclError try: print(Tcl().getvar('tcl_platform(threaded)')) except TclError: pass """ )) # yapf: disable if not threaded: # This is a feature breaking problem, so exit. raise SystemExit( "The installed tcl version is not threaded. PyInstaller only supports the splash screen " "using threaded tcl." ) def generate_script(self): """ Generate the script for the splash screen. If minify_script is True, all unnecessary parts will be removed. """ d = {} if self.text_pos is not None: logger.debug("Add text support to splash screen") d.update({ 'pad_x': self.text_pos[0], 'pad_y': self.text_pos[1], 'color': self.text_color, 'font': self.text_font, 'font_size': self.text_size, 'default_text': self.text_default, }) script = splash_templates.build_script(text_options=d) if self.minify_script: # Remove any documentation, empty lines and unnecessary spaces script = '\n'.join( line for line in map(lambda l: l.strip(), script.splitlines()) if not line.startswith('#') # documentation and line # empty lines ) # Remove unnecessary spaces script = re.sub(' +', ' ', script) # Write script to disk, so that it is transparent to the user what script is executed. with open(self.script_name, "w") as script_file: script_file.write(script) return script @staticmethod def _uses_tkinter(binaries): # Test for _tkinter instead of tkinter, because a user might use a different wrapping library for tk. return '_tkinter' in binaries.filenames @staticmethod def _find_rundir(structure): # First try a name the user could understand, if one would find the directory. rundir = '__splash%s' candidate = rundir % "" counter = 0 # Run this loop as long as a folder exist named like rundir. In most cases __splash will be sufficient and this # loop wont enter. while any(e[0].startswith(candidate + os.sep) for e in structure): # just append to rundir a counter candidate = rundir % str(counter) counter += 1 # The SPLASH_DATA_HEADER structure limits the name to be 16 bytes at maximum. So if we exceed the limit # raise an error. This will never happen, since there are 10^8 different possibilities, but just in case. assert len(candidate) <= 16 return candidate
def assemble(self): """ This method is the MAIN method for finding all necessary files to be bundled. """ from PyInstaller.config import CONF for m in self.excludes: logger.debug("Excluding module '%s'" % m) self.graph = initialize_modgraph(excludes=self.excludes, user_hook_dirs=self.hookspath) # TODO Find a better place where to put 'base_library.zip' and when to created it. # For Python 3 it is necessary to create file 'base_library.zip' # containing core Python modules. In Python 3 some built-in modules # are written in pure Python. base_library.zip is a way how to have # those modules as "built-in". libzip_filename = os.path.join(CONF['workpath'], 'base_library.zip') create_py3_base_library(libzip_filename, graph=self.graph) # Bundle base_library.zip as data file. # Data format of TOC item: ('relative_path_in_dist_dir', 'absolute_path_on_disk', 'DATA') self.datas.append( (os.path.basename(libzip_filename), libzip_filename, 'DATA')) # Expand sys.path of module graph. # The attribute is the set of paths to use for imports: sys.path, # plus our loader, plus other paths from e.g. --path option). self.graph.path = self.pathex + self.graph.path self.graph.set_setuptools_nspackages() logger.info("running Analysis %s", self.tocbasename) # Get paths to Python and, in Windows, the manifest. python = compat.python_executable if not is_win: # Linux/MacOS: get a real, non-link path to the running Python executable. while os.path.islink(python): python = os.path.join(os.path.dirname(python), os.readlink(python)) depmanifest = None else: # Windows: Create a manifest to embed into built .exe, containing the same # dependencies as python.exe. depmanifest = winmanifest.Manifest( type_="win32", name=CONF['specnm'], processorArchitecture=winmanifest.processor_architecture(), version=(1, 0, 0, 0)) depmanifest.filename = os.path.join( CONF['workpath'], CONF['specnm'] + ".exe.manifest") # We record "binaries" separately from the modulegraph, as there # is no way to record those dependencies in the graph. These include # the python executable and any binaries added by hooks later. # "binaries" are not the same as "extensions" which are .so or .dylib # that are found and recorded as extension nodes in the graph. # Reset seen variable before running bindepend. We use bindepend only for # the python executable. bindepend.seen.clear() # Add binary and assembly dependencies of Python.exe. # This also ensures that its assembly depencies under Windows get added to the # built .exe's manifest. Python 2.7 extension modules have no assembly # dependencies, and rely on the app-global dependencies set by the .exe. self.binaries.extend( bindepend.Dependencies([('', python, '')], manifest=depmanifest, redirects=self.binding_redirects)[1:]) if is_win: depmanifest.writeprettyxml() ### Module graph. # # Construct the module graph of import relationships between modules # required by this user's application. For each entry point (top-level # user-defined Python script), all imports originating from this entry # point are recursively parsed into a subgraph of the module graph. This # subgraph is then connected to this graph's root node, ensuring # imported module nodes will be reachable from the root node -- which is # is (arbitrarily) chosen to be the first entry point's node. # List to hold graph nodes of scripts and runtime hooks in use order. priority_scripts = [] # Assume that if the script does not exist, Modulegraph will raise error. # Save the graph nodes of each in sequence. for script in self.inputs: logger.info("Analyzing %s", script) priority_scripts.append(self.graph.add_script(script)) # Analyze the script's hidden imports (named on the command line) self.graph.add_hiddenimports(self.hiddenimports) ### Post-graph hooks. self.graph.process_post_graph_hooks(self) # Update 'binaries' TOC and 'datas' TOC. deps_proc = DependencyProcessor(self.graph, self.graph._additional_files_cache) self.binaries.extend(deps_proc.make_binaries_toc()) self.datas.extend(deps_proc.make_datas_toc()) self.zipped_data.extend(deps_proc.make_zipped_data_toc()) # Note: zipped eggs are collected below ### Look for dlls that are imported by Python 'ctypes' module. # First get code objects of all modules that import 'ctypes'. logger.info('Looking for ctypes DLLs') # dict like: {'module1': code_obj, 'module2': code_obj} ctypes_code_objs = self.graph.get_code_using("ctypes") for name, co in ctypes_code_objs.items(): # Get dlls that might be needed by ctypes. logger.debug('Scanning %s for shared libraries or dlls', name) try: ctypes_binaries = scan_code_for_ctypes(co) self.binaries.extend(set(ctypes_binaries)) except Exception as ex: raise RuntimeError(f"Failed to scan the module '{name}'. " f"This is a bug. Please report it.") from ex self.datas.extend((dest, source, "DATA") for (dest, source) in format_binaries_and_datas( self.graph.metadata_required())) # Analyze run-time hooks. # Run-time hooks has to be executed before user scripts. Add them # to the beginning of 'priority_scripts'. priority_scripts = self.graph.analyze_runtime_hooks( self.custom_runtime_hooks) + priority_scripts # 'priority_scripts' is now a list of the graph nodes of custom runtime # hooks, then regular runtime hooks, then the PyI loader scripts. # Further on, we will make sure they end up at the front of self.scripts ### Extract the nodes of the graph as TOCs for further processing. # Initialize the scripts list with priority scripts in the proper order. self.scripts = self.graph.nodes_to_toc(priority_scripts) # Extend the binaries list with all the Extensions modulegraph has found. self.binaries = self.graph.make_binaries_toc(self.binaries) # Fill the "pure" list with pure Python modules. assert len(self.pure) == 0 self.pure = self.graph.make_pure_toc() # And get references to module code objects constructed by ModuleGraph # to avoid writing .pyc/pyo files to hdd. self.pure._code_cache = self.graph.get_code_objects() # Add remaining binary dependencies - analyze Python C-extensions and what # DLLs they depend on. logger.info('Looking for dynamic libraries') self.binaries.extend( bindepend.Dependencies(self.binaries, redirects=self.binding_redirects)) ### Include zipped Python eggs. logger.info('Looking for eggs') self.zipfiles.extend(deps_proc.make_zipfiles_toc()) # Verify that Python dynamic library can be found. # Without dynamic Python library PyInstaller cannot continue. self._check_python_library(self.binaries) if is_win: # Remove duplicate redirects self.binding_redirects[:] = list(set(self.binding_redirects)) logger.info("Found binding redirects: \n%s", self.binding_redirects) # Filter binaries to adjust path of extensions that come from # python's lib-dynload directory. Prefix them with lib-dynload # so that we'll collect them into subdirectory instead of # directly into _MEIPASS for idx, tpl in enumerate(self.binaries): name, path, typecode = tpl if typecode == 'EXTENSION' \ and not os.path.dirname(os.path.normpath(name)) \ and os.path.basename(os.path.dirname(path)) == 'lib-dynload': name = os.path.join('lib-dynload', name) self.binaries[idx] = (name, path, typecode) # Place Python source in data files for the noarchive case. if self.noarchive: # Create a new TOC of ``(dest path for .pyc, source for .py, type)``. new_toc = TOC() for name, path, typecode in self.pure: assert typecode == 'PYMODULE' # Transform a python module name into a file name. name = name.replace('.', os.sep) # Special case: modules have an implied filename to add. if os.path.splitext(os.path.basename(path))[0] == '__init__': name += os.sep + '__init__' # Append the extension for the compiled result. # In python 3.5 (PEP-488) .pyo files were replaced by # .opt-1.pyc and .opt-2.pyc. However, it seems that for # bytecode-only module distribution, we always need to # use the .pyc extension. name += '.pyc' new_toc.append((name, path, typecode)) # Put the result of byte-compiling this TOC in datas. Mark all entries as data. for name, path, typecode in compile_py_files( new_toc, CONF['workpath']): self.datas.append((name, path, 'DATA')) # Store no source in the archive. self.pure = TOC() # Write warnings about missing modules. self._write_warnings() # Write debug information about hte graph self._write_graph_debug()
class BUNDLE(Target): def __init__(self, *args, **kws): from PyInstaller.config import CONF # BUNDLE only has a sense under Mac OS X, it's a noop on other platforms if not is_darwin: return # get a path to a .icns icon for the app bundle. self.icon = kws.get('icon') if not self.icon: # --icon not specified; use the default in the pyinstaller folder self.icon = os.path.join( os.path.dirname(os.path.dirname(__file__)), 'bootloader', 'images', 'icon-windowed.icns') else: # user gave an --icon=path. If it is relative, make it # relative to the spec file location. if not os.path.isabs(self.icon): self.icon = os.path.join(CONF['specpath'], self.icon) # ensure icon path is absolute self.icon = os.path.abspath(self.icon) Target.__init__(self) # .app bundle is created in DISTPATH. self.name = kws.get('name', None) base_name = os.path.basename(self.name) self.name = os.path.join(CONF['distpath'], base_name) self.appname = os.path.splitext(base_name)[0] self.version = kws.get("version", "0.0.0") self.toc = TOC() self.strip = False self.upx = False self.console = True self.target_arch = None self.codesign_identity = None self.entitlements_file = None # .app bundle identifier for Code Signing self.bundle_identifier = kws.get('bundle_identifier') if not self.bundle_identifier: # Fallback to appname. self.bundle_identifier = self.appname self.info_plist = kws.get('info_plist', None) for arg in args: if isinstance(arg, EXE): self.toc.append( (os.path.basename(arg.name), arg.name, arg.typ)) self.toc.extend(arg.dependencies) self.strip = arg.strip self.upx = arg.upx self.upx_exclude = arg.upx_exclude self.console = arg.console self.target_arch = arg.target_arch self.codesign_identity = arg.codesign_identity self.entitlements_file = arg.entitlements_file elif isinstance(arg, TOC): self.toc.extend(arg) # TOC doesn't have a strip or upx attribute, so there is no way for us to # tell which cache we should draw from. elif isinstance(arg, COLLECT): self.toc.extend(arg.toc) self.strip = arg.strip_binaries self.upx = arg.upx_binaries self.upx_exclude = arg.upx_exclude self.console = arg.console self.target_arch = arg.target_arch self.codesign_identity = arg.codesign_identity self.entitlements_file = arg.entitlements_file else: logger.info("unsupported entry %s", arg.__class__.__name__) # Now, find values for app filepath (name), app name (appname), and name # of the actual executable (exename) from the first EXECUTABLE item in # toc, which might have come from a COLLECT too (not from an EXE). for inm, name, typ in self.toc: if typ == "EXECUTABLE": self.exename = name break self.__postinit__() _GUTS = ( # BUNDLE always builds, just want the toc to be written out ('toc', None), ) def _check_guts(self, data, last_build): # BUNDLE always needs to be executed, since it will clean the output # directory anyway to make sure there is no existing cruft accumulating return 1 def assemble(self): if _check_path_overlap(self.name) and os.path.isdir(self.name): _rmtree(self.name) logger.info("Building BUNDLE %s", self.tocbasename) # Create a minimal Mac bundle structure os.makedirs(os.path.join(self.name, "Contents", "MacOS")) os.makedirs(os.path.join(self.name, "Contents", "Resources")) os.makedirs(os.path.join(self.name, "Contents", "Frameworks")) # Copy icns icon to Resources directory. if os.path.exists(self.icon): shutil.copy(self.icon, os.path.join(self.name, 'Contents', 'Resources')) else: logger.warning("icon not found %s", self.icon) # Key/values for a minimal Info.plist file info_plist_dict = { "CFBundleDisplayName": self.appname, "CFBundleName": self.appname, # Required by 'codesign' utility. # The value for CFBundleIdentifier is used as the default unique # name of your program for Code Signing purposes. # It even identifies the APP for access to restricted OS X areas # like Keychain. # # The identifier used for signing must be globally unique. The usal # form for this identifier is a hierarchical name in reverse DNS # notation, starting with the toplevel domain, followed by the # company name, followed by the department within the company, and # ending with the product name. Usually in the form: # com.mycompany.department.appname # Cli option --osx-bundle-identifier sets this value. "CFBundleIdentifier": self.bundle_identifier, "CFBundleExecutable": os.path.basename(self.exename), "CFBundleIconFile": os.path.basename(self.icon), "CFBundleInfoDictionaryVersion": "6.0", "CFBundlePackageType": "APPL", "CFBundleShortVersionString": self.version, } # Set some default values. # But they still can be overwritten by the user. if self.console: # Setting EXE console=True implies LSBackgroundOnly=True. info_plist_dict['LSBackgroundOnly'] = True else: # Let's use high resolution by default. info_plist_dict['NSHighResolutionCapable'] = True # Merge info_plist settings from spec file if isinstance(self.info_plist, dict) and self.info_plist: info_plist_dict.update(self.info_plist) plist_filename = os.path.join(self.name, "Contents", "Info.plist") with open(plist_filename, "wb") as plist_fh: plistlib.dump(info_plist_dict, plist_fh) links = [] _QT_BASE_PATH = {'PySide2', 'PySide6', 'PyQt5', 'PySide6'} for inm, fnm, typ in self.toc: # Adjust name for extensions, if applicable inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ) # Copy files from cache. This ensures that are used files with relative # paths to dynamic library dependencies (@executable_path) base_path = inm.split('/', 1)[0] if typ in ('EXTENSION', 'BINARY'): fnm = checkCache(fnm, strip=self.strip, upx=self.upx, upx_exclude=self.upx_exclude, dist_nm=inm, target_arch=self.target_arch, codesign_identity=self.codesign_identity, entitlements_file=self.entitlements_file) # Add most data files to a list for symlinking later. if typ == 'DATA' and base_path not in _QT_BASE_PATH: links.append((inm, fnm)) else: tofnm = os.path.join(self.name, "Contents", "MacOS", inm) todir = os.path.dirname(tofnm) if not os.path.exists(todir): os.makedirs(todir) if os.path.isdir(fnm): # beacuse 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) logger.info('Moving BUNDLE data files to Resource directory') # Mac OS X Code Signing does not work when .app bundle contains # data files in dir ./Contents/MacOS. # # Put all data files in ./Resources and create symlinks in ./MacOS. bin_dir = os.path.join(self.name, 'Contents', 'MacOS') res_dir = os.path.join(self.name, 'Contents', 'Resources') for inm, fnm in links: tofnm = os.path.join(res_dir, inm) todir = os.path.dirname(tofnm) if not os.path.exists(todir): os.makedirs(todir) if os.path.isdir(fnm): # beacuse 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) base_path = os.path.split(inm)[0] if base_path: if not os.path.exists(os.path.join(bin_dir, inm)): path = '' for part in iter(base_path.split(os.path.sep)): # Build path from previous path and the next part of the base path path = os.path.join(path, part) try: relative_source_path = os.path.relpath( os.path.join(res_dir, path), os.path.split(os.path.join(bin_dir, path))[0]) dest_path = os.path.join(bin_dir, path) os.symlink(relative_source_path, dest_path) break except FileExistsError: pass if not os.path.exists(os.path.join(bin_dir, inm)): relative_source_path = os.path.relpath( os.path.join(res_dir, inm), os.path.split(os.path.join(bin_dir, inm))[0]) dest_path = os.path.join(bin_dir, inm) os.symlink(relative_source_path, dest_path) else: # If path is empty, e.g., a top level file, try to just symlink the file os.symlink( os.path.relpath( os.path.join(res_dir, inm), os.path.split(os.path.join(bin_dir, inm))[0]), os.path.join(bin_dir, inm)) # Sign the bundle logger.info('Signing the BUNDLE...') try: osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep=True) except Exception as e: logger.warning("Error while signing the bundle: %s", e) logger.warning("You will need to sign the bundle manually!") logger.info("Building BUNDLE %s completed successfully.", self.tocbasename)
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 keywork arguments: name The name of the directory to be built. """ from ..config import CONF Target.__init__(self) self.strip_binaries = kws.get('strip', False) self.console = True 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 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) toc = add_suffix_to_extensions(self.toc) for inm, fnm, typ in toc: 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) if typ in ('EXTENSION', 'BINARY'): fnm = checkCache(fnm, strip=self.strip_binaries, upx=(self.upx_binaries and (is_win or is_cygwin)), dist_nm=inm) if typ != 'DEPENDENCY': if os.path.isdir(fnm): # beacuse 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)
def test_append_other_case(): # should not be added if the filenames are the same on a case-insensitive system. toc = TOC(ELEMS1) toc.append(('EnCodIngs', '/usr/lib/python2.7/encodings.py', 'BINARY')) expected = list(ELEMS1) assert toc == expected