From 6d01b6e93b0aae95aa11974660889b3e31ea9059 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Mon, 6 Jul 2026 22:59:41 +1000 Subject: [PATCH] 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::tm::add prepends each argument, head of tcl::tm::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/ 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 --- CHANGELOG.md | 5 ++ punkproject.toml | 2 +- ...llfilter-0.2.2.tm => shellfilter-0.2.3.tm} | 42 +++++++---- src/modules/shellfilter-999999.0a1.0.tm | 40 +++++++---- src/modules/shellfilter-buildversion.txt | 5 +- .../custom/_project/punk.basic/src/make.tcl | 71 ++++--------------- ...llfilter-0.2.2.tm => shellfilter-0.2.3.tm} | 42 +++++++---- .../_project/punk.project-0.1/src/make.tcl | 71 ++++--------------- ...llfilter-0.2.2.tm => shellfilter-0.2.3.tm} | 42 +++++++---- .../_project/punk.shell-0.1/src/make.tcl | 71 ++++--------------- src/tests/runtests.tcl | 22 +++++- 11 files changed, 184 insertions(+), 229 deletions(-) rename src/bootsupport/modules/{shellfilter-0.2.2.tm => shellfilter-0.2.3.tm} (98%) rename src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/{shellfilter-0.2.2.tm => shellfilter-0.2.3.tm} (98%) rename src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/{shellfilter-0.2.2.tm => shellfilter-0.2.3.tm} (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd38a10..0a728ccd 100644 --- a/CHANGELOG.md +++ b/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/` 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. diff --git a/punkproject.toml b/punkproject.toml index 4ee92bf2..c25d048f 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.2.5" +version = "0.2.6" diff --git a/src/bootsupport/modules/shellfilter-0.2.2.tm b/src/bootsupport/modules/shellfilter-0.2.3.tm similarity index 98% rename from src/bootsupport/modules/shellfilter-0.2.2.tm rename to src/bootsupport/modules/shellfilter-0.2.3.tm index 2a1e75c5..4f64e822 100644 --- a/src/bootsupport/modules/shellfilter-0.2.2.tm +++ b/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 }] diff --git a/src/modules/shellfilter-999999.0a1.0.tm b/src/modules/shellfilter-999999.0a1.0.tm index 31ddf847..572d6b5b 100644 --- a/src/modules/shellfilter-999999.0a1.0.tm +++ b/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'"} diff --git a/src/modules/shellfilter-buildversion.txt b/src/modules/shellfilter-buildversion.txt index bc664671..0cc93910 100644 --- a/src/modules/shellfilter-buildversion.txt +++ b/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. \ No newline at end of file +#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) \ No newline at end of file diff --git a/src/project_layouts/custom/_project/punk.basic/src/make.tcl b/src/project_layouts/custom/_project/punk.basic/src/make.tcl index 4a936c7b..a45272bf 100644 --- a/src/project_layouts/custom/_project/punk.basic/src/make.tcl +++ b/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" diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.3.tm similarity index 98% rename from src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm rename to src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.3.tm index 2a1e75c5..4f64e822 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm +++ b/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 }] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl index 4a936c7b..a45272bf 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl +++ b/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" diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.3.tm similarity index 98% rename from src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm rename to src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.3.tm index 2a1e75c5..4f64e822 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm +++ b/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 }] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl index 4a936c7b..a45272bf 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl +++ b/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" diff --git a/src/tests/runtests.tcl b/src/tests/runtests.tcl index a0994362..7863de26 100644 --- a/src/tests/runtests.tcl +++ b/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 /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/ runtime payload that would otherwise supply Thread. +lappend test_auto_path [file dirname [info library]] #------------------------------------