tcl_tk.py 9.9 KB

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