[5049] | 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 |
---|