123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- # -*- tcl -*-
- #
- # Searching for Tcl Modules. Defines a procedure, declares it as the
- # primary command for finding packages, however also uses the former
- # 'package unknown' command as a fallback.
- #
- # Locates all possible packages in a directory via a less restricted
- # glob. The targeted directory is derived from the name of the
- # requested package. I.e. the TM scan will look only at directories
- # which can contain the requested package. It will register all
- # packages it found in the directory so that future requests have a
- # higher chance of being fulfilled by the ifneeded database without
- # having to come to us again.
- #
- # We do not remember where we have been and simply rescan targeted
- # directories when invoked again. The reasoning is this:
- #
- # - The only way we get back to the same directory is if someone is
- # trying to [package require] something that wasn't there on the
- # first scan.
- #
- # Either
- # 1) It is there now: If we rescan, you get it; if not you don't.
- #
- # This covers the possibility that the application asked for a
- # package late, and the package was actually added to the
- # installation after the application was started. It shoukld
- # still be able to find it.
- #
- # 2) It still is not there: Either way, you don't get it, but the
- # rescan takes time. This is however an error case and we dont't
- # care that much about it
- #
- # 3) It was there the first time; but for some reason a "package
- # forget" has been run, and "package" doesn't know about it
- # anymore.
- #
- # This can be an indication that the application wishes to reload
- # some functionality. And should work as well.
- #
- # Note that this also strikes a balance between doing a glob targeting
- # a single package, and thus most likely requiring multiple globs of
- # the same directory when the application is asking for many packages,
- # and trying to glob for _everything_ in all subdirectories when
- # looking for a package, which comes with a heavy startup cost.
- #
- # We scan for regular packages only if no satisfying module was found.
- namespace eval ::tcl::tm {
- # Default paths. None yet.
- variable paths {}
- # The regex pattern a file name has to match to make it a Tcl Module.
- set pkgpattern {^([_[:alpha:]][:_[:alnum:]]*)-([[:digit:]].*)[.]tm$}
- # Export the public API
- namespace export path
- namespace ensemble create -command path -subcommands {add remove list}
- }
- # ::tcl::tm::path implementations --
- #
- # Public API to the module path. See specification.
- #
- # Arguments
- # cmd - The subcommand to execute
- # args - The paths to add/remove. Must not appear querying the
- # path with 'list'.
- #
- # Results
- # No result for subcommands 'add' and 'remove'. A list of paths
- # for 'list'.
- #
- # Sideeffects
- # The subcommands 'add' and 'remove' manipulate the list of
- # paths to search for Tcl Modules. The subcommand 'list' has no
- # sideeffects.
- proc ::tcl::tm::add {path args} {
- # PART OF THE ::tcl::tm::path ENSEMBLE
- #
- # The path is added at the head to the list of module paths.
- #
- # The command enforces the restriction that no path may be an
- # ancestor directory of any other path on the list. If the new
- # path violates this restriction an error wil be raised.
- #
- # If the path is already present as is no error will be raised and
- # no action will be taken.
- variable paths
- # We use a copy of the path as source during validation, and
- # extend it as well. Because we not only have to detect if the new
- # paths are bogus with respect to the existing paths, but also
- # between themselves. Otherwise we can still add bogus paths, by
- # specifying them in a single call. This makes the use of the new
- # paths simpler as well, a trivial assignment of the collected
- # paths to the official state var.
- set newpaths $paths
- foreach p [linsert $args 0 $path] {
- if {$p in $newpaths} {
- # Ignore a path already on the list.
- continue
- }
- # Search for paths which are subdirectories of the new one. If
- # there are any then the new path violates the restriction
- # about ancestors.
- set pos [lsearch -glob $newpaths ${p}/*]
- # Cannot use "in", we need the position for the message.
- if {$pos >= 0} {
- return -code error \
- "$p is ancestor of existing module path [lindex $newpaths $pos]."
- }
- # Now look for existing paths which are ancestors of the new
- # one. This reverse question forces us to loop over the
- # existing paths, as each element is the pattern, not the new
- # path :(
- foreach ep $newpaths {
- if {[string match ${ep}/* $p]} {
- return -code error \
- "$p is subdirectory of existing module path $ep."
- }
- }
- set newpaths [linsert $newpaths 0 $p]
- }
- # The validation of the input is complete and successful, and
- # everything in newpaths is either an old path, or added. We can
- # now extend the official list of paths, a simple assignment is
- # sufficient.
- set paths $newpaths
- return
- }
- proc ::tcl::tm::remove {path args} {
- # PART OF THE ::tcl::tm::path ENSEMBLE
- #
- # Removes the path from the list of module paths. The command is
- # silently ignored if the path is not on the list.
- variable paths
- foreach p [linsert $args 0 $path] {
- set pos [lsearch -exact $paths $p]
- if {$pos >= 0} {
- set paths [lreplace $paths $pos $pos]
- }
- }
- }
- proc ::tcl::tm::list {} {
- # PART OF THE ::tcl::tm::path ENSEMBLE
- variable paths
- return $paths
- }
- # ::tcl::tm::UnknownHandler --
- #
- # Unknown handler for Tcl Modules, i.e. packages in module form.
- #
- # Arguments
- # original - Original [package unknown] procedure.
- # name - Name of desired package.
- # version - Version of desired package. Can be the
- # empty string.
- # exact - Either -exact or ommitted.
- #
- # Name, version, and exact are used to determine
- # satisfaction. The original is called iff no satisfaction was
- # achieved. The name is also used to compute the directory to
- # target in the search.
- #
- # Results
- # None.
- #
- # Sideeffects
- # May populate the package ifneeded database with additional
- # provide scripts.
- proc ::tcl::tm::UnknownHandler {original name args} {
- # Import the list of paths to search for packages in module form.
- # Import the pattern used to check package names in detail.
- variable paths
- variable pkgpattern
- # Without paths to search we can do nothing. (Except falling back
- # to the regular search).
- if {[llength $paths]} {
- set pkgpath [string map {:: /} $name]
- set pkgroot [file dirname $pkgpath]
- if {$pkgroot eq "."} {
- set pkgroot ""
- }
- # We don't remember a copy of the paths while looping. Tcl
- # Modules are unable to change the list while we are searching
- # for them. This also simplifies the loop, as we cannot get
- # additional directories while iterating over the list. A
- # simple foreach is sufficient.
- set satisfied 0
- foreach path $paths {
- if {![interp issafe] && ![file exists $path]} {
- continue
- }
- set currentsearchpath [file join $path $pkgroot]
- if {![interp issafe] && ![file exists $currentsearchpath]} {
- continue
- }
- set strip [llength [file split $path]]
- # We can't use glob in safe interps, so enclose the following
- # in a catch statement, where we get the module files out
- # of the subdirectories. In other words, Tcl Modules are
- # not-functional in such an interpreter. This is the same
- # as for the command "tclPkgUnknown", i.e. the search for
- # regular packages.
- catch {
- # We always look for _all_ possible modules in the current
- # path, to get the max result out of the glob.
- foreach file [glob -nocomplain -directory $currentsearchpath *.tm] {
- set pkgfilename [join [lrange [file split $file] $strip end] ::]
- if {![regexp -- $pkgpattern $pkgfilename --> pkgname pkgversion]} {
- # Ignore everything not matching our pattern
- # for package names.
- continue
- }
- if {[catch {package vcompare $pkgversion 0}]} {
- # Ignore everything where the version part is
- # not acceptable to "package vcompare".
- continue
- }
- if {[package ifneeded $pkgname $pkgversion] ne {}} {
- # There's already a provide script registered for
- # this version of this package. Since all units of
- # code claiming to be the same version of the same
- # package ought to be identical, just stick with
- # the one we already have.
- continue
- }
- # We have found a candidate, generate a "provide
- # script" for it, and remember it. Note that we
- # are using ::list to do this; locally [list]
- # means something else without the namespace
- # specifier.
- # NOTE. When making changes to the format of the
- # provide command generated below CHECK that the
- # 'LOCATE' procedure in core file
- # 'platform/shell.tcl' still understands it, or,
- # if not, update its implementation appropriately.
- #
- # Right now LOCATE's implementation assumes that
- # the path of the package file is the last element
- # in the list.
- package ifneeded $pkgname $pkgversion \
- "[::list package provide $pkgname $pkgversion];[::list source -encoding utf-8 $file]"
- # We abort in this unknown handler only if we got
- # a satisfying candidate for the requested
- # package. Otherwise we still have to fallback to
- # the regular package search to complete the
- # processing.
- if {($pkgname eq $name)
- && [package vsatisfies $pkgversion {*}$args]} {
- set satisfied 1
- # We do not abort the loop, and keep adding
- # provide scripts for every candidate in the
- # directory, just remember to not fall back to
- # the regular search anymore.
- }
- }
- }
- }
- if {$satisfied} {
- return
- }
- }
- # Fallback to previous command, if existing. See comment above
- # about ::list...
- if {[llength $original]} {
- uplevel 1 $original [::linsert $args 0 $name]
- }
- }
- # ::tcl::tm::Defaults --
- #
- # Determines the default search paths.
- #
- # Arguments
- # None
- #
- # Results
- # None.
- #
- # Sideeffects
- # May add paths to the list of defaults.
- proc ::tcl::tm::Defaults {} {
- global env tcl_platform
- lassign [split [info tclversion] .] major minor
- set exe [file normalize [info nameofexecutable]]
- # Note that we're using [::list], not [list] because [list] means
- # something other than [::list] in this namespace.
- roots [::list \
- [file dirname [info library]] \
- [file join [file dirname [file dirname $exe]] lib] \
- ]
- if {$tcl_platform(platform) eq "windows"} {
- set sep ";"
- } else {
- set sep ":"
- }
- for {set n $minor} {$n >= 0} {incr n -1} {
- foreach ev [::list \
- TCL${major}.${n}_TM_PATH \
- TCL${major}_${n}_TM_PATH \
- ] {
- if {![info exists env($ev)]} continue
- foreach p [split $env($ev) $sep] {
- path add $p
- }
- }
- }
- return
- }
- # ::tcl::tm::roots --
- #
- # Public API to the module path. See specification.
- #
- # Arguments
- # paths - List of 'root' paths to derive search paths from.
- #
- # Results
- # No result.
- #
- # Sideeffects
- # Calls 'path add' to paths to the list of module search paths.
- proc ::tcl::tm::roots {paths} {
- lassign [split [package present Tcl] .] major minor
- foreach pa $paths {
- set p [file join $pa tcl$major]
- for {set n $minor} {$n >= 0} {incr n -1} {
- set px [file join $p ${major}.${n}]
- if {![interp issafe]} { set px [file normalize $px] }
- path add $px
- }
- set px [file join $p site-tcl]
- if {![interp issafe]} { set px [file normalize $px] }
- path add $px
- }
- return
- }
- # Initialization. Set up the default paths, then insert the new
- # handler into the chain.
- if {![interp issafe]} { ::tcl::tm::Defaults }
|