SpaceLife-Updater/venv/lib64/python3.12/site-packages/PyInstaller/utils/hooks/tcl_tk.py

276 lines
10 KiB
Python
Executable File

#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, 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 os
from PyInstaller import compat
from PyInstaller import isolated
from PyInstaller import log as logging
from PyInstaller.building.datastruct import Tree
from PyInstaller.depend import bindepend
logger = logging.getLogger(__name__)
TK_ROOTNAME = 'tk'
TCL_ROOTNAME = 'tcl'
@isolated.decorate
def _get_tcl_tk_info():
"""
Isolated-subprocess helper to retrieve the basic Tcl/Tk information:
- tcl_dir = path to the Tcl library/data directory.
- tcl_version = Tcl version
- tk_version = Tk version
- tcl_theaded = boolean indicating whether Tcl/Tk is built with multi-threading support.
"""
try:
import tkinter
from _tkinter import TCL_VERSION, TK_VERSION
except ImportError:
# tkinter unavailable
return None, None, None, False
try:
tcl = tkinter.Tcl()
except tkinter.TclError: # e.g. "Can't find a usable init.tcl in the following directories: ..."
return None, None, None, False
# Query the location of Tcl library/data directory.
tcl_dir = tcl.eval("info library")
# Check if Tcl/Tk is built with multi-threaded support (built with --enable-threads), as indicated by the presence
# of optional `threaded` member in `tcl_platform` array.
try:
tcl.getvar("tcl_platform(threaded)") # Ignore the actual value.
tcl_threaded = True
except tkinter.TclError:
tcl_threaded = False
return tcl_dir, TCL_VERSION, TK_VERSION, tcl_threaded
# Populate the variables. If `tkinter` is unavailable, the values are set to `None` or `False`.
(
tcl_dir,
tcl_version,
tk_version,
tcl_threaded,
) = _get_tcl_tk_info()
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
# On macOS, system encoding is UTF-8
with open(init_resource, 'r', encoding='utf8') 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_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
for _, lib_path in bindepend.get_imports(tkinter_ext_file): # (name, fullpath) tuple
if lib_path is None:
continue # Skip unresolved entries
# For comparison, take basename of lib_path. On macOS, lib_name returned by get_imports is in fact referenced
# name, which is not necessarily just a basename.
lib_name = os.path.basename(lib_path)
lib_name_lower = lib_name.lower() # lower-case for comparisons
if 'tcl' in lib_name_lower:
tcl_lib = lib_name
tcl_libpath = lib_path
elif 'tk' in lib_name_lower:
tk_lib = lib_name
tk_libpath = lib_path
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 code below.
else:
# On Windows and linux, data directories have standard layout that is handled by code below.
pass
# The Tcl library location is already stored in `tcl_dir` global variable. The Tk library is in the same prefix, so
# construct the path using `tk_version` global variable.
tk_dir = os.path.join(os.path.dirname(tcl_dir), f"tk{tk_version}")
return tcl_dir, 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_major_version = tcl_version.split('.')[0]
modules_dirname = f"tcl{tcl_major_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 []
# Collect Tcl and Tk scripts from their corresponding library/data directories. In contrast to source directories,
# which are typically versioned (tcl8.6, tk8.6), the target directories are unversioned (tcl, tk); they are added
# to the Tcl/Tk search path via runtime hook for _tkinter, which sets the `TCL_LIBRARY` and `TK_LIBRARY` environment
# variables.
tcltree = Tree(tcl_root, prefix=TCL_ROOTNAME, excludes=['demos', '*.lib', 'tclConfig.sh'])
tktree = Tree(tk_root, prefix=TK_ROOTNAME, 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