diff --git a/src/bootsupport/modules/punk/args-0.12.6.tm b/src/bootsupport/modules/punk/args-0.12.7.tm similarity index 99% rename from src/bootsupport/modules/punk/args-0.12.6.tm rename to src/bootsupport/modules/punk/args-0.12.7.tm index 3d9acb4c..0b52a7c3 100644 --- a/src/bootsupport/modules/punk/args-0.12.6.tm +++ b/src/bootsupport/modules/punk/args-0.12.7.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.12.6 +# Application punk::args 0.12.7 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.12.6] +#[manpage_begin punkshell_module_punk::args 0 0.12.7] #[copyright "2024"] #[titledesc {args parsing}] [comment {-- Name section and table of contents description --}] #[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}] @@ -4765,8 +4765,26 @@ tcl::namespace::eval punk::args { } else { set needed [list] foreach pkgns $nslist { - if {[string match (autodef)* $pkgns]} { - set pkgns [string range $pkgns 9 end] + if {[string match {(*)*} $pkgns]} { + #tag-prefixed id 'namespace' e.g (autodef)::myensemble, or a + #script-level id family e.g (script)::punkboot, (script)::punkexe. + #Not a real namespace: the scan records documenting packages under + #the TAGGED key (namespace qualifiers of the tagged id), so consult + #namespace_docpackages with the original key first, then strip the + #tag for the real-namespace probes below ((autodef) ids also map to + #a real namespace; a bare tag or tag-only qualifier has nothing more + #to probe). + if {[dict exists $namespace_docpackages $pkgns]} { + foreach docns [dict get $namespace_docpackages $pkgns] { + if {$docns ni $loaded_packages && $docns ni $needed} { + lappend needed $docns + } + } + } + set pkgns [string range $pkgns [expr {[string first ) $pkgns]+1}] end] + if {$pkgns eq ""} { + continue + } } if {![string match ::* $pkgns]} { puts stderr "warning: update_definitions received unqualified ns: $pkgns" @@ -14071,7 +14089,7 @@ package provide punk::args [tcl::namespace::eval punk::args { tcl::namespace::path {::punk::args::lib ::punk::args::system} variable pkg punk::args variable version - set version 0.12.6 + set version 0.12.7 }] return diff --git a/src/bootsupport/modules/punk/libunknown-0.2.1.tm b/src/bootsupport/modules/punk/libunknown-0.2.2.tm similarity index 97% rename from src/bootsupport/modules/punk/libunknown-0.2.1.tm rename to src/bootsupport/modules/punk/libunknown-0.2.2.tm index 84d84eb8..471c5e0e 100644 --- a/src/bootsupport/modules/punk/libunknown-0.2.1.tm +++ b/src/bootsupport/modules/punk/libunknown-0.2.2.tm @@ -7,7 +7,7 @@ # (C) 2025 # # @@ Meta Begin -# Application punk::libunknown 0.2.1 +# Application punk::libunknown 0.2.2 # Meta platform tcl # Meta license MIT # @@ Meta End @@ -18,6 +18,19 @@ # from src/modules/AGENTS.md "Versioning And Releases" - bumping means # renaming the file AND updating the Meta line above, the manpage_begin line # below and the provide-block version, then appending a line here): +#0.2.2 - zipfs_tm_UnknownHandler: the builtin-static shortcircuit probe interp no +# longer leaks (G-120). The probe child interp was created under the fixed +# name ptest and deleted only on the non-error paths; an unversioned +# 'package require foo' of a static package made the version check +# 'package vsatisfies $v' throw (wrong # args - vsatisfies needs >=1 +# requirement), skipping the delete. The leaked child held registrations +# on the process std channels (Tcl shares them into non-safe child +# interps), so an explicit 'chan close stderr' left the OS handle alive +# and the std slot occupied - the next opened channel got a fileN name +# instead of recycling into stderr. Now: auto-named probe interp, deleted +# in a try/finally; zero-requirement requires treat any provided version +# as satisfying; probe errors fall through to the normal tm search +# instead of aborting the require. #0.2.1 - zipfs_tm_UnknownHandler: the zipfs static-path can_skip_update state is # written keyed by the SPECIFIC searchpath actually scanned (tm path + # package namespace subdir) to match its reader and libunknown::init's @@ -42,7 +55,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::libunknown 0 0.2.1] +#[manpage_begin punkshell_module_punk::libunknown 0 0.2.2] #[copyright "2025"] #[titledesc {Module API}] [comment {-- Name section and table of contents description --}] #[moddesc {-}] [comment {-- Description at end of page heading --}] @@ -190,22 +203,36 @@ tcl::namespace::eval ::punk::libunknown { set loaded [lsearch -inline -index 1 -nocase [info loaded] $name] if {[llength $loaded] == 2 && [lindex $loaded 0] eq ""} { lassign $loaded _ cased_name - interp create ptest - ptest eval [list load {} $cased_name] - set static_version [ptest eval [list package provide [string tolower $cased_name]]] - set pname [string tolower $cased_name] - if {$static_version eq ""} { - set static_version [ptest eval [list package provide $cased_name]] - set pname $cased_name - } - if {$static_version ne ""} { - if {[package vsatisfies $static_version {*}$args]} { - package ifneeded $pname $static_version [list load {} $cased_name] - interp delete ptest - return + #Version-probe the static package in a throwaway child interp. + #The child must never outlive this block: a (non-safe) child interp + #holds registrations on the process std channels, so a leaked probe + #interp disables Tcl's std-slot recycling process-wide (after + #'chan close stderr' the OS handle stays alive and the slot stays + #occupied, so the next opened channel cannot become stderr) - G-120. + set probeinterp [interp create] + try { + interp eval $probeinterp [list load {} $cased_name] + set static_version [interp eval $probeinterp [list package provide [string tolower $cased_name]]] + set pname [string tolower $cased_name] + if {$static_version eq ""} { + set static_version [interp eval $probeinterp [list package provide $cased_name]] + set pname $cased_name + } + if {$static_version ne ""} { + #zero requirements (plain 'package require foo') accepts any + #provided version; package vsatisfies errors without at least + #one requirement argument. + if {![llength $args] || [package vsatisfies $static_version {*}$args]} { + package ifneeded $pname $static_version [list load {} $cased_name] + return + } } + } on error {} { + #probe failure (e.g a static whose init needs more than a bare + #child interp) - not fatal: fall through to the normal tm search. + } finally { + interp delete $probeinterp } - interp delete ptest } #------------------------------ @@ -2130,7 +2157,7 @@ namespace eval ::punk::args::register { package provide punk::libunknown [tcl::namespace::eval ::punk::libunknown { variable pkg punk::libunknown variable version - set version 0.2.1 + set version 0.2.2 }] return diff --git a/src/bootsupport/modules/punk/mod-0.1.1.tm b/src/bootsupport/modules/punk/mod-0.1.1.tm index 35559708..ac781c3c 100644 --- a/src/bootsupport/modules/punk/mod-0.1.1.tm +++ b/src/bootsupport/modules/punk/mod-0.1.1.tm @@ -71,12 +71,16 @@ namespace eval punk::mod::cli { } } dict set appinfo versions $versions - #todo - natsort! - set sorted_versions [lsort $versions] - set latest [lindex $sorted_versions 0] - if {$latest eq "" && [llength $sorted_versions] > 1} { - set latest [lindex $sorted_versions 1] + #version-aware selection (2026-07-22 audit): was a plain lexical lsort + #(1.10 sorts before 1.9) with lindex 0 - the LOWEST - as 'latest'. + #Empty version (unversioned main) is excluded from comparison (package + #vcompare errors on empty operands); latest = highest by vcompare. + set realversions {} + foreach v $versions { + if {$v ne ""} {lappend realversions $v} } + set sorted_versions [lsort -command {package vcompare} $realversions] + set latest [lindex $sorted_versions end] ;#"" when only an unversioned main exists dict set appinfo latest $latest dict set appinfo bases $bases diff --git a/src/bootsupport/modules/punk/repl-0.5.2.tm b/src/bootsupport/modules/punk/repl-0.5.3.tm similarity index 99% rename from src/bootsupport/modules/punk/repl-0.5.2.tm rename to src/bootsupport/modules/punk/repl-0.5.3.tm index be2506bc..911a7d4e 100644 --- a/src/bootsupport/modules/punk/repl-0.5.2.tm +++ b/src/bootsupport/modules/punk/repl-0.5.3.tm @@ -4290,6 +4290,7 @@ namespace eval repl { package require punk::lib package require punk::args catch {package require punk::args::moduledoc::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical + catch {package require punk::args::moduledoc::punkexe} ;#launch subcommand family documentation (G-118) - not critical package require textblock } @@ -4470,6 +4471,7 @@ namespace eval repl { catch {package require punk::args::moduledoc::parser} #----------- catch {package require punk::args::moduledoc::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical + catch {package require punk::args::moduledoc::punkexe} ;#launch subcommand family documentation (G-118) - not critical } errM]} { puts stderr "========================" puts stderr "code interp error:" @@ -4565,7 +4567,7 @@ namespace eval ::punk::args::register { package provide punk::repl [namespace eval punk::repl { variable version - set version 0.5.2 + set version 0.5.3 }] #repl::start $program_read_stdin_pipe diff --git a/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.2.tm similarity index 97% rename from src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.1.tm rename to src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.2.tm index 84d84eb8..471c5e0e 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.2.2.tm @@ -7,7 +7,7 @@ # (C) 2025 # # @@ Meta Begin -# Application punk::libunknown 0.2.1 +# Application punk::libunknown 0.2.2 # Meta platform tcl # Meta license MIT # @@ Meta End @@ -18,6 +18,19 @@ # from src/modules/AGENTS.md "Versioning And Releases" - bumping means # renaming the file AND updating the Meta line above, the manpage_begin line # below and the provide-block version, then appending a line here): +#0.2.2 - zipfs_tm_UnknownHandler: the builtin-static shortcircuit probe interp no +# longer leaks (G-120). The probe child interp was created under the fixed +# name ptest and deleted only on the non-error paths; an unversioned +# 'package require foo' of a static package made the version check +# 'package vsatisfies $v' throw (wrong # args - vsatisfies needs >=1 +# requirement), skipping the delete. The leaked child held registrations +# on the process std channels (Tcl shares them into non-safe child +# interps), so an explicit 'chan close stderr' left the OS handle alive +# and the std slot occupied - the next opened channel got a fileN name +# instead of recycling into stderr. Now: auto-named probe interp, deleted +# in a try/finally; zero-requirement requires treat any provided version +# as satisfying; probe errors fall through to the normal tm search +# instead of aborting the require. #0.2.1 - zipfs_tm_UnknownHandler: the zipfs static-path can_skip_update state is # written keyed by the SPECIFIC searchpath actually scanned (tm path + # package namespace subdir) to match its reader and libunknown::init's @@ -42,7 +55,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::libunknown 0 0.2.1] +#[manpage_begin punkshell_module_punk::libunknown 0 0.2.2] #[copyright "2025"] #[titledesc {Module API}] [comment {-- Name section and table of contents description --}] #[moddesc {-}] [comment {-- Description at end of page heading --}] @@ -190,22 +203,36 @@ tcl::namespace::eval ::punk::libunknown { set loaded [lsearch -inline -index 1 -nocase [info loaded] $name] if {[llength $loaded] == 2 && [lindex $loaded 0] eq ""} { lassign $loaded _ cased_name - interp create ptest - ptest eval [list load {} $cased_name] - set static_version [ptest eval [list package provide [string tolower $cased_name]]] - set pname [string tolower $cased_name] - if {$static_version eq ""} { - set static_version [ptest eval [list package provide $cased_name]] - set pname $cased_name - } - if {$static_version ne ""} { - if {[package vsatisfies $static_version {*}$args]} { - package ifneeded $pname $static_version [list load {} $cased_name] - interp delete ptest - return + #Version-probe the static package in a throwaway child interp. + #The child must never outlive this block: a (non-safe) child interp + #holds registrations on the process std channels, so a leaked probe + #interp disables Tcl's std-slot recycling process-wide (after + #'chan close stderr' the OS handle stays alive and the slot stays + #occupied, so the next opened channel cannot become stderr) - G-120. + set probeinterp [interp create] + try { + interp eval $probeinterp [list load {} $cased_name] + set static_version [interp eval $probeinterp [list package provide [string tolower $cased_name]]] + set pname [string tolower $cased_name] + if {$static_version eq ""} { + set static_version [interp eval $probeinterp [list package provide $cased_name]] + set pname $cased_name + } + if {$static_version ne ""} { + #zero requirements (plain 'package require foo') accepts any + #provided version; package vsatisfies errors without at least + #one requirement argument. + if {![llength $args] || [package vsatisfies $static_version {*}$args]} { + package ifneeded $pname $static_version [list load {} $cased_name] + return + } } + } on error {} { + #probe failure (e.g a static whose init needs more than a bare + #child interp) - not fatal: fall through to the normal tm search. + } finally { + interp delete $probeinterp } - interp delete ptest } #------------------------------ @@ -2130,7 +2157,7 @@ namespace eval ::punk::args::register { package provide punk::libunknown [tcl::namespace::eval ::punk::libunknown { variable pkg punk::libunknown variable version - set version 0.2.1 + set version 0.2.2 }] return