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