123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- #-----------------------------------------------------------------------------
- # 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
|