#----------------------------------------------------------------------------- # Copyright (c) 2013-2021, PyInstaller Development Team. # # Distributed under the terms of the GNU General Public License (version 2 # or later) with exception for distributing the bootloader. # # The full license is in the file COPYING.txt, distributed with this software. # # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception) #----------------------------------------------------------------------------- import locale import os from PyInstaller import compat from PyInstaller import log as logging from PyInstaller.building.datastruct import Tree from PyInstaller.depend import bindepend from PyInstaller.utils import hooks as hookutils logger = logging.getLogger(__name__) TK_ROOTNAME = 'tk' TCL_ROOTNAME = 'tcl' def _warn_if_activetcl_or_teapot_installed(tcl_root, tcltree): """ If the current Tcl installation is a Teapot-distributed version of ActiveTcl *and* the current platform is macOS, log a non-fatal warning that the resulting executable will (probably) fail to run on non-host systems. PyInstaller does *not* freeze all ActiveTcl dependencies -- including Teapot, which is typically ignorable. Since Teapot is *not* ignorable in this case, this function warns of impending failure. See Also ------- https://github.com/pyinstaller/pyinstaller/issues/621 """ import macholib.util # System libraries do not experience this problem. if macholib.util.in_system_path(tcl_root): return # Absolute path of the "init.tcl" script. try: init_resource = [r[1] for r in tcltree if r[1].endswith('init.tcl')][0] except IndexError: # If such script could not be found, silently return. return mentions_activetcl = False mentions_teapot = False # TCL/TK reads files using the system encoding: # https://www.tcl.tk/doc/howto/i18n.html#system_encoding with open(init_resource, 'r', encoding=locale.getpreferredencoding()) as init_file: for line in init_file.readlines(): line = line.strip().lower() if line.startswith('#'): continue if 'activetcl' in line: mentions_activetcl = True if 'teapot' in line: mentions_teapot = True if mentions_activetcl and mentions_teapot: break if mentions_activetcl and mentions_teapot: logger.warning( """ You appear to be using an ActiveTcl build of Tcl/Tk, which PyInstaller has difficulty freezing. To fix this, comment out all references to "teapot" in: %s See https://github.com/pyinstaller/pyinstaller/issues/621 for more information. """ % init_resource ) def _find_tcl_tk_dir(): """ Get a platform-agnostic 2-tuple of the absolute paths of the top-level external data directories for both Tcl and Tk, respectively. Returns ------- list 2-tuple that contains the values of `${TCL_LIBRARY}` and `${TK_LIBRARY}`, respectively. """ # Python code to get path to TCL_LIBRARY. tcl_root = hookutils.exec_statement('from tkinter import Tcl; print(Tcl().eval("info library"))') tk_version = hookutils.exec_statement('from _tkinter import TK_VERSION; print(TK_VERSION)') # TK_LIBRARY is in the same prefix as Tcl. tk_root = os.path.join(os.path.dirname(tcl_root), 'tk%s' % tk_version) return tcl_root, tk_root def find_tcl_tk_shared_libs(tkinter_ext_file): """ Find Tcl and Tk shared libraries against which the _tkinter module is linked. Returns ------- list list containing two tuples, one for Tcl and one for Tk library, where each tuple contains the library name and its full path, i.e., [(tcl_lib, tcl_libpath), (tk_lib, tk_libpath)]. If a library is not found, the corresponding tuple elements are set to None. """ tcl_lib = None tcl_libpath = None tk_lib = None tk_libpath = None # Do not use bindepend.selectImports, as it ignores libraries seen during previous invocations. _tkinter_imports = bindepend.getImports(tkinter_ext_file) def _get_library_path(lib): if not compat.is_win and not compat.is_cygwin: # Non-Windows systems return the path of the library. path = lib else: # We need to find the library. path = bindepend.getfullnameof(lib) return path for lib in _tkinter_imports: # On some platforms, full path to the shared library is returned. So check only basename to prevent false # positive matches due to words tcl or tk being contained in the path. lib_name = os.path.basename(lib) lib_name_lower = lib_name.lower() # lower-case for comparisons if 'tcl' in lib_name_lower: tcl_lib = lib_name tcl_libpath = _get_library_path(lib) elif 'tk' in lib_name_lower: tk_lib = lib_name tk_libpath = _get_library_path(lib) return [(tcl_lib, tcl_libpath), (tk_lib, tk_libpath)] def _find_tcl_tk(tkinter_ext_file): """ Get a platform-specific 2-tuple of the absolute paths of the top-level external data directories for both Tcl and Tk, respectively. Returns ------- list 2-tuple that contains the values of `${TCL_LIBRARY}` and `${TK_LIBRARY}`, respectively. """ if compat.is_darwin: # On macOS, _tkinter extension is linked either against the system Tcl/Tk framework (older homebrew python, # python3 from XCode tools) or against bundled Tcl/Tk library (recent python.org builds, recent homebrew # python with python-tk). PyInstaller does not bundle data from system frameworks (as it does not not collect # shared libraries from them, either), so we need to determine what kind of Tcl/Tk we are dealing with. libs = find_tcl_tk_shared_libs(tkinter_ext_file) # Check the full path to the Tcl library. path_to_tcl = libs[0][1] # Starting with macOS 11, system libraries are hidden (unless both Python and PyInstaller's bootloader are built # against MacOS 11.x SDK). Therefore, libs may end up empty; but that implicitly indicates that the system # framework is used, so return (None, None) to inform the caller. if path_to_tcl is None: return None, None # Check if the path corresponds to the system framework, i.e., [/System]/Library/Frameworks/Tcl.framework/Tcl if 'Library/Frameworks/Tcl.framework' in path_to_tcl: return None, None # Do not collect system framework's data. # Bundled copy of Tcl/Tk; in this case, the dynamic library is # /Library/Frameworks/Python.framework/Versions/3.x/lib/libtcl8.6.dylib # and the data directories have standard layout that is handled by _find_tcl_tk_dir(). return _find_tcl_tk_dir() else: # On Windows and linux, data directories have standard layout that is handled by _find_tcl_tk_dir(). return _find_tcl_tk_dir() def _collect_tcl_modules(tcl_root): """ Get a list of TOC-style 3-tuples describing Tcl modules. The modules directory is separate from the library/data one, and is located at $tcl_root/../tclX, where X is the major Tcl version. Returns ------- Tree Such list, if the modules directory exists. """ # Obtain Tcl major version. tcl_version = hookutils.exec_statement('from tkinter import Tcl; print(Tcl().eval("info tclversion"))') tcl_version = tcl_version.split('.')[0] modules_dirname = 'tcl' + str(tcl_version) modules_path = os.path.join(tcl_root, '..', modules_dirname) if not os.path.isdir(modules_path): logger.warning('Tcl modules directory %s does not exist.', modules_path) return [] return Tree(modules_path, prefix=modules_dirname) def collect_tcl_tk_files(tkinter_ext_file): """ Get a list of TOC-style 3-tuples describing all external Tcl/Tk data files. Returns ------- Tree Such list. """ # Find Tcl and Tk data directory by analyzing the _tkinter extension. tcl_root, tk_root = _find_tcl_tk(tkinter_ext_file) # On macOS, we do not collect system libraries. Therefore, if system Tcl/Tk framework is used, it makes no sense to # collect its data, either. In this case, _find_tcl_tk() will return None, None - either deliberately (we found the # data paths, but ignore them) or not (starting with macOS 11, the data path cannot be found until shared library # discovery is fixed). if compat.is_darwin and not tcl_root and not tk_root: logger.info( "Not collecting Tcl/Tk data - either python is using macOS\' system Tcl/Tk framework, or Tcl/Tk data " "directories could not be found." ) return [] # TODO Shouldn't these be fatal exceptions? if not tcl_root: logger.error('Tcl/Tk improperly installed on this system.') return [] if not os.path.isdir(tcl_root): logger.error('Tcl data directory "%s" not found.', tcl_root) return [] if not os.path.isdir(tk_root): logger.error('Tk data directory "%s" not found.', tk_root) return [] tcltree = Tree(tcl_root, prefix='tcl', excludes=['demos', '*.lib', 'tclConfig.sh']) tktree = Tree(tk_root, prefix='tk', excludes=['demos', '*.lib', 'tkConfig.sh']) # If the current Tcl installation is a Teapot-distributed version of ActiveTcl and the current platform is Mac OS, # warn that this is bad. if compat.is_darwin: _warn_if_activetcl_or_teapot_installed(tcl_root, tcltree) # Collect Tcl modules. tclmodulestree = _collect_tcl_modules(tcl_root) return tcltree + tktree + tclmodulestree