Tcl Source Code

View Ticket
Login
Ticket UUID: a998df01f3256f79f4d7d635517130012b4001f5
Title: Attempt to load platform package in a pkgIndex.tcl generates circular dependency error
Type: Bug Version: 8.7
Submitter: apnadkarni Created on: 2022-07-04 17:08:12
Subsystem: 39. Package Manager Assigned To: jan.nijtmans
Priority: 5 Medium Severity: Minor
Status: Closed Last Modified: 2022-07-05 09:43:39
Resolution: Invalid Closed By: apnadkarni
    Closed on: 2022-07-05 09:43:39
Description:

If the platform package is required within a pkgIndex.tcl file for a package X, a circular dependency error is generated on package require X. The errors are:

error reading package index file d:/tcl/xx/p/pkgIndex.tcl: circular package dependency: attempt to provide platform 1.0.18 requires platform
error reading package index file d:/tcl/xx/p/pkgIndex.tcl: circular package dependency: attempt to provide platform 1.0.18 requires platform
error reading package index file d:/tcl/xx/p/pkgIndex.tcl: circular package dependency: attempt to provide platform 1.0.18 requires platform
.
.

This does not happen with 8.6. It also does not seem to happen if other packages are required within the pkgIndex.tcl e.g. the registry package or if the platform package is loaded beforehand.

A sample testpkg is attached.

User Comments: apnadkarni added on 2022-07-05 09:43:27:

I'll close this with one last comment. I know the workarounds possible using apply etc. I already do this in some cases, see for example https://github.com/apnadkarni/tcl-cffi/blob/main/pkgIndex.tcl.in.

My beef is with the breaking of valid (as in nothing banned this) 8.6 pkgIndex.tcl scripts that took the simpler approach.


jan.nijtmans added on 2022-07-05 09:11:00:

> I'd construct the pkgIndex.tcl like this:

Seconded


dkf added on 2022-07-05 09:01:44:

I'd construct the pkgIndex.tcl like this:

package ifneeded testpkg 0.1 [list apply {{dir} {
    package require platform
    set plat [platform::identify]
    set ext [info sharedlibextension]
    uplevel 1 [list load [file join $dir $plat mybinaryextension.$ext]]
}} $dir]
The uplevel 1 wrapper is, of course, hyper-correctness that only a few C packages would ever need. You could probably get away with:
package ifneeded testpkg 0.1 [list apply {{dir} {
    package require platform
    set plat [platform::identify]
    set ext [info sharedlibextension]
    load [file join $dir $plat mybinaryextension.$ext]
}} $dir]
As you can see, this gives you local variables and simple substitution rules without needing an extra command. (It's only a problem if you want to support 8.4 and before, but that's out of all support now...)


jan.nijtmans added on 2022-07-05 06:42:22:

Of course, a ';' is missing, and "platform::identity" should be delayed:

    package require platform
    package ifneeded testpkg 0.1 "package require platform; \
    load \[file join [list $dir] \[platform::identify\] mybinaryextension.[info sharedlibextension]\]"

I wouldn't use "platform::identify", since "platform::identify" contains the OS version as well: Do you really want separate directories for MacOS 10.4, up to 12.4? Maybe "platform::generic" is better. Or $tcl_platform(os) (but that wouldn't distingish processor) or $tcl_platform(os)/$tcl_platform(machine)


jan.nijtmans added on 2022-07-05 06:20:39:

In stead of this:

    package require platform
    package ifneeded testpkg 0.1 [list load [file join $dir [platform::identify] mybinaryextension.[info sharedlibextension]]

You should do:

    package require platform
    package ifneeded testpkg 0.1 "package require platform \
    [list load [file join $dir [platform::identify] mybinaryextension.[info sharedlibextension]]"

Then the "package require platform" is executed when testpkg is required.

The problem with your approach is that the following could happen:

$ tclsh8.7
$ package files platform; # empty result, since "platform" is not loaded yet
$ package require Thread
% package files platform; # "platform" is - unexpectedly - loaded now
/usr/local/lib/tcl8/8.4/platform-1.0.18.tm
(whether this happens or not depends on the order the "pkgIndex.tcl" files are found)

So, a "package require Thread" results in pkgIndex.tcl files being sourced (in order to see where "Thread" is. This has a side-effect that the "platform" package is loaded, but it is not needed by "Thread" at all.

It should be documented that a "pkgIndex.tcl" file should not contain another "package require" nor "package load", unless it's within a "package ifneeded".


apnadkarni added on 2022-07-05 02:37:51:

I understand why this is happening but that does not mean it's not a bug.

  • I don't think it is documented anywhere that pkgIndex.tcl cannot contain a package required or that it should not do a load.

  • In fact this has been working code for all releases up to this point. If 8.7 changes this, it is no longer 8.6 compatible in my view.

  • TIP 459 does not mention this additional new restriction on pkgIndex.tcl files at all.

Given the above, I'm not sure why you say the correct way is to move it to testpkg.tcl. I would say it's a workaround for a bug!

As to why not move it into the testpkg.tcl file, the reason is that there may not be a testpkg.tcl file at all, only a binary. The example I gave was just for illustration. The actual pkgIndex.tcl file would look something like this (pseudo-code, no existence checks etc)

package require platform
package ifneeded testpkg 0.1 [list load [file join $dir [platform::identify] mybinaryextension.[info sharedlibextension]]

This permits a multi-platform install/distribution. You are now saying pkgIndex.tcl should not do a load either.

Now in 8.7, an extra unnecessary .tcl file is needed where none was needed before. This is a step back but more important, binary extensions as above will no longer load in 8.7. Not just that, they actually break the package loading for all packages.

Re-opening as I do not consider this as an invalid ticket or user error.

/Ashok


jan.nijtmans added on 2022-07-04 19:09:05:

Well - first - if 'testpkg' needs 'platform', `the correct` way to do this is as follows:

########## pkgIndex.tcl ########
package ifneeded testpkg 0.1 [list source [file join $dir testpkg.tcl]]

########## testpkg.tcl ######## package require platform; # <-- should be here, not in pkgIndex.tcl proc testproc {} {puts "testpkg loaded"} package provide testpkg 0.1

The pkgIndex system is complex. If a package is not found when doing a "package require", more file paths are searched for a 'pkgIndex' file. Therefore, a 'pkgIndex' file should only contain commands which have no side-effects, it should NEVER do another 'package require' directly or a 'load'. 'pkgIndex' files should hardly do anything more than 'package ifneeded'.

The reason Tcl 8.7 discovers this and 8.6 doesn't is TIP #459. Tcl 8.7 keeps track, which files are sourced in which package initialization, so 'package files' can report it. Maybe the error-message could be better .....

Does this explanation make it a little bit more clear?


apnadkarni added on 2022-07-04 17:24:55:

The errorInfo from the above:

% set errorInfo
circular package dependency: attempt to provide platform 1.0.18 requires platform
    while executing
"package require platform"
    (file "d:/tcl/xx/p/pkgIndex.tcl" line 1)
    invoked from within
"::source -nopkg d:/tcl/xx/p/pkgIndex.tcl"
    ("uplevel" body line 1)
    invoked from within
"uplevel 1 [list ::source -nopkg $filename]"
    (procedure "::tcl::Pkg::source" line 5)
    invoked from within
"::tcl::Pkg::source $file"

Attachments: