Browse Source

shellfilter 0.2.3: survive infra errors in run; runtests tm order + Thread path

shellfilter::run now wraps everything between tee-stack add and removal in
try/finally so the callers stdout/stderr are always restored, and the
decorative stack-status table rendering in log::critical blocks is
catch-guarded. Previously an error there aborted the run after diverting
the channels and before restoring them, leaving process stdout/stderr
broken with all subsequent output silently lost - observed when a stale
same-version textblock snapshot (old option-value syntax) called the new
punk::ansi sgr_merge_singles inside the stack-status render during tomlish
test runs.

runtests.tcl:
- testinterp tm path order: bootsupport moved to last test_tmlist element
  so it wins same-version module ties over vendormodules. Rules verified
  experimentally on Tcl 9.0.3 (identical under standard package unknown
  and punk::libunknown): tcl:™️:add prepends each argument, head of
  tcl:™️:list wins same-version ties, so the last element of a
  single-call add list wins.
- testinterp auto_path gains the parent of [info library] so binary
  packages such as Thread (needed by shellrun) resolve in projects
  without a root lib_tcl<N>/<arch> payload.

Project version 0.2.6. Verified: full shellspy suite at baseline (517
pass / exec-14.3 known); tomlish ported suite 184/184; reconstructed
stale-module scenario now completes with captured results and live
stdout instead of silent loss.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 3 weeks ago
parent
commit
6d01b6e93b
  1. 5
      CHANGELOG.md
  2. 2
      punkproject.toml
  3. 42
      src/bootsupport/modules/shellfilter-0.2.3.tm
  4. 40
      src/modules/shellfilter-999999.0a1.0.tm
  5. 5
      src/modules/shellfilter-buildversion.txt
  6. 71
      src/project_layouts/custom/_project/punk.basic/src/make.tcl
  7. 42
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.3.tm
  8. 71
      src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl
  9. 42
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.3.tm
  10. 71
      src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl
  11. 22
      src/tests/runtests.tcl

5
CHANGELOG.md

@ -5,6 +5,11 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy.
## [0.2.6] - 2026-07-06
- shellfilter 0.2.3: `shellfilter::run` now guarantees tee-stack removal (stdout/stderr restoration) via try/finally even when its body errors, and the decorative stack-status table rendering in `log::critical` blocks is catch-guarded. Previously an error there (observed: a stale same-version textblock snapshot calling the new punk::ansi `sgr_merge_singles` with old option syntax) aborted the run between channel diversion and restoration, leaving the process's stdout/stderr broken and all subsequent output silently lost.
- runtests.tcl: testinterp tm path order fixed so the bootsupport snapshot wins same-version module ties over vendormodules (verified rule on Tcl 9.0.3, standard and punk::libunknown scanners: `tcl::tm::add` prepends, head of `tcl::tm::list` wins ties, so the last `test_tmlist` element wins); testinterp auto_path gains the parent of `[info library]` so binary packages such as Thread resolve in projects without a root `lib_tcl<N>/<arch>` payload.
## [0.2.5] - 2026-07-06
- `make.tcl vendorupdate` now checks each vendor source project's fossil/git checkout state and warns (non-fatal, once per VCS root) when pulling from a dirty tree — vendored artifacts built from uncommitted source have no committed provenance. Enforcement policy (abort/override, bootsupport coverage) tracked by goal G-026.

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.2.5"
version = "0.2.6"

42
src/bootsupport/modules/shellfilter-0.2.2.tm → src/bootsupport/modules/shellfilter-0.2.3.tm

@ -2849,9 +2849,19 @@ namespace eval shellfilter {
set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]]
set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]]
#From here to the stack::remove calls the caller's channels are diverted:
#any error escaping this region would leave stdout/stderr broken for the rest of the
#process with all subsequent output silently lost - hence the try/finally below, and
#catch-guards on decorative logging (stack-status table rendering runs punk::ansi/textblock
#code that can fail e.g when mismatched module snapshots are loaded - a rendering failure
#must degrade to a log note, never abort the run).
log::critical {
::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]"
if {[catch {punk::ansi::ansistrip [shellfilter::stack status]} stackstatus]} {
set stackstatus "(shellfilter::stack status unavailable: $stackstatus)"
}
::shellfilter::log::write $runtag "$stackstatus\nchan names:[chan names]"
}
try {
# need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this
# If non os-level channel - the command can't be run with the redirection
# stderr/stdout can be run with non-os handles in the call -
@ -2919,22 +2929,28 @@ namespace eval shellfilter {
#----------------------------------------------------------------------------------------------
#todo - change to log::debug
log::critical {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
if {[catch {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
}
} errstatus]} {
set stackstatus "(shellfilter::stack status unavailable: $errstatus)"
}
::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]"
}
#----------------------------------------------------------------------------------------------
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
} finally {
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack - must happen even if the body above errored,
#or the caller's stdout/stderr stay diverted and all subsequent process output is silently lost.
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
}
log::info {::shellfilter::log::write $runtag " return '$exitinfo'"}
@ -3811,5 +3827,5 @@ namespace eval shellfilter {
package provide shellfilter [namespace eval shellfilter {
variable version
set version 0.2.2
set version 0.2.3
}]

40
src/modules/shellfilter-999999.0a1.0.tm

@ -2849,9 +2849,19 @@ namespace eval shellfilter {
set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]]
set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]]
#From here to the stack::remove calls the caller's channels are diverted:
#any error escaping this region would leave stdout/stderr broken for the rest of the
#process with all subsequent output silently lost - hence the try/finally below, and
#catch-guards on decorative logging (stack-status table rendering runs punk::ansi/textblock
#code that can fail e.g when mismatched module snapshots are loaded - a rendering failure
#must degrade to a log note, never abort the run).
log::critical {
::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]"
if {[catch {punk::ansi::ansistrip [shellfilter::stack status]} stackstatus]} {
set stackstatus "(shellfilter::stack status unavailable: $stackstatus)"
}
::shellfilter::log::write $runtag "$stackstatus\nchan names:[chan names]"
}
try {
# need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this
# If non os-level channel - the command can't be run with the redirection
# stderr/stdout can be run with non-os handles in the call -
@ -2919,22 +2929,28 @@ namespace eval shellfilter {
#----------------------------------------------------------------------------------------------
#todo - change to log::debug
log::critical {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
if {[catch {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
}
} errstatus]} {
set stackstatus "(shellfilter::stack status unavailable: $errstatus)"
}
::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]"
}
#----------------------------------------------------------------------------------------------
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
} finally {
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack - must happen even if the body above errored,
#or the caller's stdout/stderr stay diverted and all subsequent process output is silently lost.
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
}
log::info {::shellfilter::log::write $runtag " return '$exitinfo'"}

5
src/modules/shellfilter-buildversion.txt

@ -1,3 +1,4 @@
0.2.2
0.2.3
#First line must be a semantic version number
#all other lines are ignored.
#all other lines are ignored.
#0.2.3 - shellfilter::run: try/finally guarantees tee-stack removal (stdout/stderr restoration) even when the body errors; decorative stack-status table rendering in log::critical blocks is catch-guarded so a rendering failure (e.g mismatched module snapshots - stale textblock vs new punk::ansi sgr_merge_singles options) degrades to a log note instead of aborting the run and leaving the process's stdout/stderr diverted (silent-output-loss failure mode found via tomlish runtests 2026-07-06)

71
src/project_layouts/custom/_project/punk.basic/src/make.tcl

@ -1972,59 +1972,12 @@ if {$::punkboot::command eq "vendorupdate"} {
#puts "-- [tcl::tm::list] --"
puts stdout "Updating vendor modules in src folder"
#Walk up from a vendor source path to the nearest fossil and/or git root and report uncommitted changes.
#Vendored artifacts built from a dirty source tree have no committed provenance (the built module files
#may correspond to no checked-in state) - warn so the source project can be committed first.
#checkedrootsvar names a dict in the caller used to report each VCS root at most once per run.
#Returns a list of warning lines (empty if clean, unversioned, already reported, or state cannot be determined).
proc vendor_source_dirty_warnings {srcpath checkedrootsvar} {
upvar 1 $checkedrootsvar checkedroots
set warnings [list]
set dir [file normalize $srcpath]
if {![file isdirectory $dir]} {
return $warnings ;#missing source location warned about elsewhere
}
set fossilroot ""
set gitroot ""
while {1} {
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} {
set fossilroot $dir
}
if {$gitroot eq "" && [file exists [file join $dir .git]]} {
set gitroot $dir
}
if {$fossilroot ne "" || $gitroot ne ""} {
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: vendorupdate could not determine fossil state of source project at $fossilroot ($errM)"
} elseif {$fchanges ne ""} {
lappend warnings "WARNING: vendorupdate source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - vendored artifacts built from a dirty tree have no committed provenance"
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot 1
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]]
} errM]} {
lappend warnings "WARNING: vendorupdate could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: vendorupdate source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - vendored artifacts built from a dirty tree have no committed provenance"
}
}
return $warnings
#dirty-checkout provenance warnings via punkboot::utils::vcs_dirty_warnings (loaded from bootsupport).
#Guarded require: vendorupdate must keep working even if the bootsupport punkboot::utils copy is
#missing or stale (it may be the very thing being repaired) - the check degrades to skipped.
set have_vcs_dirty_check [expr {![catch {package require punkboot::utils}] && [llength [info commands ::punkboot::utils::vcs_dirty_warnings]]}]
if {!$have_vcs_dirty_check} {
puts stderr "WARNING: vendorupdate dirty-checkout provenance check unavailable (punkboot::utils vcs_dirty_warnings not loadable from bootsupport) - continuing without it"
}
proc vendor_localupdate {projectroot} {
@ -2079,11 +2032,13 @@ if {$::punkboot::command eq "vendorupdate"} {
set srclocation [file join $localpath $module_subpath]
}
#dirty-checkout provenance check - each VCS root reported at most once per run
set normsrclocation [file normalize $srclocation]
if {![dict exists $checked_source_paths $normsrclocation]} {
dict set checked_source_paths $normsrclocation 1
foreach w [vendor_source_dirty_warnings $normsrclocation checked_vcs_roots] {
puts stderr $w
if {$::have_vcs_dirty_check} {
set normsrclocation [file normalize $srclocation]
if {![dict exists $checked_source_paths $normsrclocation]} {
dict set checked_source_paths $normsrclocation 1
foreach w [::punkboot::utils::vcs_dirty_warnings $normsrclocation checked_vcs_roots vendorupdate] {
puts stderr $w
}
}
}
#puts stdout "$relpath $module $module_subpath $srclocation"

42
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm → src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.3.tm

@ -2849,9 +2849,19 @@ namespace eval shellfilter {
set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]]
set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]]
#From here to the stack::remove calls the caller's channels are diverted:
#any error escaping this region would leave stdout/stderr broken for the rest of the
#process with all subsequent output silently lost - hence the try/finally below, and
#catch-guards on decorative logging (stack-status table rendering runs punk::ansi/textblock
#code that can fail e.g when mismatched module snapshots are loaded - a rendering failure
#must degrade to a log note, never abort the run).
log::critical {
::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]"
if {[catch {punk::ansi::ansistrip [shellfilter::stack status]} stackstatus]} {
set stackstatus "(shellfilter::stack status unavailable: $stackstatus)"
}
::shellfilter::log::write $runtag "$stackstatus\nchan names:[chan names]"
}
try {
# need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this
# If non os-level channel - the command can't be run with the redirection
# stderr/stdout can be run with non-os handles in the call -
@ -2919,22 +2929,28 @@ namespace eval shellfilter {
#----------------------------------------------------------------------------------------------
#todo - change to log::debug
log::critical {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
if {[catch {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
}
} errstatus]} {
set stackstatus "(shellfilter::stack status unavailable: $errstatus)"
}
::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]"
}
#----------------------------------------------------------------------------------------------
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
} finally {
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack - must happen even if the body above errored,
#or the caller's stdout/stderr stay diverted and all subsequent process output is silently lost.
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
}
log::info {::shellfilter::log::write $runtag " return '$exitinfo'"}
@ -3811,5 +3827,5 @@ namespace eval shellfilter {
package provide shellfilter [namespace eval shellfilter {
variable version
set version 0.2.2
set version 0.2.3
}]

71
src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl

@ -1972,59 +1972,12 @@ if {$::punkboot::command eq "vendorupdate"} {
#puts "-- [tcl::tm::list] --"
puts stdout "Updating vendor modules in src folder"
#Walk up from a vendor source path to the nearest fossil and/or git root and report uncommitted changes.
#Vendored artifacts built from a dirty source tree have no committed provenance (the built module files
#may correspond to no checked-in state) - warn so the source project can be committed first.
#checkedrootsvar names a dict in the caller used to report each VCS root at most once per run.
#Returns a list of warning lines (empty if clean, unversioned, already reported, or state cannot be determined).
proc vendor_source_dirty_warnings {srcpath checkedrootsvar} {
upvar 1 $checkedrootsvar checkedroots
set warnings [list]
set dir [file normalize $srcpath]
if {![file isdirectory $dir]} {
return $warnings ;#missing source location warned about elsewhere
}
set fossilroot ""
set gitroot ""
while {1} {
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} {
set fossilroot $dir
}
if {$gitroot eq "" && [file exists [file join $dir .git]]} {
set gitroot $dir
}
if {$fossilroot ne "" || $gitroot ne ""} {
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: vendorupdate could not determine fossil state of source project at $fossilroot ($errM)"
} elseif {$fchanges ne ""} {
lappend warnings "WARNING: vendorupdate source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - vendored artifacts built from a dirty tree have no committed provenance"
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot 1
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]]
} errM]} {
lappend warnings "WARNING: vendorupdate could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: vendorupdate source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - vendored artifacts built from a dirty tree have no committed provenance"
}
}
return $warnings
#dirty-checkout provenance warnings via punkboot::utils::vcs_dirty_warnings (loaded from bootsupport).
#Guarded require: vendorupdate must keep working even if the bootsupport punkboot::utils copy is
#missing or stale (it may be the very thing being repaired) - the check degrades to skipped.
set have_vcs_dirty_check [expr {![catch {package require punkboot::utils}] && [llength [info commands ::punkboot::utils::vcs_dirty_warnings]]}]
if {!$have_vcs_dirty_check} {
puts stderr "WARNING: vendorupdate dirty-checkout provenance check unavailable (punkboot::utils vcs_dirty_warnings not loadable from bootsupport) - continuing without it"
}
proc vendor_localupdate {projectroot} {
@ -2079,11 +2032,13 @@ if {$::punkboot::command eq "vendorupdate"} {
set srclocation [file join $localpath $module_subpath]
}
#dirty-checkout provenance check - each VCS root reported at most once per run
set normsrclocation [file normalize $srclocation]
if {![dict exists $checked_source_paths $normsrclocation]} {
dict set checked_source_paths $normsrclocation 1
foreach w [vendor_source_dirty_warnings $normsrclocation checked_vcs_roots] {
puts stderr $w
if {$::have_vcs_dirty_check} {
set normsrclocation [file normalize $srclocation]
if {![dict exists $checked_source_paths $normsrclocation]} {
dict set checked_source_paths $normsrclocation 1
foreach w [::punkboot::utils::vcs_dirty_warnings $normsrclocation checked_vcs_roots vendorupdate] {
puts stderr $w
}
}
}
#puts stdout "$relpath $module $module_subpath $srclocation"

42
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm → src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.3.tm

@ -2849,9 +2849,19 @@ namespace eval shellfilter {
set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]]
set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]]
#From here to the stack::remove calls the caller's channels are diverted:
#any error escaping this region would leave stdout/stderr broken for the rest of the
#process with all subsequent output silently lost - hence the try/finally below, and
#catch-guards on decorative logging (stack-status table rendering runs punk::ansi/textblock
#code that can fail e.g when mismatched module snapshots are loaded - a rendering failure
#must degrade to a log note, never abort the run).
log::critical {
::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]"
if {[catch {punk::ansi::ansistrip [shellfilter::stack status]} stackstatus]} {
set stackstatus "(shellfilter::stack status unavailable: $stackstatus)"
}
::shellfilter::log::write $runtag "$stackstatus\nchan names:[chan names]"
}
try {
# need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this
# If non os-level channel - the command can't be run with the redirection
# stderr/stdout can be run with non-os handles in the call -
@ -2919,22 +2929,28 @@ namespace eval shellfilter {
#----------------------------------------------------------------------------------------------
#todo - change to log::debug
log::critical {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
if {[catch {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
}
} errstatus]} {
set stackstatus "(shellfilter::stack status unavailable: $errstatus)"
}
::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]"
}
#----------------------------------------------------------------------------------------------
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
} finally {
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack - must happen even if the body above errored,
#or the caller's stdout/stderr stay diverted and all subsequent process output is silently lost.
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
}
log::info {::shellfilter::log::write $runtag " return '$exitinfo'"}
@ -3811,5 +3827,5 @@ namespace eval shellfilter {
package provide shellfilter [namespace eval shellfilter {
variable version
set version 0.2.2
set version 0.2.3
}]

71
src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl

@ -1972,59 +1972,12 @@ if {$::punkboot::command eq "vendorupdate"} {
#puts "-- [tcl::tm::list] --"
puts stdout "Updating vendor modules in src folder"
#Walk up from a vendor source path to the nearest fossil and/or git root and report uncommitted changes.
#Vendored artifacts built from a dirty source tree have no committed provenance (the built module files
#may correspond to no checked-in state) - warn so the source project can be committed first.
#checkedrootsvar names a dict in the caller used to report each VCS root at most once per run.
#Returns a list of warning lines (empty if clean, unversioned, already reported, or state cannot be determined).
proc vendor_source_dirty_warnings {srcpath checkedrootsvar} {
upvar 1 $checkedrootsvar checkedroots
set warnings [list]
set dir [file normalize $srcpath]
if {![file isdirectory $dir]} {
return $warnings ;#missing source location warned about elsewhere
}
set fossilroot ""
set gitroot ""
while {1} {
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} {
set fossilroot $dir
}
if {$gitroot eq "" && [file exists [file join $dir .git]]} {
set gitroot $dir
}
if {$fossilroot ne "" || $gitroot ne ""} {
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: vendorupdate could not determine fossil state of source project at $fossilroot ($errM)"
} elseif {$fchanges ne ""} {
lappend warnings "WARNING: vendorupdate source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - vendored artifacts built from a dirty tree have no committed provenance"
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot 1
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]]
} errM]} {
lappend warnings "WARNING: vendorupdate could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: vendorupdate source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - vendored artifacts built from a dirty tree have no committed provenance"
}
}
return $warnings
#dirty-checkout provenance warnings via punkboot::utils::vcs_dirty_warnings (loaded from bootsupport).
#Guarded require: vendorupdate must keep working even if the bootsupport punkboot::utils copy is
#missing or stale (it may be the very thing being repaired) - the check degrades to skipped.
set have_vcs_dirty_check [expr {![catch {package require punkboot::utils}] && [llength [info commands ::punkboot::utils::vcs_dirty_warnings]]}]
if {!$have_vcs_dirty_check} {
puts stderr "WARNING: vendorupdate dirty-checkout provenance check unavailable (punkboot::utils vcs_dirty_warnings not loadable from bootsupport) - continuing without it"
}
proc vendor_localupdate {projectroot} {
@ -2079,11 +2032,13 @@ if {$::punkboot::command eq "vendorupdate"} {
set srclocation [file join $localpath $module_subpath]
}
#dirty-checkout provenance check - each VCS root reported at most once per run
set normsrclocation [file normalize $srclocation]
if {![dict exists $checked_source_paths $normsrclocation]} {
dict set checked_source_paths $normsrclocation 1
foreach w [vendor_source_dirty_warnings $normsrclocation checked_vcs_roots] {
puts stderr $w
if {$::have_vcs_dirty_check} {
set normsrclocation [file normalize $srclocation]
if {![dict exists $checked_source_paths $normsrclocation]} {
dict set checked_source_paths $normsrclocation 1
foreach w [::punkboot::utils::vcs_dirty_warnings $normsrclocation checked_vcs_roots vendorupdate] {
puts stderr $w
}
}
}
#puts stdout "$relpath $module $module_subpath $srclocation"

22
src/tests/runtests.tcl

@ -271,23 +271,43 @@ proc runtests_print_slowest_tests {slowest_tests show_timings} {
#------------------------------------
#for the tests running in child processes,
#use the unbuilt modules/libraries under development in preference to the installed versions.
#ORDERING NOTE (rules verified experimentally on Tcl 9.0.3, 2026-07-06 - identical results
#under Tcl's standard 'package unknown' scanner and punk::libunknown):
# - tcl::tm::add PREPENDS each of its arguments in turn, so a single call
# 'tcl::tm::add {*}$test_tmlist' produces tcl::tm::list in REVERSED test_tmlist order
# (the last element of test_tmlist ends up at the head of tcl::tm::list).
# - For the SAME module name-version found under multiple tm paths, the copy under the
# path nearest the HEAD of tcl::tm::list wins (first-scanned registration is kept).
# - Net effect: the LAST element of test_tmlist wins same-version ties.
#Different-version selection is unaffected by order: dev modules (999999.0a1.0) win via
#'package prefer latest' wherever they sit. But bootsupport and vendormodules snapshots can
#carry the SAME version of a module with different code (a version-bump-discipline failure in
#the source project, but it happens - observed 2026-07-06 with a stale vendormodules
#textblock 0.1.3 shadowing the fresh bootsupport copy and breaking shellfilter::run).
#bootsupport is deliberately the LAST test_tmlist element so the punkshell-managed bootsupport
#snapshot wins any same-version tie.
set test_tmlist [list]
set ifneeded_script ""
lappend test_tmlist [file normalize $test_base/../modules] ;#ie <projectroot>/src/modules
append ifneeded_script [punk::tcltestrun::tm_path_additional_ifneeded [file normalize $test_base/../modules] $project_root]
lappend test_tmlist [file normalize $test_base/../modules_tcl$tcl_major]
append ifneeded_script [punk::tcltestrun::tm_path_additional_ifneeded [file normalize $test_base/../modules_tcl$tcl_major] $project_root]
lappend test_tmlist [file normalize $test_base/../bootsupport/modules]
#vendored module dependencies (e.g voo) must be resolvable by modules/tests using them
lappend test_tmlist [file normalize $test_base/../vendormodules]
if {[file isdirectory $test_base/../vendormodules_tcl$tcl_major]} {
lappend test_tmlist [file normalize $test_base/../vendormodules_tcl$tcl_major]
}
lappend test_tmlist [file normalize $test_base/../bootsupport/modules] ;#after vendormodules - wins same-version ties
set test_auto_path [list]
lappend test_auto_path [file normalize $test_base/../lib]
lappend test_auto_path [file normalize $test_base/../lib/tcl$tcl_major]
lappend test_auto_path [file normalize $test_base/../../lib_tcl$tcl_major/$arch]
lappend test_auto_path [info library]
#the parent of [info library] is where an installation's binary packages usually live
#(e.g Thread, needed by shellrun in the testinterp) - the default tclsh auto_path has it,
#but we replace auto_path wholesale in the testinterp, and not every project carries a
#root lib_tcl<N>/<arch> runtime payload that would otherwise supply Thread.
lappend test_auto_path [file dirname [info library]]
#------------------------------------

Loading…
Cancel
Save