| 1 | # -*- tcl -*- | 
|---|
| 2 | # | 
|---|
| 3 | # Searching for Tcl Modules. Defines a procedure, declares it as the | 
|---|
| 4 | # primary command for finding packages, however also uses the former | 
|---|
| 5 | # 'package unknown' command as a fallback. | 
|---|
| 6 | # | 
|---|
| 7 | # Locates all possible packages in a directory via a less restricted | 
|---|
| 8 | # glob. The targeted directory is derived from the name of the | 
|---|
| 9 | # requested package. I.e. the TM scan will look only at directories | 
|---|
| 10 | # which can contain the requested package. It will register all | 
|---|
| 11 | # packages it found in the directory so that future requests have a | 
|---|
| 12 | # higher chance of being fulfilled by the ifneeded database without | 
|---|
| 13 | # having to come to us again. | 
|---|
| 14 | # | 
|---|
| 15 | # We do not remember where we have been and simply rescan targeted | 
|---|
| 16 | # directories when invoked again. The reasoning is this: | 
|---|
| 17 | # | 
|---|
| 18 | # - The only way we get back to the same directory is if someone is | 
|---|
| 19 | #   trying to [package require] something that wasn't there on the | 
|---|
| 20 | #   first scan. | 
|---|
| 21 | # | 
|---|
| 22 | #   Either | 
|---|
| 23 | #   1) It is there now:  If we rescan, you get it; if not you don't. | 
|---|
| 24 | # | 
|---|
| 25 | #      This covers the possibility that the application asked for a | 
|---|
| 26 | #      package late, and the package was actually added to the | 
|---|
| 27 | #      installation after the application was started. It shoukld | 
|---|
| 28 | #      still be able to find it. | 
|---|
| 29 | # | 
|---|
| 30 | #   2) It still is not there: Either way, you don't get it, but the | 
|---|
| 31 | #      rescan takes time. This is however an error case and we dont't | 
|---|
| 32 | #      care that much about it | 
|---|
| 33 | # | 
|---|
| 34 | #   3) It was there the first time; but for some reason a "package | 
|---|
| 35 | #      forget" has been run, and "package" doesn't know about it | 
|---|
| 36 | #      anymore. | 
|---|
| 37 | # | 
|---|
| 38 | #      This can be an indication that the application wishes to reload | 
|---|
| 39 | #      some functionality. And should work as well. | 
|---|
| 40 | # | 
|---|
| 41 | # Note that this also strikes a balance between doing a glob targeting | 
|---|
| 42 | # a single package, and thus most likely requiring multiple globs of | 
|---|
| 43 | # the same directory when the application is asking for many packages, | 
|---|
| 44 | # and trying to glob for _everything_ in all subdirectories when | 
|---|
| 45 | # looking for a package, which comes with a heavy startup cost. | 
|---|
| 46 | # | 
|---|
| 47 | # We scan for regular packages only if no satisfying module was found. | 
|---|
| 48 |  | 
|---|
| 49 | namespace eval ::tcl::tm { | 
|---|
| 50 | # Default paths. None yet. | 
|---|
| 51 |  | 
|---|
| 52 | variable paths {} | 
|---|
| 53 |  | 
|---|
| 54 | # The regex pattern a file name has to match to make it a Tcl Module. | 
|---|
| 55 |  | 
|---|
| 56 | set pkgpattern {^([_[:alpha:]][:_[:alnum:]]*)-([[:digit:]].*)[.]tm$} | 
|---|
| 57 |  | 
|---|
| 58 | # Export the public API | 
|---|
| 59 |  | 
|---|
| 60 | namespace export path | 
|---|
| 61 | namespace ensemble create -command path -subcommand {add remove list} | 
|---|
| 62 | } | 
|---|
| 63 |  | 
|---|
| 64 | # ::tcl::tm::path implementations -- | 
|---|
| 65 | # | 
|---|
| 66 | #       Public API to the module path. See specification. | 
|---|
| 67 | # | 
|---|
| 68 | # Arguments | 
|---|
| 69 | #       cmd -   The subcommand to execute | 
|---|
| 70 | #       args -  The paths to add/remove. Must not appear querying the | 
|---|
| 71 | #               path with 'list'. | 
|---|
| 72 | # | 
|---|
| 73 | # Results | 
|---|
| 74 | #       No result for subcommands 'add' and 'remove'. A list of paths | 
|---|
| 75 | #       for 'list'. | 
|---|
| 76 | # | 
|---|
| 77 | # Sideeffects | 
|---|
| 78 | #       The subcommands 'add' and 'remove' manipulate the list of | 
|---|
| 79 | #       paths to search for Tcl Modules. The subcommand 'list' has no | 
|---|
| 80 | #       sideeffects. | 
|---|
| 81 |  | 
|---|
| 82 | proc ::tcl::tm::add {path args} { | 
|---|
| 83 | # PART OF THE ::tcl::tm::path ENSEMBLE | 
|---|
| 84 | # | 
|---|
| 85 | # The path is added at the head to the list of module paths. | 
|---|
| 86 | # | 
|---|
| 87 | # The command enforces the restriction that no path may be an | 
|---|
| 88 | # ancestor directory of any other path on the list. If the new | 
|---|
| 89 | # path violates this restriction an error wil be raised. | 
|---|
| 90 | # | 
|---|
| 91 | # If the path is already present as is no error will be raised and | 
|---|
| 92 | # no action will be taken. | 
|---|
| 93 |  | 
|---|
| 94 | variable paths | 
|---|
| 95 |  | 
|---|
| 96 | # We use a copy of the path as source during validation, and | 
|---|
| 97 | # extend it as well. Because we not only have to detect if the new | 
|---|
| 98 | # paths are bogus with respect to the existing paths, but also | 
|---|
| 99 | # between themselves. Otherwise we can still add bogus paths, by | 
|---|
| 100 | # specifying them in a single call. This makes the use of the new | 
|---|
| 101 | # paths simpler as well, a trivial assignment of the collected | 
|---|
| 102 | # paths to the official state var. | 
|---|
| 103 |  | 
|---|
| 104 | set newpaths $paths | 
|---|
| 105 | foreach p [linsert $args 0 $path] { | 
|---|
| 106 | if {$p in $newpaths} { | 
|---|
| 107 | # Ignore a path already on the list. | 
|---|
| 108 | continue | 
|---|
| 109 | } | 
|---|
| 110 |  | 
|---|
| 111 | # Search for paths which are subdirectories of the new one. If | 
|---|
| 112 | # there are any then the new path violates the restriction | 
|---|
| 113 | # about ancestors. | 
|---|
| 114 |  | 
|---|
| 115 | set pos [lsearch -glob $newpaths ${p}/*] | 
|---|
| 116 | # Cannot use "in", we need the position for the message. | 
|---|
| 117 | if {$pos >= 0} { | 
|---|
| 118 | return -code error \ | 
|---|
| 119 | "$p is ancestor of existing module path [lindex $newpaths $pos]." | 
|---|
| 120 | } | 
|---|
| 121 |  | 
|---|
| 122 | # Now look for existing paths which are ancestors of the new | 
|---|
| 123 | # one. This reverse question forces us to loop over the | 
|---|
| 124 | # existing paths, as each element is the pattern, not the new | 
|---|
| 125 | # path :( | 
|---|
| 126 |  | 
|---|
| 127 | foreach ep $newpaths { | 
|---|
| 128 | if {[string match ${ep}/* $p]} { | 
|---|
| 129 | return -code error \ | 
|---|
| 130 | "$p is subdirectory of existing module path $ep." | 
|---|
| 131 | } | 
|---|
| 132 | } | 
|---|
| 133 |  | 
|---|
| 134 | set newpaths [linsert $newpaths 0 $p] | 
|---|
| 135 | } | 
|---|
| 136 |  | 
|---|
| 137 | # The validation of the input is complete and successful, and | 
|---|
| 138 | # everything in newpaths is either an old path, or added. We can | 
|---|
| 139 | # now extend the official list of paths, a simple assignment is | 
|---|
| 140 | # sufficient. | 
|---|
| 141 |  | 
|---|
| 142 | set paths $newpaths | 
|---|
| 143 | return | 
|---|
| 144 | } | 
|---|
| 145 |  | 
|---|
| 146 | proc ::tcl::tm::remove {path args} { | 
|---|
| 147 | # PART OF THE ::tcl::tm::path ENSEMBLE | 
|---|
| 148 | # | 
|---|
| 149 | # Removes the path from the list of module paths. The command is | 
|---|
| 150 | # silently ignored if the path is not on the list. | 
|---|
| 151 |  | 
|---|
| 152 | variable paths | 
|---|
| 153 |  | 
|---|
| 154 | foreach p [linsert $args 0 $path] { | 
|---|
| 155 | set pos [lsearch -exact $paths $p] | 
|---|
| 156 | if {$pos >= 0} { | 
|---|
| 157 | set paths [lreplace $paths $pos $pos] | 
|---|
| 158 | } | 
|---|
| 159 | } | 
|---|
| 160 | } | 
|---|
| 161 |  | 
|---|
| 162 | proc ::tcl::tm::list {} { | 
|---|
| 163 | # PART OF THE ::tcl::tm::path ENSEMBLE | 
|---|
| 164 |  | 
|---|
| 165 | variable paths | 
|---|
| 166 | return  $paths | 
|---|
| 167 | } | 
|---|
| 168 |  | 
|---|
| 169 | # ::tcl::tm::UnknownHandler -- | 
|---|
| 170 | # | 
|---|
| 171 | #       Unknown handler for Tcl Modules, i.e. packages in module form. | 
|---|
| 172 | # | 
|---|
| 173 | # Arguments | 
|---|
| 174 | #       original        - Original [package unknown] procedure. | 
|---|
| 175 | #       name            - Name of desired package. | 
|---|
| 176 | #       version         - Version of desired package. Can be the | 
|---|
| 177 | #                         empty string. | 
|---|
| 178 | #       exact           - Either -exact or ommitted. | 
|---|
| 179 | # | 
|---|
| 180 | #       Name, version, and exact are used to determine | 
|---|
| 181 | #       satisfaction. The original is called iff no satisfaction was | 
|---|
| 182 | #       achieved. The name is also used to compute the directory to | 
|---|
| 183 | #       target in the search. | 
|---|
| 184 | # | 
|---|
| 185 | # Results | 
|---|
| 186 | #       None. | 
|---|
| 187 | # | 
|---|
| 188 | # Sideeffects | 
|---|
| 189 | #       May populate the package ifneeded database with additional | 
|---|
| 190 | #       provide scripts. | 
|---|
| 191 |  | 
|---|
| 192 | proc ::tcl::tm::UnknownHandler {original name args} { | 
|---|
| 193 | # Import the list of paths to search for packages in module form. | 
|---|
| 194 | # Import the pattern used to check package names in detail. | 
|---|
| 195 |  | 
|---|
| 196 | variable paths | 
|---|
| 197 | variable pkgpattern | 
|---|
| 198 |  | 
|---|
| 199 | # Without paths to search we can do nothing. (Except falling back | 
|---|
| 200 | # to the regular search). | 
|---|
| 201 |  | 
|---|
| 202 | if {[llength $paths]} { | 
|---|
| 203 | set pkgpath [string map {:: /} $name] | 
|---|
| 204 | set pkgroot [file dirname $pkgpath] | 
|---|
| 205 | if {$pkgroot eq "."} { | 
|---|
| 206 | set pkgroot "" | 
|---|
| 207 | } | 
|---|
| 208 |  | 
|---|
| 209 | # We don't remember a copy of the paths while looping. Tcl | 
|---|
| 210 | # Modules are unable to change the list while we are searching | 
|---|
| 211 | # for them. This also simplifies the loop, as we cannot get | 
|---|
| 212 | # additional directories while iterating over the list. A | 
|---|
| 213 | # simple foreach is sufficient. | 
|---|
| 214 |  | 
|---|
| 215 | set satisfied 0 | 
|---|
| 216 | foreach path $paths { | 
|---|
| 217 | if {![file exists $path]} { | 
|---|
| 218 | continue | 
|---|
| 219 | } | 
|---|
| 220 | set currentsearchpath [file join $path $pkgroot] | 
|---|
| 221 | if {![file exists $currentsearchpath]} { | 
|---|
| 222 | continue | 
|---|
| 223 | } | 
|---|
| 224 | set strip [llength [file split $path]] | 
|---|
| 225 |  | 
|---|
| 226 | # We can't use glob in safe interps, so enclose the following | 
|---|
| 227 | # in a catch statement, where we get the module files out | 
|---|
| 228 | # of the subdirectories. In other words, Tcl Modules are | 
|---|
| 229 | # not-functional in such an interpreter. This is the same | 
|---|
| 230 | # as for the command "tclPkgUnknown", i.e. the search for | 
|---|
| 231 | # regular packages. | 
|---|
| 232 |  | 
|---|
| 233 | catch { | 
|---|
| 234 | # We always look for _all_ possible modules in the current | 
|---|
| 235 | # path, to get the max result out of the glob. | 
|---|
| 236 |  | 
|---|
| 237 | foreach file [glob -nocomplain -directory $currentsearchpath *.tm] { | 
|---|
| 238 | set pkgfilename [join [lrange [file split $file] $strip end] ::] | 
|---|
| 239 |  | 
|---|
| 240 | if {![regexp -- $pkgpattern $pkgfilename --> pkgname pkgversion]} { | 
|---|
| 241 | # Ignore everything not matching our pattern | 
|---|
| 242 | # for package names. | 
|---|
| 243 | continue | 
|---|
| 244 | } | 
|---|
| 245 | if {[catch {package vcompare $pkgversion 0}]} { | 
|---|
| 246 | # Ignore everything where the version part is | 
|---|
| 247 | # not acceptable to "package vcompare". | 
|---|
| 248 | continue | 
|---|
| 249 | } | 
|---|
| 250 |  | 
|---|
| 251 | # We have found a candidate, generate a "provide | 
|---|
| 252 | # script" for it, and remember it.  Note that we | 
|---|
| 253 | # are using ::list to do this; locally [list] | 
|---|
| 254 | # means something else without the namespace | 
|---|
| 255 | # specifier. | 
|---|
| 256 |  | 
|---|
| 257 | package ifneeded $pkgname $pkgversion [::list source -encoding utf-8 $file] | 
|---|
| 258 |  | 
|---|
| 259 | # We abort in this unknown handler only if we got | 
|---|
| 260 | # a satisfying candidate for the requested | 
|---|
| 261 | # package. Otherwise we still have to fallback to | 
|---|
| 262 | # the regular package search to complete the | 
|---|
| 263 | # processing. | 
|---|
| 264 |  | 
|---|
| 265 | if { | 
|---|
| 266 | ($pkgname eq $name) && | 
|---|
| 267 | [package vsatisfies $pkgversion {*}$args] | 
|---|
| 268 | } then { | 
|---|
| 269 | set satisfied 1 | 
|---|
| 270 | # We do not abort the loop, and keep adding | 
|---|
| 271 | # provide scripts for every candidate in the | 
|---|
| 272 | # directory, just remember to not fall back to | 
|---|
| 273 | # the regular search anymore. | 
|---|
| 274 | } | 
|---|
| 275 | } | 
|---|
| 276 | } | 
|---|
| 277 | } | 
|---|
| 278 |  | 
|---|
| 279 | if {$satisfied} { | 
|---|
| 280 | return | 
|---|
| 281 | } | 
|---|
| 282 | } | 
|---|
| 283 |  | 
|---|
| 284 | # Fallback to previous command, if existing.  See comment above | 
|---|
| 285 | # about ::list... | 
|---|
| 286 |  | 
|---|
| 287 | if {[llength $original]} { | 
|---|
| 288 | uplevel 1 $original [::linsert $args 0 $name] | 
|---|
| 289 | } | 
|---|
| 290 | } | 
|---|
| 291 |  | 
|---|
| 292 | # ::tcl::tm::Defaults -- | 
|---|
| 293 | # | 
|---|
| 294 | #       Determines the default search paths. | 
|---|
| 295 | # | 
|---|
| 296 | # Arguments | 
|---|
| 297 | #       None | 
|---|
| 298 | # | 
|---|
| 299 | # Results | 
|---|
| 300 | #       None. | 
|---|
| 301 | # | 
|---|
| 302 | # Sideeffects | 
|---|
| 303 | #       May add paths to the list of defaults. | 
|---|
| 304 |  | 
|---|
| 305 | proc ::tcl::tm::Defaults {} { | 
|---|
| 306 | global env tcl_platform | 
|---|
| 307 |  | 
|---|
| 308 | lassign [split [info tclversion] .] major minor | 
|---|
| 309 | set exe [file normalize [info nameofexecutable]] | 
|---|
| 310 |  | 
|---|
| 311 | # Note that we're using [::list], not [list] because [list] means | 
|---|
| 312 | # something other than [::list] in this namespace. | 
|---|
| 313 | roots [::list \ | 
|---|
| 314 | [file dirname [info library]] \ | 
|---|
| 315 | [file join [file dirname [file dirname $exe]] lib] \ | 
|---|
| 316 | ] | 
|---|
| 317 |  | 
|---|
| 318 | if {$tcl_platform(platform) eq "windows"} { | 
|---|
| 319 | set sep ";" | 
|---|
| 320 | } else { | 
|---|
| 321 | set sep ":" | 
|---|
| 322 | } | 
|---|
| 323 | for {set n $minor} {$n >= 0} {incr n -1} { | 
|---|
| 324 | foreach ev [::list \ | 
|---|
| 325 | TCL${major}.${n}_TM_PATH \ | 
|---|
| 326 | TCL${major}_${n}_TM_PATH \ | 
|---|
| 327 | ] { | 
|---|
| 328 | if {![info exists env($ev)]} continue | 
|---|
| 329 | foreach p [split $env($ev) $sep] { | 
|---|
| 330 | path add $p | 
|---|
| 331 | } | 
|---|
| 332 | } | 
|---|
| 333 | } | 
|---|
| 334 | return | 
|---|
| 335 | } | 
|---|
| 336 |  | 
|---|
| 337 | # ::tcl::tm::roots -- | 
|---|
| 338 | # | 
|---|
| 339 | #       Public API to the module path. See specification. | 
|---|
| 340 | # | 
|---|
| 341 | # Arguments | 
|---|
| 342 | #       paths - List of 'root' paths to derive search paths from. | 
|---|
| 343 | # | 
|---|
| 344 | # Results | 
|---|
| 345 | #       No result. | 
|---|
| 346 | # | 
|---|
| 347 | # Sideeffects | 
|---|
| 348 | #       Calls 'path add' to paths to the list of module search paths. | 
|---|
| 349 |  | 
|---|
| 350 | proc ::tcl::tm::roots {paths} { | 
|---|
| 351 | foreach {major minor} [split [info tclversion] .] break | 
|---|
| 352 | foreach pa $paths { | 
|---|
| 353 | set p [file join $pa tcl$major] | 
|---|
| 354 | for {set n $minor} {$n >= 0} {incr n -1} { | 
|---|
| 355 | path add [file normalize [file join $p ${major}.${n}]] | 
|---|
| 356 | } | 
|---|
| 357 | path add [file normalize [file join $p site-tcl]] | 
|---|
| 358 | } | 
|---|
| 359 | return | 
|---|
| 360 | } | 
|---|
| 361 |  | 
|---|
| 362 | # Initialization. Set up the default paths, then insert the new | 
|---|
| 363 | # handler into the chain. | 
|---|
| 364 |  | 
|---|
| 365 | ::tcl::tm::Defaults | 
|---|