diff --git a/src/vendormodules/dictn-0.1.1.tm b/src/bootsupport/modules/dictn-0.1.2.tm similarity index 91% rename from src/vendormodules/dictn-0.1.1.tm rename to src/bootsupport/modules/dictn-0.1.2.tm index c9ef87f2..2ed2b1ef 100644 --- a/src/vendormodules/dictn-0.1.1.tm +++ b/src/bootsupport/modules/dictn-0.1.2.tm @@ -7,7 +7,7 @@ # (C) 2023 # # @@ Meta Begin -# Application dictn 0.1.1 +# Application dictn 0.1.2 # Meta platform tcl # Meta license # @@ Meta End @@ -74,15 +74,17 @@ proc ::dictn::get {dictval {path {}}} { return [dict get $dictval {*}$path] } -proc ::dictn::getdef {dictval path default} { - return [dict getdef $dictval {*}$path $default] -} - -proc ::dictn::getwithdefault {dictval path default} { - return [dict getdef $dictval {*}$path $default] -} if {[info commands ::tcl::dict::getdef] ne ""} { + #tcl 9+ + proc ::dictn::getdef {dictval path default} { + return [dict getdef $dictval {*}$path $default] + } + + proc ::dictn::getwithdefault {dictval path default} { + return [dict getdef $dictval {*}$path $default] + } + proc ::dictn::incr {dictvar path {increment {}} } { if {$increment eq ""} { ::set increment 1 @@ -101,6 +103,21 @@ if {[info commands ::tcl::dict::getdef] ne ""} { } } } else { + #tcl < 9 + proc ::dictn::getdef {dictval path default} { + if {[tcl::dict::exists $dictval {*}$path]} { + return [tcl::dict::get $dictval {*}$path] + } else { + return $default + } + } + proc ::dictn::getwithdefault {dictval path default} { + if {[tcl::dict::exists $dictval {*}$path]} { + return [tcl::dict::get $dictval {*}$path] + } else { + return $default + } + } proc ::dictn::incr {dictvar path {increment {}} } { if {$increment eq ""} { ::set increment 1 @@ -344,6 +361,6 @@ proc ::dictn::with {dictvar path args} { ## Ready package provide dictn [namespace eval dictn { variable version - ::set version 0.1.1 + ::set version 0.1.2 }] return \ No newline at end of file diff --git a/src/bootsupport/modules/modpod-0.1.3.tm b/src/bootsupport/modules/modpod-0.1.3.tm new file mode 100644 index 00000000..44da4684 --- /dev/null +++ b/src/bootsupport/modules/modpod-0.1.3.tm @@ -0,0 +1,704 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2024 +# +# @@ Meta Begin +# Application modpod 0.1.3 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin modpod_module_modpod 0 0.1.3] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require modpod] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of modpod +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by modpod +#[list_begin itemized] + +package require Tcl 8.6- +package require struct::set ;#review +package require punk::lib +package require punk::args +#*** !doctools +#[item] [package {Tcl 8.6-}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::class { + #*** !doctools + #[subsection {Namespace modpod::class}] + #[para] class definitions + if {[info commands [namespace current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + } +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod { + namespace export {[a-z]*}; # Convention: export all lowercase + + variable connected + if {![info exists connected(to)]} { + set connected(to) list + } + variable modpodscript + set modpodscript [info script] + if {[string tolower [file extension $modpodscript]] eq ".tcl"} { + set connected(self) [file dirname $modpodscript] + } else { + #expecting a .tm + set connected(self) $modpodscript + } + variable loadables [info sharedlibextension] + variable sourceables {.tcl .tk} ;# .tm ? + + #*** !doctools + #[subsection {Namespace modpod}] + #[para] Core API functions for modpod + #[list_begin definitions] + + + + #proc sample1 {p1 args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [opt {?option value...?}]] + # #[para]Description of sample1 + # return "ok" + #} + + #old tar connect mechanism - review - not needed? + proc connect {args} { + puts stderr "modpod::connect--->>$args" + set argd [punk::args::get_dict { + @id -id ::modpod::connect + -type -default "" + @values -min 1 -max 1 + path -type string -minsize 1 -help "path to .tm file or toplevel .tcl script within #modpod-- folder (unwrapped modpod)" + } $args] + catch { + punk::lib::showdict $argd ;#heavy dependencies + } + set opt_path [dict get $argd values path] + variable connected + set original_connectpath $opt_path + set modpodpath [modpod::system::normalize $opt_path] ;# + + if {$modpodpath in $connected(to)} { + return [dict create ok ALREADY_CONNECTED] + } + lappend connected(to) $modpodpath + + set connected(connectpath,$opt_path) $original_connectpath + set is_sourced [expr {[file normalize $modpodpath] eq [file normalize [info script]]}] + + set connected(location,$modpodpath) [file dirname $modpodpath] + set connected(startdata,$modpodpath) -1 + set connected(type,$modpodpath) [dict get $argd opts -type] + set connected(fh,$modpodpath) "" + + if {[string range [file tail $modpodpath] 0 7] eq "#modpod-"} { + set connected(type,$modpodpath) "unwrapped" + lassign [::split [file tail [file dirname $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname [file dirname $modpodpath]] + + } else { + #connect to .tm but may still be unwrapped version available + lassign [::split [file rootname [file tail $modpodath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname $modpodpath] + if {$connected(type,$modpodpath) ne "unwrapped"} { + #Not directly connected to unwrapped version - but may still be redirected there + set unwrappedFolder [file join $connected(location,$modpodpath) #modpod-$connected(package,$modpodpath)-$connected(version,$modpodpath)] + if {[file exists $unwrappedFolder]} { + #folder with exact version-match must exist for redirect to 'unwrapped' + set con(type,$modpodpath) "modpod-redirecting" + } + } + + } + set unwrapped_tm_file [file join $this_pkg_tm_folder] "[set connected(package,$modpodpath)]-[set connected(version,$modpodpath)].tm" + set connected(tmfile,$modpodpath) + set tail_segments [list] + set lcase_tmfile_segments [string tolower [file split $this_pkg_tm_folder]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [struct::set intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $this_pkg_tm_folder] [llength $mpath_segments] end] + break + } + } + if {[llength $tail_segments]} { + set connected(fullpackage,$modpodpath) [join [concat $tail_segments [set connected(package,$modpodpath)]] ::] ;#full name of package as used in package require + } else { + set connected(fullpackage,$modpodpath) [set connected(package,$modpodpath)] + } + + switch -exact -- $connected(type,$modpodpath) { + "modpod-redirecting" { + #redirect to the unwrapped version + set loadscript_name [file join $unwrappedFolder #modpod-loadscript-$con(package,$modpod).tcl] + + } + "unwrapped" { + if {[info commands ::thread::id] ne ""} { + set from [pid],[thread::id] + } else { + set from [pid] + } + #::modpod::Puts stderr "$from-> Package $connected(package,$modpodpath)-$connected(version,$modpodpath) is using unwrapped version: $modpodpath" + return [list ok ""] + } + default { + #autodetect .tm - zip/tar ? + #todo - use vfs ? + + #connect to tarball - start at 1st header + set connected(startdata,$modpodpath) 0 + set fh [open $modpodpath r] + set connected(fh,$modpodpath) $fh + fconfigure $fh -encoding iso8859-1 -translation binary -eofchar {} + + if {$connected(startdata,$modpodpath) >= 0} { + #verify we have a valid tar header + if {![catch {::modpod::system::tar::readHeader [red $fh 512]}]} { + seek $fh $connected(startdata,$modpodpath) start + return [list ok $fh] + } else { + #error "cannot verify tar header" + } + } + lpop connected(to) end + set connected(startdata,$modpodpath) -1 + unset connected(fh,$modpodpath) + catch {close $fh} + return [dict create err {Does not appear to be a valid modpod}] + } + } + } + proc disconnect {{modpod ""}} { + variable connected + if {![llength $connected(to)]} { + return 0 + } + if {$modpod eq ""} { + puts stderr "modpod::disconnect WARNING: modpod not explicitly specified. Disconnecting last connected: [lindex $connected(to) end]" + set modpod [lindex $connected(to) end] + } + + if {[set posn [lsearch $connected(to) $modpod]] == -1} { + puts stderr "modpod::disconnect WARNING: disconnect called when not connected: $modpod" + return 0 + } + if {[string length $connected(fh,$modpod)]} { + close $connected(fh,$modpod) + } + array unset connected *,$modpod + set connected(to) [lreplace $connected(to) $posn $posn] + return 1 + } + proc get {args} { + set argd [punk::args::get_dict { + -from -default "" -help "path to pod" + *values -min 1 -max 1 + filename + } $args] + set frompod [dict get $argd opts -from] + set filename [dict get $argd values filename] + + variable connected + #//review + set modpod [::modpod::system::connect_if_not $frompod] + set fh $connected(fh,$modpod) + if {$connected(type,$modpod) eq "unwrapped"} { + #for unwrapped connection - $connected(location) already points to the #modpod-pkg-ver folder + if {[string range $filename 0 0 eq "/"]} { + #absolute path (?) + set path [file join $connected(location,$modpod) .. [string trim $filename /]] + } else { + #relative path - use #modpod-xxx as base + set path [file join $connected(location,$modpod) $filename] + } + set fd [open $path r] + #utf-8? + #fconfigure $fd -encoding iso8859-1 -translation binary + return [list ok [lindex [list [read $fd] [close $fd]] 0]] + } else { + #read from vfs + puts stderr "get $filename from wrapped pod '$frompod' not implemented" + } + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::lib { + namespace export {[a-z]*}; # Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace modpod::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + proc is_valid_tm_version {versionpart} { + #Needs to be suitable for use with Tcl's 'package vcompare' + if {![catch [list package vcompare $versionparts $versionparts]]} { + return 1 + } else { + return 0 + } + } + + #zipfile is a pure zip at this point - ie no script/exe header + proc make_zip_modpod {args} { + set argd [punk::args::get_dict { + @id -id ::modpod::lib::make_zip_modpod + -offsettype -default "archive" -choices {archive file} -help\ + "Whether zip offsets are relative to start of file or start of zip-data within the file. + 'archive' relative offsets are easier to work with (for writing/updating) in tools such as 7zip,peazip, + but other tools may be easier with 'file' relative offsets. (e.g info-zip,pkzip) + info-zip's 'zip -A' can sometimes convert archive-relative to file-relative. + -offsettype archive is equivalent to plain 'cat prefixfile zipfile > modulefile'" + @values -min 2 -max 2 + zipfile -type path -minsize 1 -help "path to plain zip file with subfolder #modpod-packagename-version containing .tm, data files and/or binaries" + outfile -type path -minsize 1 -help "path to output file. Name should be of the form packagename-version.tm" + } $args] + set zipfile [dict get $argd values zipfile] + set outfile [dict get $argd values outfile] + set opt_offsettype [dict get $argd opts -offsettype] + + + set mount_stub [string map [list %offsettype% $opt_offsettype] { + #zip file with Tcl loader prepended. Requires either builtin zipfs, or vfs::zip to mount while zipped. + #Alternatively unzip so that extracted #modpod-package-version folder is in same folder as .tm file. + #generated using: modpod::lib::make_zip_modpod -offsettype %offsettype% + if {[catch {file normalize [info script]} modfile]} { + error "modpod zip stub error. Unable to determine module path. (possible safe interp restrictions?)" + } + if {$modfile eq "" || ![file exists $modfile]} { + error "modpod zip stub error. Unable to determine module path" + } + set moddir [file dirname $modfile] + set mod_and_ver [file rootname [file tail $modfile]] + lassign [split $mod_and_ver -] moduletail version + if {[file exists $moddir/#modpod-$mod_and_ver.tm]} { + source $moddir/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #determine module namespace so we can mount appropriately + proc intersect {A B} { + if {[llength $A] == 0} {return {}} + if {[llength $B] == 0} {return {}} + 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 + } + set lcase_tmfile_segments [string tolower [file split $moddir]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $moddir] [llength $mpath_segments] end] ;#use properly cased tail + break + } + } + if {[llength $tail_segments]} { + set fullpackage [join [concat $tail_segments $moduletail] ::] ;#full name of package as used in package require + set mount_at #modpod/[file join {*}$tail_segments]/#mounted-modpod-$mod_and_ver + } else { + set fullpackage $moduletail + set mount_at #modpod/#mounted-modpod-$mod_and_ver + } + + if {[info commands tcl::zipfs::mount] ne ""} { + #argument order changed to be consistent with vfs::zip::Mount etc + #early versions: zipfs::Mount mountpoint zipname + #since 2023-09: zipfs::Mount zipname mountpoint + #don't use 'file exists' when testing mountpoints. (some versions at least give massive delays on windows platform for non-existance) + #This is presumably related to // being interpreted as a network path + set mountpoints [dict keys [tcl::zipfs::mount]] + if {"//zipfs:/$mount_at" ni $mountpoints} { + #despite API change tcl::zipfs package version was unfortunately not updated - so we don't know argument order without trying it + if {[catch { + #tcl::zipfs::mount $modfile //zipfs:/#mounted-modpod-$mod_and_ver ;#extremely slow if this is a wrong guess (artifact of aforementioned file exists issue ?) + #puts "tcl::zipfs::mount $modfile $mount_at" + tcl::zipfs::mount $modfile $mount_at + } errM]} { + #try old api + if {![catch {tcl::zipfs::mount //zipfs:/$mount_at $modfile}]} { + puts stderr "modpod stub>>> tcl::zipfs::mount failed.\nbut old api: tcl::zipfs::mount succeeded\n tcl::zipfs::mount //zipfs://$mount_at $modfile" + puts stderr "Consider upgrading tcl runtime to one with fixed zipfs API" + } + } + if {![file exists //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + puts stderr "modpod stub>>> mount at //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm failed\n zipfs mounts: [zipfs mount]" + #tcl::zipfs::unmount //zipfs:/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + # #modpod-$mod_and_ver subdirectory always present in the archive so it can be conveniently extracted and run in that form + source //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #fallback to slower vfs::zip + #NB. We don't create the intermediate dirs - but the mount still works + if {![file exists $moddir/$mount_at]} { + if {[catch {package require vfs::zip} errM]} { + set msg "Unable to load vfs::zip package to mount module $mod_and_ver (and zipfs not available either)" + append msg \n "If neither zipfs or vfs::zip are available - the module can still be loaded by manually unzipping the file $modfile in place." + append msg \n "The unzipped data will all be contained in a folder named #modpod-$mod_and_ver in the same parent folder as $modfile" + error $msg + } else { + set fd [vfs::zip::Mount $modfile $moddir/$mount_at] + if {![file exists $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + vfs::zip::Unmount $fd $moddir/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + } + source $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } + } + #zipped data follows + }] + #todo - test if supplied zipfile has #modpod-loadcript.tcl or some other script/executable before even creating? + append mount_stub \x1A + modpod::system::make_mountable_zip $zipfile $outfile $mount_stub $opt_offsettype + + } + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval modpod::system { + #*** !doctools + #[subsection {Namespace modpod::system}] + #[para] Internal functions that are not part of the API + + #deflate,store only supported + + #zipfile here is plain zip - no script/exe prefix part. + proc make_mountable_zip {zipfile outfile mount_stub {offsettype "archive"}} { + set inzip [open $zipfile r] + fconfigure $inzip -encoding iso8859-1 -translation binary + set out [open $outfile w+] + fconfigure $out -encoding iso8859-1 -translation binary + puts -nonewline $out $mount_stub + set stuboffset [tell $out] + lappend report "stub size: $stuboffset" + fcopy $inzip $out + close $inzip + + set size [tell $out] + lappend report "modpod::system::make_mountable_zip" + lappend report "tmfile : [file tail $outfile]" + lappend report "output size : $size" + lappend report "offsettype : $offsettype" + + if {$offsettype eq "file"} { + #make zip offsets relative to start of whole file including prepended script. + #same offset structure as Tcl's older 'zipfs mkimg' as at 2024-10 + #2025 - zipfs mkimg fixed to use 'archive' offset. + #not editable by 7z,nanazip,peazip + + #we aren't adding any new files/folders so we can edit the offsets in place + + #Now seek in $out to find the end of directory signature: + #The structure itself is 24 bytes Long, followed by a maximum of 64Kbytes text + if {$size < 65559} { + set tailsearch_start 0 + } else { + set tailsearch_start [expr {$size - 65559}] + } + seek $out $tailsearch_start + set data [read $out] + #EOCD - End of Central Directory record + #PK\5\6 + set start_of_end [string last "\x50\x4b\x05\x06" $data] + #set start_of_end [expr {$start_of_end + $seek}] + #incr start_of_end $seek + set filerelative_eocd_posn [expr {$start_of_end + $tailsearch_start}] + + lappend report "kitfile-relative START-OF-EOCD: $filerelative_eocd_posn" + + seek $out $filerelative_eocd_posn + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + lappend report "End of central directory: [array get eocd]" + seek $out [expr {$filerelative_eocd_posn+16}] + + #adjust offset of start of central directory by the length of our sfx stub + puts -nonewline $out [binary format i [expr {$eocd(diroffset) + $stuboffset}]] + flush $out + + seek $out $filerelative_eocd_posn + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + # 0x06054b50 - end of central dir signature + puts stderr "$end_of_ctrl_dir" + puts stderr "comment_len: $eocd(comment_len)" + puts stderr "eocd sig: $eocd(signature) [punk::lib::dec2hex $eocd(signature)]" + lappend report "New dir offset: $eocd(diroffset)" + lappend report "Adjusting $eocd(totalnum) zip file items." + catch { + punk::lib::showdict -roottype list -chan stderr $report ;#heavy dependencies + } + + seek $out $eocd(diroffset) + for {set i 0} {$i <$eocd(totalnum)} {incr i} { + set current_file [tell $out] + set fileheader [read $out 46] + puts -------------- + puts [ansistring VIEW -lf 1 $fileheader] + puts -------------- + #binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + # x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + + binary scan $fileheader ic4sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + set ::last_header $fileheader + + puts "sig: $x(sig) (hex: [punk::lib::dec2hex $x(sig)])" + puts "ver: $x(version)" + puts "method: $x(method)" + + #PK\1\2 + #33639248 dec = 0x02014b50 - central directory file header signature + if { $x(sig) != 33639248 } { + error "modpod::system::make_mountable_zip Bad file header signature at item $i: dec:$x(sig) hex:[punk::lib::dec2hex $x(sig)]" + } + + foreach size $x(lengths) var {filename extrafield comment} { + if { $size > 0 } { + set x($var) [read $out $size] + } else { + set x($var) "" + } + } + set next_file [tell $out] + lappend report "file $i: $x(offset) $x(sizes) $x(filename)" + + seek $out [expr {$current_file+42}] + puts -nonewline $out [binary format i [expr {$x(offset)+$stuboffset}]] + + #verify: + flush $out + seek $out $current_file + set fileheader [read $out 46] + lappend report "old $x(offset) + $stuboffset" + binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + lappend report "new $x(offset)" + + seek $out $next_file + } + } + + close $out + #pdict/showdict reuire punk & textlib - ie lots of dependencies + #don't fall over just because of that + catch { + punk::lib::showdict -roottype list -chan stderr $report + } + #puts [join $report \n] + return + } + + proc connect_if_not {{podpath ""}} { + upvar ::modpod::connected connected + set podpath [::modpod::system::normalize $podpath] + set docon 0 + if {![llength $connected(to)]} { + if {![string length $podpath]} { + error "modpod::system::connect_if_not - Not connected to a modpod file, and no podpath specified" + } else { + set docon 1 + } + } else { + if {![string length $podpath]} { + set podpath [lindex $connected(to) end] + puts stderr "modpod::system::connect_if_not WARNING: using last connected modpod:$podpath for operation\n -podpath not explicitly specified during operation: [info level -1]" + } else { + if {$podpath ni $connected(to)} { + set docon 1 + } + } + } + if {$docon} { + if {[lindex [modpod::connect $podpath]] 0] ne "ok"} { + error "modpod::system::connect_if_not error. file $podpath does not seem to be a valid modpod" + } else { + return $podpath + } + } + #we were already connected + return $podpath + } + + proc myversion {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myversion should only be called from within a loading modpod" + } + set fname [file tail [file rootname [file normalize $script]]] + set scriptdir [file dirname $script] + + if {![string match "#modpod-*" $fname]} { + lassign [lrange [split $fname -] end-1 end] _pkgname version + } else { + lassign [scan [file tail [file rootname $script]] {#modpod-loadscript-%[a-z]-%s}] _pkgname version + if {![string length $version]} { + #try again on the name of the containing folder + lassign [scan [file tail $scriptdir] {#modpod-%[a-z]-%s}] _pkgname version + #todo - proper walk up the directory tree + if {![string length $version]} { + #try again on the grandparent folder (this is a standard depth for sourced .tcl files in a modpod) + lassign [scan [file tail [file dirname $scriptdir]] {#modpod-%[a-z]-%s}] _pkgname version + } + } + } + + #tarjar::Log debug "'myversion' determined version for [info script]: $version" + return $version + } + + proc myname {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myname should only be called from within a loading modpod" + } + return $connected(fullpackage,$script) + } + proc myfullname {} { + upvar ::modpod::connected connected + set script [info script] + #set script [::tarjar::normalize $script] + set script [file normalize $script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myfullname should only be called from within a loading tarjar" + } + return $::tarjar::connected(fullpackage,$script) + } + proc normalize {path} { + #newer versions of Tcl don't do tilde sub + + #Tcl's 'file normalize' seems to do some unfortunate tilde substitution on windows.. (at least for relative paths) + # we take the assumption here that if Tcl's tilde substitution is required - it should be done before the path is provided to this function. + set matilda "<_tarjar_tilde_placeholder_>" ;#token that is *unlikely* to occur in the wild, and is somewhat self describing in case it somehow ..escapes.. + set path [string map [list ~ $matilda] $path] ;#give our tildes to matilda to look after + set path [file normalize $path] + #set path [string tolower $path] ;#must do this after file normalize + return [string map [list $matilda ~] $path] ;#get our tildes back. +} +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide modpod [namespace eval modpod { + variable pkg modpod + variable version + set version 0.1.3 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/bootsupport/modules/tomlish-1.1.6.tm b/src/bootsupport/modules/tomlish-1.1.6.tm index dddcd0bb..a562545a 100644 --- a/src/bootsupport/modules/tomlish-1.1.6.tm +++ b/src/bootsupport/modules/tomlish-1.1.6.tm @@ -1512,7 +1512,7 @@ namespace eval tomlish { if {[::tomlish::utils::is_int $tok]} { set tag INT } else { - if {[string is integer -strict $tok]} { + if {[::tomlish::utils::string_is_integer -strict $tok]} { #didn't qualify as a toml int - but still an int #probably means is_int is limiting size and not accepting bigints (configurable?) #or it didn't qualify due to more than 1 leading zero @@ -2027,6 +2027,30 @@ namespace eval tomlish::utils { #[para] #[list_begin definitions] + #------------------------------------------------------------------------------ + # Tcl 8.6 support + #------------------------------------------------------------------------------ + if {[catch {tcl::string::is dict {}}]} { + proc string_is_dict {str} { + #we don't support -strict or -failindex for this fallback + expr {[::tcl::string::is list $str] && ([llength $str] % 2 == 0)} + } + } else { + proc string_is_dict {str} { + #we don't support -strict or -failindex for this fallback even though underlying supports it + ::tcl::string::is dict $str + } + } + if {![string is integer [expr {2**32}]]} { + proc string_is_integer {args} { + ::tcl::string::is entier {*}$args + } + } else { + proc string_is_integer {args} { + ::tcl::string::is integer {*}$args + } + } + #------------------------------------------------------------------------------ #basic generic quote matching for single and double quotes @@ -2695,7 +2719,7 @@ namespace eval tomlish::utils { set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![tcl::string::is integer -strict $numeric_value]} { + if {![::tomlish::utils::string_is_integer -strict $numeric_value]} { return 0 } @@ -2795,7 +2819,7 @@ namespace eval tomlish::utils { set dposn [string first . $str] if {$dposn > -1 } { set d3 [string range $str $dposn-1 $dposn+1] - if {![string is integer -strict [string index $d3 0]] || ![string is integer -strict [string index $d3 2]]} { + if {![::tomlish::utils::string_is_integer -strict [string index $d3 0]] || ![::tomlish::utils::string_is_integer -strict [string index $d3 2]]} { return 0 } } @@ -6213,7 +6237,7 @@ namespace eval tomlish::typedhuddle { } float { set dtype FLOAT - if {[string is integer -strict $hval]} { + if {[::tomlish::utils::string_is_integer -strict $hval]} { #json FLOAT specified as integer - must have dot for toml set hval [expr {double($hval)}] } @@ -6589,7 +6613,7 @@ namespace eval tomlish::dict { #consider x.y={type="spud",value="blah"} #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[string is dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} + expr {[::tomlish::utils::string_is_dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} } #simple types only - not containers? @@ -6634,7 +6658,7 @@ namespace eval tomlish::dict { # Check that each leaf is a typeval or typeval dict #importantly: must accept empty dict leaves e.g {x {}} proc is_typeval_dict {d {checkarrays 0}} { - if {![string is dict $d]} { + if {![::tomlish::utils::string_is_dict $d]} { return 0 } dict for {k v} $d { @@ -7896,7 +7920,7 @@ namespace eval tomlish::dict::path { ::set v 0 ::set vdict [dict create] foreach a $args { - if {![string is dict $a]} { + if {![::tomlish::utils::string_is_dict $a]} { error "tomlish::dict::path::lappend error - lappended arguments must already be in the tomlish form {type value } or be a dict with such forms as leaves" } } @@ -8240,6 +8264,7 @@ namespace eval tomlish::system { #[para] For pure integer indices the performance should be equivalent set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 + #'only' supports 2**32 max index on tcl < 9.0 - ok. if {[string is integer -strict $index]} { #can match +i -i #avoid even the lseq overhead when the index is simple diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/dictn-0.1.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/dictn-0.1.2.tm new file mode 100644 index 00000000..2ed2b1ef --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/dictn-0.1.2.tm @@ -0,0 +1,366 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2023 +# +# @@ Meta Begin +# Application dictn 0.1.2 +# Meta platform tcl +# Meta license +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval dictn { + namespace export {[a-z]*} + namespace ensemble create +} + + +## ::dictn::append +#This can of course 'ruin' a nested dict if applied to the wrong element +# - i.e using the string op 'append' on an element that is itself a nested dict is analogous to the standard Tcl: +# %set list {a b {c d}} +# %append list x +# a b {c d}x +# IOW - don't do that unless you really know that's what you want. +# +proc ::dictn::append {dictvar path {value {}}} { + if {[llength $path] == 1} { + uplevel 1 [list dict append $dictvar $path $value] + } else { + upvar 1 $dictvar dvar + + ::set str [dict get $dvar {*}$path] + append str $val + dict set dvar {*}$path $str + } +} + +proc ::dictn::create {args} { + ::set data {} + foreach {path val} $args { + dict set data {*}$path $val + } + return $data +} + +proc ::dictn::exists {dictval path} { + return [dict exists $dictval {*}$path] +} + +proc ::dictn::filter {dictval path filterType args} { + ::set sub [dict get $dictval {*}$path] + dict filter $sub $filterType {*}$args +} + +proc ::dictn::for {keyvalvars dictval path body} { + ::set sub [dict get $dictval {*}$path] + dict for $keyvalvars $sub $body +} + +proc ::dictn::get {dictval {path {}}} { + return [dict get $dictval {*}$path] +} + + +if {[info commands ::tcl::dict::getdef] ne ""} { + #tcl 9+ + proc ::dictn::getdef {dictval path default} { + return [dict getdef $dictval {*}$path $default] + } + + proc ::dictn::getwithdefault {dictval path default} { + return [dict getdef $dictval {*}$path $default] + } + + proc ::dictn::incr {dictvar path {increment {}} } { + if {$increment eq ""} { + ::set increment 1 + } + if {[llength $path] == 1} { + uplevel 1 [list dict incr $dictvar $path $increment] + } else { + upvar 1 $dictvar dvar + if {![::info exists dvar]} { + dict set dvar {*}$path $increment + } else { + ::set newval [expr {[dict getdef $dvar {*}$path 0] + $increment}] + dict set dvar {*}$path $newval + } + return $dvar + } + } +} else { + #tcl < 9 + proc ::dictn::getdef {dictval path default} { + if {[tcl::dict::exists $dictval {*}$path]} { + return [tcl::dict::get $dictval {*}$path] + } else { + return $default + } + } + proc ::dictn::getwithdefault {dictval path default} { + if {[tcl::dict::exists $dictval {*}$path]} { + return [tcl::dict::get $dictval {*}$path] + } else { + return $default + } + } + proc ::dictn::incr {dictvar path {increment {}} } { + if {$increment eq ""} { + ::set increment 1 + } + if {[llength $path] == 1} { + uplevel 1 [list dict incr $dictvar $path $increment] + } else { + upvar 1 $dictvar dvar + if {![::info exists dvar]} { + dict set dvar {*}$path $increment + } else { + if {![dict exists $dvar {*}$path]} { + ::set val 0 + } else { + ::set val [dict get $dvar {*}$path] + } + ::set newval [expr {$val + $increment}] + dict set dvar {*}$path $newval + } + return $dvar + } + } +} + +proc ::dictn::info {dictval {path {}}} { + if {![string length $path]} { + return [dict info $dictval] + } else { + ::set sub [dict get $dictval {*}$path] + return [dict info $sub] + } +} + +proc ::dictn::keys {dictval {path {}} {glob {}}} { + ::set sub [dict get $dictval {*}$path] + if {[string length $glob]} { + return [dict keys $sub $glob] + } else { + return [dict keys $sub] + } +} + +proc ::dictn::lappend {dictvar path args} { + if {[llength $path] == 1} { + uplevel 1 [list dict lappend $dictvar $path {*}$args] + } else { + upvar 1 $dictvar dvar + + ::set list [dict get $dvar {*}$path] + ::lappend list {*}$args + dict set dvar {*}$path $list + } +} + +proc ::dictn::merge {args} { + error "nested merge not yet supported" +} + +#dictn remove dictionaryValue ?path ...? +proc ::dictn::remove {dictval args} { + ::set basic [list] ;#buffer basic (1element path) removals to do in a single call. + + foreach path $args { + if {[llength $path] == 1} { + ::lappend basic $path + } else { + #extract,modify,replace + ::set subpath [lrange $path 0 end-1] + + ::set sub [dict get $dictval {*}$subpath] + ::set sub [dict remove $sub [lindex $path end]] + + dict set dictval {*}$subpath $sub + } + } + + if {[llength $basic]} { + return [dict remove $dictval {*}$basic] + } else { + return $dictval + } +} + + +proc ::dictn::replace {dictval args} { + ::set basic [list] ;#buffer basic (1element path) replacements to do in a single call. + + foreach {path val} $args { + if {[llength $path] == 1} { + ::lappend basic $path $val + } else { + #extract,modify,replace + ::set subpath [lrange $path 0 end-1] + + ::set sub [dict get $dictval {*}$subpath] + ::set sub [dict replace $sub [lindex $path end] $val] + + dict set dictval {*}$subpath $sub + } + } + + + if {[llength $basic]} { + return [dict replace $dictval {*}$basic] + } else { + return $dictval + } +} + + +proc ::dictn::set {dictvar path newval} { + upvar 1 $dictvar dvar + return [dict set dvar {*}$path $newval] +} + +proc ::dictn::size {dictval {path {}}} { + return [dict size [dict get $dictval {*}$path]] +} + +proc ::dictn::unset {dictvar path} { + upvar 1 $dictvar dvar + return [dict unset dvar {*}$path +} + +proc ::dictn::update {dictvar args} { + ::set body [lindex $args end] + ::set maplist [lrange $args 0 end-1] + + upvar 1 $dictvar dvar + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path]} { + uplevel 1 [list set $var [dict get $dvar $path]] + } + } + + catch {uplevel 1 $body} result + + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path]} { + upvar 1 $var $var + if {![::info exists $var]} { + uplevel 1 [list dict unset $dictvar {*}$path] + } else { + uplevel 1 [list dict set $dictvar {*}$path [::set $var]] + } + } + } + return $result +} + +#an experiment. +proc ::dictn::Applyupdate {dictvar args} { + ::set body [lindex $args end] + ::set maplist [lrange $args 0 end-1] + + upvar 1 $dictvar dvar + + ::set headscript "" + ::set i 0 + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path]} { + #uplevel 1 [list set $var [dict get $dvar $path]] + ::lappend arglist $var + ::lappend vallist [dict get $dvar {*}$path] + ::append headscript [string map [list %i% $i %v% $var] {upvar 1 %v% %v%; set %v% [lindex $args %i%]} ] + ::append headscript \n + ::incr i + } + } + + ::set body $headscript\r\n$body + + puts stderr "BODY: $body" + + #set result [apply [list args $body] {*}$vallist] + catch {apply [list args $body] {*}$vallist} result + + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path] && [::info exists $var]} { + dict set dvar {*}$path [::set $var] + } + } + return $result +} + +proc ::dictn::values {dictval {path {}} {glob {}}} { + ::set sub [dict get $dictval {*}$path] + if {[string length $glob]} { + return [dict values $sub $glob] + } else { + return [dict values $sub] + } +} + +# Standard form: +#'dictn with dictVariable path body' +# +# Extended form: +#'dictn with dictVariable path arrayVariable body' +# +proc ::dictn::with {dictvar path args} { + if {[llength $args] == 1} { + ::set body [lindex $args 0] + return [uplevel 1 [list dict with $dictvar {*}$path $body]] + } else { + upvar 1 $dictvar dvar + ::lassign $args arrayname body + + upvar 1 $arrayname arr + array set arr [dict get $dvar {*}$path] + ::set prevkeys [array names arr] + + catch {uplevel 1 $body} result + + + foreach k $prevkeys { + if {![::info exists arr($k)]} { + dict unset $dvar {*}$path $k + } + } + foreach k [array names arr] { + dict set $dvar {*}$path $k $arr($k) + } + + return $result + } +} + + + + + + + + + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide dictn [namespace eval dictn { + variable version + ::set version 0.1.2 +}] +return \ No newline at end of file diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/modpod-0.1.3.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/modpod-0.1.3.tm new file mode 100644 index 00000000..44da4684 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/modpod-0.1.3.tm @@ -0,0 +1,704 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2024 +# +# @@ Meta Begin +# Application modpod 0.1.3 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin modpod_module_modpod 0 0.1.3] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require modpod] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of modpod +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by modpod +#[list_begin itemized] + +package require Tcl 8.6- +package require struct::set ;#review +package require punk::lib +package require punk::args +#*** !doctools +#[item] [package {Tcl 8.6-}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::class { + #*** !doctools + #[subsection {Namespace modpod::class}] + #[para] class definitions + if {[info commands [namespace current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + } +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod { + namespace export {[a-z]*}; # Convention: export all lowercase + + variable connected + if {![info exists connected(to)]} { + set connected(to) list + } + variable modpodscript + set modpodscript [info script] + if {[string tolower [file extension $modpodscript]] eq ".tcl"} { + set connected(self) [file dirname $modpodscript] + } else { + #expecting a .tm + set connected(self) $modpodscript + } + variable loadables [info sharedlibextension] + variable sourceables {.tcl .tk} ;# .tm ? + + #*** !doctools + #[subsection {Namespace modpod}] + #[para] Core API functions for modpod + #[list_begin definitions] + + + + #proc sample1 {p1 args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [opt {?option value...?}]] + # #[para]Description of sample1 + # return "ok" + #} + + #old tar connect mechanism - review - not needed? + proc connect {args} { + puts stderr "modpod::connect--->>$args" + set argd [punk::args::get_dict { + @id -id ::modpod::connect + -type -default "" + @values -min 1 -max 1 + path -type string -minsize 1 -help "path to .tm file or toplevel .tcl script within #modpod-- folder (unwrapped modpod)" + } $args] + catch { + punk::lib::showdict $argd ;#heavy dependencies + } + set opt_path [dict get $argd values path] + variable connected + set original_connectpath $opt_path + set modpodpath [modpod::system::normalize $opt_path] ;# + + if {$modpodpath in $connected(to)} { + return [dict create ok ALREADY_CONNECTED] + } + lappend connected(to) $modpodpath + + set connected(connectpath,$opt_path) $original_connectpath + set is_sourced [expr {[file normalize $modpodpath] eq [file normalize [info script]]}] + + set connected(location,$modpodpath) [file dirname $modpodpath] + set connected(startdata,$modpodpath) -1 + set connected(type,$modpodpath) [dict get $argd opts -type] + set connected(fh,$modpodpath) "" + + if {[string range [file tail $modpodpath] 0 7] eq "#modpod-"} { + set connected(type,$modpodpath) "unwrapped" + lassign [::split [file tail [file dirname $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname [file dirname $modpodpath]] + + } else { + #connect to .tm but may still be unwrapped version available + lassign [::split [file rootname [file tail $modpodath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname $modpodpath] + if {$connected(type,$modpodpath) ne "unwrapped"} { + #Not directly connected to unwrapped version - but may still be redirected there + set unwrappedFolder [file join $connected(location,$modpodpath) #modpod-$connected(package,$modpodpath)-$connected(version,$modpodpath)] + if {[file exists $unwrappedFolder]} { + #folder with exact version-match must exist for redirect to 'unwrapped' + set con(type,$modpodpath) "modpod-redirecting" + } + } + + } + set unwrapped_tm_file [file join $this_pkg_tm_folder] "[set connected(package,$modpodpath)]-[set connected(version,$modpodpath)].tm" + set connected(tmfile,$modpodpath) + set tail_segments [list] + set lcase_tmfile_segments [string tolower [file split $this_pkg_tm_folder]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [struct::set intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $this_pkg_tm_folder] [llength $mpath_segments] end] + break + } + } + if {[llength $tail_segments]} { + set connected(fullpackage,$modpodpath) [join [concat $tail_segments [set connected(package,$modpodpath)]] ::] ;#full name of package as used in package require + } else { + set connected(fullpackage,$modpodpath) [set connected(package,$modpodpath)] + } + + switch -exact -- $connected(type,$modpodpath) { + "modpod-redirecting" { + #redirect to the unwrapped version + set loadscript_name [file join $unwrappedFolder #modpod-loadscript-$con(package,$modpod).tcl] + + } + "unwrapped" { + if {[info commands ::thread::id] ne ""} { + set from [pid],[thread::id] + } else { + set from [pid] + } + #::modpod::Puts stderr "$from-> Package $connected(package,$modpodpath)-$connected(version,$modpodpath) is using unwrapped version: $modpodpath" + return [list ok ""] + } + default { + #autodetect .tm - zip/tar ? + #todo - use vfs ? + + #connect to tarball - start at 1st header + set connected(startdata,$modpodpath) 0 + set fh [open $modpodpath r] + set connected(fh,$modpodpath) $fh + fconfigure $fh -encoding iso8859-1 -translation binary -eofchar {} + + if {$connected(startdata,$modpodpath) >= 0} { + #verify we have a valid tar header + if {![catch {::modpod::system::tar::readHeader [red $fh 512]}]} { + seek $fh $connected(startdata,$modpodpath) start + return [list ok $fh] + } else { + #error "cannot verify tar header" + } + } + lpop connected(to) end + set connected(startdata,$modpodpath) -1 + unset connected(fh,$modpodpath) + catch {close $fh} + return [dict create err {Does not appear to be a valid modpod}] + } + } + } + proc disconnect {{modpod ""}} { + variable connected + if {![llength $connected(to)]} { + return 0 + } + if {$modpod eq ""} { + puts stderr "modpod::disconnect WARNING: modpod not explicitly specified. Disconnecting last connected: [lindex $connected(to) end]" + set modpod [lindex $connected(to) end] + } + + if {[set posn [lsearch $connected(to) $modpod]] == -1} { + puts stderr "modpod::disconnect WARNING: disconnect called when not connected: $modpod" + return 0 + } + if {[string length $connected(fh,$modpod)]} { + close $connected(fh,$modpod) + } + array unset connected *,$modpod + set connected(to) [lreplace $connected(to) $posn $posn] + return 1 + } + proc get {args} { + set argd [punk::args::get_dict { + -from -default "" -help "path to pod" + *values -min 1 -max 1 + filename + } $args] + set frompod [dict get $argd opts -from] + set filename [dict get $argd values filename] + + variable connected + #//review + set modpod [::modpod::system::connect_if_not $frompod] + set fh $connected(fh,$modpod) + if {$connected(type,$modpod) eq "unwrapped"} { + #for unwrapped connection - $connected(location) already points to the #modpod-pkg-ver folder + if {[string range $filename 0 0 eq "/"]} { + #absolute path (?) + set path [file join $connected(location,$modpod) .. [string trim $filename /]] + } else { + #relative path - use #modpod-xxx as base + set path [file join $connected(location,$modpod) $filename] + } + set fd [open $path r] + #utf-8? + #fconfigure $fd -encoding iso8859-1 -translation binary + return [list ok [lindex [list [read $fd] [close $fd]] 0]] + } else { + #read from vfs + puts stderr "get $filename from wrapped pod '$frompod' not implemented" + } + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::lib { + namespace export {[a-z]*}; # Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace modpod::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + proc is_valid_tm_version {versionpart} { + #Needs to be suitable for use with Tcl's 'package vcompare' + if {![catch [list package vcompare $versionparts $versionparts]]} { + return 1 + } else { + return 0 + } + } + + #zipfile is a pure zip at this point - ie no script/exe header + proc make_zip_modpod {args} { + set argd [punk::args::get_dict { + @id -id ::modpod::lib::make_zip_modpod + -offsettype -default "archive" -choices {archive file} -help\ + "Whether zip offsets are relative to start of file or start of zip-data within the file. + 'archive' relative offsets are easier to work with (for writing/updating) in tools such as 7zip,peazip, + but other tools may be easier with 'file' relative offsets. (e.g info-zip,pkzip) + info-zip's 'zip -A' can sometimes convert archive-relative to file-relative. + -offsettype archive is equivalent to plain 'cat prefixfile zipfile > modulefile'" + @values -min 2 -max 2 + zipfile -type path -minsize 1 -help "path to plain zip file with subfolder #modpod-packagename-version containing .tm, data files and/or binaries" + outfile -type path -minsize 1 -help "path to output file. Name should be of the form packagename-version.tm" + } $args] + set zipfile [dict get $argd values zipfile] + set outfile [dict get $argd values outfile] + set opt_offsettype [dict get $argd opts -offsettype] + + + set mount_stub [string map [list %offsettype% $opt_offsettype] { + #zip file with Tcl loader prepended. Requires either builtin zipfs, or vfs::zip to mount while zipped. + #Alternatively unzip so that extracted #modpod-package-version folder is in same folder as .tm file. + #generated using: modpod::lib::make_zip_modpod -offsettype %offsettype% + if {[catch {file normalize [info script]} modfile]} { + error "modpod zip stub error. Unable to determine module path. (possible safe interp restrictions?)" + } + if {$modfile eq "" || ![file exists $modfile]} { + error "modpod zip stub error. Unable to determine module path" + } + set moddir [file dirname $modfile] + set mod_and_ver [file rootname [file tail $modfile]] + lassign [split $mod_and_ver -] moduletail version + if {[file exists $moddir/#modpod-$mod_and_ver.tm]} { + source $moddir/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #determine module namespace so we can mount appropriately + proc intersect {A B} { + if {[llength $A] == 0} {return {}} + if {[llength $B] == 0} {return {}} + 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 + } + set lcase_tmfile_segments [string tolower [file split $moddir]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $moddir] [llength $mpath_segments] end] ;#use properly cased tail + break + } + } + if {[llength $tail_segments]} { + set fullpackage [join [concat $tail_segments $moduletail] ::] ;#full name of package as used in package require + set mount_at #modpod/[file join {*}$tail_segments]/#mounted-modpod-$mod_and_ver + } else { + set fullpackage $moduletail + set mount_at #modpod/#mounted-modpod-$mod_and_ver + } + + if {[info commands tcl::zipfs::mount] ne ""} { + #argument order changed to be consistent with vfs::zip::Mount etc + #early versions: zipfs::Mount mountpoint zipname + #since 2023-09: zipfs::Mount zipname mountpoint + #don't use 'file exists' when testing mountpoints. (some versions at least give massive delays on windows platform for non-existance) + #This is presumably related to // being interpreted as a network path + set mountpoints [dict keys [tcl::zipfs::mount]] + if {"//zipfs:/$mount_at" ni $mountpoints} { + #despite API change tcl::zipfs package version was unfortunately not updated - so we don't know argument order without trying it + if {[catch { + #tcl::zipfs::mount $modfile //zipfs:/#mounted-modpod-$mod_and_ver ;#extremely slow if this is a wrong guess (artifact of aforementioned file exists issue ?) + #puts "tcl::zipfs::mount $modfile $mount_at" + tcl::zipfs::mount $modfile $mount_at + } errM]} { + #try old api + if {![catch {tcl::zipfs::mount //zipfs:/$mount_at $modfile}]} { + puts stderr "modpod stub>>> tcl::zipfs::mount failed.\nbut old api: tcl::zipfs::mount succeeded\n tcl::zipfs::mount //zipfs://$mount_at $modfile" + puts stderr "Consider upgrading tcl runtime to one with fixed zipfs API" + } + } + if {![file exists //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + puts stderr "modpod stub>>> mount at //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm failed\n zipfs mounts: [zipfs mount]" + #tcl::zipfs::unmount //zipfs:/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + # #modpod-$mod_and_ver subdirectory always present in the archive so it can be conveniently extracted and run in that form + source //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #fallback to slower vfs::zip + #NB. We don't create the intermediate dirs - but the mount still works + if {![file exists $moddir/$mount_at]} { + if {[catch {package require vfs::zip} errM]} { + set msg "Unable to load vfs::zip package to mount module $mod_and_ver (and zipfs not available either)" + append msg \n "If neither zipfs or vfs::zip are available - the module can still be loaded by manually unzipping the file $modfile in place." + append msg \n "The unzipped data will all be contained in a folder named #modpod-$mod_and_ver in the same parent folder as $modfile" + error $msg + } else { + set fd [vfs::zip::Mount $modfile $moddir/$mount_at] + if {![file exists $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + vfs::zip::Unmount $fd $moddir/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + } + source $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } + } + #zipped data follows + }] + #todo - test if supplied zipfile has #modpod-loadcript.tcl or some other script/executable before even creating? + append mount_stub \x1A + modpod::system::make_mountable_zip $zipfile $outfile $mount_stub $opt_offsettype + + } + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval modpod::system { + #*** !doctools + #[subsection {Namespace modpod::system}] + #[para] Internal functions that are not part of the API + + #deflate,store only supported + + #zipfile here is plain zip - no script/exe prefix part. + proc make_mountable_zip {zipfile outfile mount_stub {offsettype "archive"}} { + set inzip [open $zipfile r] + fconfigure $inzip -encoding iso8859-1 -translation binary + set out [open $outfile w+] + fconfigure $out -encoding iso8859-1 -translation binary + puts -nonewline $out $mount_stub + set stuboffset [tell $out] + lappend report "stub size: $stuboffset" + fcopy $inzip $out + close $inzip + + set size [tell $out] + lappend report "modpod::system::make_mountable_zip" + lappend report "tmfile : [file tail $outfile]" + lappend report "output size : $size" + lappend report "offsettype : $offsettype" + + if {$offsettype eq "file"} { + #make zip offsets relative to start of whole file including prepended script. + #same offset structure as Tcl's older 'zipfs mkimg' as at 2024-10 + #2025 - zipfs mkimg fixed to use 'archive' offset. + #not editable by 7z,nanazip,peazip + + #we aren't adding any new files/folders so we can edit the offsets in place + + #Now seek in $out to find the end of directory signature: + #The structure itself is 24 bytes Long, followed by a maximum of 64Kbytes text + if {$size < 65559} { + set tailsearch_start 0 + } else { + set tailsearch_start [expr {$size - 65559}] + } + seek $out $tailsearch_start + set data [read $out] + #EOCD - End of Central Directory record + #PK\5\6 + set start_of_end [string last "\x50\x4b\x05\x06" $data] + #set start_of_end [expr {$start_of_end + $seek}] + #incr start_of_end $seek + set filerelative_eocd_posn [expr {$start_of_end + $tailsearch_start}] + + lappend report "kitfile-relative START-OF-EOCD: $filerelative_eocd_posn" + + seek $out $filerelative_eocd_posn + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + lappend report "End of central directory: [array get eocd]" + seek $out [expr {$filerelative_eocd_posn+16}] + + #adjust offset of start of central directory by the length of our sfx stub + puts -nonewline $out [binary format i [expr {$eocd(diroffset) + $stuboffset}]] + flush $out + + seek $out $filerelative_eocd_posn + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + # 0x06054b50 - end of central dir signature + puts stderr "$end_of_ctrl_dir" + puts stderr "comment_len: $eocd(comment_len)" + puts stderr "eocd sig: $eocd(signature) [punk::lib::dec2hex $eocd(signature)]" + lappend report "New dir offset: $eocd(diroffset)" + lappend report "Adjusting $eocd(totalnum) zip file items." + catch { + punk::lib::showdict -roottype list -chan stderr $report ;#heavy dependencies + } + + seek $out $eocd(diroffset) + for {set i 0} {$i <$eocd(totalnum)} {incr i} { + set current_file [tell $out] + set fileheader [read $out 46] + puts -------------- + puts [ansistring VIEW -lf 1 $fileheader] + puts -------------- + #binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + # x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + + binary scan $fileheader ic4sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + set ::last_header $fileheader + + puts "sig: $x(sig) (hex: [punk::lib::dec2hex $x(sig)])" + puts "ver: $x(version)" + puts "method: $x(method)" + + #PK\1\2 + #33639248 dec = 0x02014b50 - central directory file header signature + if { $x(sig) != 33639248 } { + error "modpod::system::make_mountable_zip Bad file header signature at item $i: dec:$x(sig) hex:[punk::lib::dec2hex $x(sig)]" + } + + foreach size $x(lengths) var {filename extrafield comment} { + if { $size > 0 } { + set x($var) [read $out $size] + } else { + set x($var) "" + } + } + set next_file [tell $out] + lappend report "file $i: $x(offset) $x(sizes) $x(filename)" + + seek $out [expr {$current_file+42}] + puts -nonewline $out [binary format i [expr {$x(offset)+$stuboffset}]] + + #verify: + flush $out + seek $out $current_file + set fileheader [read $out 46] + lappend report "old $x(offset) + $stuboffset" + binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + lappend report "new $x(offset)" + + seek $out $next_file + } + } + + close $out + #pdict/showdict reuire punk & textlib - ie lots of dependencies + #don't fall over just because of that + catch { + punk::lib::showdict -roottype list -chan stderr $report + } + #puts [join $report \n] + return + } + + proc connect_if_not {{podpath ""}} { + upvar ::modpod::connected connected + set podpath [::modpod::system::normalize $podpath] + set docon 0 + if {![llength $connected(to)]} { + if {![string length $podpath]} { + error "modpod::system::connect_if_not - Not connected to a modpod file, and no podpath specified" + } else { + set docon 1 + } + } else { + if {![string length $podpath]} { + set podpath [lindex $connected(to) end] + puts stderr "modpod::system::connect_if_not WARNING: using last connected modpod:$podpath for operation\n -podpath not explicitly specified during operation: [info level -1]" + } else { + if {$podpath ni $connected(to)} { + set docon 1 + } + } + } + if {$docon} { + if {[lindex [modpod::connect $podpath]] 0] ne "ok"} { + error "modpod::system::connect_if_not error. file $podpath does not seem to be a valid modpod" + } else { + return $podpath + } + } + #we were already connected + return $podpath + } + + proc myversion {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myversion should only be called from within a loading modpod" + } + set fname [file tail [file rootname [file normalize $script]]] + set scriptdir [file dirname $script] + + if {![string match "#modpod-*" $fname]} { + lassign [lrange [split $fname -] end-1 end] _pkgname version + } else { + lassign [scan [file tail [file rootname $script]] {#modpod-loadscript-%[a-z]-%s}] _pkgname version + if {![string length $version]} { + #try again on the name of the containing folder + lassign [scan [file tail $scriptdir] {#modpod-%[a-z]-%s}] _pkgname version + #todo - proper walk up the directory tree + if {![string length $version]} { + #try again on the grandparent folder (this is a standard depth for sourced .tcl files in a modpod) + lassign [scan [file tail [file dirname $scriptdir]] {#modpod-%[a-z]-%s}] _pkgname version + } + } + } + + #tarjar::Log debug "'myversion' determined version for [info script]: $version" + return $version + } + + proc myname {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myname should only be called from within a loading modpod" + } + return $connected(fullpackage,$script) + } + proc myfullname {} { + upvar ::modpod::connected connected + set script [info script] + #set script [::tarjar::normalize $script] + set script [file normalize $script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myfullname should only be called from within a loading tarjar" + } + return $::tarjar::connected(fullpackage,$script) + } + proc normalize {path} { + #newer versions of Tcl don't do tilde sub + + #Tcl's 'file normalize' seems to do some unfortunate tilde substitution on windows.. (at least for relative paths) + # we take the assumption here that if Tcl's tilde substitution is required - it should be done before the path is provided to this function. + set matilda "<_tarjar_tilde_placeholder_>" ;#token that is *unlikely* to occur in the wild, and is somewhat self describing in case it somehow ..escapes.. + set path [string map [list ~ $matilda] $path] ;#give our tildes to matilda to look after + set path [file normalize $path] + #set path [string tolower $path] ;#must do this after file normalize + return [string map [list $matilda ~] $path] ;#get our tildes back. +} +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide modpod [namespace eval modpod { + variable pkg modpod + variable version + set version 0.1.3 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/tomlish-1.1.6.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/tomlish-1.1.6.tm index dddcd0bb..a562545a 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/tomlish-1.1.6.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/tomlish-1.1.6.tm @@ -1512,7 +1512,7 @@ namespace eval tomlish { if {[::tomlish::utils::is_int $tok]} { set tag INT } else { - if {[string is integer -strict $tok]} { + if {[::tomlish::utils::string_is_integer -strict $tok]} { #didn't qualify as a toml int - but still an int #probably means is_int is limiting size and not accepting bigints (configurable?) #or it didn't qualify due to more than 1 leading zero @@ -2027,6 +2027,30 @@ namespace eval tomlish::utils { #[para] #[list_begin definitions] + #------------------------------------------------------------------------------ + # Tcl 8.6 support + #------------------------------------------------------------------------------ + if {[catch {tcl::string::is dict {}}]} { + proc string_is_dict {str} { + #we don't support -strict or -failindex for this fallback + expr {[::tcl::string::is list $str] && ([llength $str] % 2 == 0)} + } + } else { + proc string_is_dict {str} { + #we don't support -strict or -failindex for this fallback even though underlying supports it + ::tcl::string::is dict $str + } + } + if {![string is integer [expr {2**32}]]} { + proc string_is_integer {args} { + ::tcl::string::is entier {*}$args + } + } else { + proc string_is_integer {args} { + ::tcl::string::is integer {*}$args + } + } + #------------------------------------------------------------------------------ #basic generic quote matching for single and double quotes @@ -2695,7 +2719,7 @@ namespace eval tomlish::utils { set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![tcl::string::is integer -strict $numeric_value]} { + if {![::tomlish::utils::string_is_integer -strict $numeric_value]} { return 0 } @@ -2795,7 +2819,7 @@ namespace eval tomlish::utils { set dposn [string first . $str] if {$dposn > -1 } { set d3 [string range $str $dposn-1 $dposn+1] - if {![string is integer -strict [string index $d3 0]] || ![string is integer -strict [string index $d3 2]]} { + if {![::tomlish::utils::string_is_integer -strict [string index $d3 0]] || ![::tomlish::utils::string_is_integer -strict [string index $d3 2]]} { return 0 } } @@ -6213,7 +6237,7 @@ namespace eval tomlish::typedhuddle { } float { set dtype FLOAT - if {[string is integer -strict $hval]} { + if {[::tomlish::utils::string_is_integer -strict $hval]} { #json FLOAT specified as integer - must have dot for toml set hval [expr {double($hval)}] } @@ -6589,7 +6613,7 @@ namespace eval tomlish::dict { #consider x.y={type="spud",value="blah"} #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[string is dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} + expr {[::tomlish::utils::string_is_dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} } #simple types only - not containers? @@ -6634,7 +6658,7 @@ namespace eval tomlish::dict { # Check that each leaf is a typeval or typeval dict #importantly: must accept empty dict leaves e.g {x {}} proc is_typeval_dict {d {checkarrays 0}} { - if {![string is dict $d]} { + if {![::tomlish::utils::string_is_dict $d]} { return 0 } dict for {k v} $d { @@ -7896,7 +7920,7 @@ namespace eval tomlish::dict::path { ::set v 0 ::set vdict [dict create] foreach a $args { - if {![string is dict $a]} { + if {![::tomlish::utils::string_is_dict $a]} { error "tomlish::dict::path::lappend error - lappended arguments must already be in the tomlish form {type value } or be a dict with such forms as leaves" } } @@ -8240,6 +8264,7 @@ namespace eval tomlish::system { #[para] For pure integer indices the performance should be equivalent set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 + #'only' supports 2**32 max index on tcl < 9.0 - ok. if {[string is integer -strict $index]} { #can match +i -i #avoid even the lseq overhead when the index is simple diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/dictn-0.1.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/dictn-0.1.2.tm new file mode 100644 index 00000000..2ed2b1ef --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/dictn-0.1.2.tm @@ -0,0 +1,366 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2023 +# +# @@ Meta Begin +# Application dictn 0.1.2 +# Meta platform tcl +# Meta license +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval dictn { + namespace export {[a-z]*} + namespace ensemble create +} + + +## ::dictn::append +#This can of course 'ruin' a nested dict if applied to the wrong element +# - i.e using the string op 'append' on an element that is itself a nested dict is analogous to the standard Tcl: +# %set list {a b {c d}} +# %append list x +# a b {c d}x +# IOW - don't do that unless you really know that's what you want. +# +proc ::dictn::append {dictvar path {value {}}} { + if {[llength $path] == 1} { + uplevel 1 [list dict append $dictvar $path $value] + } else { + upvar 1 $dictvar dvar + + ::set str [dict get $dvar {*}$path] + append str $val + dict set dvar {*}$path $str + } +} + +proc ::dictn::create {args} { + ::set data {} + foreach {path val} $args { + dict set data {*}$path $val + } + return $data +} + +proc ::dictn::exists {dictval path} { + return [dict exists $dictval {*}$path] +} + +proc ::dictn::filter {dictval path filterType args} { + ::set sub [dict get $dictval {*}$path] + dict filter $sub $filterType {*}$args +} + +proc ::dictn::for {keyvalvars dictval path body} { + ::set sub [dict get $dictval {*}$path] + dict for $keyvalvars $sub $body +} + +proc ::dictn::get {dictval {path {}}} { + return [dict get $dictval {*}$path] +} + + +if {[info commands ::tcl::dict::getdef] ne ""} { + #tcl 9+ + proc ::dictn::getdef {dictval path default} { + return [dict getdef $dictval {*}$path $default] + } + + proc ::dictn::getwithdefault {dictval path default} { + return [dict getdef $dictval {*}$path $default] + } + + proc ::dictn::incr {dictvar path {increment {}} } { + if {$increment eq ""} { + ::set increment 1 + } + if {[llength $path] == 1} { + uplevel 1 [list dict incr $dictvar $path $increment] + } else { + upvar 1 $dictvar dvar + if {![::info exists dvar]} { + dict set dvar {*}$path $increment + } else { + ::set newval [expr {[dict getdef $dvar {*}$path 0] + $increment}] + dict set dvar {*}$path $newval + } + return $dvar + } + } +} else { + #tcl < 9 + proc ::dictn::getdef {dictval path default} { + if {[tcl::dict::exists $dictval {*}$path]} { + return [tcl::dict::get $dictval {*}$path] + } else { + return $default + } + } + proc ::dictn::getwithdefault {dictval path default} { + if {[tcl::dict::exists $dictval {*}$path]} { + return [tcl::dict::get $dictval {*}$path] + } else { + return $default + } + } + proc ::dictn::incr {dictvar path {increment {}} } { + if {$increment eq ""} { + ::set increment 1 + } + if {[llength $path] == 1} { + uplevel 1 [list dict incr $dictvar $path $increment] + } else { + upvar 1 $dictvar dvar + if {![::info exists dvar]} { + dict set dvar {*}$path $increment + } else { + if {![dict exists $dvar {*}$path]} { + ::set val 0 + } else { + ::set val [dict get $dvar {*}$path] + } + ::set newval [expr {$val + $increment}] + dict set dvar {*}$path $newval + } + return $dvar + } + } +} + +proc ::dictn::info {dictval {path {}}} { + if {![string length $path]} { + return [dict info $dictval] + } else { + ::set sub [dict get $dictval {*}$path] + return [dict info $sub] + } +} + +proc ::dictn::keys {dictval {path {}} {glob {}}} { + ::set sub [dict get $dictval {*}$path] + if {[string length $glob]} { + return [dict keys $sub $glob] + } else { + return [dict keys $sub] + } +} + +proc ::dictn::lappend {dictvar path args} { + if {[llength $path] == 1} { + uplevel 1 [list dict lappend $dictvar $path {*}$args] + } else { + upvar 1 $dictvar dvar + + ::set list [dict get $dvar {*}$path] + ::lappend list {*}$args + dict set dvar {*}$path $list + } +} + +proc ::dictn::merge {args} { + error "nested merge not yet supported" +} + +#dictn remove dictionaryValue ?path ...? +proc ::dictn::remove {dictval args} { + ::set basic [list] ;#buffer basic (1element path) removals to do in a single call. + + foreach path $args { + if {[llength $path] == 1} { + ::lappend basic $path + } else { + #extract,modify,replace + ::set subpath [lrange $path 0 end-1] + + ::set sub [dict get $dictval {*}$subpath] + ::set sub [dict remove $sub [lindex $path end]] + + dict set dictval {*}$subpath $sub + } + } + + if {[llength $basic]} { + return [dict remove $dictval {*}$basic] + } else { + return $dictval + } +} + + +proc ::dictn::replace {dictval args} { + ::set basic [list] ;#buffer basic (1element path) replacements to do in a single call. + + foreach {path val} $args { + if {[llength $path] == 1} { + ::lappend basic $path $val + } else { + #extract,modify,replace + ::set subpath [lrange $path 0 end-1] + + ::set sub [dict get $dictval {*}$subpath] + ::set sub [dict replace $sub [lindex $path end] $val] + + dict set dictval {*}$subpath $sub + } + } + + + if {[llength $basic]} { + return [dict replace $dictval {*}$basic] + } else { + return $dictval + } +} + + +proc ::dictn::set {dictvar path newval} { + upvar 1 $dictvar dvar + return [dict set dvar {*}$path $newval] +} + +proc ::dictn::size {dictval {path {}}} { + return [dict size [dict get $dictval {*}$path]] +} + +proc ::dictn::unset {dictvar path} { + upvar 1 $dictvar dvar + return [dict unset dvar {*}$path +} + +proc ::dictn::update {dictvar args} { + ::set body [lindex $args end] + ::set maplist [lrange $args 0 end-1] + + upvar 1 $dictvar dvar + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path]} { + uplevel 1 [list set $var [dict get $dvar $path]] + } + } + + catch {uplevel 1 $body} result + + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path]} { + upvar 1 $var $var + if {![::info exists $var]} { + uplevel 1 [list dict unset $dictvar {*}$path] + } else { + uplevel 1 [list dict set $dictvar {*}$path [::set $var]] + } + } + } + return $result +} + +#an experiment. +proc ::dictn::Applyupdate {dictvar args} { + ::set body [lindex $args end] + ::set maplist [lrange $args 0 end-1] + + upvar 1 $dictvar dvar + + ::set headscript "" + ::set i 0 + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path]} { + #uplevel 1 [list set $var [dict get $dvar $path]] + ::lappend arglist $var + ::lappend vallist [dict get $dvar {*}$path] + ::append headscript [string map [list %i% $i %v% $var] {upvar 1 %v% %v%; set %v% [lindex $args %i%]} ] + ::append headscript \n + ::incr i + } + } + + ::set body $headscript\r\n$body + + puts stderr "BODY: $body" + + #set result [apply [list args $body] {*}$vallist] + catch {apply [list args $body] {*}$vallist} result + + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path] && [::info exists $var]} { + dict set dvar {*}$path [::set $var] + } + } + return $result +} + +proc ::dictn::values {dictval {path {}} {glob {}}} { + ::set sub [dict get $dictval {*}$path] + if {[string length $glob]} { + return [dict values $sub $glob] + } else { + return [dict values $sub] + } +} + +# Standard form: +#'dictn with dictVariable path body' +# +# Extended form: +#'dictn with dictVariable path arrayVariable body' +# +proc ::dictn::with {dictvar path args} { + if {[llength $args] == 1} { + ::set body [lindex $args 0] + return [uplevel 1 [list dict with $dictvar {*}$path $body]] + } else { + upvar 1 $dictvar dvar + ::lassign $args arrayname body + + upvar 1 $arrayname arr + array set arr [dict get $dvar {*}$path] + ::set prevkeys [array names arr] + + catch {uplevel 1 $body} result + + + foreach k $prevkeys { + if {![::info exists arr($k)]} { + dict unset $dvar {*}$path $k + } + } + foreach k [array names arr] { + dict set $dvar {*}$path $k $arr($k) + } + + return $result + } +} + + + + + + + + + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide dictn [namespace eval dictn { + variable version + ::set version 0.1.2 +}] +return \ No newline at end of file diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/modpod-0.1.3.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/modpod-0.1.3.tm new file mode 100644 index 00000000..44da4684 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/modpod-0.1.3.tm @@ -0,0 +1,704 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2024 +# +# @@ Meta Begin +# Application modpod 0.1.3 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin modpod_module_modpod 0 0.1.3] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require modpod] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of modpod +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by modpod +#[list_begin itemized] + +package require Tcl 8.6- +package require struct::set ;#review +package require punk::lib +package require punk::args +#*** !doctools +#[item] [package {Tcl 8.6-}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::class { + #*** !doctools + #[subsection {Namespace modpod::class}] + #[para] class definitions + if {[info commands [namespace current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + } +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod { + namespace export {[a-z]*}; # Convention: export all lowercase + + variable connected + if {![info exists connected(to)]} { + set connected(to) list + } + variable modpodscript + set modpodscript [info script] + if {[string tolower [file extension $modpodscript]] eq ".tcl"} { + set connected(self) [file dirname $modpodscript] + } else { + #expecting a .tm + set connected(self) $modpodscript + } + variable loadables [info sharedlibextension] + variable sourceables {.tcl .tk} ;# .tm ? + + #*** !doctools + #[subsection {Namespace modpod}] + #[para] Core API functions for modpod + #[list_begin definitions] + + + + #proc sample1 {p1 args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [opt {?option value...?}]] + # #[para]Description of sample1 + # return "ok" + #} + + #old tar connect mechanism - review - not needed? + proc connect {args} { + puts stderr "modpod::connect--->>$args" + set argd [punk::args::get_dict { + @id -id ::modpod::connect + -type -default "" + @values -min 1 -max 1 + path -type string -minsize 1 -help "path to .tm file or toplevel .tcl script within #modpod-- folder (unwrapped modpod)" + } $args] + catch { + punk::lib::showdict $argd ;#heavy dependencies + } + set opt_path [dict get $argd values path] + variable connected + set original_connectpath $opt_path + set modpodpath [modpod::system::normalize $opt_path] ;# + + if {$modpodpath in $connected(to)} { + return [dict create ok ALREADY_CONNECTED] + } + lappend connected(to) $modpodpath + + set connected(connectpath,$opt_path) $original_connectpath + set is_sourced [expr {[file normalize $modpodpath] eq [file normalize [info script]]}] + + set connected(location,$modpodpath) [file dirname $modpodpath] + set connected(startdata,$modpodpath) -1 + set connected(type,$modpodpath) [dict get $argd opts -type] + set connected(fh,$modpodpath) "" + + if {[string range [file tail $modpodpath] 0 7] eq "#modpod-"} { + set connected(type,$modpodpath) "unwrapped" + lassign [::split [file tail [file dirname $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname [file dirname $modpodpath]] + + } else { + #connect to .tm but may still be unwrapped version available + lassign [::split [file rootname [file tail $modpodath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname $modpodpath] + if {$connected(type,$modpodpath) ne "unwrapped"} { + #Not directly connected to unwrapped version - but may still be redirected there + set unwrappedFolder [file join $connected(location,$modpodpath) #modpod-$connected(package,$modpodpath)-$connected(version,$modpodpath)] + if {[file exists $unwrappedFolder]} { + #folder with exact version-match must exist for redirect to 'unwrapped' + set con(type,$modpodpath) "modpod-redirecting" + } + } + + } + set unwrapped_tm_file [file join $this_pkg_tm_folder] "[set connected(package,$modpodpath)]-[set connected(version,$modpodpath)].tm" + set connected(tmfile,$modpodpath) + set tail_segments [list] + set lcase_tmfile_segments [string tolower [file split $this_pkg_tm_folder]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [struct::set intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $this_pkg_tm_folder] [llength $mpath_segments] end] + break + } + } + if {[llength $tail_segments]} { + set connected(fullpackage,$modpodpath) [join [concat $tail_segments [set connected(package,$modpodpath)]] ::] ;#full name of package as used in package require + } else { + set connected(fullpackage,$modpodpath) [set connected(package,$modpodpath)] + } + + switch -exact -- $connected(type,$modpodpath) { + "modpod-redirecting" { + #redirect to the unwrapped version + set loadscript_name [file join $unwrappedFolder #modpod-loadscript-$con(package,$modpod).tcl] + + } + "unwrapped" { + if {[info commands ::thread::id] ne ""} { + set from [pid],[thread::id] + } else { + set from [pid] + } + #::modpod::Puts stderr "$from-> Package $connected(package,$modpodpath)-$connected(version,$modpodpath) is using unwrapped version: $modpodpath" + return [list ok ""] + } + default { + #autodetect .tm - zip/tar ? + #todo - use vfs ? + + #connect to tarball - start at 1st header + set connected(startdata,$modpodpath) 0 + set fh [open $modpodpath r] + set connected(fh,$modpodpath) $fh + fconfigure $fh -encoding iso8859-1 -translation binary -eofchar {} + + if {$connected(startdata,$modpodpath) >= 0} { + #verify we have a valid tar header + if {![catch {::modpod::system::tar::readHeader [red $fh 512]}]} { + seek $fh $connected(startdata,$modpodpath) start + return [list ok $fh] + } else { + #error "cannot verify tar header" + } + } + lpop connected(to) end + set connected(startdata,$modpodpath) -1 + unset connected(fh,$modpodpath) + catch {close $fh} + return [dict create err {Does not appear to be a valid modpod}] + } + } + } + proc disconnect {{modpod ""}} { + variable connected + if {![llength $connected(to)]} { + return 0 + } + if {$modpod eq ""} { + puts stderr "modpod::disconnect WARNING: modpod not explicitly specified. Disconnecting last connected: [lindex $connected(to) end]" + set modpod [lindex $connected(to) end] + } + + if {[set posn [lsearch $connected(to) $modpod]] == -1} { + puts stderr "modpod::disconnect WARNING: disconnect called when not connected: $modpod" + return 0 + } + if {[string length $connected(fh,$modpod)]} { + close $connected(fh,$modpod) + } + array unset connected *,$modpod + set connected(to) [lreplace $connected(to) $posn $posn] + return 1 + } + proc get {args} { + set argd [punk::args::get_dict { + -from -default "" -help "path to pod" + *values -min 1 -max 1 + filename + } $args] + set frompod [dict get $argd opts -from] + set filename [dict get $argd values filename] + + variable connected + #//review + set modpod [::modpod::system::connect_if_not $frompod] + set fh $connected(fh,$modpod) + if {$connected(type,$modpod) eq "unwrapped"} { + #for unwrapped connection - $connected(location) already points to the #modpod-pkg-ver folder + if {[string range $filename 0 0 eq "/"]} { + #absolute path (?) + set path [file join $connected(location,$modpod) .. [string trim $filename /]] + } else { + #relative path - use #modpod-xxx as base + set path [file join $connected(location,$modpod) $filename] + } + set fd [open $path r] + #utf-8? + #fconfigure $fd -encoding iso8859-1 -translation binary + return [list ok [lindex [list [read $fd] [close $fd]] 0]] + } else { + #read from vfs + puts stderr "get $filename from wrapped pod '$frompod' not implemented" + } + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::lib { + namespace export {[a-z]*}; # Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace modpod::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + proc is_valid_tm_version {versionpart} { + #Needs to be suitable for use with Tcl's 'package vcompare' + if {![catch [list package vcompare $versionparts $versionparts]]} { + return 1 + } else { + return 0 + } + } + + #zipfile is a pure zip at this point - ie no script/exe header + proc make_zip_modpod {args} { + set argd [punk::args::get_dict { + @id -id ::modpod::lib::make_zip_modpod + -offsettype -default "archive" -choices {archive file} -help\ + "Whether zip offsets are relative to start of file or start of zip-data within the file. + 'archive' relative offsets are easier to work with (for writing/updating) in tools such as 7zip,peazip, + but other tools may be easier with 'file' relative offsets. (e.g info-zip,pkzip) + info-zip's 'zip -A' can sometimes convert archive-relative to file-relative. + -offsettype archive is equivalent to plain 'cat prefixfile zipfile > modulefile'" + @values -min 2 -max 2 + zipfile -type path -minsize 1 -help "path to plain zip file with subfolder #modpod-packagename-version containing .tm, data files and/or binaries" + outfile -type path -minsize 1 -help "path to output file. Name should be of the form packagename-version.tm" + } $args] + set zipfile [dict get $argd values zipfile] + set outfile [dict get $argd values outfile] + set opt_offsettype [dict get $argd opts -offsettype] + + + set mount_stub [string map [list %offsettype% $opt_offsettype] { + #zip file with Tcl loader prepended. Requires either builtin zipfs, or vfs::zip to mount while zipped. + #Alternatively unzip so that extracted #modpod-package-version folder is in same folder as .tm file. + #generated using: modpod::lib::make_zip_modpod -offsettype %offsettype% + if {[catch {file normalize [info script]} modfile]} { + error "modpod zip stub error. Unable to determine module path. (possible safe interp restrictions?)" + } + if {$modfile eq "" || ![file exists $modfile]} { + error "modpod zip stub error. Unable to determine module path" + } + set moddir [file dirname $modfile] + set mod_and_ver [file rootname [file tail $modfile]] + lassign [split $mod_and_ver -] moduletail version + if {[file exists $moddir/#modpod-$mod_and_ver.tm]} { + source $moddir/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #determine module namespace so we can mount appropriately + proc intersect {A B} { + if {[llength $A] == 0} {return {}} + if {[llength $B] == 0} {return {}} + 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 + } + set lcase_tmfile_segments [string tolower [file split $moddir]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $moddir] [llength $mpath_segments] end] ;#use properly cased tail + break + } + } + if {[llength $tail_segments]} { + set fullpackage [join [concat $tail_segments $moduletail] ::] ;#full name of package as used in package require + set mount_at #modpod/[file join {*}$tail_segments]/#mounted-modpod-$mod_and_ver + } else { + set fullpackage $moduletail + set mount_at #modpod/#mounted-modpod-$mod_and_ver + } + + if {[info commands tcl::zipfs::mount] ne ""} { + #argument order changed to be consistent with vfs::zip::Mount etc + #early versions: zipfs::Mount mountpoint zipname + #since 2023-09: zipfs::Mount zipname mountpoint + #don't use 'file exists' when testing mountpoints. (some versions at least give massive delays on windows platform for non-existance) + #This is presumably related to // being interpreted as a network path + set mountpoints [dict keys [tcl::zipfs::mount]] + if {"//zipfs:/$mount_at" ni $mountpoints} { + #despite API change tcl::zipfs package version was unfortunately not updated - so we don't know argument order without trying it + if {[catch { + #tcl::zipfs::mount $modfile //zipfs:/#mounted-modpod-$mod_and_ver ;#extremely slow if this is a wrong guess (artifact of aforementioned file exists issue ?) + #puts "tcl::zipfs::mount $modfile $mount_at" + tcl::zipfs::mount $modfile $mount_at + } errM]} { + #try old api + if {![catch {tcl::zipfs::mount //zipfs:/$mount_at $modfile}]} { + puts stderr "modpod stub>>> tcl::zipfs::mount failed.\nbut old api: tcl::zipfs::mount succeeded\n tcl::zipfs::mount //zipfs://$mount_at $modfile" + puts stderr "Consider upgrading tcl runtime to one with fixed zipfs API" + } + } + if {![file exists //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + puts stderr "modpod stub>>> mount at //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm failed\n zipfs mounts: [zipfs mount]" + #tcl::zipfs::unmount //zipfs:/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + # #modpod-$mod_and_ver subdirectory always present in the archive so it can be conveniently extracted and run in that form + source //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #fallback to slower vfs::zip + #NB. We don't create the intermediate dirs - but the mount still works + if {![file exists $moddir/$mount_at]} { + if {[catch {package require vfs::zip} errM]} { + set msg "Unable to load vfs::zip package to mount module $mod_and_ver (and zipfs not available either)" + append msg \n "If neither zipfs or vfs::zip are available - the module can still be loaded by manually unzipping the file $modfile in place." + append msg \n "The unzipped data will all be contained in a folder named #modpod-$mod_and_ver in the same parent folder as $modfile" + error $msg + } else { + set fd [vfs::zip::Mount $modfile $moddir/$mount_at] + if {![file exists $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + vfs::zip::Unmount $fd $moddir/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + } + source $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } + } + #zipped data follows + }] + #todo - test if supplied zipfile has #modpod-loadcript.tcl or some other script/executable before even creating? + append mount_stub \x1A + modpod::system::make_mountable_zip $zipfile $outfile $mount_stub $opt_offsettype + + } + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval modpod::system { + #*** !doctools + #[subsection {Namespace modpod::system}] + #[para] Internal functions that are not part of the API + + #deflate,store only supported + + #zipfile here is plain zip - no script/exe prefix part. + proc make_mountable_zip {zipfile outfile mount_stub {offsettype "archive"}} { + set inzip [open $zipfile r] + fconfigure $inzip -encoding iso8859-1 -translation binary + set out [open $outfile w+] + fconfigure $out -encoding iso8859-1 -translation binary + puts -nonewline $out $mount_stub + set stuboffset [tell $out] + lappend report "stub size: $stuboffset" + fcopy $inzip $out + close $inzip + + set size [tell $out] + lappend report "modpod::system::make_mountable_zip" + lappend report "tmfile : [file tail $outfile]" + lappend report "output size : $size" + lappend report "offsettype : $offsettype" + + if {$offsettype eq "file"} { + #make zip offsets relative to start of whole file including prepended script. + #same offset structure as Tcl's older 'zipfs mkimg' as at 2024-10 + #2025 - zipfs mkimg fixed to use 'archive' offset. + #not editable by 7z,nanazip,peazip + + #we aren't adding any new files/folders so we can edit the offsets in place + + #Now seek in $out to find the end of directory signature: + #The structure itself is 24 bytes Long, followed by a maximum of 64Kbytes text + if {$size < 65559} { + set tailsearch_start 0 + } else { + set tailsearch_start [expr {$size - 65559}] + } + seek $out $tailsearch_start + set data [read $out] + #EOCD - End of Central Directory record + #PK\5\6 + set start_of_end [string last "\x50\x4b\x05\x06" $data] + #set start_of_end [expr {$start_of_end + $seek}] + #incr start_of_end $seek + set filerelative_eocd_posn [expr {$start_of_end + $tailsearch_start}] + + lappend report "kitfile-relative START-OF-EOCD: $filerelative_eocd_posn" + + seek $out $filerelative_eocd_posn + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + lappend report "End of central directory: [array get eocd]" + seek $out [expr {$filerelative_eocd_posn+16}] + + #adjust offset of start of central directory by the length of our sfx stub + puts -nonewline $out [binary format i [expr {$eocd(diroffset) + $stuboffset}]] + flush $out + + seek $out $filerelative_eocd_posn + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + # 0x06054b50 - end of central dir signature + puts stderr "$end_of_ctrl_dir" + puts stderr "comment_len: $eocd(comment_len)" + puts stderr "eocd sig: $eocd(signature) [punk::lib::dec2hex $eocd(signature)]" + lappend report "New dir offset: $eocd(diroffset)" + lappend report "Adjusting $eocd(totalnum) zip file items." + catch { + punk::lib::showdict -roottype list -chan stderr $report ;#heavy dependencies + } + + seek $out $eocd(diroffset) + for {set i 0} {$i <$eocd(totalnum)} {incr i} { + set current_file [tell $out] + set fileheader [read $out 46] + puts -------------- + puts [ansistring VIEW -lf 1 $fileheader] + puts -------------- + #binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + # x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + + binary scan $fileheader ic4sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + set ::last_header $fileheader + + puts "sig: $x(sig) (hex: [punk::lib::dec2hex $x(sig)])" + puts "ver: $x(version)" + puts "method: $x(method)" + + #PK\1\2 + #33639248 dec = 0x02014b50 - central directory file header signature + if { $x(sig) != 33639248 } { + error "modpod::system::make_mountable_zip Bad file header signature at item $i: dec:$x(sig) hex:[punk::lib::dec2hex $x(sig)]" + } + + foreach size $x(lengths) var {filename extrafield comment} { + if { $size > 0 } { + set x($var) [read $out $size] + } else { + set x($var) "" + } + } + set next_file [tell $out] + lappend report "file $i: $x(offset) $x(sizes) $x(filename)" + + seek $out [expr {$current_file+42}] + puts -nonewline $out [binary format i [expr {$x(offset)+$stuboffset}]] + + #verify: + flush $out + seek $out $current_file + set fileheader [read $out 46] + lappend report "old $x(offset) + $stuboffset" + binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + lappend report "new $x(offset)" + + seek $out $next_file + } + } + + close $out + #pdict/showdict reuire punk & textlib - ie lots of dependencies + #don't fall over just because of that + catch { + punk::lib::showdict -roottype list -chan stderr $report + } + #puts [join $report \n] + return + } + + proc connect_if_not {{podpath ""}} { + upvar ::modpod::connected connected + set podpath [::modpod::system::normalize $podpath] + set docon 0 + if {![llength $connected(to)]} { + if {![string length $podpath]} { + error "modpod::system::connect_if_not - Not connected to a modpod file, and no podpath specified" + } else { + set docon 1 + } + } else { + if {![string length $podpath]} { + set podpath [lindex $connected(to) end] + puts stderr "modpod::system::connect_if_not WARNING: using last connected modpod:$podpath for operation\n -podpath not explicitly specified during operation: [info level -1]" + } else { + if {$podpath ni $connected(to)} { + set docon 1 + } + } + } + if {$docon} { + if {[lindex [modpod::connect $podpath]] 0] ne "ok"} { + error "modpod::system::connect_if_not error. file $podpath does not seem to be a valid modpod" + } else { + return $podpath + } + } + #we were already connected + return $podpath + } + + proc myversion {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myversion should only be called from within a loading modpod" + } + set fname [file tail [file rootname [file normalize $script]]] + set scriptdir [file dirname $script] + + if {![string match "#modpod-*" $fname]} { + lassign [lrange [split $fname -] end-1 end] _pkgname version + } else { + lassign [scan [file tail [file rootname $script]] {#modpod-loadscript-%[a-z]-%s}] _pkgname version + if {![string length $version]} { + #try again on the name of the containing folder + lassign [scan [file tail $scriptdir] {#modpod-%[a-z]-%s}] _pkgname version + #todo - proper walk up the directory tree + if {![string length $version]} { + #try again on the grandparent folder (this is a standard depth for sourced .tcl files in a modpod) + lassign [scan [file tail [file dirname $scriptdir]] {#modpod-%[a-z]-%s}] _pkgname version + } + } + } + + #tarjar::Log debug "'myversion' determined version for [info script]: $version" + return $version + } + + proc myname {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myname should only be called from within a loading modpod" + } + return $connected(fullpackage,$script) + } + proc myfullname {} { + upvar ::modpod::connected connected + set script [info script] + #set script [::tarjar::normalize $script] + set script [file normalize $script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myfullname should only be called from within a loading tarjar" + } + return $::tarjar::connected(fullpackage,$script) + } + proc normalize {path} { + #newer versions of Tcl don't do tilde sub + + #Tcl's 'file normalize' seems to do some unfortunate tilde substitution on windows.. (at least for relative paths) + # we take the assumption here that if Tcl's tilde substitution is required - it should be done before the path is provided to this function. + set matilda "<_tarjar_tilde_placeholder_>" ;#token that is *unlikely* to occur in the wild, and is somewhat self describing in case it somehow ..escapes.. + set path [string map [list ~ $matilda] $path] ;#give our tildes to matilda to look after + set path [file normalize $path] + #set path [string tolower $path] ;#must do this after file normalize + return [string map [list $matilda ~] $path] ;#get our tildes back. +} +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide modpod [namespace eval modpod { + variable pkg modpod + variable version + set version 0.1.3 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/tomlish-1.1.6.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/tomlish-1.1.6.tm index dddcd0bb..a562545a 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/tomlish-1.1.6.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/tomlish-1.1.6.tm @@ -1512,7 +1512,7 @@ namespace eval tomlish { if {[::tomlish::utils::is_int $tok]} { set tag INT } else { - if {[string is integer -strict $tok]} { + if {[::tomlish::utils::string_is_integer -strict $tok]} { #didn't qualify as a toml int - but still an int #probably means is_int is limiting size and not accepting bigints (configurable?) #or it didn't qualify due to more than 1 leading zero @@ -2027,6 +2027,30 @@ namespace eval tomlish::utils { #[para] #[list_begin definitions] + #------------------------------------------------------------------------------ + # Tcl 8.6 support + #------------------------------------------------------------------------------ + if {[catch {tcl::string::is dict {}}]} { + proc string_is_dict {str} { + #we don't support -strict or -failindex for this fallback + expr {[::tcl::string::is list $str] && ([llength $str] % 2 == 0)} + } + } else { + proc string_is_dict {str} { + #we don't support -strict or -failindex for this fallback even though underlying supports it + ::tcl::string::is dict $str + } + } + if {![string is integer [expr {2**32}]]} { + proc string_is_integer {args} { + ::tcl::string::is entier {*}$args + } + } else { + proc string_is_integer {args} { + ::tcl::string::is integer {*}$args + } + } + #------------------------------------------------------------------------------ #basic generic quote matching for single and double quotes @@ -2695,7 +2719,7 @@ namespace eval tomlish::utils { set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![tcl::string::is integer -strict $numeric_value]} { + if {![::tomlish::utils::string_is_integer -strict $numeric_value]} { return 0 } @@ -2795,7 +2819,7 @@ namespace eval tomlish::utils { set dposn [string first . $str] if {$dposn > -1 } { set d3 [string range $str $dposn-1 $dposn+1] - if {![string is integer -strict [string index $d3 0]] || ![string is integer -strict [string index $d3 2]]} { + if {![::tomlish::utils::string_is_integer -strict [string index $d3 0]] || ![::tomlish::utils::string_is_integer -strict [string index $d3 2]]} { return 0 } } @@ -6213,7 +6237,7 @@ namespace eval tomlish::typedhuddle { } float { set dtype FLOAT - if {[string is integer -strict $hval]} { + if {[::tomlish::utils::string_is_integer -strict $hval]} { #json FLOAT specified as integer - must have dot for toml set hval [expr {double($hval)}] } @@ -6589,7 +6613,7 @@ namespace eval tomlish::dict { #consider x.y={type="spud",value="blah"} #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[string is dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} + expr {[::tomlish::utils::string_is_dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} } #simple types only - not containers? @@ -6634,7 +6658,7 @@ namespace eval tomlish::dict { # Check that each leaf is a typeval or typeval dict #importantly: must accept empty dict leaves e.g {x {}} proc is_typeval_dict {d {checkarrays 0}} { - if {![string is dict $d]} { + if {![::tomlish::utils::string_is_dict $d]} { return 0 } dict for {k v} $d { @@ -7896,7 +7920,7 @@ namespace eval tomlish::dict::path { ::set v 0 ::set vdict [dict create] foreach a $args { - if {![string is dict $a]} { + if {![::tomlish::utils::string_is_dict $a]} { error "tomlish::dict::path::lappend error - lappended arguments must already be in the tomlish form {type value } or be a dict with such forms as leaves" } } @@ -8240,6 +8264,7 @@ namespace eval tomlish::system { #[para] For pure integer indices the performance should be equivalent set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 + #'only' supports 2**32 max index on tcl < 9.0 - ok. if {[string is integer -strict $index]} { #can match +i -i #avoid even the lseq overhead when the index is simple diff --git a/src/vendormodules/dictn-0.1.2.tm b/src/vendormodules/dictn-0.1.2.tm new file mode 100644 index 00000000..2ed2b1ef --- /dev/null +++ b/src/vendormodules/dictn-0.1.2.tm @@ -0,0 +1,366 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2023 +# +# @@ Meta Begin +# Application dictn 0.1.2 +# Meta platform tcl +# Meta license +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval dictn { + namespace export {[a-z]*} + namespace ensemble create +} + + +## ::dictn::append +#This can of course 'ruin' a nested dict if applied to the wrong element +# - i.e using the string op 'append' on an element that is itself a nested dict is analogous to the standard Tcl: +# %set list {a b {c d}} +# %append list x +# a b {c d}x +# IOW - don't do that unless you really know that's what you want. +# +proc ::dictn::append {dictvar path {value {}}} { + if {[llength $path] == 1} { + uplevel 1 [list dict append $dictvar $path $value] + } else { + upvar 1 $dictvar dvar + + ::set str [dict get $dvar {*}$path] + append str $val + dict set dvar {*}$path $str + } +} + +proc ::dictn::create {args} { + ::set data {} + foreach {path val} $args { + dict set data {*}$path $val + } + return $data +} + +proc ::dictn::exists {dictval path} { + return [dict exists $dictval {*}$path] +} + +proc ::dictn::filter {dictval path filterType args} { + ::set sub [dict get $dictval {*}$path] + dict filter $sub $filterType {*}$args +} + +proc ::dictn::for {keyvalvars dictval path body} { + ::set sub [dict get $dictval {*}$path] + dict for $keyvalvars $sub $body +} + +proc ::dictn::get {dictval {path {}}} { + return [dict get $dictval {*}$path] +} + + +if {[info commands ::tcl::dict::getdef] ne ""} { + #tcl 9+ + proc ::dictn::getdef {dictval path default} { + return [dict getdef $dictval {*}$path $default] + } + + proc ::dictn::getwithdefault {dictval path default} { + return [dict getdef $dictval {*}$path $default] + } + + proc ::dictn::incr {dictvar path {increment {}} } { + if {$increment eq ""} { + ::set increment 1 + } + if {[llength $path] == 1} { + uplevel 1 [list dict incr $dictvar $path $increment] + } else { + upvar 1 $dictvar dvar + if {![::info exists dvar]} { + dict set dvar {*}$path $increment + } else { + ::set newval [expr {[dict getdef $dvar {*}$path 0] + $increment}] + dict set dvar {*}$path $newval + } + return $dvar + } + } +} else { + #tcl < 9 + proc ::dictn::getdef {dictval path default} { + if {[tcl::dict::exists $dictval {*}$path]} { + return [tcl::dict::get $dictval {*}$path] + } else { + return $default + } + } + proc ::dictn::getwithdefault {dictval path default} { + if {[tcl::dict::exists $dictval {*}$path]} { + return [tcl::dict::get $dictval {*}$path] + } else { + return $default + } + } + proc ::dictn::incr {dictvar path {increment {}} } { + if {$increment eq ""} { + ::set increment 1 + } + if {[llength $path] == 1} { + uplevel 1 [list dict incr $dictvar $path $increment] + } else { + upvar 1 $dictvar dvar + if {![::info exists dvar]} { + dict set dvar {*}$path $increment + } else { + if {![dict exists $dvar {*}$path]} { + ::set val 0 + } else { + ::set val [dict get $dvar {*}$path] + } + ::set newval [expr {$val + $increment}] + dict set dvar {*}$path $newval + } + return $dvar + } + } +} + +proc ::dictn::info {dictval {path {}}} { + if {![string length $path]} { + return [dict info $dictval] + } else { + ::set sub [dict get $dictval {*}$path] + return [dict info $sub] + } +} + +proc ::dictn::keys {dictval {path {}} {glob {}}} { + ::set sub [dict get $dictval {*}$path] + if {[string length $glob]} { + return [dict keys $sub $glob] + } else { + return [dict keys $sub] + } +} + +proc ::dictn::lappend {dictvar path args} { + if {[llength $path] == 1} { + uplevel 1 [list dict lappend $dictvar $path {*}$args] + } else { + upvar 1 $dictvar dvar + + ::set list [dict get $dvar {*}$path] + ::lappend list {*}$args + dict set dvar {*}$path $list + } +} + +proc ::dictn::merge {args} { + error "nested merge not yet supported" +} + +#dictn remove dictionaryValue ?path ...? +proc ::dictn::remove {dictval args} { + ::set basic [list] ;#buffer basic (1element path) removals to do in a single call. + + foreach path $args { + if {[llength $path] == 1} { + ::lappend basic $path + } else { + #extract,modify,replace + ::set subpath [lrange $path 0 end-1] + + ::set sub [dict get $dictval {*}$subpath] + ::set sub [dict remove $sub [lindex $path end]] + + dict set dictval {*}$subpath $sub + } + } + + if {[llength $basic]} { + return [dict remove $dictval {*}$basic] + } else { + return $dictval + } +} + + +proc ::dictn::replace {dictval args} { + ::set basic [list] ;#buffer basic (1element path) replacements to do in a single call. + + foreach {path val} $args { + if {[llength $path] == 1} { + ::lappend basic $path $val + } else { + #extract,modify,replace + ::set subpath [lrange $path 0 end-1] + + ::set sub [dict get $dictval {*}$subpath] + ::set sub [dict replace $sub [lindex $path end] $val] + + dict set dictval {*}$subpath $sub + } + } + + + if {[llength $basic]} { + return [dict replace $dictval {*}$basic] + } else { + return $dictval + } +} + + +proc ::dictn::set {dictvar path newval} { + upvar 1 $dictvar dvar + return [dict set dvar {*}$path $newval] +} + +proc ::dictn::size {dictval {path {}}} { + return [dict size [dict get $dictval {*}$path]] +} + +proc ::dictn::unset {dictvar path} { + upvar 1 $dictvar dvar + return [dict unset dvar {*}$path +} + +proc ::dictn::update {dictvar args} { + ::set body [lindex $args end] + ::set maplist [lrange $args 0 end-1] + + upvar 1 $dictvar dvar + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path]} { + uplevel 1 [list set $var [dict get $dvar $path]] + } + } + + catch {uplevel 1 $body} result + + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path]} { + upvar 1 $var $var + if {![::info exists $var]} { + uplevel 1 [list dict unset $dictvar {*}$path] + } else { + uplevel 1 [list dict set $dictvar {*}$path [::set $var]] + } + } + } + return $result +} + +#an experiment. +proc ::dictn::Applyupdate {dictvar args} { + ::set body [lindex $args end] + ::set maplist [lrange $args 0 end-1] + + upvar 1 $dictvar dvar + + ::set headscript "" + ::set i 0 + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path]} { + #uplevel 1 [list set $var [dict get $dvar $path]] + ::lappend arglist $var + ::lappend vallist [dict get $dvar {*}$path] + ::append headscript [string map [list %i% $i %v% $var] {upvar 1 %v% %v%; set %v% [lindex $args %i%]} ] + ::append headscript \n + ::incr i + } + } + + ::set body $headscript\r\n$body + + puts stderr "BODY: $body" + + #set result [apply [list args $body] {*}$vallist] + catch {apply [list args $body] {*}$vallist} result + + foreach {path var} $maplist { + if {[dict exists $dvar {*}$path] && [::info exists $var]} { + dict set dvar {*}$path [::set $var] + } + } + return $result +} + +proc ::dictn::values {dictval {path {}} {glob {}}} { + ::set sub [dict get $dictval {*}$path] + if {[string length $glob]} { + return [dict values $sub $glob] + } else { + return [dict values $sub] + } +} + +# Standard form: +#'dictn with dictVariable path body' +# +# Extended form: +#'dictn with dictVariable path arrayVariable body' +# +proc ::dictn::with {dictvar path args} { + if {[llength $args] == 1} { + ::set body [lindex $args 0] + return [uplevel 1 [list dict with $dictvar {*}$path $body]] + } else { + upvar 1 $dictvar dvar + ::lassign $args arrayname body + + upvar 1 $arrayname arr + array set arr [dict get $dvar {*}$path] + ::set prevkeys [array names arr] + + catch {uplevel 1 $body} result + + + foreach k $prevkeys { + if {![::info exists arr($k)]} { + dict unset $dvar {*}$path $k + } + } + foreach k [array names arr] { + dict set $dvar {*}$path $k $arr($k) + } + + return $result + } +} + + + + + + + + + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide dictn [namespace eval dictn { + variable version + ::set version 0.1.2 +}] +return \ No newline at end of file diff --git a/src/vendormodules/modpod-0.1.3.tm b/src/vendormodules/modpod-0.1.3.tm new file mode 100644 index 00000000..44da4684 --- /dev/null +++ b/src/vendormodules/modpod-0.1.3.tm @@ -0,0 +1,704 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# 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) 2024 +# +# @@ Meta Begin +# Application modpod 0.1.3 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin modpod_module_modpod 0 0.1.3] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require modpod] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of modpod +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by modpod +#[list_begin itemized] + +package require Tcl 8.6- +package require struct::set ;#review +package require punk::lib +package require punk::args +#*** !doctools +#[item] [package {Tcl 8.6-}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::class { + #*** !doctools + #[subsection {Namespace modpod::class}] + #[para] class definitions + if {[info commands [namespace current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + } +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod { + namespace export {[a-z]*}; # Convention: export all lowercase + + variable connected + if {![info exists connected(to)]} { + set connected(to) list + } + variable modpodscript + set modpodscript [info script] + if {[string tolower [file extension $modpodscript]] eq ".tcl"} { + set connected(self) [file dirname $modpodscript] + } else { + #expecting a .tm + set connected(self) $modpodscript + } + variable loadables [info sharedlibextension] + variable sourceables {.tcl .tk} ;# .tm ? + + #*** !doctools + #[subsection {Namespace modpod}] + #[para] Core API functions for modpod + #[list_begin definitions] + + + + #proc sample1 {p1 args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [opt {?option value...?}]] + # #[para]Description of sample1 + # return "ok" + #} + + #old tar connect mechanism - review - not needed? + proc connect {args} { + puts stderr "modpod::connect--->>$args" + set argd [punk::args::get_dict { + @id -id ::modpod::connect + -type -default "" + @values -min 1 -max 1 + path -type string -minsize 1 -help "path to .tm file or toplevel .tcl script within #modpod-- folder (unwrapped modpod)" + } $args] + catch { + punk::lib::showdict $argd ;#heavy dependencies + } + set opt_path [dict get $argd values path] + variable connected + set original_connectpath $opt_path + set modpodpath [modpod::system::normalize $opt_path] ;# + + if {$modpodpath in $connected(to)} { + return [dict create ok ALREADY_CONNECTED] + } + lappend connected(to) $modpodpath + + set connected(connectpath,$opt_path) $original_connectpath + set is_sourced [expr {[file normalize $modpodpath] eq [file normalize [info script]]}] + + set connected(location,$modpodpath) [file dirname $modpodpath] + set connected(startdata,$modpodpath) -1 + set connected(type,$modpodpath) [dict get $argd opts -type] + set connected(fh,$modpodpath) "" + + if {[string range [file tail $modpodpath] 0 7] eq "#modpod-"} { + set connected(type,$modpodpath) "unwrapped" + lassign [::split [file tail [file dirname $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname [file dirname $modpodpath]] + + } else { + #connect to .tm but may still be unwrapped version available + lassign [::split [file rootname [file tail $modpodath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname $modpodpath] + if {$connected(type,$modpodpath) ne "unwrapped"} { + #Not directly connected to unwrapped version - but may still be redirected there + set unwrappedFolder [file join $connected(location,$modpodpath) #modpod-$connected(package,$modpodpath)-$connected(version,$modpodpath)] + if {[file exists $unwrappedFolder]} { + #folder with exact version-match must exist for redirect to 'unwrapped' + set con(type,$modpodpath) "modpod-redirecting" + } + } + + } + set unwrapped_tm_file [file join $this_pkg_tm_folder] "[set connected(package,$modpodpath)]-[set connected(version,$modpodpath)].tm" + set connected(tmfile,$modpodpath) + set tail_segments [list] + set lcase_tmfile_segments [string tolower [file split $this_pkg_tm_folder]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [struct::set intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $this_pkg_tm_folder] [llength $mpath_segments] end] + break + } + } + if {[llength $tail_segments]} { + set connected(fullpackage,$modpodpath) [join [concat $tail_segments [set connected(package,$modpodpath)]] ::] ;#full name of package as used in package require + } else { + set connected(fullpackage,$modpodpath) [set connected(package,$modpodpath)] + } + + switch -exact -- $connected(type,$modpodpath) { + "modpod-redirecting" { + #redirect to the unwrapped version + set loadscript_name [file join $unwrappedFolder #modpod-loadscript-$con(package,$modpod).tcl] + + } + "unwrapped" { + if {[info commands ::thread::id] ne ""} { + set from [pid],[thread::id] + } else { + set from [pid] + } + #::modpod::Puts stderr "$from-> Package $connected(package,$modpodpath)-$connected(version,$modpodpath) is using unwrapped version: $modpodpath" + return [list ok ""] + } + default { + #autodetect .tm - zip/tar ? + #todo - use vfs ? + + #connect to tarball - start at 1st header + set connected(startdata,$modpodpath) 0 + set fh [open $modpodpath r] + set connected(fh,$modpodpath) $fh + fconfigure $fh -encoding iso8859-1 -translation binary -eofchar {} + + if {$connected(startdata,$modpodpath) >= 0} { + #verify we have a valid tar header + if {![catch {::modpod::system::tar::readHeader [red $fh 512]}]} { + seek $fh $connected(startdata,$modpodpath) start + return [list ok $fh] + } else { + #error "cannot verify tar header" + } + } + lpop connected(to) end + set connected(startdata,$modpodpath) -1 + unset connected(fh,$modpodpath) + catch {close $fh} + return [dict create err {Does not appear to be a valid modpod}] + } + } + } + proc disconnect {{modpod ""}} { + variable connected + if {![llength $connected(to)]} { + return 0 + } + if {$modpod eq ""} { + puts stderr "modpod::disconnect WARNING: modpod not explicitly specified. Disconnecting last connected: [lindex $connected(to) end]" + set modpod [lindex $connected(to) end] + } + + if {[set posn [lsearch $connected(to) $modpod]] == -1} { + puts stderr "modpod::disconnect WARNING: disconnect called when not connected: $modpod" + return 0 + } + if {[string length $connected(fh,$modpod)]} { + close $connected(fh,$modpod) + } + array unset connected *,$modpod + set connected(to) [lreplace $connected(to) $posn $posn] + return 1 + } + proc get {args} { + set argd [punk::args::get_dict { + -from -default "" -help "path to pod" + *values -min 1 -max 1 + filename + } $args] + set frompod [dict get $argd opts -from] + set filename [dict get $argd values filename] + + variable connected + #//review + set modpod [::modpod::system::connect_if_not $frompod] + set fh $connected(fh,$modpod) + if {$connected(type,$modpod) eq "unwrapped"} { + #for unwrapped connection - $connected(location) already points to the #modpod-pkg-ver folder + if {[string range $filename 0 0 eq "/"]} { + #absolute path (?) + set path [file join $connected(location,$modpod) .. [string trim $filename /]] + } else { + #relative path - use #modpod-xxx as base + set path [file join $connected(location,$modpod) $filename] + } + set fd [open $path r] + #utf-8? + #fconfigure $fd -encoding iso8859-1 -translation binary + return [list ok [lindex [list [read $fd] [close $fd]] 0]] + } else { + #read from vfs + puts stderr "get $filename from wrapped pod '$frompod' not implemented" + } + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::lib { + namespace export {[a-z]*}; # Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace modpod::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + proc is_valid_tm_version {versionpart} { + #Needs to be suitable for use with Tcl's 'package vcompare' + if {![catch [list package vcompare $versionparts $versionparts]]} { + return 1 + } else { + return 0 + } + } + + #zipfile is a pure zip at this point - ie no script/exe header + proc make_zip_modpod {args} { + set argd [punk::args::get_dict { + @id -id ::modpod::lib::make_zip_modpod + -offsettype -default "archive" -choices {archive file} -help\ + "Whether zip offsets are relative to start of file or start of zip-data within the file. + 'archive' relative offsets are easier to work with (for writing/updating) in tools such as 7zip,peazip, + but other tools may be easier with 'file' relative offsets. (e.g info-zip,pkzip) + info-zip's 'zip -A' can sometimes convert archive-relative to file-relative. + -offsettype archive is equivalent to plain 'cat prefixfile zipfile > modulefile'" + @values -min 2 -max 2 + zipfile -type path -minsize 1 -help "path to plain zip file with subfolder #modpod-packagename-version containing .tm, data files and/or binaries" + outfile -type path -minsize 1 -help "path to output file. Name should be of the form packagename-version.tm" + } $args] + set zipfile [dict get $argd values zipfile] + set outfile [dict get $argd values outfile] + set opt_offsettype [dict get $argd opts -offsettype] + + + set mount_stub [string map [list %offsettype% $opt_offsettype] { + #zip file with Tcl loader prepended. Requires either builtin zipfs, or vfs::zip to mount while zipped. + #Alternatively unzip so that extracted #modpod-package-version folder is in same folder as .tm file. + #generated using: modpod::lib::make_zip_modpod -offsettype %offsettype% + if {[catch {file normalize [info script]} modfile]} { + error "modpod zip stub error. Unable to determine module path. (possible safe interp restrictions?)" + } + if {$modfile eq "" || ![file exists $modfile]} { + error "modpod zip stub error. Unable to determine module path" + } + set moddir [file dirname $modfile] + set mod_and_ver [file rootname [file tail $modfile]] + lassign [split $mod_and_ver -] moduletail version + if {[file exists $moddir/#modpod-$mod_and_ver.tm]} { + source $moddir/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #determine module namespace so we can mount appropriately + proc intersect {A B} { + if {[llength $A] == 0} {return {}} + if {[llength $B] == 0} {return {}} + 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 + } + set lcase_tmfile_segments [string tolower [file split $moddir]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $moddir] [llength $mpath_segments] end] ;#use properly cased tail + break + } + } + if {[llength $tail_segments]} { + set fullpackage [join [concat $tail_segments $moduletail] ::] ;#full name of package as used in package require + set mount_at #modpod/[file join {*}$tail_segments]/#mounted-modpod-$mod_and_ver + } else { + set fullpackage $moduletail + set mount_at #modpod/#mounted-modpod-$mod_and_ver + } + + if {[info commands tcl::zipfs::mount] ne ""} { + #argument order changed to be consistent with vfs::zip::Mount etc + #early versions: zipfs::Mount mountpoint zipname + #since 2023-09: zipfs::Mount zipname mountpoint + #don't use 'file exists' when testing mountpoints. (some versions at least give massive delays on windows platform for non-existance) + #This is presumably related to // being interpreted as a network path + set mountpoints [dict keys [tcl::zipfs::mount]] + if {"//zipfs:/$mount_at" ni $mountpoints} { + #despite API change tcl::zipfs package version was unfortunately not updated - so we don't know argument order without trying it + if {[catch { + #tcl::zipfs::mount $modfile //zipfs:/#mounted-modpod-$mod_and_ver ;#extremely slow if this is a wrong guess (artifact of aforementioned file exists issue ?) + #puts "tcl::zipfs::mount $modfile $mount_at" + tcl::zipfs::mount $modfile $mount_at + } errM]} { + #try old api + if {![catch {tcl::zipfs::mount //zipfs:/$mount_at $modfile}]} { + puts stderr "modpod stub>>> tcl::zipfs::mount failed.\nbut old api: tcl::zipfs::mount succeeded\n tcl::zipfs::mount //zipfs://$mount_at $modfile" + puts stderr "Consider upgrading tcl runtime to one with fixed zipfs API" + } + } + if {![file exists //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + puts stderr "modpod stub>>> mount at //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm failed\n zipfs mounts: [zipfs mount]" + #tcl::zipfs::unmount //zipfs:/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + # #modpod-$mod_and_ver subdirectory always present in the archive so it can be conveniently extracted and run in that form + source //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #fallback to slower vfs::zip + #NB. We don't create the intermediate dirs - but the mount still works + if {![file exists $moddir/$mount_at]} { + if {[catch {package require vfs::zip} errM]} { + set msg "Unable to load vfs::zip package to mount module $mod_and_ver (and zipfs not available either)" + append msg \n "If neither zipfs or vfs::zip are available - the module can still be loaded by manually unzipping the file $modfile in place." + append msg \n "The unzipped data will all be contained in a folder named #modpod-$mod_and_ver in the same parent folder as $modfile" + error $msg + } else { + set fd [vfs::zip::Mount $modfile $moddir/$mount_at] + if {![file exists $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + vfs::zip::Unmount $fd $moddir/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + } + source $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } + } + #zipped data follows + }] + #todo - test if supplied zipfile has #modpod-loadcript.tcl or some other script/executable before even creating? + append mount_stub \x1A + modpod::system::make_mountable_zip $zipfile $outfile $mount_stub $opt_offsettype + + } + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval modpod::system { + #*** !doctools + #[subsection {Namespace modpod::system}] + #[para] Internal functions that are not part of the API + + #deflate,store only supported + + #zipfile here is plain zip - no script/exe prefix part. + proc make_mountable_zip {zipfile outfile mount_stub {offsettype "archive"}} { + set inzip [open $zipfile r] + fconfigure $inzip -encoding iso8859-1 -translation binary + set out [open $outfile w+] + fconfigure $out -encoding iso8859-1 -translation binary + puts -nonewline $out $mount_stub + set stuboffset [tell $out] + lappend report "stub size: $stuboffset" + fcopy $inzip $out + close $inzip + + set size [tell $out] + lappend report "modpod::system::make_mountable_zip" + lappend report "tmfile : [file tail $outfile]" + lappend report "output size : $size" + lappend report "offsettype : $offsettype" + + if {$offsettype eq "file"} { + #make zip offsets relative to start of whole file including prepended script. + #same offset structure as Tcl's older 'zipfs mkimg' as at 2024-10 + #2025 - zipfs mkimg fixed to use 'archive' offset. + #not editable by 7z,nanazip,peazip + + #we aren't adding any new files/folders so we can edit the offsets in place + + #Now seek in $out to find the end of directory signature: + #The structure itself is 24 bytes Long, followed by a maximum of 64Kbytes text + if {$size < 65559} { + set tailsearch_start 0 + } else { + set tailsearch_start [expr {$size - 65559}] + } + seek $out $tailsearch_start + set data [read $out] + #EOCD - End of Central Directory record + #PK\5\6 + set start_of_end [string last "\x50\x4b\x05\x06" $data] + #set start_of_end [expr {$start_of_end + $seek}] + #incr start_of_end $seek + set filerelative_eocd_posn [expr {$start_of_end + $tailsearch_start}] + + lappend report "kitfile-relative START-OF-EOCD: $filerelative_eocd_posn" + + seek $out $filerelative_eocd_posn + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + lappend report "End of central directory: [array get eocd]" + seek $out [expr {$filerelative_eocd_posn+16}] + + #adjust offset of start of central directory by the length of our sfx stub + puts -nonewline $out [binary format i [expr {$eocd(diroffset) + $stuboffset}]] + flush $out + + seek $out $filerelative_eocd_posn + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + # 0x06054b50 - end of central dir signature + puts stderr "$end_of_ctrl_dir" + puts stderr "comment_len: $eocd(comment_len)" + puts stderr "eocd sig: $eocd(signature) [punk::lib::dec2hex $eocd(signature)]" + lappend report "New dir offset: $eocd(diroffset)" + lappend report "Adjusting $eocd(totalnum) zip file items." + catch { + punk::lib::showdict -roottype list -chan stderr $report ;#heavy dependencies + } + + seek $out $eocd(diroffset) + for {set i 0} {$i <$eocd(totalnum)} {incr i} { + set current_file [tell $out] + set fileheader [read $out 46] + puts -------------- + puts [ansistring VIEW -lf 1 $fileheader] + puts -------------- + #binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + # x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + + binary scan $fileheader ic4sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + set ::last_header $fileheader + + puts "sig: $x(sig) (hex: [punk::lib::dec2hex $x(sig)])" + puts "ver: $x(version)" + puts "method: $x(method)" + + #PK\1\2 + #33639248 dec = 0x02014b50 - central directory file header signature + if { $x(sig) != 33639248 } { + error "modpod::system::make_mountable_zip Bad file header signature at item $i: dec:$x(sig) hex:[punk::lib::dec2hex $x(sig)]" + } + + foreach size $x(lengths) var {filename extrafield comment} { + if { $size > 0 } { + set x($var) [read $out $size] + } else { + set x($var) "" + } + } + set next_file [tell $out] + lappend report "file $i: $x(offset) $x(sizes) $x(filename)" + + seek $out [expr {$current_file+42}] + puts -nonewline $out [binary format i [expr {$x(offset)+$stuboffset}]] + + #verify: + flush $out + seek $out $current_file + set fileheader [read $out 46] + lappend report "old $x(offset) + $stuboffset" + binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + lappend report "new $x(offset)" + + seek $out $next_file + } + } + + close $out + #pdict/showdict reuire punk & textlib - ie lots of dependencies + #don't fall over just because of that + catch { + punk::lib::showdict -roottype list -chan stderr $report + } + #puts [join $report \n] + return + } + + proc connect_if_not {{podpath ""}} { + upvar ::modpod::connected connected + set podpath [::modpod::system::normalize $podpath] + set docon 0 + if {![llength $connected(to)]} { + if {![string length $podpath]} { + error "modpod::system::connect_if_not - Not connected to a modpod file, and no podpath specified" + } else { + set docon 1 + } + } else { + if {![string length $podpath]} { + set podpath [lindex $connected(to) end] + puts stderr "modpod::system::connect_if_not WARNING: using last connected modpod:$podpath for operation\n -podpath not explicitly specified during operation: [info level -1]" + } else { + if {$podpath ni $connected(to)} { + set docon 1 + } + } + } + if {$docon} { + if {[lindex [modpod::connect $podpath]] 0] ne "ok"} { + error "modpod::system::connect_if_not error. file $podpath does not seem to be a valid modpod" + } else { + return $podpath + } + } + #we were already connected + return $podpath + } + + proc myversion {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myversion should only be called from within a loading modpod" + } + set fname [file tail [file rootname [file normalize $script]]] + set scriptdir [file dirname $script] + + if {![string match "#modpod-*" $fname]} { + lassign [lrange [split $fname -] end-1 end] _pkgname version + } else { + lassign [scan [file tail [file rootname $script]] {#modpod-loadscript-%[a-z]-%s}] _pkgname version + if {![string length $version]} { + #try again on the name of the containing folder + lassign [scan [file tail $scriptdir] {#modpod-%[a-z]-%s}] _pkgname version + #todo - proper walk up the directory tree + if {![string length $version]} { + #try again on the grandparent folder (this is a standard depth for sourced .tcl files in a modpod) + lassign [scan [file tail [file dirname $scriptdir]] {#modpod-%[a-z]-%s}] _pkgname version + } + } + } + + #tarjar::Log debug "'myversion' determined version for [info script]: $version" + return $version + } + + proc myname {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myname should only be called from within a loading modpod" + } + return $connected(fullpackage,$script) + } + proc myfullname {} { + upvar ::modpod::connected connected + set script [info script] + #set script [::tarjar::normalize $script] + set script [file normalize $script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myfullname should only be called from within a loading tarjar" + } + return $::tarjar::connected(fullpackage,$script) + } + proc normalize {path} { + #newer versions of Tcl don't do tilde sub + + #Tcl's 'file normalize' seems to do some unfortunate tilde substitution on windows.. (at least for relative paths) + # we take the assumption here that if Tcl's tilde substitution is required - it should be done before the path is provided to this function. + set matilda "<_tarjar_tilde_placeholder_>" ;#token that is *unlikely* to occur in the wild, and is somewhat self describing in case it somehow ..escapes.. + set path [string map [list ~ $matilda] $path] ;#give our tildes to matilda to look after + set path [file normalize $path] + #set path [string tolower $path] ;#must do this after file normalize + return [string map [list $matilda ~] $path] ;#get our tildes back. +} +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide modpod [namespace eval modpod { + variable pkg modpod + variable version + set version 0.1.3 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/vendormodules/tomlish-1.1.6.tm b/src/vendormodules/tomlish-1.1.6.tm index dddcd0bb..a562545a 100644 --- a/src/vendormodules/tomlish-1.1.6.tm +++ b/src/vendormodules/tomlish-1.1.6.tm @@ -1512,7 +1512,7 @@ namespace eval tomlish { if {[::tomlish::utils::is_int $tok]} { set tag INT } else { - if {[string is integer -strict $tok]} { + if {[::tomlish::utils::string_is_integer -strict $tok]} { #didn't qualify as a toml int - but still an int #probably means is_int is limiting size and not accepting bigints (configurable?) #or it didn't qualify due to more than 1 leading zero @@ -2027,6 +2027,30 @@ namespace eval tomlish::utils { #[para] #[list_begin definitions] + #------------------------------------------------------------------------------ + # Tcl 8.6 support + #------------------------------------------------------------------------------ + if {[catch {tcl::string::is dict {}}]} { + proc string_is_dict {str} { + #we don't support -strict or -failindex for this fallback + expr {[::tcl::string::is list $str] && ([llength $str] % 2 == 0)} + } + } else { + proc string_is_dict {str} { + #we don't support -strict or -failindex for this fallback even though underlying supports it + ::tcl::string::is dict $str + } + } + if {![string is integer [expr {2**32}]]} { + proc string_is_integer {args} { + ::tcl::string::is entier {*}$args + } + } else { + proc string_is_integer {args} { + ::tcl::string::is integer {*}$args + } + } + #------------------------------------------------------------------------------ #basic generic quote matching for single and double quotes @@ -2695,7 +2719,7 @@ namespace eval tomlish::utils { set numeric_value [tcl::string::map {_ ""} $str] ;#allow some earlier tcl versions which don't support underscores #use Tcl's integer check to ensure we don't let things like 3e4 through - which is a float (would need to be 0x3e4 for hex) - if {![tcl::string::is integer -strict $numeric_value]} { + if {![::tomlish::utils::string_is_integer -strict $numeric_value]} { return 0 } @@ -2795,7 +2819,7 @@ namespace eval tomlish::utils { set dposn [string first . $str] if {$dposn > -1 } { set d3 [string range $str $dposn-1 $dposn+1] - if {![string is integer -strict [string index $d3 0]] || ![string is integer -strict [string index $d3 2]]} { + if {![::tomlish::utils::string_is_integer -strict [string index $d3 0]] || ![::tomlish::utils::string_is_integer -strict [string index $d3 2]]} { return 0 } } @@ -6213,7 +6237,7 @@ namespace eval tomlish::typedhuddle { } float { set dtype FLOAT - if {[string is integer -strict $hval]} { + if {[::tomlish::utils::string_is_integer -strict $hval]} { #json FLOAT specified as integer - must have dot for toml set hval [expr {double($hval)}] } @@ -6589,7 +6613,7 @@ namespace eval tomlish::dict { #consider x.y={type="spud",value="blah"} #The value of type will itself have already been converted to {type STRING value spud} ie never a single element. #check the length of the type as a quick way to see it's a tag - not something else masqerading. - expr {[string is dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} + expr {[::tomlish::utils::string_is_dict $d] && [dict size $d] == 2 && [dict exists $d type] && [dict exists $d value] && [llength [dict get $d type]] == 1} } #simple types only - not containers? @@ -6634,7 +6658,7 @@ namespace eval tomlish::dict { # Check that each leaf is a typeval or typeval dict #importantly: must accept empty dict leaves e.g {x {}} proc is_typeval_dict {d {checkarrays 0}} { - if {![string is dict $d]} { + if {![::tomlish::utils::string_is_dict $d]} { return 0 } dict for {k v} $d { @@ -7896,7 +7920,7 @@ namespace eval tomlish::dict::path { ::set v 0 ::set vdict [dict create] foreach a $args { - if {![string is dict $a]} { + if {![::tomlish::utils::string_is_dict $a]} { error "tomlish::dict::path::lappend error - lappended arguments must already be in the tomlish form {type value } or be a dict with such forms as leaves" } } @@ -8240,6 +8264,7 @@ namespace eval tomlish::system { #[para] For pure integer indices the performance should be equivalent set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 + #'only' supports 2**32 max index on tcl < 9.0 - ok. if {[string is integer -strict $index]} { #can match +i -i #avoid even the lseq overhead when the index is simple