tm.tcl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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. namespace eval ::tcl::tm {
  49. # Default paths. None yet.
  50. variable paths {}
  51. # The regex pattern a file name has to match to make it a Tcl Module.
  52. set pkgpattern {^([_[:alpha:]][:_[:alnum:]]*)-([[:digit:]].*)[.]tm$}
  53. # Export the public API
  54. namespace export path
  55. namespace ensemble create -command path -subcommands {add remove list}
  56. }
  57. # ::tcl::tm::path implementations --
  58. #
  59. # Public API to the module path. See specification.
  60. #
  61. # Arguments
  62. # cmd - The subcommand to execute
  63. # args - The paths to add/remove. Must not appear querying the
  64. # path with 'list'.
  65. #
  66. # Results
  67. # No result for subcommands 'add' and 'remove'. A list of paths
  68. # for 'list'.
  69. #
  70. # Sideeffects
  71. # The subcommands 'add' and 'remove' manipulate the list of
  72. # paths to search for Tcl Modules. The subcommand 'list' has no
  73. # sideeffects.
  74. proc ::tcl::tm::add {path args} {
  75. # PART OF THE ::tcl::tm::path ENSEMBLE
  76. #
  77. # The path is added at the head to the list of module paths.
  78. #
  79. # The command enforces the restriction that no path may be an
  80. # ancestor directory of any other path on the list. If the new
  81. # path violates this restriction an error wil be raised.
  82. #
  83. # If the path is already present as is no error will be raised and
  84. # no action will be taken.
  85. variable paths
  86. # We use a copy of the path as source during validation, and
  87. # extend it as well. Because we not only have to detect if the new
  88. # paths are bogus with respect to the existing paths, but also
  89. # between themselves. Otherwise we can still add bogus paths, by
  90. # specifying them in a single call. This makes the use of the new
  91. # paths simpler as well, a trivial assignment of the collected
  92. # paths to the official state var.
  93. set newpaths $paths
  94. foreach p [linsert $args 0 $path] {
  95. if {$p in $newpaths} {
  96. # Ignore a path already on the list.
  97. continue
  98. }
  99. # Search for paths which are subdirectories of the new one. If
  100. # there are any then the new path violates the restriction
  101. # about ancestors.
  102. set pos [lsearch -glob $newpaths ${p}/*]
  103. # Cannot use "in", we need the position for the message.
  104. if {$pos >= 0} {
  105. return -code error \
  106. "$p is ancestor of existing module path [lindex $newpaths $pos]."
  107. }
  108. # Now look for existing paths which are ancestors of the new
  109. # one. This reverse question forces us to loop over the
  110. # existing paths, as each element is the pattern, not the new
  111. # path :(
  112. foreach ep $newpaths {
  113. if {[string match ${ep}/* $p]} {
  114. return -code error \
  115. "$p is subdirectory of existing module path $ep."
  116. }
  117. }
  118. set newpaths [linsert $newpaths 0 $p]
  119. }
  120. # The validation of the input is complete and successful, and
  121. # everything in newpaths is either an old path, or added. We can
  122. # now extend the official list of paths, a simple assignment is
  123. # sufficient.
  124. set paths $newpaths
  125. return
  126. }
  127. proc ::tcl::tm::remove {path args} {
  128. # PART OF THE ::tcl::tm::path ENSEMBLE
  129. #
  130. # Removes the path from the list of module paths. The command is
  131. # silently ignored if the path is not on the list.
  132. variable paths
  133. foreach p [linsert $args 0 $path] {
  134. set pos [lsearch -exact $paths $p]
  135. if {$pos >= 0} {
  136. set paths [lreplace $paths $pos $pos]
  137. }
  138. }
  139. }
  140. proc ::tcl::tm::list {} {
  141. # PART OF THE ::tcl::tm::path ENSEMBLE
  142. variable paths
  143. return $paths
  144. }
  145. # ::tcl::tm::UnknownHandler --
  146. #
  147. # Unknown handler for Tcl Modules, i.e. packages in module form.
  148. #
  149. # Arguments
  150. # original - Original [package unknown] procedure.
  151. # name - Name of desired package.
  152. # version - Version of desired package. Can be the
  153. # empty string.
  154. # exact - Either -exact or ommitted.
  155. #
  156. # Name, version, and exact are used to determine
  157. # satisfaction. The original is called iff no satisfaction was
  158. # achieved. The name is also used to compute the directory to
  159. # target in the search.
  160. #
  161. # Results
  162. # None.
  163. #
  164. # Sideeffects
  165. # May populate the package ifneeded database with additional
  166. # provide scripts.
  167. proc ::tcl::tm::UnknownHandler {original name args} {
  168. # Import the list of paths to search for packages in module form.
  169. # Import the pattern used to check package names in detail.
  170. variable paths
  171. variable pkgpattern
  172. # Without paths to search we can do nothing. (Except falling back
  173. # to the regular search).
  174. if {[llength $paths]} {
  175. set pkgpath [string map {:: /} $name]
  176. set pkgroot [file dirname $pkgpath]
  177. if {$pkgroot eq "."} {
  178. set pkgroot ""
  179. }
  180. # We don't remember a copy of the paths while looping. Tcl
  181. # Modules are unable to change the list while we are searching
  182. # for them. This also simplifies the loop, as we cannot get
  183. # additional directories while iterating over the list. A
  184. # simple foreach is sufficient.
  185. set satisfied 0
  186. foreach path $paths {
  187. if {![interp issafe] && ![file exists $path]} {
  188. continue
  189. }
  190. set currentsearchpath [file join $path $pkgroot]
  191. if {![interp issafe] && ![file exists $currentsearchpath]} {
  192. continue
  193. }
  194. set strip [llength [file split $path]]
  195. # We can't use glob in safe interps, so enclose the following
  196. # in a catch statement, where we get the module files out
  197. # of the subdirectories. In other words, Tcl Modules are
  198. # not-functional in such an interpreter. This is the same
  199. # as for the command "tclPkgUnknown", i.e. the search for
  200. # regular packages.
  201. catch {
  202. # We always look for _all_ possible modules in the current
  203. # path, to get the max result out of the glob.
  204. foreach file [glob -nocomplain -directory $currentsearchpath *.tm] {
  205. set pkgfilename [join [lrange [file split $file] $strip end] ::]
  206. if {![regexp -- $pkgpattern $pkgfilename --> pkgname pkgversion]} {
  207. # Ignore everything not matching our pattern
  208. # for package names.
  209. continue
  210. }
  211. if {[catch {package vcompare $pkgversion 0}]} {
  212. # Ignore everything where the version part is
  213. # not acceptable to "package vcompare".
  214. continue
  215. }
  216. if {[package ifneeded $pkgname $pkgversion] ne {}} {
  217. # There's already a provide script registered for
  218. # this version of this package. Since all units of
  219. # code claiming to be the same version of the same
  220. # package ought to be identical, just stick with
  221. # the one we already have.
  222. continue
  223. }
  224. # We have found a candidate, generate a "provide
  225. # script" for it, and remember it. Note that we
  226. # are using ::list to do this; locally [list]
  227. # means something else without the namespace
  228. # specifier.
  229. # NOTE. When making changes to the format of the
  230. # provide command generated below CHECK that the
  231. # 'LOCATE' procedure in core file
  232. # 'platform/shell.tcl' still understands it, or,
  233. # if not, update its implementation appropriately.
  234. #
  235. # Right now LOCATE's implementation assumes that
  236. # the path of the package file is the last element
  237. # in the list.
  238. package ifneeded $pkgname $pkgversion \
  239. "[::list package provide $pkgname $pkgversion];[::list source -encoding utf-8 $file]"
  240. # We abort in this unknown handler only if we got
  241. # a satisfying candidate for the requested
  242. # package. Otherwise we still have to fallback to
  243. # the regular package search to complete the
  244. # processing.
  245. if {($pkgname eq $name)
  246. && [package vsatisfies $pkgversion {*}$args]} {
  247. set satisfied 1
  248. # We do not abort the loop, and keep adding
  249. # provide scripts for every candidate in the
  250. # directory, just remember to not fall back to
  251. # the regular search anymore.
  252. }
  253. }
  254. }
  255. }
  256. if {$satisfied} {
  257. return
  258. }
  259. }
  260. # Fallback to previous command, if existing. See comment above
  261. # about ::list...
  262. if {[llength $original]} {
  263. uplevel 1 $original [::linsert $args 0 $name]
  264. }
  265. }
  266. # ::tcl::tm::Defaults --
  267. #
  268. # Determines the default search paths.
  269. #
  270. # Arguments
  271. # None
  272. #
  273. # Results
  274. # None.
  275. #
  276. # Sideeffects
  277. # May add paths to the list of defaults.
  278. proc ::tcl::tm::Defaults {} {
  279. global env tcl_platform
  280. lassign [split [info tclversion] .] major minor
  281. set exe [file normalize [info nameofexecutable]]
  282. # Note that we're using [::list], not [list] because [list] means
  283. # something other than [::list] in this namespace.
  284. roots [::list \
  285. [file dirname [info library]] \
  286. [file join [file dirname [file dirname $exe]] lib] \
  287. ]
  288. if {$tcl_platform(platform) eq "windows"} {
  289. set sep ";"
  290. } else {
  291. set sep ":"
  292. }
  293. for {set n $minor} {$n >= 0} {incr n -1} {
  294. foreach ev [::list \
  295. TCL${major}.${n}_TM_PATH \
  296. TCL${major}_${n}_TM_PATH \
  297. ] {
  298. if {![info exists env($ev)]} continue
  299. foreach p [split $env($ev) $sep] {
  300. path add $p
  301. }
  302. }
  303. }
  304. return
  305. }
  306. # ::tcl::tm::roots --
  307. #
  308. # Public API to the module path. See specification.
  309. #
  310. # Arguments
  311. # paths - List of 'root' paths to derive search paths from.
  312. #
  313. # Results
  314. # No result.
  315. #
  316. # Sideeffects
  317. # Calls 'path add' to paths to the list of module search paths.
  318. proc ::tcl::tm::roots {paths} {
  319. lassign [split [package present Tcl] .] major minor
  320. foreach pa $paths {
  321. set p [file join $pa tcl$major]
  322. for {set n $minor} {$n >= 0} {incr n -1} {
  323. set px [file join $p ${major}.${n}]
  324. if {![interp issafe]} { set px [file normalize $px] }
  325. path add $px
  326. }
  327. set px [file join $p site-tcl]
  328. if {![interp issafe]} { set px [file normalize $px] }
  329. path add $px
  330. }
  331. return
  332. }
  333. # Initialization. Set up the default paths, then insert the new
  334. # handler into the chain.
  335. if {![interp issafe]} { ::tcl::tm::Defaults }