bpo-34956: Fix macOS _tkinter use of Tcl/Tk in /Library/Frameworks (GH-20171)

_tkinter now builds and links with non-system Tcl and Tk frameworks if they
are installed in /Library/Frameworks as had been the case on older releases
of macOS. If a macOS SDK is explicitly configured, by using ./configure
--enable-universalsdk= or -isysroot, only a Library/Frameworks directory in
the SDK itself is searched. The default behavior can still be overridden with
configure --with-tcltk-includes and --with-tcltk-libs.
This commit is contained in:
Ned Deily 2020-05-18 04:32:38 -04:00 committed by GitHub
parent 58205a0217
commit 1731d6da26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 116 additions and 43 deletions

View File

@ -0,0 +1,6 @@
_tkinter now builds and links with non-system Tcl and Tk frameworks if they
are installed in /Library/Frameworks as had been the case on older releases
of macOS. If a macOS SDK is explicitly configured, by using ./configure
--enable-universalsdk= or -isysroot, only a Library/Frameworks directory in
the SDK itself is searched. The default behavior can still be overridden with
configure --with-tcltk-includes and --with-tcltk-libs.

153
setup.py
View File

@ -150,7 +150,9 @@ def sysroot_paths(make_vars, subdirs):
break
return dirs
MACOS_SDK_ROOT = None
MACOS_SDK_SPECIFIED = None
def macosx_sdk_root():
"""Return the directory of the current macOS SDK.
@ -162,8 +164,9 @@ def macosx_sdk_root():
(The SDK may be supplied via Xcode or via the Command Line Tools).
The SDK paths used by Apple-supplied tool chains depend on the
setting of various variables; see the xcrun man page for more info.
Also sets MACOS_SDK_SPECIFIED for use by macosx_sdk_specified().
"""
global MACOS_SDK_ROOT
global MACOS_SDK_ROOT, MACOS_SDK_SPECIFIED
# If already called, return cached result.
if MACOS_SDK_ROOT:
@ -173,8 +176,10 @@ def macosx_sdk_root():
m = re.search(r'-isysroot\s*(\S+)', cflags)
if m is not None:
MACOS_SDK_ROOT = m.group(1)
MACOS_SDK_SPECIFIED = MACOS_SDK_ROOT != '/'
else:
MACOS_SDK_ROOT = '/'
MACOS_SDK_SPECIFIED = False
cc = sysconfig.get_config_var('CC')
tmpfile = '/tmp/setup_sdk_root.%d' % os.getpid()
try:
@ -203,6 +208,28 @@ def macosx_sdk_root():
return MACOS_SDK_ROOT
def macosx_sdk_specified():
"""Returns true if an SDK was explicitly configured.
True if an SDK was selected at configure time, either by specifying
--enable-universalsdk=(something other than no or /) or by adding a
-isysroot option to CFLAGS. In some cases, like when making
decisions about macOS Tk framework paths, we need to be able to
know whether the user explicitly asked to build with an SDK versus
the implicit use of an SDK when header files are no longer
installed on a running system by the Command Line Tools.
"""
global MACOS_SDK_SPECIFIED
# If already called, return cached result.
if MACOS_SDK_SPECIFIED:
return MACOS_SDK_SPECIFIED
# Find the sdk root and set MACOS_SDK_SPECIFIED
macosx_sdk_root()
return MACOS_SDK_SPECIFIED
def is_macosx_sdk_path(path):
"""
Returns True if 'path' can be located in an OSX SDK
@ -1830,31 +1857,73 @@ class PyBuildExt(build_ext):
return True
def detect_tkinter_darwin(self):
# The _tkinter module, using frameworks. Since frameworks are quite
# different the UNIX search logic is not sharable.
# Build default _tkinter on macOS using Tcl and Tk frameworks.
#
# The macOS native Tk (AKA Aqua Tk) and Tcl are most commonly
# built and installed as macOS framework bundles. However,
# for several reasons, we cannot take full advantage of the
# Apple-supplied compiler chain's -framework options here.
# Instead, we need to find and pass to the compiler the
# absolute paths of the Tcl and Tk headers files we want to use
# and the absolute path to the directory containing the Tcl
# and Tk frameworks for linking.
#
# We want to handle here two common use cases on macOS:
# 1. Build and link with system-wide third-party or user-built
# Tcl and Tk frameworks installed in /Library/Frameworks.
# 2. Build and link using a user-specified macOS SDK so that the
# built Python can be exported to other systems. In this case,
# search only the SDK's /Library/Frameworks (normally empty)
# and /System/Library/Frameworks.
#
# Any other use case should be able to be handled explicitly by
# using the options described above in detect_tkinter_explicitly().
# In particular it would be good to handle here the case where
# you want to build and link with a framework build of Tcl and Tk
# that is not in /Library/Frameworks, say, in your private
# $HOME/Library/Frameworks directory or elsewhere. It turns
# out to be difficult to make that work automtically here
# without bringing into play more tools and magic. That case
# can be hamdled using a recipe with the right arguments
# to detect_tkinter_explicitly().
#
# Note also that the fallback case here is to try to use the
# Apple-supplied Tcl and Tk frameworks in /System/Library but
# be forewarned that they are deprecated by Apple and typically
# out-of-date and buggy; their use should be avoided if at
# all possible by installing a newer version of Tcl and Tk in
# /Library/Frameworks before bwfore building Python without
# an explicit SDK or by configuring build arguments explicitly.
from os.path import join, exists
framework_dirs = [
'/Library/Frameworks',
'/System/Library/Frameworks/',
join(os.getenv('HOME'), '/Library/Frameworks')
]
sysroot = macosx_sdk_root()
sysroot = macosx_sdk_root() # path to the SDK or '/'
# Find the directory that contains the Tcl.framework and Tk.framework
# bundles.
# XXX distutils should support -F!
if macosx_sdk_specified():
# Use case #2: an SDK other than '/' was specified.
# Only search there.
framework_dirs = [
join(sysroot, 'Library', 'Frameworks'),
join(sysroot, 'System', 'Library', 'Frameworks'),
]
else:
# Use case #1: no explicit SDK selected.
# Search the local system-wide /Library/Frameworks,
# not the one in the default SDK, othewise fall back to
# /System/Library/Frameworks whose header files may be in
# the default SDK or, on older systems, actually installed.
framework_dirs = [
join('/', 'Library', 'Frameworks'),
join(sysroot, 'System', 'Library', 'Frameworks'),
]
# Find the directory that contains the Tcl.framework and
# Tk.framework bundles.
for F in framework_dirs:
# both Tcl.framework and Tk.framework should be present
for fw in 'Tcl', 'Tk':
if is_macosx_sdk_path(F):
if not exists(join(sysroot, F[1:], fw + '.framework')):
break
else:
if not exists(join(F, fw + '.framework')):
break
if not exists(join(F, fw + '.framework')):
break
else:
# ok, F is now directory with both frameworks. Continure
# building
@ -1864,24 +1933,16 @@ class PyBuildExt(build_ext):
# will now resume.
return False
# For 8.4a2, we must add -I options that point inside the Tcl and Tk
# frameworks. In later release we should hopefully be able to pass
# the -F option to gcc, which specifies a framework lookup path.
#
include_dirs = [
join(F, fw + '.framework', H)
for fw in ('Tcl', 'Tk')
for H in ('Headers', 'Versions/Current/PrivateHeaders')
for H in ('Headers',)
]
# For 8.4a2, the X11 headers are not included. Rather than include a
# complicated search, this is a hard-coded path. It could bail out
# if X11 libs are not found...
include_dirs.append('/usr/X11R6/include')
frameworks = ['-framework', 'Tcl', '-framework', 'Tk']
# Add the base framework directory as well
compile_args = ['-F', F]
# All existing framework builds of Tcl/Tk don't support 64-bit
# architectures.
# Do not build tkinter for archs that this Tk was not built with.
cflags = sysconfig.get_config_vars('CFLAGS')[0]
archs = re.findall(r'-arch\s+(\w+)', cflags)
@ -1889,13 +1950,9 @@ class PyBuildExt(build_ext):
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
# Note: cannot use os.popen or subprocess here, that
# requires extensions that are not available here.
if is_macosx_sdk_path(F):
run_command("file %s/Tk.framework/Tk | grep 'for architecture' > %s"%(os.path.join(sysroot, F[1:]), tmpfile))
else:
run_command("file %s/Tk.framework/Tk | grep 'for architecture' > %s"%(F, tmpfile))
run_command(
"file {}/Tk.framework/Tk | grep 'for architecture' > {}".format(F, tmpfile)
)
with open(tmpfile) as fp:
detected_archs = []
for ln in fp:
@ -1904,16 +1961,26 @@ class PyBuildExt(build_ext):
detected_archs.append(ln.split()[-1])
os.unlink(tmpfile)
arch_args = []
for a in detected_archs:
frameworks.append('-arch')
frameworks.append(a)
arch_args.append('-arch')
arch_args.append(a)
compile_args += arch_args
link_args = [','.join(['-Wl', '-F', F, '-framework', 'Tcl', '-framework', 'Tk']), *arch_args]
# The X11/xlib.h file bundled in the Tk sources can cause function
# prototype warnings from the compiler. Since we cannot easily fix
# that, suppress the warnings here instead.
if '-Wstrict-prototypes' in cflags.split():
compile_args.append('-Wno-strict-prototypes')
self.add(Extension('_tkinter', ['_tkinter.c', 'tkappinit.c'],
define_macros=[('WITH_APPINIT', 1)],
include_dirs=include_dirs,
libraries=[],
extra_compile_args=frameworks[2:],
extra_link_args=frameworks))
extra_compile_args=compile_args,
extra_link_args=link_args))
return True
def detect_tkinter(self):