You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1775 lines
87 KiB

# -*- tcl -*-
# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2025
#
# @@ Meta Begin
# Application punk::libunknown 0.1
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin shellspy_module_punk::libunknown 0.1]
#[copyright "2025"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
#[require punk::libunknown]
#[keywords module]
#[description]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Overview]
#[para] overview of punk::libunknown
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by punk::libunknown
#[list_begin itemized]
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6}]
# #package require frobz
# #*** !doctools
# #[item] [package {frobz}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
tcl::namespace::eval punk::libunknown {
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Base namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection {Namespace punk::libunknown}]
#[para] Core API functions for punk::libunknown
#[list_begin definitions]
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
variable PUNKARGS
variable PUNKARGS_aliases
lappend PUNKARGS [list {
@id -id "(package)punk::libunknown"
@package -name "punk::libunknown" -help\
"Experimental set of replacements for default 'package unknown' entries."
}]
variable epoch
#if {![info exists epoch]} {
# set tmstate [dict create 0 {}]
# set pkgstate [dict create 0 {}]
# set tminfo [dict create current 0 epochs $tmstate]
# set pkginfo [dict create current 0 epochs $pkgstate]
# set epoch [dict create tm $tminfo pkg $pkginfo]
#}
variable has_package_files
if {[catch {package files foobaz}]} {
set has_package_files 0
} else {
set has_package_files 1
}
if {[info commands ::tcl::Pkg::source] ne ""} {
interp alias "" ::punk::libunknown::tcl_Pkg_source "" ::tcl::Pkg::source
} else {
#early 8.6 - pre tip459?
#we don't have
#::source -nopkg <file>
proc tcl_Pkg_source {filename} {
uplevel 1 [list ::source $filename]
}
}
#will use standard mechanism for non zipfs paths in the tm list.
proc zipfs_tm_UnknownHandler {original name args} {
# Import the list of paths to search for packages in module form.
# Import the pattern used to check package names in detail.
variable epoch
set pkg_epoch [dict get $epoch tm current]
set must_scan 0
if {[dict exists $epoch tm untracked $name]} {
set must_scan 1
#a package that was in the package database at the start - is now being searched for as unknown
#our epoch info is not reliable for pre-known packages - so increment the epoch and fully clear the 'added' paths even in zipfs to do proper scan
#review
#epoch_incr_pkg clearadded
#epoch_incr_tm clearadded
#puts ">>>> removing untracked tm: $name"
dict unset epoch tm untracked $name
#whie it is not the most common configuration - a package could be provided both as a .tm and by packageIndex.tcl files
}
#variable paths
upvar ::tcl::tm::paths paths
#variable pkgpattern
upvar ::tcl::tm::pkgpattern pkgpattern
# Without paths to search we can do nothing. (Except falling back to the
# regular search).
set tid [format %-19s -]
catch {set tid [thread::id]}
if {[llength $paths]} {
set pkgpath [string map {:: /} $name]
set pkgroot [file dirname $pkgpath]
if {$pkgroot eq "."} {
set pkgroot ""
}
# We don't remember a copy of the paths while looping. Tcl Modules are
# unable to change the list while we are searching for them. This also
# simplifies the loop, as we cannot get additional directories while
# iterating over the list. A simple foreach is sufficient.
if {[info commands ::tcl::zipfs::root] ne ""} {
set zipfsroot [tcl::zipfs::root]
set has_zipfs 1
} else {
set zipfsroot "//zipfs:/" ;#doesn't matter much what we use here - don't expect in tm list if no zipfs commands
set has_zipfs 0
}
set satisfied 0
foreach path $paths {
if {![interp issafe] && ![file exists $path]} {
continue
}
set currentsearchpath $path
set specificsearchpath [file join $path $pkgroot]
# Get the module files out of the subdirectories.
# - Safe Base interpreters have a restricted "glob" command that
# works in this case.
# - The "catch" was essential when there was no safe glob and every
# call in a safe interp failed; it is retained only for corner
# cases in which the eventual call to glob returns an error.
set use_epoch_for_all 1
if {$use_epoch_for_all || [string match $zipfsroot* $path]} {
if {!$must_scan && [dict exists $epoch tm epochs $pkg_epoch indexes $specificsearchpath]} {
#indexes are actual .tm files here
set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $specificsearchpath]]
#puts "--->zipfs_tm_UnknownHandler $tid llength tmfiles ( cached ): [format %4d [llength $tmfiles]] name:$name searchpath:$currentsearchpath"
} else {
if {![interp issafe] && ![file exists $specificsearchpath]} {
dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath [dict create]
continue
}
dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath [dict create]
# #################################################################
if {$has_zipfs && [string match $zipfsroot* $path]} {
#The entire tm tre is available so quickly from the zipfs::list call - that we can gather all at once.
set tmfiles [::tcl::zipfs::list $currentsearchpath/*.tm] ;#could theoretically be a dir - this is effectively a tree traversal
foreach tm_path $tmfiles {
dict set epoch tm epochs $pkg_epoch indexes [file dirname $tm_path] $tm_path $pkg_epoch
}
#retrieval using tcl::zipfs::list got (and cached) extras - limit to specificsearchpath
set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $specificsearchpath]]
} else {
#set tmfiles [glob -nocomplain -directory $currentsearchpath *.tm]
set tmfiles [glob -nocomplain -directory $specificsearchpath *.tm]
foreach tm_path $tmfiles {
#dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath $tm_path $pkg_epoch
dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath $tm_path $pkg_epoch
}
}
#puts "--->zipfs_tm_UnknownHandler $tid llength tmfiles (UNcached): [format %4d [llength $tmfiles]] name:$name searchpath:$currentsearchpath"
# #################################################################
}
if {![llength $tmfiles]} {
continue
}
# like normal processing - but track added (for static zipfs)
set can_skip_update 0
if {[string match $zipfsroot* $path]} {
#static tm location
if {[dict exists $epoch tm epochs $pkg_epoch added $specificsearchpath]} {
if {![dict exists $epoch tm epochs $pkg_epoch added $specificsearchpath $name]} {
#$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again.
#puts stderr "zipfs_tm_UnknownHandler $tid CAN SKIP orig:$original name:$name args:$args searchpath:$currentsearchpath"
set can_skip_update 1
} else {
#if this name is in 'added' then we must have done something like a package forget or it wouldn't come back to package unknown
#dict unset epoch tm epochs $pkg_epoch added $currentsearchpath $name
}
}
}
if {!$can_skip_update} {
set strip [llength [file split $path]]
set found_name_in_currentsearchpath 0 ;#for negative cache by epoch
if {[catch {
foreach file $tmfiles {
set pkgfilename [join [lrange [file split $file] $strip end] ::]
if {![regexp -- $pkgpattern $pkgfilename --> pkgname pkgversion]} {
# Ignore everything not matching our pattern for
# package names.
continue
}
try {
package vcompare $pkgversion 0
} on error {} {
# Ignore everything where the version part is not
# acceptable to "package vcompare".
continue
}
if {([package ifneeded $pkgname $pkgversion] ne {})
&& (![interp issafe])
} {
# There's already a provide script registered for
# this version of this package. Since all units of
# code claiming to be the same version of the same
# package ought to be identical, just stick with
# the one we already have.
# This does not apply to Safe Base interpreters because
# the token-to-directory mapping may have changed.
#JMN - review.
#dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion]
dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch
if {$must_scan} {
#however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked
dict unset epoch tm untracked $pkgname
}
if {$pkgname eq $name} {
#can occur multiple times, different versions
#record package name as found in this path whether version satisfies or not
set found_name_in_currentsearchpath 1
}
#don't override the ifneeded script - for tm files the first encountered 'wins'.
continue
}
# We have found a candidate, generate a "provide script"
# for it, and remember it. Note that we are using ::list
# to do this; locally [list] means something else without
# the namespace specifier.
# NOTE. When making changes to the format of the provide
# command generated below CHECK that the 'LOCATE'
# procedure in core file 'platform/shell.tcl' still
# understands it, or, if not, update its implementation
# appropriately.
#
# Right now LOCATE's implementation assumes that the path
# of the package file is the last element in the list.
package ifneeded $pkgname $pkgversion \
"[::list package provide $pkgname $pkgversion];[::list source $file]"
#JMN
#store only once for each name, although there may be multiple versions of same package within this searchpath
#dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion]
dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch
#pkgname here could be the 'name' passed at the beggning - or other .tms at the same location.
#we can't always remove other .tms from 'tm untracked' because the search for name might skip some locations.
if {$must_scan} {
#however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked
dict unset epoch tm untracked $pkgname
}
# We abort in this unknown handler only if we got a
# satisfying candidate for the requested package.
# Otherwise we still have to fallback to the regular
# package search to complete the processing.
if {($pkgname eq $name)
&& [package vsatisfies $pkgversion {*}$args]} {
set satisfied 1
# We do not abort the loop, and keep adding provide
# scripts for every candidate in the directory, just
# remember to not fall back to the regular search
# anymore.
#(obsolete for libunknown - review)
}
if {$pkgname eq $name} {
#can occur multiple times, different versions
#record package name as found in this path whether version satisfies or not
set found_name_in_currentsearchpath 1
}
}
} errMsg]} {
puts stderr "zipfs_tm_Unknownhandler: error for tm file $file searchpath:$currentsearchpath"
}
}
} else {
#non zipfs tm path - normal processing
# We always look for _all_ possible modules in the current
# path, to get the max result out of the glob.
set tmfiles [glob -nocomplain -directory $currentsearchpath *.tm]
set strip [llength [file split $path]]
catch {
foreach file $tmfiles {
set pkgfilename [join [lrange [file split $file] $strip end] ::]
if {![regexp -- $pkgpattern $pkgfilename --> pkgname pkgversion]} {
# Ignore everything not matching our pattern for
# package names.
continue
}
try {
package vcompare $pkgversion 0
} on error {} {
# Ignore everything where the version part is not
# acceptable to "package vcompare".
continue
}
if {([package ifneeded $pkgname $pkgversion] ne {})
&& (![interp issafe])
} {
# There's already a provide script registered for
# this version of this package. Since all units of
# code claiming to be the same version of the same
# package ought to be identical, just stick with
# the one we already have.
# This does not apply to Safe Base interpreters because
# the token-to-directory mapping may have changed.
continue
}
# We have found a candidate, generate a "provide script"
# for it, and remember it. Note that we are using ::list
# to do this; locally [list] means something else without
# the namespace specifier.
# NOTE. When making changes to the format of the provide
# command generated below CHECK that the 'LOCATE'
# procedure in core file 'platform/shell.tcl' still
# understands it, or, if not, update its implementation
# appropriately.
#
# Right now LOCATE's implementation assumes that the path
# of the package file is the last element in the list.
package ifneeded $pkgname $pkgversion \
"[::list package provide $pkgname $pkgversion];[::list source $file]"
# We abort in this unknown handler only if we got a
# satisfying candidate for the requested package.
# Otherwise we still have to fallback to the regular
# package search to complete the processing.
if {($pkgname eq $name)
&& [package vsatisfies $pkgversion {*}$args]} {
set satisfied 1
# We do not abort the loop, and keep adding provide
# scripts for every candidate in the directory, just
# remember to not fall back to the regular search
# anymore.
}
}
}
}
##ZZZ
}
#if {$satisfied} {
# return
#}
}
# Fallback to previous command, if existing. See comment above about
# ::list...
if {[llength $original]} {
#puts "zipfs_tm_UnknownHandler passing on to: $original [::linsert $args 0 $name]"
uplevel 1 $original [::linsert $args 0 $name]
}
}
proc zipfs_tclPkgUnknown {name args} {
#puts "-> zipfs_tclPkgUnknown $name $args EXPERIMENTAL"
variable epoch
set pkg_epoch [dict get $epoch pkg current]
#review - the ifneeded script is not the only thing required in a new interp.. consider tclIndex files and auto_load mechanism.
#also the pkgIndex.tcl could possibly provide a different ifneeded script based on interp issafe (or other interp specific things?)
#if {[dict exists $epoch scripts $name]} {
# set vscripts [dict get $epoch scripts $name]
# dict for {v scr} $vscripts {
# puts ">package ifneeded $name $v"
# package ifneeded $name $v $scr
# }
# return
#}
set must_scan 0
if {[dict exists $epoch pkg untracked $name]} {
#a package that was in the package database at the start - is now being searched for as unknown
#(due to a package forget?)
#our epoch info is not valid for pre-known packages - so setting must_scan to true
set must_scan 1
#puts ">>>> removing pkg untracked: $name"
dict unset epoch pkg untracked $name
}
#global auto_path env
global auto_path
if {![info exists auto_path]} {
return
}
set tid [format %-19s -]
catch {set tid [thread::id]}
if {[info commands ::tcl::zipfs::root] ne ""} {
set zipfsroot [tcl::zipfs::root]
set has_zipfs 1
} else {
set zipfsroot "//zipfs:/" ;#doesn't matter too much what we use here - don't expect in tm list if no zipfs commands
set has_zipfs 0
}
#review - think about this
#typical dict size might be 800 packages - values are versions
#we probably don't need to create/destroy it for each iteration of the wile.
#question is whether some pkgIndex.tcl files may do a package forget? They probably don't/shouldn't(?) Does that matter here anyway?
set before_dict [dict create]
#J2
#siblings that have been affected by source scripts - need to retest ifneeded scripts at end for proper ordering.
set refresh_dict [dict create]
#Note that autopath is being processed from the end to the front
#ie last lappended first. This means if there are duplicate versions earlier in the list,
#they will be the last to call 'package provide' for that version and so their provide script will 'win'.
#This means we should have faster filesystems such as zipfs earlier in the list.
# Cache the auto_path, because it may change while we run through the
# first set of pkgIndex.tcl files
set old_path [set use_path $auto_path]
while {[llength $use_path]} {
set dir [lindex $use_path end]
# Make sure we only scan each directory one time.
if {[info exists tclSeenPath($dir)]} {
set use_path [lrange $use_path 0 end-1]
continue
}
set tclSeenPath($dir) 1
# Get the pkgIndex.tcl files in subdirectories of auto_path directories.
# - Safe Base interpreters have a restricted "glob" command that
# works in this case.
# - The "catch" was essential when there was no safe glob and every
# call in a safe interp failed; it is retained only for corner
# cases in which the eventual call to glob returns an error.
set use_epoch_for_all 1
if {$use_epoch_for_all || [string match $zipfsroot* $dir]} {
set currentsearchpath $dir
if {!$must_scan && [dict exists $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]} {
set indexfiles [dict keys [dict get $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]]
#puts stderr "--->zipfs_tclPkgUnknown $tid llength tmfiles ( cached ): [format %4d [llength $indexfiles]] name:$name searchpath:$currentsearchpath"
} else {
dict set epoch pkg epochs $pkg_epoch indexes $currentsearchpath [dict create]
# #################################################################
set indexpaths [glob -directory $currentsearchpath -join -nocomplain * pkgIndex.tcl]
foreach idxpath $indexpaths {
dict set epoch pkg epochs $pkg_epoch indexes $currentsearchpath $idxpath 1
}
set indexfiles [dict keys [dict get $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]]
#puts stderr "--->zipfs_tclPkgUnknown $tid llength tmfiles (UNcached): [format %4d [llength $indexfiles]] name:$name searchpath:$currentsearchpath"
# #################################################################
}
if {![llength $indexfiles]} {
continue
}
set can_skip_sourcing 0
#if {$has_zipfs && [string match $zipfsroot* $dir]} {
#static auto_path dirs
if {!$must_scan} {
if {[dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath]} {
if {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} {
#$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again.
#puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath"
set can_skip_sourcing 1
} else {
#if this name is in added then we must have done a package forget or it wouldn't come back to package unknown ?
#remove it and let it be readded if it's still provided by this path?
#probably doesn't make sense for static path?
#dict unset epoch pkg epochs $pkg_epoch added $currentsearchpath $name
}
}
}
#}
#An edge case exception is that after a package forget, a deliberate call to 'package require non-existant'
#will not trigger rescans for all versions of other packages.
#A rescan of a specific package for all versions can still be triggered with a package require for
#an exact non-existant version. e.g package require md5 0-0
#(or misordered min max e.g package require md5 1-0 i.e a deliberately unsatisfiable version range)
set sourced 0
set just_added [dict create]
set just_changed [dict create]
#set sourced_files [list]
#J2
#set can_skip_sourcing 0
if {!$can_skip_sourcing} {
#Note - naive comparison of before_pkgs vs after_pkgs isn't quite enough to tell if something was added. It could have added a version.
#this will stop us rescanning everything properly by doing a 'package require nonexistant'
#use 'info exists' to only call package names once and then append?
#This could be problematic? (re-entrant tclPkgUnknown in some pkgIndex scripts?) pkgIndex.tcl scripts "shouldn't" do this?
if {![info exists before_pkgs]} {
set before_pkgs [package names]
#update the before_dict which persists across while loop
#we need to track the actual 'ifneeded' script not just version numbers,
#because the last ifneeded script processed for each version is the one that ultimately applies.
foreach bp $before_pkgs {
#dict set before_dict $bp [package versions $bp]
foreach v [package versions $bp] {
dict set before_dict $bp $v [package ifneeded $bp $v]
}
}
}
#set before_pkgs [package names]
#catch {
foreach file $indexfiles {
set dir [file dirname $file]
if {![info exists procdDirs($dir)]} {
try {
#if {[string match //zipfs*registry* $file]} {
# puts stderr "----->0 sourcing zipfs file $file"
#}
incr sourced ;#count as sourced even if source fails; keep before actual source action
#::tcl::Pkg::source $file
#lappend sourced_files $file
tcl_Pkg_source $file
} trap {POSIX EACCES} {} {
# $file was not readable; silently ignore
puts stderr "zipfs_tclPkgUnknown file unreadable '$file' while trying to load $name (1)"
continue
} on error msg {
if {[regexp {version conflict for package} $msg]} {
# In case of version conflict, silently ignore
puts stderr "zipfs_tclPkgUnknown version conflict sourcing '$file' while trying to load $name (1)\nmsg:$msg"
continue
}
tclLog "error reading package index file $file: $msg"
} on ok {} {
set procdDirs($dir) 1
}
}
#each source operation could affect auto_path - and thus increment the pkg epoch (via trace on ::auto_path)
#e.g tcllib pkgIndex.tcl appends to auto_path
set pkg_epoch [dict get $epoch pkg current]
}
#}
set dir [lindex $use_path end]
if {![info exists procdDirs($dir)]} {
set file [file join $dir pkgIndex.tcl]
# safe interps usually don't have "file exists",
if {([interp issafe] || [file exists $file])} {
try {
#puts "----->2 sourcing $file"
incr sourced
#lappend sourced_files $file
#::tcl::Pkg::source $file
tcl_Pkg_source $file
} trap {POSIX EACCES} {} {
# $file was not readable; silently ignore
puts stderr "zipfs_tclPkgUnknown file unreadable '$file' while trying to load $name (2)"
continue
} on error msg {
if {[regexp {version conflict for package} $msg]} {
# In case of version conflict, silently ignore
puts stderr "zipfs_tclPkgUnknown version conflict sourcing '$file' while trying to load $name (2)\nmsg:$msg"
continue
}
tclLog "error reading package index file $file: $msg"
} on ok {} {
set procdDirs($dir) 1
}
set pkg_epoch [dict get $epoch pkg current]
}
}
#dict set epoch pkg epochs $pkg_epoch added $currentsearchpath [dict create]
#avoid calculating package and version diffs if nothing was actually sourced
if {$sourced > 0} {
if {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath]} {
dict set epoch pkg epochs $pkg_epoch added $currentsearchpath [dict create]
#ensure there is an empty entry for the path if no packages added or changed versions
}
set after_pkgs [package names]
#puts "@@@@pkg epochs $pkg_epoch searchpath:$currentsearchpath name:$name before: [llength $before_pkgs] after: [llength $after_pkgs]"
if {[llength $after_pkgs] > [llength $before_pkgs]} {
foreach a $after_pkgs {
foreach v [package versions $a] {
if {![dict exists $before_dict $a $v]} {
dict set just_added $a $v 1
set iscript [package ifneeded $a $v]
#J2
#dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a [dict create e $pkg_epoch v $v]
dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a $v [dict create e $pkg_epoch scr $iscript]
if {$must_scan} {
dict unset epoch pkg untracked $a
}
}
}
}
}
#-----------------
#if {[dict size $just_added]} {
# puts stderr "\x1b\[31m>>>zipfs_tclPkgUnknown called on name:$name added [dict size $just_added] from searchpath:$currentsearchpath\x1b\[m"
# puts stderr ">>> [join [lrange [dict keys $just_added] 0 10] \n]..."
#} else {
# tclLog ">>>zipfs_tclPkgUnknown called on name:$name Nothing added for searchpath:$currentsearchpath"
# if {[string match twapi* $name]} {
# tclLog ">>>zipfs_tclPkgUnknown: sourced_files:"
# foreach f $sourced_files {
# puts ">>> $f"
# }
# }
# if {$currentsearchpath in "//zipfs:/app //zipfs:/app/tcl_library"} {
# puts " before_pkgs: [llength $before_pkgs]"
# puts " lsearch msgcat: [lsearch $before_pkgs msgcat]"
# puts " after_pkgs: [llength $after_pkgs]"
# puts " \x1b\31mlsearch msgcat: [lsearch $after_pkgs msgcat]\x1b\[m"
# if {[lsearch $after_pkgs msgcat] >=0} {
# set versions [package versions msgcat]
# puts "msgcat versions: $versions"
# foreach v $versions {
# puts "\x1b\[32m $v ifneeded: [package ifneeded msgcat $v] \x1b\[m"
# }
# }
# }
#}
#-----------------
#review - just because this searchpath didn't add a package or add a version for the package
#it doesn't mean there wasn't a version of this package supplied there
#It may just be the same version as one we've already found.
#The last one found (earlier in auto_path) for a version is the one that supplies the final 'package provide' statement (by overriding it)
#
dict for {bp bpversionscripts} $before_dict {
#if {!$must_scan && ![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $bp]} {
# #puts -nonewline .
# continue
#}
dict for {bv bscript} $bpversionscripts {
set nowscript [package ifneeded $bp $bv]
if {$bscript ne $nowscript} {
#ifneeded script has changed. The same version of bp was supplied on this path.
#As it's processed later - it will be the one in effect.
#dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp [dict create e $pkg_epoch v $bv]
dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp $bv [dict create e $pkg_epoch scr $nowscript]
dict set before_dict $bp $bv $nowscript
dict set just_changed $bp $bv 1
#j2
if {$must_scan} {
dict unset epoch pkg untracked $bp
}
}
}
}
#update before_pkgs & before_dict for next path
dict for {newp vdict} $just_added {
if {$newp ni $before_pkgs} {
lappend before_pkgs $newp
}
dict for {v _} $vdict {
set nowscript [package ifneeded $newp $v]
dict set before_dict $newp $v $nowscript
}
}
}
}
} else {
#normal processing - not a static filesystem - we can't skip.
set indexfiles [glob -directory $dir -join -nocomplain * pkgIndex.tcl]
catch {
foreach file $indexfiles {
set dir [file dirname $file]
if {![info exists procdDirs($dir)]} {
try {
#puts "----->1 sourcing $file"
::tcl::Pkg::source $file
} trap {POSIX EACCES} {} {
# $file was not readable; silently ignore
continue
} on error msg {
if {[regexp {version conflict for package} $msg]} {
# In case of version conflict, silently ignore
continue
}
tclLog "error reading package index file $file: $msg"
} on ok {} {
set procdDirs($dir) 1
}
}
}
}
set dir [lindex $use_path end]
if {![info exists procdDirs($dir)]} {
set file [file join $dir pkgIndex.tcl]
# safe interps usually don't have "file exists",
if {([interp issafe] || [file exists $file])} {
try {
#puts "----->2 sourcing $file"
::tcl::Pkg::source $file
} trap {POSIX EACCES} {} {
# $file was not readable; silently ignore
continue
} on error msg {
if {[regexp {version conflict for package} $msg]} {
# In case of version conflict, silently ignore
continue
}
tclLog "error reading package index file $file: $msg"
} on ok {} {
set procdDirs($dir) 1
}
}
}
}
set use_path [lrange $use_path 0 end-1]
# Check whether any of the index scripts we [source]d above set a new
# value for $::auto_path. If so, then find any new directories on the
# $::auto_path, and lappend them to the $use_path we are working from.
# This gives index scripts the (arguably unwise) power to expand the
# index script search path while the search is in progress.
set index 0
if {[llength $old_path] == [llength $auto_path]} {
foreach dir $auto_path old $old_path {
if {$dir ne $old} {
# This entry in $::auto_path has changed.
break
}
incr index
}
}
# $index now points to the first element of $auto_path that has
# changed, or the beginning if $auto_path has changed length Scan the
# new elements of $auto_path for directories to add to $use_path.
# Don't add directories we've already seen, or ones already on the
# $use_path.
foreach dir [lrange $auto_path $index end] {
if {![info exists tclSeenPath($dir)] && ($dir ni $use_path)} {
lappend use_path $dir
}
}
set old_path $auto_path
dict for {pkg versions} $just_changed {
foreach v [dict keys $versions] {
dict set refresh_dict $pkg $v 1
}
}
dict for {pkg versions} $just_added {
foreach v [dict keys $versions] {
dict set refresh_dict $pkg $v 1
}
}
}
#refresh ifneeded scripts for just_added/just_changed
#review: searchpaths are in auto_path order - earliest has precedence for any particular pkg-version
#REVIEW: what is to stop an auto_path package e.g from os, overriding a .tm ifneeded script from an item earlier in the package_mode list configured in punk's main.tcl?
#e.g when package_mode is {dev-os} we don't want a pkgIndex package from ::env(TCLLIBPATH) overriding a .tm from the dev paths (even if version nums the same)
#conversely we do want a dev path pkIndex package overriding an existing ifneeded script from a .tm in os
#to accomodate this - we may need to maintain a subdict in epoch of paths/path-prefixes to package_mode members os, dev, internal
#this 'refresh' is really a 'reversion' to what was already stored in epoch pkg epochs <currente> added
#
set e [dict get $epoch pkg current]
set pkgvdone [dict create]
set dict_added [dict get $epoch pkg epochs $e added]
#keys are in reverse order due to tclPkgUnknown processing order
set ordered_searchpaths [lreverse [dict keys $dict_added]];# ordered as in auto_path
dict for {pkg versiond} $refresh_dict {
set versions [dict keys $versiond]
puts stderr "---->pkg:$pkg versions: $versions"
foreach searchpath $ordered_searchpaths {
set addedinfo [dict get $dict_added $searchpath]
set vidx -1
foreach v $versions {
incr vidx
if {[dict exists $addedinfo $pkg $v]} {
ledit versions $vidx $vidx
set iscript [dict get $addedinfo $pkg $v scr]
#todo - find the iscript in the '$epoch pkg epochs <e> added paths' lists and determine os vs dev vs internal
#(scanning for path directly in the ifneeded script for pkgs is potentially error prone)
#for .tm ifneeded scripts - the syntax is simple enough to determine directly (and ifneeded scr not stored for those anyway)
set justaddedscript [package ifneeded $pkg $v]
if {$justaddedscript ne $iscript} {
puts "---->refreshing $pkg $v - reverting to already stored from path:$searchpath versions: $versions"
package ifneeded $pkg $v $iscript
#dict set pkgvdone $pkg $v 1
}
}
}
if {[llength $versions] == 0} {
break
}
}
}
#puts "zipfs_tclPkgUnknown DONE"
}
variable last_auto_path
variable last_tm_paths
proc epoch_incr_pkg {args} {
if {[catch {
variable last_auto_path
global auto_path
upvar ::punk::libunknown::epoch epoch
dict set epoch scripts {}
set prev_e [dict get $epoch pkg current]
set current_e [expr {$prev_e + 1}]
# -------------
puts stderr "--> pkg epoch $prev_e -> $current_e"
puts stderr "args: $args"
puts stderr "last_auto: $last_auto_path"
puts stderr "auto_path: $auto_path"
# -------------
if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} {
#The auto_path changed, and is a pure addition of entry/entries
#commonly this is occurs where a single entry is added by a pkgIndex.Tcl
#e.g tcllib adds its base dir so that all pkgIndex.tcl files in subdirs are subsequently found
#consider autopath
#c:/libbase //zipfs:/app/libbase
#if both contain a tcllib folder with pkgIndex.tcl that extends auto_path, the auto_path extends as follows:
# -> c:/libbase //zipfs:/app/libbase //zipfs:/app/libbase/tcllib
# -> c:/libbase //zipfs:/app/libbase //zipfs:/app/libbase/tcllib c:/libbase/tcllib
#the tclPkgUnknown usedir loop (working from end of list towards beginning) will process these changes the first time dynamically
#as they occur:
#ie //zipfs:/app/libbase //zipfs:/app/libbase/tcllib c:/libbase c:/libbase/tcllib
#A subsequent scan by tclPkgUnknown on the extended auto_path would process in the order:
#c:/libbase/tcllib c:/libbase //zipfs:/app/libbase/tcllib //zipfs:/app/libbase
#re-order the new additions to come immediately following the longest common prefix entry
set newitems [punk::libunknown::lib::ldiff $auto_path $last_auto_path]
set update $last_auto_path
#no ledit or punk::lib::compat::ledit for 8.6 - so use linsert
foreach new $newitems {
set offset 0
set has_prefix 0
foreach ap [lreverse $update] {
if {[string match $ap* $new]} {
set has_prefix 1
break
}
incr offset
}
if {$has_prefix} {
set update [linsert $update end-$offset $new]
} else {
lappend update $new
}
}
set auto_path $update
}
#else - if auto_path change wasn't just extra entries - leave as user specified
#review.
set last_auto_path $auto_path
# -------------
dict set epoch pkg current $current_e
dict set epoch pkg epochs $current_e [dict create]
if {[info commands ::tcl::zipfs::root] ne ""} {
set has_zipfs 1
} else {
set has_zipfs 0
}
if {[dict exists $epoch pkg epochs $prev_e indexes]} {
#bring across each previous 'indexes' record *if* searchpath is within zipfs root static filesystem
# and searchpath is still a path below an auto_path entry.
if {$has_zipfs} {
set zroot [zipfs root]
dict for {searchpath indexfiles} [dict get $epoch pkg epochs $prev_e indexes] {
if {[string match $zroot* $searchpath]} {
set stillvalid 0
foreach a $auto_path {
if {[string match $a* $searchpath]} {
set stillvalid 1
break
}
}
if {$stillvalid} {
dict set epoch pkg epochs $current_e indexes $searchpath $indexfiles
}
}
}
}
#----------------------------------------
#store basic stats for previous epoch instead of all data.
set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e indexes]]
set index_count 0
dict for {searchpath indexfiles} [dict get $epoch pkg epochs $prev_e indexes] {
#update prev epoch to be basic statistical info only
incr index_count [llength $indexfiles]
}
dict set epoch pkg epochs $prev_e indexes_history [dict create searchpath_count $index_searchpath_count index_count $index_count]
dict unset epoch pkg epochs $prev_e indexes
#----------------------------------------
} else {
dict set epoch pkg epochs $prev_e indexes_history [dict create searchpath_count 0 index_count 0]
}
if {[dict exists $epoch pkg epochs $prev_e added]} {
if {"clearadded" in $args} {
dict set epoch pkg epochs $current_e added [dict create]
} else {
if {$has_zipfs} {
set zroot [zipfs root]
set prev_added [dict get $epoch pkg epochs $prev_e added]
set keep_added [dict filter $prev_added key $zroot*]
#bring across - each lib will have previous epoch number as the value indicating epoch in which it was found
#dict set epoch pkg epochs $current_e added [dict get $epoch pkg epochs $prev_e added]
dict set epoch pkg epochs $current_e added $keep_added
} else {
dict set epoch pkg epochs $current_e added [dict create]
}
}
#store basic stats for previous epoch
#------------------------------------
set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e added]]
set lib_count 0
dict for {searchpath libinfo} [dict get $epoch pkg epochs $prev_e added] {
dict for {lib e} $libinfo {
if {$e == $prev_e} {
incr lib_count
}
}
}
dict set epoch pkg epochs $prev_e added_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count]
dict unset epoch pkg epochs $prev_e added
#------------------------------------
} else {
dict set epoch pkg epochs $prev_e added_history [dict create searchpath_count 0 lib_count 0]
}
} errM]} {
puts stderr "epoch_incr_pkg error\n $errM\n$::errorInfo"
}
}
proc epoch_incr_tm {args} {
if {[catch {
upvar ::punk::libunknown::epoch epoch
dict set epoch scripts {}
set prev_e [dict get $epoch tm current]
set current_e [expr {$prev_e + 1}]
dict set epoch tm current $current_e
dict set epoch tm epochs $current_e [dict create]
set tmlist [tcl::tm::list]
if {[info commands ::tcl::zipfs::root] ne ""} {
set has_zipfs 1
} else {
set has_zipfs 0
}
if {[dict exists $epoch tm epochs $prev_e indexes]} {
#bring across the previous indexes records if static filesystem (zipfs)
if {$has_zipfs} {
set zroot [zipfs root]
dict for {searchpath indexfiles} [dict get $epoch tm epochs $prev_e indexes] {
if {[string match $zroot* $searchpath]} {
#check all valid for current state of tcl::tm::list
set stillvalid 0
foreach tm_path $tmlist {
if {[string match $tm_path* $searchpath]} {
set stillvalid 1
break
}
}
if {$stillvalid} {
dict set epoch tm epochs $current_e indexes $searchpath $indexfiles
}
}
}
}
set index_searchpath_count [dict size [dict get $epoch tm epochs $prev_e indexes]]
set index_count 0
dict for {searchpath indexfiles} [dict get $epoch tm epochs $prev_e indexes] {
#update prev epoch to be basic statistical info only
incr index_count [llength $indexfiles]
}
dict set epoch tm epochs $prev_e indexes_history [dict create searchpath_count $index_searchpath_count index_count $index_count]
dict unset epoch tm epochs $prev_e indexes
} else {
dict set epoch tm epochs $prev_e indexes_history [dict create searchpath_count 0 index_count 0]
}
if {[dict exists $epoch tm epochs $prev_e added]} {
#todo? cycle through non-statics and add pkgs to pkg untracked if we are deleting 'added' records?
if {"clearadded" in $args} {
dict set epoch tm epochs $current_e added [dict create]
} else {
#bring across - each lib will have previous epoch number
#dict set epoch tm epochs $current_e added [dict get $epoch tm epochs $prev_e added]
if {$has_zipfs} {
set zroot [zipfs root]
dict set epoch tm epochs $current_e added [dict filter [dict get $epoch tm epochs $prev_e added] key $zroot*]
} else {
dict set epoch tm epochs $current_e added [dict create]
}
}
set index_searchpath_count [dict size [dict get $epoch tm epochs $prev_e added]]
set lib_count 0
dict for {searchpath libinfo} [dict get $epoch tm epochs $prev_e added] {
dict for {lib e} $libinfo {
if {$e == $prev_e} {
incr lib_count
}
}
}
dict set epoch tm epochs $prev_e added_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count]
dict unset epoch tm epochs $prev_e added
} else {
dict set epoch tm epochs $prev_e added_history [dict create searchpath_count 0 lib_count 0]
}
} errM]} {
puts stderr "epoch_incr_tm error\n $errM"
}
}
#see what basic info we can gather *quickly* about the indexes for each version of a pkg that the package db knows about.
#we want no calls out to the actual filesystem - but we can use some 'file' calls such as 'file dirname', 'file split' (review -safe interp problem)
#in practice the info is only available for tm modules
proc packagedb_indexinfo {pkg} {
if {[string match ::* $pkg]} {
error "packagedb_indexinfo: package name required - not a fully qualified namespace beginning with :: Received:'$pkg'"
}
set versions [lsort -command {package vcompare} [package versions $pkg]]
if {[llength $versions] == 0} {
set v [package provide $pkg]
}
set versionlist [list]
foreach v $versions {
set ifneededscript [package ifneeded $pkg $v]
if {[string trim $ifneededscript] eq ""} {
lappend versionlist [list $v type unknown index "" indexbase ""]
continue
}
set scriptlines [split $ifneededscript \n]
if {[llength $scriptlines] > 1} {
lappend versionlist [list $v type unknown index "" indexbase ""]
continue
}
if {[catch {llength $ifneededscript}]} {
#scripts aren't necessarily 'list shaped' - we don't want to get into the weeds trying to make sense of arbitrary scripts.
lappend versionlist [list $v type unknown index "" indexbase ""]
continue
}
if {[lindex $ifneededscript 0] eq "package" && [lindex $ifneededscript 1] eq "provide" && [file extension [lindex $ifneededscript end]] eq ".tm"} {
set tmfile [lindex $ifneededscript end]
set nspath [namespace qualifiers $pkg]
if {$nspath eq ""} {
set base [file dirname $tmfile]
} else {
set nsparts [string map {:: " "} $nspath] ;#*naive* split - we are assuming (fairly reasonably) there are no namespaces containing spaces for a .tm module
set pathparts [file split [file dirname $tmfile]]
set baseparts [lrange $pathparts 0 end-[llength $nsparts]]
set base [file join {*}$baseparts]
}
lappend versionlist [list $v type tm index $tmfile indexbase $base script $ifneededscript]
} else {
#we could guess at the pkgindex.tcl file used based on simple pkg ifneeded scripts .tcl path compared to ::auto_index
#but without hitting filesystem to verify - it's unsatisfactory
lappend versionlist [list $v type unknown index "" indexbase "" script $ifneededscript]
}
}
return $versionlist
}
proc init {args} {
variable last_auto_path
set last_auto_path [set ::auto_path]
variable last_tm_paths
set last_tm_paths [set ::tcl::tm::paths]
set callerposn [lsearch $args -caller]
if {$callerposn > -1} {
set caller [lindex $args $callerposn+1]
#puts stderr "\x1b\[1\;33m punk::libunknown::init - caller:$caller\x1b\[m"
#puts stderr "punk::libunknown::init auto_path : $::auto_path"
#puts stderr "punk::libunknown::init tcl::tm::list: [tcl::tm::list]"
}
if {[catch {tcl::tm::list} tmlist]} {
set tmlist [list]
}
set apath [list]
if {[info commands tcl::tm::list] ne ""} {
set tmlist [tcl::tm::list]
}
if {[info exists ::auto_path]} {
set apath $::auto_path
}
if {![llength $tmlist] && ![llength $apath]} {
#shouldn't happen - be noisy about it for now
puts stderr "punk::libunknown::init - init while empty/unreadable tcl::tm::list and empty/unreadable ::auto_path"
}
if {[namespace origin ::package] eq "::punk::libunknown::package"} {
#This is far from conclusive - there may be other renamers (e.g commandstack)
return
}
if {[info commands ::punk::libunknown::package] ne ""} {
puts stderr "punk::libunknown::init already done - unnecessary call? info frame -1: [info frame -1]"
return
}
variable epoch
if {![info exists epoch]} {
set tmstate [dict create 0 {added {}}]
set pkgstate [dict create 0 {added {}}]
set tminfo [dict create current 0 epochs $tmstate untracked [dict create]]
set pkginfo [dict create current 0 epochs $pkgstate untracked [dict create]]
set epoch [dict create scripts {} tm $tminfo pkg $pkginfo]
#untracked: package names at time of punk::libunknown::init call - or passed with epoch when sharing epoch to another interp.
#The epoch state will need to be incremented and cleared of any 'added' records if any of these are requested during a package unknown call
#Because they were loaded prior to us tracking the epochs - and without trying to examine the ifneeded scripts we don't know the exact paths
#which were scanned to load them. Our 'added' key entries will not contain them because they weren't unknown
} else {
#we're accepting a pre-provided 'epoch' record (probably from another interp)
#the tm untracked and pkg untracked dicts indicate for which packages the pkg added, tm added etc data are not conclusive
#test
#todo?
}
#upon first libunknown::init in the interp, we need to add any of this interp's already known packages to the (possibly existing) tm untracked and pkg untracked dicts.
#(unless we can use packagedb_indexinfo to determine what was previously scanned?)
# review - what if the auto_path or tcl::tm::list was changed between initial scan and call of libunknown::init???
# This is likely a common scenario?!!!
# For now this is a probable flaw in the logic - we need to ensure libunknown::init is done first thing
# or suffer additional scans.. or document ??
#ideally init should be called in each interp before any scans for packages so that the list of untracked is minimized.
set pkgnames [package names]
foreach p $pkgnames {
if {[string tolower $p] in {punk::libunknown tcl::zlib tcloo tcl::oo tcl}} {
continue
}
set versions [package versions $p]
if {[llength $versions] == 0} {
continue
}
set versionlist [packagedb_indexinfo $p]
if {[llength $versionlist] == 0} {
continue
} else {
foreach vdata $versionlist {
#dict set epoch scripts $p [lindex $vdata 0] [package ifneeded $p [lindex $vdata 0]]
dict set epoch scripts $p [lindex $vdata 0] [lindex $vdata 8]]
}
if {[lsearch -index 6 $versionlist ""] > -1} {
#There exists at least one empty indexbase for this package - we have to treat it as untracked
dict set epoch tm untracked $p "" ;#value unimportant
dict set epoch pkg untracked $p "" ;#value unimportant
} else {
#update the epoch info with where the tm versions came from
#(not tracking version numbers in epoch - just package to the indexbase)
foreach vdata $versionlist {
lassign $vdata v _t type _index index _indexbase indexbase _script iscript
if {$type eq "tm"} {
if {![dict exists $epoch tm epochs 0 added $indexbase]} {
#dict set epoch tm epochs 0 added $indexbase [dict create $p [dict create e 0 v $v]]
dict set epoch tm epochs 0 added $indexbase $p $v [dict create e 0 scr $iscript]
} else {
set idxadded [dict get $epoch tm epochs 0 added $indexbase]
#dict set idxadded $p [dict create e 0 v $v]
dict set idxadded $p $v [dict create e 0 scr $iscript]
dict set epoch tm epochs 0 added $indexbase $idxadded
}
dict unset epoch tm untracked $p
} elseif {$type eq "pkg"} {
#todo? tcl doesn't give us good introspection on package indexes for packages
#dict unset epoch pkg untracked $p
}
}
}
}
}
#-------------------------------------------------------------
#set all_untracked [dict keys [dict get $epoch untracked]]
#puts stderr "\x1b\[1\;33m punk::libunknown::init - pkg all_untracked:\x1b\[m [dict size [dict get $epoch pkg untracked]]"
#if {[dict exists $epoch pkg untracked msgcat]} {
# puts stderr "\x1b\[1\;32m punk::libunknown::init msgcat found in pkg untracked \x1b\[m "
# set versions [package versions msgcat]
# puts stderr "versions: $versions"
# foreach v $versions {
# puts stdout "v $v ifneeded: [package ifneeded msgcat $v]"
# }
#} else {
# puts stderr "\x1b\[1\;31m punk::libunknown::init msgcat NOT found in pkg untracked \x1b\[m "
#}
#puts stderr "\x1b\[1\;33m punk::libunknown::init - tm all_untracked:\x1b\[m [dict size [dict get $epoch tm untracked]]"
#if {[dict exists $epoch tm untracked msgcat]} {
# puts stderr "\x1b\[1\;32m punk::libunknown::init msgcat found in tm untracked \x1b\[m "
# set versions [package versions msgcat]
# puts stderr "versions: $versions"
# foreach v $versions {
# puts stdout "v $v ifneeded: [package ifneeded msgcat $v]"
# }
#} else {
# puts stderr "\x1b\[1\;31m punk::libunknown::init msgcat NOT found in tm untracked \x1b\[m "
#}
#-------------------------------------------------------------
trace add variable ::auto_path write ::punk::libunknown::epoch_incr_pkg
trace add variable ::tcl::tm::paths write ::punk::libunknown::epoch_incr_tm
#set stackrecord [commandstack::rename_command -renamer punk::libunknown package {args} {
# #::package override installed by punk::libunknown::init
#}
proc package args {
switch -- [lindex $args 0] {
fo - for - forge - forget {
variable has_package_files
#experimental - silently disallow forgetting things that didn't involve sourcing files
#What about static libs that also sourced files?
#packages loaded by c extensions?
#forgetting Tcl or tcl seems to be a bad idea - package require doesn't work afterwards (independent of this pkg)
set forgets_requested [lrange $args 1 end]
set ok_forgets [list]
upvar ::punk::libunknown::epoch epoch
foreach p $forgets_requested {
#'package files' not avail in early 8.6
#There can be other custom 'package ifneeded' scripts that don't use source - but still need to be forgotten.
#a basic/trivial case: 'package ifneeded aaa 0.1.1 {package provide aaa 0.1.1}'
#it could also use 'eval' instead of sourcing.
#For this reason - we shouldn't use 'package files' as any sort of indication of forgetability
#if {$p ni {tcl Tcl} && (!$has_package_files || [package provide $p] eq "" || ($has_package_files && [package provide $p] ne "" && [llength [package files $p]] > 0))} {
# lappend ok_forgets $p
#}
#What then? Hardcoded only for now?
if {$p ni {tcl Tcl tcl::oo tk}} {
#tcl::oo returns a comment only for its package provide script "# Already present, OK?"
# - so we can't use empty 'ifneeded' script as a determinant.
set vpresent [package provide $p]
if {$vpresent ne ""} {
#There could theoretically be other ifneeded scripts registered - but if the one in use is empty
#we'll use that as the criteria to disallow forget - REVIEW
set ifneededscript [package ifneeded $p $vpresent]
if {[string trim $ifneededscript] ne ""} {
lappend ok_forgets $p
dict unset epoch scripts $p
}
} else {
#not loaded - but may have registered ifneeded script(s) in the package database
#assume ok to forget
lappend ok_forgets $p
dict unset epoch scripts $p
}
}
}
if {[llength $ok_forgets]} {
return [::package:: forget {*}$ok_forgets]
} else {
return
}
}
ep - epo - epoc - epoch {
upvar ::punk::libunknown::epoch epoch
set epoch_args [lrange $args 1 end]
switch -- [llength $epoch_args] {
0 {
set tm_epoch [dict get $epoch tm current]
set pkg_epoch [dict get $epoch pkg current]
return [dict create tm $tm_epoch pkg $pkg_epoch]
}
1 {
switch -- [lindex $epoch_args 0] {
tm {
set cur [dict get $epoch tm current]
return [dict create $cur [dict get $epoch tm epochs $cur]]
}
pkg {
set cur [dict get $epoch pkg current]
return [dict create $cur [dict get $epoch pkg epochs $cur]]
}
incr {
epoch_incr_pkg
epoch_incr_tm
}
default {
error "package epoch [lindex $epoch_args 0] unsupported - known options: tm pkg incr"
}
}
}
2 {
set a2 [list [lindex $epoch_args 0] [lindex $epoch_args 1]]
switch -- $a2 {
{pkg incr} - {incr pkg} {
epoch_incr_pkg
}
{tm incr} - {incr tm} {
epoch_incr_tm
}
default {
set which [lindex $epoch_args 0]
set index [lindex $epoch_args 1]
if {$which in {pkg tm}} {
set epochs [dict keys [dict get $epoch $which epochs]]
if {[catch {
set epochinfo [dict get $epoch $which epochs $index]
} errM]} {
error "package epoch $which <idx> unable to use index $index"
}
return $epochinfo
} else {
error "package epoch {*}$a2 unsupported - expected 'pkg incr' or 'tm incr' or 'pkg <index>' or 'tm <index>'"
}
}
}
}
default {
set which [lindex $epoch_args 0]
set index [lindex $epoch_args 1]
set keys [lrange $epoch_args 2 end]
if {$which in {pkg tm}} {
set epochs [dict keys [dict get $epoch $which epochs]]
if {[catch {
set epochinfo [dict get $epoch $which epochs $index]
} errM]} {
error "package epoch $which <idx> unable to use index $index"
}
if {![dict exists $epochinfo {*}$keys]} {
set topkeys [dict keys $epochinfo]
error "package epoch $which $index $keys not found. Toplevel keys: $topkeys"
}
return [dict get $epochinfo {*}$keys]
} else {
error "package epoch <args> unimplemented"
}
}
}
}
default {
return [::package:: {*}$args]
}
}
}
rename ::package ::package::
#all lowercase procs already exported from ::punk::libunknown
namespace eval :: [list ::namespace import ::punk::libunknown::package]
#if {[info commands ::tcl::zipfs::root] ne ""} {
# set has_zipfs_tm 0
# foreach t $tmlist {
# if {[string match [::tcl::zipfs::root]* $t]} {
# set has_zipfs_tm 1
# break ;#zipfs_tm_UnknownHandler can handle either - a single zipfs path is enough
# }
# }
# set has_zipfs_auto 0
# foreach a $apath {
# if {[string match [::tcl::zipfs::root]* $a]} {
# set has_zipfs_auto 1
# break
# }
# }
# if {$has_zipfs_tm || $has_zipfs_auto} {
# if {$has_zipfs_tm && $has_zipfs_auto} {
# package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::punk::libunknown::zipfs_tclPkgUnknown}
# } elseif {$has_zipfs_tm} {
# package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::tclPkgUnknown}
# } else {
# #must only have auto
# #puts "tmlist : $tmlist"
# #puts "autopath: $apath"
# package unknown {::tcl::tm::UnknownHandler ::punk::libunknown::zipfs_tclPkgUnknown}
# }
# }
# #review - tm and auto_path entries for safebase interps are obscured. For now we will ignore and defaults will apply.
# #to load in safebase anyway - module would probably have to be passed to interp as source to eval?
#}
if {![interp issafe]} {
#J2
package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::punk::libunknown::zipfs_tclPkgUnknown}
#package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::tclPkgUnknown}
}
}
proc default {} {
package unknown {::tcl::tm::UnknownHandler ::tclPkgUnknown}
}
proc package_query {pkgname} {
variable epoch
if {[dict exists $epoch tm untracked $pkgname]} {
set pkg_info "$pkgname tm UNTRACKED"
} else {
set pkg_info "$pkgname not in tm untracked"
}
if {[dict exists $epoch pkg untracked $pkgname]} {
append pkg_info \n "$pkgname pkg UNTRACKED"
} else {
append pkg_info \n "$pkgname not in pkg untracked"
}
set pkg_epoch [dict get $epoch pkg current]
#set epoch_info [dict get $epoch pkg epochs $pkg_epoch]
#pkg entries are processed by package unknown in reverse - so their order of creaation is opposite to ::auto_path
set r_added [dict create]
foreach path [lreverse [dict keys [dict get $epoch pkg epochs $pkg_epoch added]]] {
dict set r_added $path [dict get $epoch pkg epochs $pkg_epoch added $path]
}
#set pkg_added [punk::lib::showdict $r_added */$pkgname]
#set added [textblock::frame -title $title $pkg_added]
set rows [list]
dict for {path pkgs} $r_added {
set c1 $path
set c2 [dict size $pkgs]
set c3 ""
if {[dict exists $pkgs $pkgname]} {
set vdict [dict get $pkgs $pkgname]
dict for {v data} $vdict {
set scriptlen [string length [dict get $data scr]]
append c3 "$v epoch[dict get $data e] ifneededchars:$scriptlen" \n
}
}
set r [list $path $c2 $c3]
lappend rows $r
}
set title "[punk::ansi::a+ green] PKG epoch $pkg_epoch - added [punk::ansi::a]"
set added [textblock::table -title $title -headers [list Path Pkgcount $pkgname] -rows $rows]
set pkg_row $added
set tm_epoch [dict get $epoch tm current]
#set tm_added [punk::lib::showdict [dict get $epoch tm epochs $tm_epoch added] */$pkgname]
set added [dict get $epoch tm epochs $tm_epoch added]
set rows [list]
dict for {path pkgs} $added {
set c1 $path
set c2 [dict size $pkgs]
set c3 ""
if {[dict exists $pkgs $pkgname]} {
set vdict [dict get $pkgs $pkgname]
dict for {v data} $vdict {
append c3 "$v $data" \n
}
}
set r [list $c1 $c2 $c3]
lappend rows $r
}
set title "TM epoch $tm_epoch - added"
#set added [textblock::frame -title $title $tm_added]
set added [textblock::table -title $title -headers [list Path Tmcount $pkgname] -rows $rows]
set tm_row $added
return $pkg_info\n$pkg_row\n$tm_row
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::libunknown ---}]
}
# == === === === === === === === === === === === === === ===
namespace eval punk::libunknown {
#for 8.6 compat
if {"::ledit" ni [info commands ::ledit]} {
#maint: taken from punk::lib
proc ledit {lvar first last args} {
upvar $lvar l
#use lindex_resolve to support for example: ledit lst end+1 end+1 h i
set fidx [lindex_resolve [llength $l] $first]
switch -exact -- $fidx {
-3 {
#index below lower bound
set pre [list]
set fidx -1
}
-2 {
#first index position is greater than index of last element in the list
set pre [lrange $l 0 end]
set fidx [llength $l]
}
default {
set pre [lrange $l 0 $first-1]
}
}
set lidx [lindex_resolve [llength $l] $last]
switch -exact -- $lidx {
-3 {
#index below lower bound
set post [lrange $l 0 end]
}
-2 {
#index above upper bound
set post [list]
}
default {
if {$lidx < $fidx} {
#from ledit man page:
#If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted.
set post [lrange $l $fidx end]
} else {
set post [lrange $l $last+1 end]
}
}
}
set l [list {*}$pre {*}$args {*}$post]
}
#maint: taken from punk::lib
proc lindex_resolve {len index} {
if {![string is integer -strict $len]} {
#<0 ?
error "lindex_resolve len must be an integer"
}
set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000
if {[string is integer -strict $index]} {
#can match +i -i
if {$index < 0} {
return -3
} elseif {$index >= $len} {
return -2
} else {
#integer may still have + sign - normalize with expr
return [expr {$index}]
}
} else {
if {[string match end* $index]} {
if {$index ne "end"} {
set op [string index $index 3]
set offset [string range $index 4 end]
if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"}
if {$op eq "+" && $offset != 0} {
return -2
}
} else {
#index is 'end'
set index [expr {$len-1}]
if {$index < 0} {
#special case - 'end' with empty list - treat end like a positive number out of bounds
return -2
} else {
return $index
}
}
if {$offset == 0} {
set index [expr {$len-1}]
if {$index < 0} {
return -2 ;#special case as above
} else {
return $index
}
} else {
#by now, if op = + then offset = 0 so we only need to handle the minus case
set index [expr {($len-1) - $offset}]
}
if {$index < 0} {
return -3
} else {
return $index
}
} else {
#plain +-<int> already handled above.
#we are trying to avoid evaluating unbraced expr of potentially insecure origin
if {[regexp {(.*)([+-])(.*)} $index _match a op b]} {
if {[string is integer -strict $a] && [string is integer -strict $b]} {
if {$op eq "-"} {
set index [expr {$a - $b}]
} else {
set index [expr {$a + $b}]
}
} else {
error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"
}
} else {
error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"
}
if {$index < 0} {
return -3
} elseif {$index >= $len} {
return -2
}
return $index
}
}
}
}
}
tcl::namespace::eval punk::libunknown::lib {
#A version of textutil::string::longestCommonPrefixList
#(also as ::punk::lib::longestCommonPrefixList)
proc longestCommonPrefix {items} {
if {[llength $items] <= 1} {
return [lindex $items 0]
}
set items [lsort $items[unset items]]
set min [lindex $items 0]
set max [lindex $items end]
#if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list)
#(sort order nothing to do with length - e.g min may be longer than max)
if {[string length $min] > [string length $max]} {
set temp $min
set min $max
set max $temp
}
set n [string length $min]
set prefix ""
set i -1
while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} {
append prefix $c
}
return $prefix
}
#maint: from punk::lib::ldiff
proc ldiff {fromlist removeitems} {
if {[llength $removeitems] == 0} {return $fromlist}
set result [list]
foreach item $fromlist {
if {$item ni $removeitems} {
lappend result $item
}
}
return $result
}
proc intersect2 {A B} {
#taken from tcl version of struct::set::Intersect
if {[llength $A] == 0} {return {}}
if {[llength $B] == 0} {return {}}
# This is slower than local vars, but more robust
if {[llength $B] > [llength $A]} {
::set res $A
::set A $B
::set B $res
}
::set res {}
foreach x $A {
::set ($x) {}
}
foreach x $B {
if {[info exists ($x)]} {
lappend res $x
}
}
return $res
}
proc is_list_all_in_list {A B} {
if {[llength $A] > [llength $B]} {return 0}
foreach x $B {
::set ($x) {}
}
foreach x $A {
if {![info exists ($x)]} {
return 0
}
}
return 1
}
}
# -----------------------------------------------------------------------------
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
# -----------------------------------------------------------------------------
# variable PUNKARGS
# variable PUNKARGS_aliases
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::libunknown
}
# -----------------------------------------------------------------------------
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::libunknown [tcl::namespace::eval punk::libunknown {
variable pkg punk::libunknown
variable version
set version 0.1
}]
return
#*** !doctools
#[manpage_end]