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