tm.tcl 12 KB

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