tcl_tk.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2013-2021, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. import locale
  12. import os
  13. from PyInstaller import compat
  14. from PyInstaller import log as logging
  15. from PyInstaller.building.datastruct import Tree
  16. from PyInstaller.depend import bindepend
  17. from PyInstaller.utils import hooks as hookutils
  18. logger = logging.getLogger(__name__)
  19. TK_ROOTNAME = 'tk'
  20. TCL_ROOTNAME = 'tcl'
  21. def _warn_if_activetcl_or_teapot_installed(tcl_root, tcltree):
  22. """
  23. If the current Tcl installation is a Teapot-distributed version of ActiveTcl *and* the current platform is macOS,
  24. log a non-fatal warning that the resulting executable will (probably) fail to run on non-host systems.
  25. PyInstaller does *not* freeze all ActiveTcl dependencies -- including Teapot, which is typically ignorable. Since
  26. Teapot is *not* ignorable in this case, this function warns of impending failure.
  27. See Also
  28. -------
  29. https://github.com/pyinstaller/pyinstaller/issues/621
  30. """
  31. import macholib.util
  32. # System libraries do not experience this problem.
  33. if macholib.util.in_system_path(tcl_root):
  34. return
  35. # Absolute path of the "init.tcl" script.
  36. try:
  37. init_resource = [r[1] for r in tcltree if r[1].endswith('init.tcl')][0]
  38. except IndexError:
  39. # If such script could not be found, silently return.
  40. return
  41. mentions_activetcl = False
  42. mentions_teapot = False
  43. # TCL/TK reads files using the system encoding:
  44. # https://www.tcl.tk/doc/howto/i18n.html#system_encoding
  45. with open(init_resource, 'r', encoding=locale.getpreferredencoding()) as init_file:
  46. for line in init_file.readlines():
  47. line = line.strip().lower()
  48. if line.startswith('#'):
  49. continue
  50. if 'activetcl' in line:
  51. mentions_activetcl = True
  52. if 'teapot' in line:
  53. mentions_teapot = True
  54. if mentions_activetcl and mentions_teapot:
  55. break
  56. if mentions_activetcl and mentions_teapot:
  57. logger.warning(
  58. """
  59. You appear to be using an ActiveTcl build of Tcl/Tk, which PyInstaller has
  60. difficulty freezing. To fix this, comment out all references to "teapot" in:
  61. %s
  62. See https://github.com/pyinstaller/pyinstaller/issues/621 for more information.
  63. """ % init_resource
  64. )
  65. def _find_tcl_tk_dir():
  66. """
  67. Get a platform-agnostic 2-tuple of the absolute paths of the top-level external data directories for both
  68. Tcl and Tk, respectively.
  69. Returns
  70. -------
  71. list
  72. 2-tuple that contains the values of `${TCL_LIBRARY}` and `${TK_LIBRARY}`, respectively.
  73. """
  74. # Python code to get path to TCL_LIBRARY.
  75. tcl_root = hookutils.exec_statement('from tkinter import Tcl; print(Tcl().eval("info library"))')
  76. tk_version = hookutils.exec_statement('from _tkinter import TK_VERSION; print(TK_VERSION)')
  77. # TK_LIBRARY is in the same prefix as Tcl.
  78. tk_root = os.path.join(os.path.dirname(tcl_root), 'tk%s' % tk_version)
  79. return tcl_root, tk_root
  80. def find_tcl_tk_shared_libs(tkinter_ext_file):
  81. """
  82. Find Tcl and Tk shared libraries against which the _tkinter module is linked.
  83. Returns
  84. -------
  85. list
  86. list containing two tuples, one for Tcl and one for Tk library, where each tuple contains the library name and
  87. its full path, i.e., [(tcl_lib, tcl_libpath), (tk_lib, tk_libpath)]. If a library is not found, the
  88. corresponding tuple elements are set to None.
  89. """
  90. tcl_lib = None
  91. tcl_libpath = None
  92. tk_lib = None
  93. tk_libpath = None
  94. # Do not use bindepend.selectImports, as it ignores libraries seen during previous invocations.
  95. _tkinter_imports = bindepend.getImports(tkinter_ext_file)
  96. def _get_library_path(lib):
  97. if not compat.is_win and not compat.is_cygwin:
  98. # Non-Windows systems return the path of the library.
  99. path = lib
  100. else:
  101. # We need to find the library.
  102. path = bindepend.getfullnameof(lib)
  103. return path
  104. for lib in _tkinter_imports:
  105. # On some platforms, full path to the shared library is returned. So check only basename to prevent false
  106. # positive matches due to words tcl or tk being contained in the path.
  107. lib_name = os.path.basename(lib)
  108. lib_name_lower = lib_name.lower() # lower-case for comparisons
  109. if 'tcl' in lib_name_lower:
  110. tcl_lib = lib_name
  111. tcl_libpath = _get_library_path(lib)
  112. elif 'tk' in lib_name_lower:
  113. tk_lib = lib_name
  114. tk_libpath = _get_library_path(lib)
  115. return [(tcl_lib, tcl_libpath), (tk_lib, tk_libpath)]
  116. def _find_tcl_tk(tkinter_ext_file):
  117. """
  118. Get a platform-specific 2-tuple of the absolute paths of the top-level external data directories for both
  119. Tcl and Tk, respectively.
  120. Returns
  121. -------
  122. list
  123. 2-tuple that contains the values of `${TCL_LIBRARY}` and `${TK_LIBRARY}`, respectively.
  124. """
  125. if compat.is_darwin:
  126. # On macOS, _tkinter extension is linked either against the system Tcl/Tk framework (older homebrew python,
  127. # python3 from XCode tools) or against bundled Tcl/Tk library (recent python.org builds, recent homebrew
  128. # python with python-tk). PyInstaller does not bundle data from system frameworks (as it does not not collect
  129. # shared libraries from them, either), so we need to determine what kind of Tcl/Tk we are dealing with.
  130. libs = find_tcl_tk_shared_libs(tkinter_ext_file)
  131. # Check the full path to the Tcl library.
  132. path_to_tcl = libs[0][1]
  133. # Starting with macOS 11, system libraries are hidden (unless both Python and PyInstaller's bootloader are built
  134. # against MacOS 11.x SDK). Therefore, libs may end up empty; but that implicitly indicates that the system
  135. # framework is used, so return (None, None) to inform the caller.
  136. if path_to_tcl is None:
  137. return None, None
  138. # Check if the path corresponds to the system framework, i.e., [/System]/Library/Frameworks/Tcl.framework/Tcl
  139. if 'Library/Frameworks/Tcl.framework' in path_to_tcl:
  140. return None, None # Do not collect system framework's data.
  141. # Bundled copy of Tcl/Tk; in this case, the dynamic library is
  142. # /Library/Frameworks/Python.framework/Versions/3.x/lib/libtcl8.6.dylib
  143. # and the data directories have standard layout that is handled by _find_tcl_tk_dir().
  144. return _find_tcl_tk_dir()
  145. else:
  146. # On Windows and linux, data directories have standard layout that is handled by _find_tcl_tk_dir().
  147. return _find_tcl_tk_dir()
  148. def _collect_tcl_modules(tcl_root):
  149. """
  150. Get a list of TOC-style 3-tuples describing Tcl modules. The modules directory is separate from the library/data
  151. one, and is located at $tcl_root/../tclX, where X is the major Tcl version.
  152. Returns
  153. -------
  154. Tree
  155. Such list, if the modules directory exists.
  156. """
  157. # Obtain Tcl major version.
  158. tcl_version = hookutils.exec_statement('from tkinter import Tcl; print(Tcl().eval("info tclversion"))')
  159. tcl_version = tcl_version.split('.')[0]
  160. modules_dirname = 'tcl' + str(tcl_version)
  161. modules_path = os.path.join(tcl_root, '..', modules_dirname)
  162. if not os.path.isdir(modules_path):
  163. logger.warning('Tcl modules directory %s does not exist.', modules_path)
  164. return []
  165. return Tree(modules_path, prefix=modules_dirname)
  166. def collect_tcl_tk_files(tkinter_ext_file):
  167. """
  168. Get a list of TOC-style 3-tuples describing all external Tcl/Tk data files.
  169. Returns
  170. -------
  171. Tree
  172. Such list.
  173. """
  174. # Find Tcl and Tk data directory by analyzing the _tkinter extension.
  175. tcl_root, tk_root = _find_tcl_tk(tkinter_ext_file)
  176. # On macOS, we do not collect system libraries. Therefore, if system Tcl/Tk framework is used, it makes no sense to
  177. # collect its data, either. In this case, _find_tcl_tk() will return None, None - either deliberately (we found the
  178. # data paths, but ignore them) or not (starting with macOS 11, the data path cannot be found until shared library
  179. # discovery is fixed).
  180. if compat.is_darwin and not tcl_root and not tk_root:
  181. logger.info(
  182. "Not collecting Tcl/Tk data - either python is using macOS\' system Tcl/Tk framework, or Tcl/Tk data "
  183. "directories could not be found."
  184. )
  185. return []
  186. # TODO Shouldn't these be fatal exceptions?
  187. if not tcl_root:
  188. logger.error('Tcl/Tk improperly installed on this system.')
  189. return []
  190. if not os.path.isdir(tcl_root):
  191. logger.error('Tcl data directory "%s" not found.', tcl_root)
  192. return []
  193. if not os.path.isdir(tk_root):
  194. logger.error('Tk data directory "%s" not found.', tk_root)
  195. return []
  196. tcltree = Tree(tcl_root, prefix='tcl', excludes=['demos', '*.lib', 'tclConfig.sh'])
  197. tktree = Tree(tk_root, prefix='tk', excludes=['demos', '*.lib', 'tkConfig.sh'])
  198. # If the current Tcl installation is a Teapot-distributed version of ActiveTcl and the current platform is Mac OS,
  199. # warn that this is bad.
  200. if compat.is_darwin:
  201. _warn_if_activetcl_or_teapot_installed(tcl_root, tcltree)
  202. # Collect Tcl modules.
  203. tclmodulestree = _collect_tcl_modules(tcl_root)
  204. return tcltree + tktree + tclmodulestree