Tk Source Code

View Ticket
Login
Ticket UUID: 1562e10c58f83f0faf2b35feb356c7ec1a0c6300
Title: Installing Tcl on macOS makes other versions of tclsh unable to find Tk.
Type: Bug Version: multiple
Submitter: marc_culler Created on: 2024-08-11 16:38:46
Subsystem: 83. Mac OS X Build Assigned To: marc_culler
Priority: 5 Medium Severity: Important
Status: Open Last Modified: 2024-08-16 13:23:35
Resolution: None Closed By: nobody
    Closed on: 2024-08-13 13:13:41
Description:
On macOS, with the framework install using the GNUmakefile in the macosx
directory, installing Tcl 9.0 makes tclsh8.6 unable to find Tk and vice
versa.

I don't know if this affects other platforms or other installation methods.

It would be convenient, especially right now, to be able to have all
versions of tclsh usable at the same time.
User Comments: marc_culler (claiming to be Marc Culler) added on 2024-08-16 13:23:35:
How about making the Tk 9.X pkgIndex.tcl check whether it is being
called by Tcl 8.Y, and, if so, to not run "package if needed Tk 9.X"?

Wouldn't that make Tk 8.Y and 9.X independent from each other?  Isn't that
what we want?

jan.nijtmans added on 2024-08-16 08:42:54:

> Why does Tcl 8.7 exit with an error when it reads a pkgIndex.tcl file for a version of Tk that has an incompatible stubs mechanism?

Tcl reads all pkgIndex.tcl files just fine. The problem occurs later. After reading all pkgIndex.tcl files, Tcl determines which is the best version to be loaded. It then inspects the "package ifneeded Tk <version>", and runs this. For tclsh8.7, the "package ifneeded Tk 9.0b4" gives:

    load /Library/Frameworks/Tk.framework/Versions/9.0/Tk
For tclsh9.0, the "package ifneeded Tk 9.0b4" gives:
    load /Library/Frameworks/Tk.framework/Versions/9.0/Tk
It's the same! That means that if `/Library/Frameworks/Tk.framework/Versions/9.0/Tk` is built against Tcl 8.7 headers, then it will work with tclsh8.7 and fail with tclsh9.0. If it's built against Tcl 9.0 headers, it will work with tclsh9.0 and fail with tclsh8.7. We cannot have both.

The solution is to build Tk 9.0 with Tcl 8.7 headers, and install it as a new shared library `tcl8Tk`, in exactly the same way as the debug version of `Tk`, `Tk_debug` is built.


jan.nijtmans added on 2024-08-15 21:26:03:

Proposed solution here

Demo:

$ tclsh8.7
% package require Tk 8
8.7b1
% info loaded
{/Library/Frameworks/Tk.framework/Versions/8.7/Tk Tk}
% exit
$ tclsh8.7
% package require Tk 9
version conflict for package "Tcl": have 8.7b1, need 8.7.0
% exit
$ tclsh8.7
% package require Tk 9
version conflict for package "Tcl": have 8.7b1, need 8.7.0
% info loaded
{/Library/Frameworks/Tk.framework/Versions/9.0/tcl8Tk Tk}
% exit
$ tclsh9.0
% package require Tk
9.0b4
% info loaded
{/Library/Frameworks/Tk.framework/Versions/9.0/Tk Tk}

Almost works ... just the "version conflict", which will disappear anyway when Tcl 8.7.0 is final.


marc_culler (claiming to be Marc Culler) added on 2024-08-14 22:49:41:
Thanks, Jan.  That sounds good.

Here is the thing that I don't understand.  Why does Tcl 8.7 exit with an error
when it reads a pkgIndex.tcl file for a version of Tk that has an incompatible
stubs mechanism?  Why doesn't it just go on and read the next pkgIndex.tcl
file on its list, until it finds one that it can load?

jan.nijtmans added on 2024-08-14 21:27:58:

> Yes, but in this commit, I added "8.6" to the path too. That doesn't seem to work .....

I reverted that now.

Now, if you install both Tk 8.7 (compiled with Tcl 8.7) and Tk 9.0 (compiled with Tcl 9.0), this is the effect:

192:tk8.7 jan$ tclsh8.7
% 
% package require Tk
interpreter uses an incompatible stubs mechanism

Since 8.7 is not going to be released anytime soon, there's still time to think how to handle that. For 8.6 and 9.0 everything works now as expected.


marc_culler (claiming to be Marc Culler) added on 2024-08-13 22:29:15:
This must be what you are worried about:

> When Tk 8.7 goes final, a "package require Tk" will open Tk 8.7.
> I don't think that's intended

Do you mean that tclsh8.6 will open Tk 8.7?  I agree that seems unintended.
But why not just handle that with Tcl version restrictions in the Tk
pkgIndex.tcl?  The Tk 8.6 pkgIndex.tcl could just return if the Tcl
version is at least 8.7.

I assume it would be OK for tclsh8.7 to open Tk 8.7, right?

Is it supposed to be OK for Tcl 9 to open Tk 8.6?  I wouldn't think so.

marc_culler (claiming to be Marc Culler) added on 2024-08-13 21:51:13:
I see that you were trying to deal with a hypothetical 8.7 issue which
has not arisen yet.  But I don't think I understand what that issue is.
Maybe you could add some details?

marc_culler (claiming to be Marc Culler) added on 2024-08-13 21:47:06:
I did not notice that.  I would not expect that to work, though.

This is what I see with the commit preceding that one, after completely
removing my build directory and recompiling both versions of Tcl and Tk.

culler@abner ~ % tclsh8.6
% package require Tk
8.6.14
                                                                           culler@abner ~ % tclsh9.0
% package require Tk
9.0b3

That is what I was looking for.

jan.nijtmans added on 2024-08-13 21:40:18:

Yes, but in this commit, I added "8.6" to the path too. That doesn't seem to work .....


marc_culler (claiming to be Marc Culler) added on 2024-08-13 21:30:33:
No, I don't agree.  First of all, it was working for me with 8.6.

Second, if you look at library/auto.tcl you will find:

        foreach d $auto_path {
            lappend dirs [file join $d $basename$version]
            if {$tcl_platform(platform) eq "unix"
                    && $tcl_platform(os) eq "Darwin"} {
                # 4. On MacOSX, check the Resources/Scripts subdir too
                lappend dirs [file join $d $basename$version Resources Scripts]
            }
        }

So there is no need to add "Resources/Scripts" to the path.

My experiments show that if PATH is a path in the auto_path directory
then tclsh will find any pkgIndex.tcl file which matches the glob:

$PATH/*/Resources/Scripts

jan.nijtmans added on 2024-08-13 21:03:34:

I don't think it works now, as it is in the core-8-6-branch. this appears to work better. Agreed?


marc_culler (claiming to be Marc Culler) added on 2024-08-13 13:13:41:
Thanks, Jan.

I knew there was some black magic involved with autoconf!

As I recall, the reason I changed AC_PREREQ was because autoconf
complained about the version not being installed.  I don't remember
the details though.

jan.nijtmans added on 2024-08-13 08:29:26:

Thanks!

I see 3 problems left.

1) AC_PREREQ is meant to specify the _minimum_ version of autoconf which is supported. For Tcl 8.6, this is 2.59, for 8.7 and 9.0 this is 2.69. We use a custom version of autoconf-2.59, because Tcl suffers from some bugs (which were all fixed in autoconf-2.69+)

2) When Tk 8.7 goes final, a "package require Tk" will open Tk 8.7. I don't think that's intended. Therefore, in Tcl 8.6 it's better to add "/8.6" to the path.

3) I'll leave that for another day.

1) and 2) are fixed now.


marc_culler (claiming to be Marc Culler) added on 2024-08-12 22:38:51:
Oh.  I see now.  If exact is not true it uses 9.0b2.  So 9.0b3 should be OK.

Moreover it is OK!

When I recompiled and reinstalled everything both tclsh versions work correctly.
I guess I was just having the usual problem of reusing old build directories.

Sorry for making noise.

Maybe I will go ahead and close this ticket before I make any more noise.

marc_culler (claiming to be Marc Culler) added on 2024-08-12 22:13:02:
Hi Jan,

OK.  You are right.  So why would Tcl 9.0b3 require exactly Tk 9.0b2
when Tk 9.0b3 exists?  Is that on purpose?

BTW, here is the change to tcl.h that makes everything work for me (after
recompiling both Tcl 9.0 and Tk 9.0):

 #   define Tcl_InitStubs(interp, version, exact) \
-	(Tcl_InitStubs)(interp, (((exact)&1) ? (version) : "9.0b2"), \
+	(Tcl_InitStubs)(interp, version, \
 	    (exact)|(TCL_MAJOR_VERSION<<8)|(TCL_MINOR_VERSION<<16), \
 	    TCL_STUB_MAGIC)

jan.nijtmans added on 2024-08-12 21:20:29:

> ... because Tcl_Init_Stubs has been modified in tcl.h to force an exact version match for Tk 9.0b3.

That's not what Tcl_InitStubs() is doing! The current implementation just accepts the compiled extension to run with "9.0b2" and higher.


marc_culler (claiming to be Marc Culler) added on 2024-08-12 20:50:09:
The change which I committed allows tclsh to find the pkgIndex.tcl files
for all installed versions of Tk.  But it does not work to allow all versions
of tclsh to load Tk.  It was working fine for me, but I forgot that I was
testing with a modified tcl.h file.

With the current tcl.h file for Tcl 9.0, you get this:

% tclsh9.0 
% package require Tk
interpreter uses an incompatible stubs mechanism

That error is being generated by Tcl_Init_Stubs and causes the package
require command to fail, without loading Tk, because Tcl_Init_Stubs has
been modified in tcl.h to force an exact version match for Tk 9.0b3.
The failure occurs when tclsh9.0 reads the pkgIndex.tcl file for Tk 8.6.

I don't understand why this exact match is being required.  But whatever
the reason, it should still be possible for tclsh9.0 to execute a
pkgIndex.tcl script for an incompatible version of Tk without failing
when a compatible version of Tk is, in fact, available.  If one version
of Tk is incompatible it should move on to the next one and not fail
until it has checked that all versions are incompatible.

I think this is a bug.  But a different bug.  The fact that it might go
away when there is a final release of Tcl / Tk 9.0 does not mean that
it is correct.

marc_culler (claiming to be Marc Culler) added on 2024-08-12 14:18:45:
I said something completely wrong in one of my posts:

  The tclsh executable does not open the Tk library as a
  dynamic library, of course.  The problem is presumably that tclsh cannot
  find the correct pkgIndex.tcl file for its associated Tk.

In fact, tclsh *does* open the Tk library as a dynamic library.  That is
how extensions work, after all.

The load command is in the pkgIndex.tcl file. It attempts to build a path
to the Tk library from the value $dir where the variable dir is set before
running the pkgIndex.tcl script to the path of the directory containing
the pkgIndex.tcl script.  The path constructed by the load command in the
Tk pkgIndex.tcl file is:
 $dir/../../Tk
That means that if the pkgIndex.tcl script has path:
  /Library/Frameworks/Tk.framework/Versions/X.Y/Resources/Scripts/pkgIndex.tcl
then the Tcl load command will use dlopen to load the shared library:
  /Library/Frameworks/Tk.framework/Versions/X.Y/Tk

marc_culler (claiming to be Marc Culler) added on 2024-08-12 13:57:41:
I found a simple 1-line fix for this.  The fix is to add one more directory
to TCL_PACKAGE_PATH in the Tcl unix/configure script.  The path that
needs to be added is:

 /Library/Frameworks/Tk.framework/Versions

(The fix would only add that for framework builds, which already
have a special TCL_PACKAGE_PATH).

Adding that path causes tclsh to search for the Tk pkgIndex.tcl file
in every directory matching the pattern:

 /Library/Frameworks/Tk.frameworks/Versions/*/Resources/Scripts

so it finds the pkgIndex.tcl for every (framework) installed version of Tk.

That unexpected search behavior is already built-in to framework builds
of Tcl.  The Tcl macosx/README file says:

  Given a potential package directory $pkg, Tcl on OSX checks for the file
  $pkg/Resources/Scripts/pkgIndex.tcl as well as the usual
  $pkg/pkgIndex.tcl. This allows building extensions as frameworks with
  all script files contained in the Resources/Scripts directory of the
  framework.

Two comments about that.  First, it seems that no one is using this feature
to package Tcl extensions as frameworks, but it does seem like a good idea.
Especially given that it solves this problem so neatly.

Second, one should be aware that a "potential package directory" does not
mean a directory in the TCL_PACKAGE_PATH.  It means a *subdirectory* of a
directory in TCL_PACKAGE_PATH.  That is what causes Tcl to check through
all of the installed Tk Versions, which is what is needed to fix this.

marc_culler (claiming to be Marc Culler) added on 2024-08-11 19:45:03:
Here is one answer to my original question:

 "How can you make both tclsh8.6 and tclsh9.0 work at the same time on the same
  system?"

The answer is:

Create a ~/.tclshrc file containing two lines:
lappend auto_path /Library/Frameworks/Tk.framework/Versions/9.0/Resources/Scripts
lappend auto_path /Library/Frameworks/Tk.framework/Versions/8.6/Resources/Scripts


It seems like there should be a better fix.  But that does work.

marc_culler (claiming to be Marc Culler) added on 2024-08-11 19:41:48:
I made some progress.

In the case where Tk 8.6 was the most recent, so tclsh9.0 was unable to find
Tk, I tried appending
 /Library/Frameworks/Tk.framework/Versions/9.0/Resources/Scripts
to the autopath list.  Then it was able to find Tk.

So perhaps the problem is that the Tcl auto_path is not being initialized
in a way which allows it to find its matching Tk.

But I don't understand how *any* tclsh is able to find *any* Tk.  The default
auto_path for tclsh9.0 is:

/Library/Frameworks/Tcl.framework/Versions/9.0/Resources/Scripts
/Library/Frameworks/Tcl.framework/Versions/9.0/Resources
/usr/local/lib
/Users/culler/Library/Tcl
/Library/Tcl
/Users/culler/Library/Frameworks
/Library/Frameworks

None of those contain a pkgIndex.tcl for the Tk package.  So tclsh must look
somewhere other than in its auto_path items.  And, wherever those other places
may be, they are not correct.

marc_culler (claiming to be Marc Culler) added on 2024-08-11 19:19:54:
Here is one hint.  I installed Tk 9.0 first, then Tk 8.6.  That means that
tclsh8.6 can find Tk but tclsh9.0 can not.

I then changed the symlink /Library/Frameworks/Tk.framework/Versions/Current
to make it point to 9.0 instead of 8.6.

That had no effect.  It was still the case that tclsh9.0 could not find Tk.

I think that means that the Versions/Current symlink is not being used by tclsh.

marc_culler (claiming to be Marc Culler) added on 2024-08-11 19:01:27:
Thanks Kevin.

Looking again, I see that the top level Resources/Scripts directory in the
Tk.framework is actually a symlink:

Resources -> Versions/Current/Resources

So maybe the issue is that all versions of tclsh are looking in the top
level Resources and therefore they all see only the most recently installed
version of Tk, which may not match.  Perhaps, instead of telling us that
they found a Tk which did not match, they just tell us that they could not
find Tk.  And of course they don't tell us where they were looking.  And
it seems that none of us know the answer to that question either.

marc_culler (claiming to be Marc Culler) added on 2024-08-11 18:46:32:
Unfortunately, it is not as simple as I thought.  (As usual!)  There already
are Scripts directories both in the top level Resources and in
Versions/X.Y/Resources.

So I don't know why tclsh is not able to find the pkgIndex.tcl file for its
associated version of Tk.  Where is it looking?  Could it be trying to use
/Library/Frameworks/Tk.framework/Versions/Current ?
That symlink would change with each install of a different version of Tk.

kevin_walzer added on 2024-08-11 18:35:00:
Marc, go ahead and make the changes to configure.ac and generate a new configure with autoconf. I've done it before.

marc_culler (claiming to be Marc Culler) added on 2024-08-11 18:22:27:
Here it is:

unix/configure.ac:    TK_LIBRARY="${libdir}/Resources/Scripts"
unix/configure.ac:    TK_PKG_DIR="Resources/Scripts"
unix/configure:    TK_LIBRARY="${libdir}/Resources/Scripts"
unix/configure:    TK_PKG_DIR="Resources/Scripts"

The actual mistake, I think, is this line in unix/configure:

TK_PKG_DIR="Resources/Scripts"

I think that should be

TK_PKG_DIR="Versions/${TK_VERSION}/Resources/Scripts"

I don't know what the rules are regarding changes to unix/configure.  Am I
allowed to edit configure.ac, generate a new configure with autoconf-2.72
and commit both configure.ac and configure?  Or is there some other black
magic involved.

marc_culler (claiming to be Marc Culler) added on 2024-08-11 17:57:49:
> means that the Tcl shared library on MacOS doesn't have a version number
> attached. That means you cannot install any two of Tcl 8.5, 8.6, 8.7 and 9.0,
> then add the paths to those shared libraries to LD_LIBRARY_PATH, and then
> expect the system to be able to keep those apart

I don't think that is correct.  First of all, the framework install uses
the Version directory of Apple's framework bundle to distinguish the versions.
So the following different versions coexist in my framework:
/Library/Frameworks/Tcl.framework/Versions/8.6/Tcl
/Library/Frameworks/Tcl.framework/Versions/9.0/Tcl

Second, there is no LD_LIBRARY_PATH involved.  Apple does not use
that variable at all.  It does have a DYLD_LIBRARY_PATH but that is also
not involved here.  The Mach-O binary format specifies the library load
paths within the executable file.  In the case of tclsh these paths are:

for tclsh8.6: /Library/Frameworks/Tcl.framework/Versions/8.6/Tcl
for tclsh9.0: /Library/Frameworks/Tcl.framework/Versions/9.0/Tcl

So that is all correct. Moreover, the problem is not that tclsh cannot
find the Tcl library. Both versions of tclsh run fine as long as you do
not attempt to load the Tk package.  However, only one of them is able
to find Tk.  The tclsh executable does not open the Tk library as a
dynamic library, of course.  The problem is presumably that tclsh cannot
find the correct pkgIndex.tcl file for its associated Tk.

While your model of how dynamic libraries work on macOS is incorrect, I
think you have nonetheless explained the problem to me.  What I now see
is that, even though the Tk libraries are correctly separated by version
using the Versions directory in the framework, the same is not true for
the Tk Scripts directories.  There is only one Scripts directory, which
gets over-written each time that a version of Tk is installed.  It is:

/Library/Frameworks/Tk.framework/Resources/Scripts

There should be separate Scripts directories for each version of Tk:

/Library/Frameworks/Tk.framework/Versions/X.Y/Resources/Scripts

It is true that Apple recommends putting text files in the Resources
directory of a framework. But they provide for both version-independent
and version-dependent Resources.  Someone apparently decided that the Tk
Scripts were independent of Tk version and used the top level Resources
instead of having separate Scripts directories for different Tk versions.

So I guess all I have to do is grep for Scripts in the source tree and
figure out where that mistake actually got implemented.  I am betting
that the mistake was made about 20 years ago.

Thanks.

jan.nijtmans added on 2024-08-11 17:08:16:

The first problem I see in the framework build of Tcl is here. The line

TCL_LIB_FILE="Tcl"
means that the Tcl shared library on MacOS doesn't have a version number attached. That means you cannot install any two of Tcl 8.5, 8.6, 8.7 and 9.0, then add the paths to those shared libraries to LD_LIBRARY_PATH, and then expect the system to be able to keep those apart.....

So, I would recommend to change this line to either

TCL_LIB_FILE="Tcl9"
or maybe
TCL_LIB_FILE="Tcl9.0"
but I don't know of framework builds accept a dot in the shared library name, and I don't know what other renamings will be needed as a result of this change.

I don't know enough about how MacOS frameworks are supposed to work.

> I don't know if this affects other platforms or other installation methods.

This problem is MacOS only.