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