Updated 2009-01-28 19:41:25 by LV

In a recent TCLCORE message, there was a request to document what the interface guarantees are between a package index script and a package unknown callback. This page created to record such documentation.

LV what is meant by "package index script" - code such as pkgIndex.tcl?

DGP The default package unknown callback, [tcl_PkgUnknown] finds the index scripts for the packages it manages in files named pkgIndex.tcl, so yes, that's the most common example of an index script.

Other package unknown callbacks might choose to retrieve or generate their index scripts from other sources, or by other methods.

Probably most important is that a package index script should never raise an error.

This can be tricky because index scripts can be evaluated in just about any kind of Tcl interpreter, by any registered [package unknown] manager, so you should not depend on things you might depend on in your more day-to-day Tcl programming.

Especially noteworthy along those lines is that a package index script might be evaluated in a Tcl interpreter for any release of Tcl from 7.5 on. This means you should not be using any Tcl 8 features in your index script until after the index script itself verifies that the interp has a recent enough Tcl in it.

Index scripts can rely on the [return] command to cleanly terminate evaluation of the index script. So, a useful technique in an index script is to check the interp for a recent enough Tcl release to support the rest of the index script, and to support the package indexed by the index script:
    if {![package vsatisfies [package provide Tcl] 8]} {return}

Likewise, an index script should not raise any of Tcl's other return codes like [break] or [continue]. It's best to think of those as causing undefined behavior, and just avoid them completely in index scripts. The actual behavior will probably depend on the internal details of the [package unknown] handler that the index script author really shouldn't know about, let alone depend on.

An index script can also depend on all of the commands built in to Tcl being available by their simple names. This does not mean the index script is evaluated in the global namespace. It only means that non-buggy [package unknown] handlers will not mask Tcl's built-in commands in the context they provide for index script evaluation.

An index script can rely on the existence of a variable known in the current context as dir. The contents of that variable are the absolute file system path to the installation directory associated with the package indexed by this index script. It is up to the [package unknown] manager to perform this initialization. Thus the [package unknown] manager keeps track of what installation directory goes with what package goes with what index script. The default [package unknown] handler, [tclPkgUnkown] achieves this by assigning to dir the name of the directory that contains a file named pkgIndex.tcl that contains the index script.

Since the index script does not know what namespace context or proc context it might be evaluated in, if it wants to access a global variable, it should either use the [global] command to bring that variable into current scope, or use a fully-qualified name for the variable (::tcl_platform) after verifying a Tcl 8 interpreter.

An index script should not call [package require] for any package. Evaluation of an index script is not about loading any package, it is only about registering a load script for later use. Other than performing that registration by calling [package ifneeded], an index script should strive to be free of side-effects.

An index script should not assume it is kept in a file and is evaluated by [source]. This means the index script should not depend on [info script] to return anything useful. The dir variable is the interface to use to discover the installation directory of the package, and that's all the index script should need to know.

Don't do this:
 package ifneeded foo 1.0 "load [file join $dir foo[info sharedlibextension]]"

That will break if $dir contains spaces. Do this instead:
 package ifneeded foo 1.0 [list load [file join $dir foo[info sharedlibextension]]]

Be sure to read the discussion at pkgIndex.tcl which provides a Tcl 8.5 alternative preferred by DKF.

On 5-jan-2004 in c.l.t., Michael Schlenker explains with respect to:

`Probably most important is that a package index script should never raise an error.'

The problem is mainly with errors thrown when package require looks for packages, not when they are found and are about to be loaded.

i.e. the following example code is deemed OK:
 proc loadMyPackage {dir} {
    if { ![CheckRequirement] } {

        # this is OK
        return -code error $errMsg

    source [file join $dir sourceFile.tcl]
 package ifneeded myPackage [list loadMyPackage $dir]

Of course, the error could also be raised inside the sourceFile.tcl.

DGP The problem with either of those approaches is they do not play well with package's handling of multiple installed versions of a package. Essentially, the [package ifneeded] tells the [package] system that a package is available to be loaded, even though the attempt to load it is going to produce an error. If this index script is for myPackage version 1.2, [package] is going to prefer loading it over myPackage version 1.1, which might not have the "error on load" problem. That would mean we're ignoring a package that works in favor of one that doesn't. It's best if you can avoid [package ifneeded] registration of packages that you know cannot successfully load in the current interp.

In an environment where you can be sure there's only one version of your package "installed" (the internals of a Starkit, perhaps?), you can probably get away with that. But in that environment a more generally correct index script will also work, so why not?

Another, perhaps less serious, problem is that the example does have the side effect of creating a [loadMyPackage] command.

Lars H: If the need is to perform several commands for loading a package, then the helper proc is quite unnecessary. A multiline package ifneeded script is straightforward to construct using format:
  package ifneeded myPackage 1.0 [format {
      package require sourceWithEncoding
      sourceWithEncoding utf-8 [file join %s myPackage.tcl]
  } [list $dir]]

Note how the helper sourceWithEncoding package is not loaded until myPackage is actually required. Also note that dir is list-quoted before it is passed to format, since it will appear as a complete word in the script (this is similar to how bind percent substitution works).

NEM: Or a lambda (in Tcl 8.5+):
 if {![package vsatisfies [package provide Tcl] 8.5]} {return}
 package ifneeded myPackage 1.0 [list apply {dir {
    package require sourceWithEncoding
    sourceWIthEncoding utf-8 [file join $dir myPackage.tcl]
 }} $dir]

(Of course, for this specific example there is now [source -encoding]).

Lars H: The big disadvantage of that is of course that it's only suitable for packages that require Tcl 8.5. An advantage is that it lets you create variables that are local to the script.