From 04193e5bd99a31b5b01bd8638938d83072f2ebd0 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Tue, 21 Jul 2026 02:03:51 +1000 Subject: [PATCH] make.tcl bootsupport re-vendor: punkcheck 0.5.0 + cli 0.5.1 (G-094), pending module syncs Batched punkcheck-managed build outputs from 'make.tcl modules' + 'make.tcl bootsupport' after the G-094 landing: punkcheck 0.4.0 -> 0.5.0 and punk::mix::cli 0.5.0 -> 0.5.1 snapshots (per the G-094 prompt re-vendor note - make.tcl now runs the unified lifecycle), plus opportunistic catch-up of stale snapshots (libunknown 0.2.0 -> 0.2.1, punk::config / punk::lib / punk::repl content syncs from current built modules). Verification under the new punkcheck: 'make.tcl modules' x2 and a full 'make.tcl project' complete exit 0 with skip/copy decisions unchanged against pre-refactor .punkcheck state - copied: 0 on every batch phase, kit installs skip 'no change detected' (the sole punk905 rebuild traces to the newly promoted tclsh905 runtime, a genuinely changed source). Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- src/bootsupport/modules/punk/config-0.2.0.tm | 1 + src/bootsupport/modules/punk/lib-0.5.0.tm | 64 +- ...ibunknown-0.2.0.tm => libunknown-0.2.1.tm} | 25 +- .../punk/mix/{cli-0.5.0.tm => cli-0.5.1.tm} | 39 +- src/bootsupport/modules/punk/repl-0.5.2.tm | 63 +- ...{punkcheck-0.4.0.tm => punkcheck-0.5.0.tm} | 803 +++++++----------- 6 files changed, 448 insertions(+), 547 deletions(-) rename src/bootsupport/modules/punk/{libunknown-0.2.0.tm => libunknown-0.2.1.tm} (98%) rename src/bootsupport/modules/punk/mix/{cli-0.5.0.tm => cli-0.5.1.tm} (96%) rename src/bootsupport/modules/{punkcheck-0.4.0.tm => punkcheck-0.5.0.tm} (81%) diff --git a/src/bootsupport/modules/punk/config-0.2.0.tm b/src/bootsupport/modules/punk/config-0.2.0.tm index 03d42b82..57ad3a41 100644 --- a/src/bootsupport/modules/punk/config-0.2.0.tm +++ b/src/bootsupport/modules/punk/config-0.2.0.tm @@ -275,6 +275,7 @@ tcl::namespace::eval punk::config { PUNK_APPS {type pathlist} PUNK_CONFIG {type string} PUNK_CONFIGSET {type string} + PUNK_FOSSIL_STORE {type string help "location of the punkshell-owned fossil clone store\n(source mirrors for buildsuites - e.g src/buildsuites/suite_tcl90/suite.tcl).\nDefault: ~/.punkshell/fossils (machine-level cache, outside deletable _build areas).\nMissing clones are seeded read-only from ~/.fossils when present, else network-cloned."} PUNK_SCRIPTLIB {type string} PUNK_AUTO_EXEC_MECHANISM {type string} PUNK_AUTO_NOEXEC {type string default 0 help "set 1 to set Tcl's ::auto_noexec true.\nStops 'unknown' from running external programs"} diff --git a/src/bootsupport/modules/punk/lib-0.5.0.tm b/src/bootsupport/modules/punk/lib-0.5.0.tm index 432617c0..24576228 100644 --- a/src/bootsupport/modules/punk/lib-0.5.0.tm +++ b/src/bootsupport/modules/punk/lib-0.5.0.tm @@ -252,11 +252,14 @@ tcl::namespace::eval punk::lib::check { #G-076: version gate for the tcl9 dead-console defect (upstream ticket f10d91c2d3, root-caused #in G-039). Shared by the 'help tcl' warning and the repl dead-console watchdog arming (both - #consult has_tclbug_console_deadspin). Empty = no released Tcl is known to contain the upstream - #fix, so every Tcl 9 windows runtime classifies as affected. Set this only after the G-039 kill - #procedure, re-run on the fixed released runtime with the watchdog disabled, shows a clean - #script-visible eof exit (see goals/G-076-tcl9-deadconsole-fix-adoption.md). - variable tclbug_console_deadspin_fixed_in "" + #consult has_tclbug_console_deadspin). Empty would mean no Tcl is known to contain the upstream + #fix (every Tcl 9 windows runtime classifies as affected). Set to 9.0.5 2026-07-20: the upstream + #fix (ticket closed 2026-07-14; merged to core-9-0-branch, whose head reports 9.0.5) passed the + #G-039 kill procedure on a zig-built core-9-0-branch runtime with the watchdog disarmed - the + #dead console arrived as a readable fileevent and the repl exited via the normal eof path + #(requires the punk::repl 0.5.2 repl_handler guard; an official release is not required per the + #amended acceptance - see goals/archive/G-076-tcl9-deadconsole-fix-adoption.md 2026-07-20 entries). + variable tclbug_console_deadspin_fixed_in "9.0.5" #pure classifier, separated for testability - facts in, verdict out proc tclbug_console_deadspin_applies {platform tclversion fixed_in} { @@ -8412,7 +8415,9 @@ namespace eval punk::lib { if {$do_libunknown} { if {[info exists ::punk::libunknown::epoch]} { append script "namespace eval ::punk::libunknown {}\n" - append script "set ::punk::libunknown::epoch [list $::punk::libunknown::epoch]\n" + #reduced copy - a verbatim copy carries 'added' short-circuit state that is + #only sound in the donor interp (see libunknown_epoch_export) + append script "set ::punk::libunknown::epoch [list [libunknown_epoch_export]]\n" } #find and source libunknown, then call init append script { @@ -8538,7 +8543,9 @@ namespace eval punk::lib { if {$do_libunknown} { if {[info exists ::punk::libunknown::epoch]} { interp eval $interp {namespace eval ::punk::libunknown {}} - interp eval $interp [list set ::punk::libunknown::epoch $::punk::libunknown::epoch] + #reduced copy - a verbatim copy carries 'added' short-circuit state that is + #only sound in the donor interp (see libunknown_epoch_export) + interp eval $interp [list set ::punk::libunknown::epoch [libunknown_epoch_export]] } #use snapshot's libunknown sourcing logic via interp eval set libunknown_script [punk::lib::snapshot_package_paths -libunknown 1] @@ -8578,6 +8585,49 @@ namespace eval punk::lib { } } + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::lib::libunknown_epoch_export + @cmd -name punk::lib::libunknown_epoch_export\ + -summary\ + "Reduced copy of the punk::libunknown epoch record, safe to seed into a fresh interp or thread."\ + -help\ + "Returns ::punk::libunknown::epoch with every per-epoch 'added' map emptied. + + The 'added' maps record which package names an interp's own scans registered from + each searchpath during each epoch - zipfs_tclPkgUnknown / zipfs_tm_UnknownHandler + consult the CURRENT epoch's map to skip re-sourcing a searchpath's indexes for a + name the path didn't provide before. That short-circuit is only sound in the interp + that performed the scans: packages the donor obtained at an EARLIER epoch, or with + no package unknown call at all (e.g Thread baked into a thread::create'd thread), + are absent from the donor's current-epoch map, so a fresh interp inheriting the + record verbatim skips their pkgIndex/tm sourcing and fails requires that stock + Tcl's tclPkgUnknown would satisfy (G-098 tkconsole diagnosis 2026-07-20). + + Epoch counters, untracked markers, ifneeded-script records and index-glob caches + are preserved; the recipient performs its own first sourcing pass per searchpath + and accumulates its own 'added' state. Errors if ::punk::libunknown::epoch is not + set - callers that tolerate an uninitialised libunknown should guard with + info exists as they would for the raw variable." + @values -min 0 -max 0 + }] + } + proc libunknown_epoch_export {} { + if {![info exists ::punk::libunknown::epoch]} { + error "punk::lib::libunknown_epoch_export - ::punk::libunknown::epoch not set (punk::libunknown not initialised in this interp?)" + } + set e $::punk::libunknown::epoch + foreach domain {tm pkg} { + if {![dict exists $e $domain epochs]} {continue} + dict for {ep state} [dict get $e $domain epochs] { + dict set state added [dict create] + dict set e $domain epochs $ep $state + } + } + return $e + } + proc valcopy {obj} { append obj2 $obj {} } diff --git a/src/bootsupport/modules/punk/libunknown-0.2.0.tm b/src/bootsupport/modules/punk/libunknown-0.2.1.tm similarity index 98% rename from src/bootsupport/modules/punk/libunknown-0.2.0.tm rename to src/bootsupport/modules/punk/libunknown-0.2.1.tm index 6319daf0..c0d92a16 100644 --- a/src/bootsupport/modules/punk/libunknown-0.2.0.tm +++ b/src/bootsupport/modules/punk/libunknown-0.2.1.tm @@ -7,7 +7,7 @@ # (C) 2025 # # @@ Meta Begin -# Application punk::libunknown 0.2.0 +# Application punk::libunknown 0.2.1 # Meta platform tcl # Meta license MIT # @@ Meta End @@ -18,6 +18,14 @@ # 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.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 +# indexbase keying. It was written keyed by the tm-list root, so resolving +# any subdir tm (e.g punk::args) poisoned the ROOT's skip map and later +# root-level requires (e.g commandstack) were skipped as known-absent - +# breaking 'shell' on zipfs kits (single //zipfs:/app/modules tm path; +# filesystem tm paths never take the skip branch). Diagnosed 2026-07-20. #0.2.0 - register_all_tm: deep discovery proc registering ifneeded scripts for # .tm modules at every namespace depth (once per tm epoch, interp-local # tm_fullscan guard); used by 'dev lib.search' by default @@ -34,7 +42,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::libunknown 0 0.2.0] +#[manpage_begin punkshell_module_punk::libunknown 0 0.2.1] #[copyright "2025"] #[titledesc {Module API}] [comment {-- Name section and table of contents description --}] #[moddesc {-}] [comment {-- Description at end of page heading --}] @@ -355,8 +363,10 @@ tcl::namespace::eval ::punk::libunknown { # the token-to-directory mapping may have changed. #JMN - review. - #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] - dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + #0.2.1: keyed by specificsearchpath (the subset actually scanned) - the + #can_skip_update reader and libunknown::init both key that way; keying by + #the tm-list root here let subdir scans poison root-level skip decisions. + dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch if {$must_scan} { #however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked dict unset epoch tm untracked $pkgname @@ -389,8 +399,9 @@ tcl::namespace::eval ::punk::libunknown { #JMN #store only once for each name, although there may be multiple versions of same package within this searchpath - #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] - dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + #0.2.1: keyed by specificsearchpath - see the matching note at the + #already-ifneeded branch above. + dict set epoch tm epochs $pkg_epoch added $specificsearchpath $pkgname $pkgversion e$pkg_epoch #pkgname here could be the 'name' passed at the beggning - or other .tms at the same location. #we can't always remove other .tms from 'tm untracked' because the search for name might skip some locations. if {$must_scan} { @@ -2119,7 +2130,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.0 + set version 0.2.1 }] return diff --git a/src/bootsupport/modules/punk/mix/cli-0.5.0.tm b/src/bootsupport/modules/punk/mix/cli-0.5.1.tm similarity index 96% rename from src/bootsupport/modules/punk/mix/cli-0.5.0.tm rename to src/bootsupport/modules/punk/mix/cli-0.5.1.tm index 33627082..325f4f07 100644 --- a/src/bootsupport/modules/punk/mix/cli-0.5.0.tm +++ b/src/bootsupport/modules/punk/mix/cli-0.5.1.tm @@ -7,7 +7,7 @@ # (C) 2023 # # @@ Meta Begin -# Application punk::mix::cli 0.5.0 +# Application punk::mix::cli 0.5.1 # Meta platform tcl # Meta license # @@ Meta End @@ -772,7 +772,6 @@ namespace eval punk::mix::cli { -glob $fileglob -max-depth 0 }] - #lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list # -- --- set installer [punkcheck::installtrack new $installername $punkcheck_file] #set installer [punkcheck::installtrack new $installername $punkcheck_file stderr] ;#with debugchannel @@ -990,7 +989,6 @@ namespace eval punk::mix::cli { } else { puts -nonewline stderr "Z" set did_skip 1 - #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] $build_event targetset_end SKIPPED } $build_event destroy @@ -1180,7 +1178,6 @@ namespace eval punk::mix::cli { } else { puts -nonewline stderr "T" set did_skip 1 - #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] $build_event targetset_end SKIPPED } $build_event destroy @@ -1293,9 +1290,6 @@ namespace eval punk::mix::cli { } #------------------------------ - # - #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$basename-$module_build_version.tm] - #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] $event targetset_init INSTALL $target_module_dir/$basename-$module_build_version.tm $event targetset_addsource $versionfile $event targetset_addsource $current_source_dir/$m @@ -1306,24 +1300,11 @@ namespace eval punk::mix::cli { $event targetset_addsource_virtual module_version $module_build_version } - #set changed_list [list] - ## -- --- --- --- --- --- - #set source_relpath [punkcheck::lib::path_relative $basedir $versionfile] - #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] - ## -- --- --- --- --- --- - #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] - #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] - ## -- --- --- --- --- --- - #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] - #set changed_list [dict get $changed_unchanged changed] - - if {\ [llength [dict get [$event targetset_source_changes] changed]]\ || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ } { - #set file_record [punkcheck::installfile_started_install $basedir $file_record] $event targetset_started # -- --- --- --- --- --- set target $target_module_dir/$basename-$module_build_version.tm @@ -1338,7 +1319,6 @@ namespace eval punk::mix::cli { #file copy -force $srcdir/$m $target lappend module_list $target # -- --- --- --- --- --- - #set file_record [punkcheck::installfile_finished_install $basedir $file_record] $event targetset_end OK } else { if {$is_interesting} { @@ -1346,7 +1326,6 @@ namespace eval punk::mix::cli { } puts -nonewline stderr "m" set did_skip 1 - #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] $event targetset_end SKIPPED } prune_superseded_target_modules $event $basedir $target_module_dir $basename $module_build_version @@ -1358,17 +1337,6 @@ namespace eval punk::mix::cli { continue } ##------------------------------ - ## - #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$m] - #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] - #set changed_list [list] - ## -- --- --- --- --- --- - #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] - #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] - ## -- --- --- --- --- --- - #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] - #set changed_list [dict get $changed_unchanged changed] - #---------- $event targetset_init INSTALL $target_module_dir/$m $event targetset_addsource $current_source_dir/$m catch { @@ -1382,7 +1350,6 @@ namespace eval punk::mix::cli { || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ } { - #set file_record [punkcheck::installfile_started_install $basedir $file_record] $event targetset_started # -- --- --- --- --- --- if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} @@ -1390,7 +1357,6 @@ namespace eval punk::mix::cli { file copy -force $current_source_dir/$m $target_module_dir puts stderr "Copied already versioned module $current_source_dir/$m to $target_module_dir" # -- --- --- --- --- --- - #set file_record [punkcheck::installfile_finished_install $basedir $file_record] $event targetset_end OK -note "already versioned module" } else { puts -nonewline stderr "f" @@ -1398,7 +1364,6 @@ namespace eval punk::mix::cli { if {$is_interesting} { puts stderr "$current_source_dir/$m [$event targetset_source_changes]" } - #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] $event targetset_end SKIPPED } prune_superseded_target_modules $event $basedir $target_module_dir $basename $tmfile_versionsegment @@ -1687,6 +1652,6 @@ namespace eval punk::mix::cli { ## Ready package provide punk::mix::cli [namespace eval punk::mix::cli { variable version - set version 0.5.0 + set version 0.5.1 }] return diff --git a/src/bootsupport/modules/punk/repl-0.5.2.tm b/src/bootsupport/modules/punk/repl-0.5.2.tm index f63942fa..8968a81c 100644 --- a/src/bootsupport/modules/punk/repl-0.5.2.tm +++ b/src/bootsupport/modules/punk/repl-0.5.2.tm @@ -1012,20 +1012,32 @@ proc repl::console_watchdog {inputchan} { } if {[catch {chan configure $inputchan -inputmode}]} { #GetConsoleMode failed - the hosting console is gone. - #Close the channel (stops the tclWinConsole.c reader thread's error busy-loop) - #and finish the repl via the normal eof path. Do not attempt a console reopen: - #with the console dead, CONIN$ cannot be opened either (app-punkshell's eof - #handling makes the same discovery and exits cleanly). - array unset console_watchdog_afterids $inputchan - catch {chan event $inputchan readable {}} - catch {chan close $inputchan} - catch {puts stderr "|repl> console_watchdog: console unavailable for '$inputchan' - closing input and finishing repl (eof)"} - set ::repl::done [list eof $inputchan] + console_input_lost $inputchan "console_watchdog: console unavailable for '$inputchan' - closing input and finishing repl (eof)" return } set console_watchdog_afterids($inputchan) [after $console_watchdog_ms [list [namespace current]::console_watchdog $inputchan]] } +proc repl::console_input_lost {inputchan msg} { + #Shared terminal path for a dead/unusable console input channel (G-039/G-076), + #used by console_watchdog (polling rescue on defect-carrying tcl9 runtimes) and by + #repl_handler (runtimes carrying the upstream f10d91c2d3 fix deliver the dead + #console as a readable event instead). + #Close the channel (stops the tclWinConsole.c reader thread's error busy-loop) + #and finish the repl via the normal eof done-path. Do not attempt a console reopen: + #with the console dead, CONIN$ cannot be opened either (app-punkshell's eof + #handling makes the same discovery and exits cleanly). + variable console_watchdog_afterids + if {[info exists console_watchdog_afterids($inputchan)]} { + catch {after cancel $console_watchdog_afterids($inputchan)} + array unset console_watchdog_afterids $inputchan + } + catch {chan event $inputchan readable {}} + catch {chan close $inputchan} + catch {puts stderr "|repl> $msg"} + set ::repl::done [list eof $inputchan] +} + namespace eval repl::argdoc { lappend PUNKARGS [list { @id -id ::repl::console_get_size @@ -1852,7 +1864,18 @@ proc repl::repl_handler {inputchan readmore prompt_config} { #According to DKF - -buffering option doesn't affect input channels set rawmode 0 set waiting_needs_reader 0 ;#set when the 8.6-console waiting-chunks path skips its read on a drained channel - the readable handler must be armed (not the after-idle reinvoke) or the pending partial line could never complete - set original_input_conf [chan configure $inputchan] ;#whether repl is in line or raw mode - we restore the inputchan (stdin) state + if {[catch {chan configure $inputchan} original_input_conf]} { + #A runtime carrying the upstream tcl9 dead-console fix (f10d91c2d3) DELIVERS a + #dead console as a readable event, and this full option probe then errors + #(e.g 'couldn't read console mode: broken pipe'). Pre-fix runtimes never notify + #script level of a dead console (the console_watchdog poll covers those), so + #reaching here with a dead console requires the fixed driver. Terminal input + #loss - same teardown as the watchdog, no reopen attempt. + set in_repl_handler [list] + console_input_lost $inputchan "repl_handler: input channel unusable ($original_input_conf) - closing input and finishing repl (eof)" + return + } + #original_input_conf: whether repl is in line or raw mode - we restore the inputchan (stdin) state if {[dict exists $original_input_conf -inputmode]} { #review - when terminal is mintty and we switch to raw mode, the change isn't relected in chan configures '-inputmode' why? @@ -2923,11 +2946,19 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #to the selected console's channels (rputs maps stdout/stderr to conout/conerr). #Interleaving between the two streams within a run is not preserved (single #terminal stream; per-console err semantics are G-011). + #The vars are guarded: if the code-interp init aborted before installing the + #junction (e.g a package resolution failure) they don't exist - treat as no + #pending output rather than erroring here, which would leave the repl reader + #disabled and wedge the session (G-098 tkconsole diagnosis 2026-07-20). set pending [thread::send $codethread { interp eval code { - set _pendinglist [list $::codeinterp::console_pending_out $::codeinterp::console_pending_err] - set ::codeinterp::console_pending_out "" - set ::codeinterp::console_pending_err "" + if {[info exists ::codeinterp::console_pending_out]} { + set _pendinglist [list $::codeinterp::console_pending_out $::codeinterp::console_pending_err] + set ::codeinterp::console_pending_out "" + set ::codeinterp::console_pending_err "" + } else { + set _pendinglist [list "" ""] + } set _pendinglist } }] @@ -3517,7 +3548,7 @@ namespace eval repl { } %replthread_interp% [list $opt_callback_interp] {*}{ } %tmlist% [list [tcl::tm::list]] {*}{ } %autopath% [list $::auto_path] {*}{ - } %lib_epoch% [list $::punk::libunknown::epoch] {*}{ + } %lib_epoch% [list [punk::lib::libunknown_epoch_export]] {*}{ } %packageprefer% [list [package prefer]] {*}{ } %staticprefixes% [list [expr {[info exists ::punkboot::static_prefixes] ? $::punkboot::static_prefixes : ""}]] {*}{ } %staticpackages% [list [expr {[info exists ::punkboot::static_packages] ? $::punkboot::static_packages : ""}]] {*}{ @@ -3923,7 +3954,9 @@ namespace eval repl { catch {punk::lib::interp_sync_package_paths code} code eval [list namespace eval ::punk::libunknown {}] catch { - code eval [list set ::punk::libunknown::epoch $::punk::libunknown::epoch] + #reduced copy - 'added' short-circuit state is only sound in the donor + #interp (see punk::lib::libunknown_epoch_export) + code eval [list set ::punk::libunknown::epoch [punk::lib::libunknown_epoch_export]] } } safebase { diff --git a/src/bootsupport/modules/punkcheck-0.4.0.tm b/src/bootsupport/modules/punkcheck-0.5.0.tm similarity index 81% rename from src/bootsupport/modules/punkcheck-0.4.0.tm rename to src/bootsupport/modules/punkcheck-0.5.0.tm index b080aebe..3b8ca952 100644 --- a/src/bootsupport/modules/punkcheck-0.4.0.tm +++ b/src/bootsupport/modules/punkcheck-0.5.0.tm @@ -108,55 +108,26 @@ namespace eval punkcheck { } - #todo - work out way to use same punkcheck file for multiple installers running concurrently. Thread? - #an installtrack objects represents an installation path from sourceroot to targetroot + #Concurrent multi-process writers of one .punkcheck file are G-095's scope - the single save chokepoint + #(installtrack method save_working_recordset) is the attachment point for its locking/atomic-rename/merge measures. + #an installtrack object represents an installation path from sourceroot to targetroot #The source and target folders should be as specific as possible but it is valid to specify for example c:/ -> c:/ (or / -> /) if source and targets within the installation operation are spread around. # + #Record lifecycle (G-094): the installtrack/installevent OO layer is the sole implementation of the + #.punkcheck record lifecycle (targetset init / add-source / started / end). Each event carries a + #persistence policy chosen at start_event: + # eager (default) - every non-QUERY targetset operation is an independent read-modify-write of the file + # deferred - targetset operations update the installtrack's in-memory working recordset only; + # the recordset is persisted by an explicit $installer flush (event-scoped save). + # An ended deferred event that was never flushed deliberately discards its updates + # (this is how -source-checksum compare mode avoids storing). + #QUERY operations are never persisted in either mode and never touch the working recordset. set objname [namespace current]::installtrack if {$objname ni [info commands $objname]} { package require oolib #FILEINFO record - target fileset with body records: INSTALL-RECORD,INSTALL-INPROGRESS,INSTALL-SKIPPED,DELETE-RECORD,DELETE-INPROGRESS,MODIFY-INPROGRESS,MODIFY-RECORD #each FILEINFO body being a list of SOURCE records - oo::class create targetset { - variable o_targets - variable o_keep_installrecords - variable o_keep_skipped - variable o_keep_inprogress - variable o_records - constructor {args} { - #set o_records [oolib::collection create [namespace current]::recordcollection] - set o_records [list] - - } - - method as_record {} { - dict create {*}{ - } tag FILEINFO {*}{ - } -targets $o_targets {*}{ - } -keep_installrecords $o_keep_installrecords {*}{ - } -keep_skipped $o_keep_skipped {*}{ - } -keep_inprogress $o_keep_inprogress {*}{ - } body $o_records {*}{ - } - } - - #retrieve last completed record for the fileset ie exclude SKIPPED,INSTALL-INPROGRESS,DELETE-INPROGRESS,MODIFY-INPROGRESS - method get_last_record {fileset_record} { - set body [dict_getwithdefault $fileset_record body [list]] - set previous_records [lrange $body 0 end-1] - #get last previous that is tagged INSTALL-RECORD,MODIFY-RECORD,DELETE-RECORD - set revlist [lreverse $previous_records] - foreach rec $revlist { - switch -- [dict get $rec tag] { - INSTALL-RECORD - MODIFY-RECORD - DELETE-RECORD - VIRTUAL-RECORD { - return $rec - } - } - } - return [list] - } - } #instances created by an installtrack object in method start_event #also in installtrack constructor - to represent existing events from the .punkcheck data @@ -175,6 +146,7 @@ namespace eval punkcheck { variable o_fileset_record variable o_installer ;#parent object variable o_debugchannel + variable o_persistence constructor {installer rel_sourceroot rel_targetroot args} { set o_installer $installer set o_debugchannel [$installer get_debugchannel] @@ -182,13 +154,18 @@ namespace eval punkcheck { set o_path_cksum_cache [dict create] set o_operation "" set defaults [dict create {*}{ - -id "" - -tsbegin "" - -config {} - -tsend "" - -types {} + -id "" + -tsbegin "" + -config {} + -tsend "" + -types {} + -persistence eager }] set opts [dict merge $defaults $args] + set o_persistence [dict get $opts -persistence] + if {$o_persistence ni [list eager deferred]} { + error "[self] constructor error: -persistence must be 'eager' or 'deferred'. Received '$o_persistence'" + } if {[dict get $opts -id] eq ""} { set o_id [punkcheck::uuid] } else { @@ -238,6 +215,12 @@ namespace eval punkcheck { method get_operation {} { return $o_operation } + method get_persistence {} { + return $o_persistence + } + method get_installer {} { + return $o_installer + } method get_targets {} { return $o_targets } @@ -251,12 +234,27 @@ namespace eval punkcheck { } method end {} { set o_ts_end [clock microseconds] + #notify parent so its active-event slot clears and a subsequent start_event is valid + $o_installer event_ended [self] } method targetset_dict {} { - punk::records_as_target_dict [$o_installer get_recordlist] + punkcheck::recordlist::records_as_target_dict [$o_installer get_recordlist] + } + #merge the event's current fileset record into the parent installtrack's working recordset + #(replace the record with matching -targets, or append when absent) + method MergeFilesetToWorking {} { + set record_list [$o_installer get_recordlist] + set targetlist [dict get $o_fileset_record -targets] + set oldrecordinfo [punkcheck::recordlist::get_file_record $targetlist $record_list] + set old_posn [dict get $oldrecordinfo position] + if {$old_posn == -1} { + lappend record_list $o_fileset_record + } else { + lset record_list $old_posn $o_fileset_record + } + $o_installer set_recordlist $record_list } - #related - installfile_begin #call init before we know if we are going to run the operation vs skip method targetset_init {operation targetset} { set known_ops [list QUERY INSTALL MODIFY DELETE VIRTUAL] @@ -300,18 +298,34 @@ namespace eval punkcheck { set o_targets [lsort -dictionary -increasing $relativepath_targetset] ;#exact sort order not critical - but must be consistent + #working recordset acquisition per persistence policy: + # eager - refresh the parent installtrack's working recordset from the file (each op is an independent read-modify-write) + # deferred - the parent's in-memory working recordset is authoritative between flushes; no file access + # QUERY - reads the freshest available state but never touches the working recordset (never persisted) + if {$o_operation eq "QUERY"} { + if {$o_persistence eq "deferred"} { + set record_list [$o_installer get_recordlist] + } else { + set record_list [punkcheck::load_records_from_file $punkcheck_file] + } + } else { + if {$o_persistence eq "eager"} { + $o_installer load_all_records + } + set record_list [$o_installer get_recordlist] + } - set record_list [punkcheck::load_records_from_file $punkcheck_file] - - #--------------------------------------------------------------------------- - #load as dict to test for dupes - #set _targetdict [my targetset_dict] - if {[catch { - set _targetdict [punkcheck::recordlist::records_as_target_dict $record_list] - } errMsg]} { - error "targetset_init operation:$operation error verifying existing records from file $punkcheck_file. Error: $errMsg" + if {$o_persistence eq "eager"} { + #--------------------------------------------------------------------------- + #load as dict to test for dupes (skipped in deferred mode - per-op scan cost, and the + #pre-G-094 batch recordset handling this mode inherits from never ran it either) + if {[catch { + set _targetdict [punkcheck::recordlist::records_as_target_dict $record_list] + } errMsg]} { + error "targetset_init operation:$operation error verifying existing records from file $punkcheck_file. Error: $errMsg" + } + #--------------------------------------------------------------------------- } - #--------------------------------------------------------------------------- set extractioninfo [punkcheck::recordlist::extract_or_create_fileset_record $o_targets $record_list] set o_fileset_record [dict get $extractioninfo record] @@ -319,23 +333,28 @@ namespace eval punkcheck { set isnew [dict get $extractioninfo isnew] set oldposition [dict get $extractioninfo oldposition] unset extractioninfo + if {$isnew && $o_debugchannel ne ""} { + puts $o_debugchannel "punkcheck: no existing FILEINFO record for targetset '$o_targets' - creating" + } #INSTALL-INPROGRESS will become INSTALL-RECORD or INSTALL-FAILED or INSTALL-SKIPPED upon finalisation #-installer and -eventid keys are added here set new_inprogress_record [dict create tag [string toupper $operation]-INPROGRESS {*}$fields -tempcontext [my as_record] body {}] - #set existing_body [dict_getwithdefault $o_fileset_record body [list]] #todo - look for existing "-INPROGRESS" records - mark as failed or incomplete? dict lappend o_fileset_record body $new_inprogress_record - if {$isnew} { - lappend record_list $o_fileset_record - } else { - #set record_list [linsert $record_list[unset record_list] $oldposition $o_fileset_record] - ledit record_list $oldposition -1 $o_fileset_record - } - if {$o_operation ne "QUERY"} { - punkcheck::save_records_to_file $record_list $punkcheck_file "targetset_init $o_operation [llength $targetset] targets" + if {$o_operation ne "QUERY" && $o_persistence eq "eager"} { + #reinsert the inprogress-marked record and persist - crash evidence in the file during long operations + if {$isnew} { + lappend record_list $o_fileset_record + } else { + ledit record_list $oldposition -1 $o_fileset_record + } + $o_installer set_recordlist $record_list + $o_installer save_working_recordset "targetset_init $o_operation [llength $targetset] targets" } + #deferred: the *-INPROGRESS state stays event-local (o_fileset_record) - only finalized records + #reach the working recordset (targetset_end), mirroring the pre-G-094 batch recordset handling. return $o_fileset_record } @@ -343,28 +362,31 @@ namespace eval punkcheck { #todo - upgrade .punkcheck format to hold more than just list of SOURCE entries in each record. # - allow arbitrary targetset_startphase targetset_endphase calls to store timestamps and calculate elapsed time method targetset_started {} { - set punkcheck_folder [file dirname [$o_installer get_checkfile]] - if {$o_operation eq "QUERY"} { - set fileinfo_body [dict get $o_fileset_record body] ;#body of FILEINFO record - set installing_record [lindex $fileinfo_body end] + if {$o_operation_start_ts eq ""} { + error "[self] targetset_started - no current operation - call targetset_init first" + } + if {![punkcheck::lib::is_file_record_inprogress $o_fileset_record]} { + error "targetset_started error: bad fileset_record - expected FILEINFO with last body element *-INPROGRESS" + } + set fileinfo_body [dict get $o_fileset_record body] ;#body of FILEINFO record + set installing_record [lindex $fileinfo_body end] - set ts_start [dict get $installing_record -ts] - set ts_now [clock microseconds] - set metadata_us [expr {$ts_now - $ts_start}] + set ts_start [dict get $installing_record -ts] + set ts_now [clock microseconds] + set metadata_us [expr {$ts_now - $ts_start}] - #?? - #JJJ - #dict set installing_record -metadata_us $metadata_us - dict set installing_record -ts_start_transfer $ts_now + dict set installing_record -metadata_us $metadata_us + dict set installing_record -ts_start_transfer $ts_now - lset fileinfo_body end $installing_record + lset fileinfo_body end $installing_record + dict set o_fileset_record body $fileinfo_body - return [dict set o_fileset_record body $fileinfo_body] - } else { - #legacy call - #saves to .punkcheck file - return [set o_fileset_record [punkcheck::installfile_started_install $punkcheck_folder $o_fileset_record]] + if {$o_operation ne "QUERY" && $o_persistence eq "eager"} { + $o_installer load_all_records + my MergeFilesetToWorking + $o_installer save_working_recordset "targetset_started $o_operation [llength $o_targets] targets" } + return $o_fileset_record } method targetset_end {status args} { set defaults [dict create {*}{ @@ -400,7 +422,6 @@ namespace eval punkcheck { set installing_record [lindex $file_record_body end] set punkcheck_file [$o_installer get_checkfile] set punkcheck_folder [file dirname $punkcheck_file] - set record_list [punkcheck::load_records_from_file $punkcheck_file] if {[dict exists $installing_record -ts_start_transfer]} { set ts_start_transfer [dict get $installing_record -ts_start_transfer] set transfer_us [expr {$operation_end_ts - $ts_start_transfer}] @@ -434,15 +455,15 @@ namespace eval punkcheck { dict set o_fileset_record body $file_record_body set o_fileset_record [punkcheck::recordlist::file_record_prune $o_fileset_record] - set oldrecordinfo [punkcheck::recordlist::get_file_record $targetlist $record_list] - set old_posn [dict get $oldrecordinfo position] - if {$old_posn == -1} { - lappend record_list $o_fileset_record - } else { - lset record_list $old_posn $o_fileset_record - } if {$o_operation ne "QUERY"} { - punkcheck::save_records_to_file $record_list $punkcheck_file "targetset_end $o_operation $status [llength $o_targets] targets" + if {$o_persistence eq "eager"} { + $o_installer load_all_records + my MergeFilesetToWorking + $o_installer save_working_recordset "targetset_end $o_operation $status [llength $o_targets] targets" + } else { + #deferred - finalized record joins the in-memory working recordset; persisted at flush + my MergeFilesetToWorking + } } set o_operation_start_ts "" set o_operation "" @@ -614,8 +635,17 @@ namespace eval punkcheck { set o_events [oolib::collection create [namespace current]::eventcollection] set eventlist [punkcheck::dict_getwithdefault $this_installer_record body [list]] foreach e $eventlist { - set eobj [punkcheck::installevent create [namespace current]::event_[my events count] [self] [dict get $e -source] [dict get $e -targets] {*}$e] - #$o_events add $e [dict get $e -id] + #map the persisted EVENT record fields to constructor options explicitly. + #(the record keys are -ts_begin/-ts_end - passing the raw record gave the constructor's + # -tsbegin/-tsend defaults, so reconstructed events took 'now' as begin and empty end, + # and any later flush/save_installer_record rewrote prior event history with those values) + set eargs [list] + lappend eargs -id [punkcheck::dict_getwithdefault $e -id ""] + lappend eargs -tsbegin [punkcheck::dict_getwithdefault $e -ts_begin ""] + lappend eargs -tsend [punkcheck::dict_getwithdefault $e -ts_end ""] + lappend eargs -types [punkcheck::dict_getwithdefault $e -types {}] + lappend eargs -config [punkcheck::dict_getwithdefault $e -config {}] + set eobj [punkcheck::installevent create [namespace current]::event_[my events count] [self] [dict get $e -source] [dict get $e -targets] {*}$eargs] $o_events add $eobj [dict get $e -id] } @@ -667,37 +697,52 @@ namespace eval punkcheck { set fields [list {*}{ } -tsiso $o_tsiso {*}{ } -ts $o_ts {*}{ - } -name $o_name\ {*}{ + } -name $o_name {*}{ } -keep_events $o_keep_events {*}{ } body $eventrecords {*}{ } ] set record [dict create tag INSTALLER {*}$fields] } - #open file and save only own records - method save_all_records {} { - my save_installer_record - #todo - save FILEINFO targetset records - } - method save_installer_record {} { - set file_records [punkcheck::load_records_from_file $o_checkfile] - + #working recordset accessors - the installtrack's o_record_list is the working copy of the + #.punkcheck recordset. Eager operations and start_event refresh it from the file; deferred + #operations treat it as authoritative between flushes. + method set_recordlist {records} { + set o_record_list $records + } + #single save chokepoint: every .punkcheck write from the record lifecycle funnels through here. + #G-095 (concurrent-writer safety: locking, atomic rename, merge-on-flush) attaches at this point. + method save_working_recordset {{trigger save_working_recordset}} { + punkcheck::save_records_to_file $o_record_list $o_checkfile $trigger $o_debugchannel + } + #sync this installer's own record (from the live event objects) into the working recordset, + #prune unreferenced events beyond -keep_events, and persist via the chokepoint. + #This is the event-scoped persistence point for deferred events. + method flush {{trigger flush}} { set this_installer_record [my as_record] - - set persistedinfo [punkcheck::recordlist::get_installer_record $o_name $file_records] + set persistedinfo [punkcheck::recordlist::get_installer_record $o_name $o_record_list] set existing_header_posn [dict get $persistedinfo position] if {$existing_header_posn == -1} { - #set file_records [linsert $file_records 0 $this_installer_record] - ledit file_records -1 -1 $this_installer_record + ledit o_record_list -1 -1 $this_installer_record + set existing_header_posn 0 } else { - lset file_records $existing_header_posn $this_installer_record + lset o_record_list $existing_header_posn $this_installer_record } - punkcheck::save_records_to_file $file_records $o_checkfile "save_installer_record" + set this_installer_record [punkcheck::recordlist::installer_record_pruneevents $this_installer_record $o_record_list] + lset o_record_list $existing_header_posn $this_installer_record + my save_working_recordset $trigger + } + #save the working recordset (installer record synced) - historical name retained. + method save_all_records {} { + my flush save_all_records + } + method save_installer_record {} { + my flush save_installer_record } method events {args} { tailcall $o_events {*}$args } - method start_event {configdict} { + method start_event {configdict args} { if {$o_active_event ne ""} { error "[self] start_event error - event already started: $o_active_event" } @@ -706,17 +751,39 @@ namespace eval punkcheck { } if {[llength $configdict] %2 != 0} { - error "[self] new_event configdict must have an even number of elements" + error "[self] start_event configdict must have an even number of elements" + } + set defaults [dict create -persistence eager] + if {[llength $args] % 2} { + error "[self] start_event arguments after configdict must be -flag value pairs. known flags: [dict keys $defaults]" + } + foreach {k v} $args { + if {$k ni [dict keys $defaults]} { + error "[self] start_event unrecognised flag '$k'. known flags: [dict keys $defaults]" + } } + set opts [dict merge $defaults $args] + set persistence [dict get $opts -persistence] + + #refresh the working recordset from the file so this event starts from persisted state. + #(coherence: a prior event's targetset writes - eager per-op saves or a deferred flush - must not + # be discarded by saving a stale constructor-era recordset here. Unflushed deferred updates are + # deliberately abandoned by this refresh - an ended deferred event that wasn't flushed chose not + # to persist.) + my load_all_records set resultinfo [punkcheck::recordlist::get_installer_record $o_name $o_record_list] set existing_header_posn [dict get $resultinfo position] if {$existing_header_posn == -1} { - error "[self] start_event - installer record missing. installer: $o_name" + #no persisted installer record yet (first event since construction - the constructor only + #creates the record in memory; this save is the first write) + set this_installer_record [punkcheck::recordlist::new_installer_record $o_name] + ledit o_record_list -1 -1 $this_installer_record + set existing_header_posn 0 } else { set this_installer_record [dict get $resultinfo record] } - set eventobj [punkcheck::installevent create [namespace current]::event_[my events count] [self] $o_rel_sourceroot $o_rel_targetroot -config $configdict] + set eventobj [punkcheck::installevent create [namespace current]::event_[my events count] [self] $o_rel_sourceroot $o_rel_targetroot -config $configdict -persistence $persistence] set eventid [$eventobj get_id] set event_record [$eventobj as_record] @@ -726,7 +793,7 @@ namespace eval punkcheck { #replace lset o_record_list $existing_header_posn $this_installer_record - punkcheck::save_records_to_file $o_record_list $o_checkfile "start_event $eventid" + my save_working_recordset "start_event $eventid" set o_active_event $eventobj my events add $eventobj $eventid return $eventobj @@ -735,7 +802,7 @@ namespace eval punkcheck { set resultinfo [punkcheck::recordlist::get_installer_record $o_name $o_record_list] } method get_recordlist {} { - return $o_recordlist + return $o_record_list } method end_event {} { if {$o_active_event eq ""} { @@ -743,131 +810,67 @@ namespace eval punkcheck { } $o_active_event end } + #called by installevent end - clears the active slot so a subsequent start_event on this + #installtrack is valid (sequential events on one instance) + method event_ended {eventobj} { + if {$o_active_event eq $eventobj} { + set o_active_event "" + } + } method get_event {} { return $o_active_event } } } - proc start_installer_event {punkcheck_file installername from_fullpath to_fullpath config} { - set eventid [punkcheck::uuid] - if {[file pathtype $from_fullpath] ne "absolute"} { - error "start_installer_event error: from_fullpath must be absolute path. Received '$from_fullpath'" - } - if {[file pathtype $to_fullpath] ne "absolute"} { - error "start_installer_event error: to_fullpath must be absolute path. Received '$to_fullpath'" - } - set punkcheck_folder [file dirname $punkcheck_file] - set rel_source [punkcheck::lib::path_relative $punkcheck_folder $from_fullpath] - set rel_target [punkcheck::lib::path_relative $punkcheck_folder $to_fullpath] - - - set record_list [punkcheck::load_records_from_file $punkcheck_file] - set resultinfo [punkcheck::recordlist::get_installer_record $installername $record_list] - set existing_header_posn [dict get $resultinfo position] - if {$existing_header_posn == -1} { - set this_installer_record [punkcheck::recordlist::new_installer_record $installername] - } else { - set this_installer_record [dict get $resultinfo record] - } - - set event_record [punkcheck::recordlist::new_installer_event_record install -id $eventid -source $rel_source -targets $rel_target -config $config] - - set this_installer_record [punkcheck::recordlist::installer_record_add_event $this_installer_record $event_record] - set this_installer_record [punkcheck::recordlist::installer_record_pruneevents $this_installer_record $record_list] - - if {$existing_header_posn == -1} { - #not found - prepend - #set record_list [linsert $record_list 0 $this_installer_record] - ledit record_list -1 -1 $this_installer_record - } else { - #replace - lset record_list $existing_header_posn $this_installer_record - } - - punkcheck::save_records_to_file $record_list $punkcheck_file "start_installer_event $eventid" - return [list eventid $eventid recordset $record_list] + #----------------------------------------------- + #G-094: the legacy proc pipeline (start_installer_event + installfile_begin/started/finished/skipped) + #is retired. The installtrack/installevent OO layer above is the sole implementation of the record + #lifecycle. These shims error with a pointer so stale callers fail loud and actionable. + proc start_installer_event {args} { + error "punkcheck::start_installer_event retired (G-094). Use: set obj \[punkcheck::installtrack new \]; \$obj set_source_target ; \$obj start_event ?-persistence eager|deferred? - see punkcheck::installfile_help" + } + proc installfile_begin {args} { + error "punkcheck::installfile_begin retired (G-094). Use the installevent method: \$event targetset_init INSTALL - see punkcheck::installfile_help" + } + proc installfile_started_install {args} { + error "punkcheck::installfile_started_install retired (G-094). Use the installevent method: \$event targetset_started - see punkcheck::installfile_help" + } + proc installfile_finished_install {args} { + error "punkcheck::installfile_finished_install retired (G-094). Use the installevent method: \$event targetset_end OK - see punkcheck::installfile_help" + } + proc installfile_skipped_install {args} { + error "punkcheck::installfile_skipped_install retired (G-094). Use the installevent method: \$event targetset_end SKIPPED - see punkcheck::installfile_help" } #----------------------------------------------- proc installfile_help {} { set msg "" - append msg "Call in order:" \n - append msg " start_installer_event (get dict with eventid and recordset keys)" - append msg " installfile_begin (to return a new INSTALLING record) - must pass in a valid eventid" \n - append msg " installfile_add_source_and_fetch_metadata (1+ times to update SOURCE record with checksum/timestamp info from source)" \n - append msg " ( - possibly with same algorithm as previous installrecord)" \n - append msg " ( - todo - search/load metadata for this source from other FILEINFO records for same installer)" \n - append msg "Finalize by calling:" \n - append msg " installfile_started_install" \n - append msg " (install the file e.g file copy)" \n - append msg " installfile_finished_install" \n - append msg " OR" \n - append msg " installfile_skipped_install" \n - } - proc installfile_begin {punkcheck_folder target_relpath installername args} { - if {[llength $args] %2 !=0} { - error "punkcheck installfile_begin args must be name-value pairs" - } - set target_relpath [lsort -dictionary -increasing $target_relpath] ;#exact sort order not critical - but must be consistent - set ts [clock microseconds] - set seconds [expr {$ts / 1000000}] - set tsiso [clock format $seconds -format "%Y-%m-%dT%H:%M:%S"] - set defaults [list {*}{ - } -tsiso $tsiso {*}{ - } -ts $ts {*}{ - } -installer $installername {*}{ - } -eventid unspecified {*}{ - } - ] - set opts [dict merge $defaults $args] - set opt_eventid [dict get $opts -eventid] - - set punkcheck_file [file join $punkcheck_folder/.punkcheck] - set record_list [load_records_from_file $punkcheck_file] - - set resultinfo [punkcheck::recordlist::get_installer_record $installername $record_list] - set installer_record_position [dict get $resultinfo position] - if {$installer_record_position == -1} { - error "installfile_begin error: Failed to retrieve installer record for installer name:'$installername' - ensure start_installer_event has been called with same installer, and -eventid is passed to installfile_begin" - } - set this_installer_record [dict get $resultinfo record] - set events [dict get $this_installer_record body] - set active_event [list] - foreach evt [lreverse $events] { - if {[dict get $evt -id] eq $opt_eventid} { - set active_event $evt - break - } - } - if {![llength $active_event]} { - error "installfile_begin error: eventid $opt_eventid not found for installer '$installername' - aborting" - } - - - set extractioninfo [punkcheck::recordlist::extract_or_create_fileset_record $target_relpath $record_list] - set file_record [dict get $extractioninfo record] - set record_list [dict get $extractioninfo recordset] - set isnew [dict get $extractioninfo isnew] - set oldposition [dict get $extractioninfo oldposition] - unset extractioninfo - - #INSTALL-INPROGRESS will become INSTALL-RECORD or INSTALL-FAILED or INSTALL-SKIPPED upon finalisation - #-installer and -eventid keys are added here - set new_installing_record [dict create tag INSTALL-INPROGRESS {*}$opts -tempcontext $active_event body {}] - #set existing_body [dict_getwithdefault $file_record body [list]] - #todo - look for existing "INSTALL-INPROGRESS" records - mark as failed? - dict lappend file_record body $new_installing_record - - if {$isnew} { - lappend record_list $file_record - } else { - #set record_list [linsert $record_list[unset record_list] $oldposition $file_record] - ledit record_list $oldposition -1 $file_record - } - - save_records_to_file $record_list $punkcheck_file "installfile_begin $installername $opt_eventid $target_relpath" - return $file_record + append msg "punkcheck record lifecycle - installtrack/installevent object API (G-094)" \n + append msg "The legacy proc pipeline (start_installer_event, installfile_begin," \n + append msg "installfile_started_install, installfile_finished_install, installfile_skipped_install)" \n + append msg "is retired - those procs now raise errors pointing here." \n + append msg "" \n + append msg "Call in order:" \n + append msg " set installer \[punkcheck::installtrack new \]" \n + append msg " \$installer set_source_target " \n + append msg " set event \[\$installer start_event ?-persistence eager|deferred?\]" \n + append msg " per targetset:" \n + append msg " \$event targetset_init " \n + append msg " \$event targetset_addsource (1+ times - records source metadata/cksums)" \n + append msg " \$event targetset_addsource_virtual (named-value sources e.g build versions)" \n + append msg " decide skip/proceed via \$event targetset_source_changes and \$event get_targets_exist" \n + append msg " \$event targetset_started (immediately before performing the operation)" \n + append msg " \$event targetset_end OK|SKIPPED|FAILED ?-note ?" \n + append msg " \$event end (or \$installer end_event - allows a subsequent start_event)" \n + append msg "" \n + append msg "Persistence policy (chosen per event at start_event):" \n + append msg " eager (default): each non-QUERY targetset operation is an immediate read-modify-write" \n + append msg " of the .punkcheck file (crash evidence via *-INPROGRESS records)." \n + append msg " deferred: targetset operations update the installtrack's in-memory recordset only;" \n + append msg " call \$installer flush to persist (event-scoped batch save - punkcheck::install uses this)." \n + append msg " An ended deferred event that was never flushed deliberately discards its record updates." \n + append msg " QUERY operations are never persisted in either mode." \n + return $msg } - #todo - ensure that removing a dependency is noticed as a change #e.g previous installrecord had 2 source records - but we now only depend on one. #The files we depended on for the previous record haven't changed themselves - but the list of files has (reduced by one) @@ -1041,7 +1044,7 @@ namespace eval punkcheck { #The -changed flag is computed by comparing -value against the matching virtual SOURCE in the last completed #install record (no filesystem access) - so virtual sources participate in targetset_source_changes like file sources. #Typical use: recording the resolved build version of a module target, so records identify which product of an - #unchanging source fileset (e.g -0.4.0.tm + -buildversion.txt) the target represents. + #unchanging source fileset (e.g -0.5.0.tm + -buildversion.txt) the target represents. proc installsource_add_virtual {file_record id value} { if {![lib::is_file_record_inprogress $file_record]} { error "installsource_add_virtual error: bad file_record - expected FILEINFO with last body element *-INPROGRESS ($file_record)" @@ -1081,114 +1084,6 @@ namespace eval punkcheck { return $file_record } - #write back to punkcheck - don't accept recordset - invalid to update anything other than the installing_record at this time - proc installfile_started_install {punkcheck_folder file_record} { - if {![lib::is_file_record_inprogress $file_record]} { - error "installfile_started_install error: bad file_record - expected FILEINFO with last body element *-INPROGRESS" - } - set punkcheck_file [file join $punkcheck_folder/.punkcheck] - set record_list [load_records_from_file $punkcheck_file] - - set file_record_body [dict get $file_record body] - set targetlist [dict get $file_record -targets] - set installing_record [lindex $file_record_body end] - - set ts_start [dict get $installing_record -ts] - set ts_now [clock microseconds] - set metadata_us [expr {$ts_now - $ts_start}] - - dict set installing_record -metadata_us $metadata_us - dict set installing_record -ts_start_transfer $ts_now - - lset file_record_body end $installing_record - - dict set file_record body $file_record_body - - - set getresult [punkcheck::recordlist::get_file_record $targetlist $record_list] - set old_posn [dict get $getresult position] - if {$old_posn == -1} { - lappend record_list $file_record - } else { - lset record_list $old_posn $file_record - } - - save_records_to_file $record_list $punkcheck_file "installfile_started_install [llength $targetlist] targets" - return $file_record - } - proc installfile_finished_install {punkcheck_folder file_record} { - if {![lib::is_file_record_inprogress $file_record]} { - error "installfile_finished_install error: bad file_record - expected FILEINFO with last body element *-INPROGRESS" - } - set punkcheck_file [file join $punkcheck_folder/.punkcheck] - set record_list [load_records_from_file $punkcheck_file] - - set file_record_body [dict get $file_record body] - set targetlist [dict get $file_record -targets] - set installing_record [lindex $file_record_body end] - - set ts_start [dict get $installing_record -ts] - set ts_start_transfer [dict get $installing_record -ts_start_transfer] - set ts_now [clock microseconds] - set elapsed_us [expr {$ts_now - $ts_start}] - set transfer_us [expr {$ts_now - $ts_start_transfer}] - dict set installing_record -transfer_us $transfer_us - dict set installing_record -elapsed_us $elapsed_us - dict unset installing_record -tempcontext - dict set installing_record tag "INSTALL-RECORD" - - lset file_record_body end $installing_record - dict set file_record body $file_record_body - - set file_record [punkcheck::recordlist::file_record_prune $file_record] - - set oldrecordinfo [punkcheck::recordlist::get_file_record $targetlist $record_list] - set old_posn [dict get $oldrecordinfo position] - if {$old_posn == -1} { - lappend record_list $file_record - } else { - lset record_list $old_posn $file_record - } - - save_records_to_file $record_list $punkcheck_file "installfile_finished_install [llength $targetlist] targets" - return $file_record - } - proc installfile_skipped_install {punkcheck_folder file_record} { - if {![lib::is_file_record_inprogress $file_record]} { - set msg "installfile_skipped_install error: bad file_record - expected FILEINFO with last body element *-INPROGRESS" - append msg \n "received:" - append msg \n $file_record - error $msg - } - set punkcheck_file [file join $punkcheck_folder/.punkcheck] - set record_list [load_records_from_file $punkcheck_file] - - set file_record_body [dict get $file_record body] - set targetlist [dict get $file_record -targets] - set installing_record [lindex $file_record_body end] - - set ts_start [dict get $installing_record -ts] - set tsnow [clock microseconds] - set elapsed_us [expr {$tsnow - $ts_start}] - dict set installing_record -elapsed_us $elapsed_us - dict set installing_record tag "INSTALL-SKIPPED" - - lset file_record_body end $installing_record - dict set file_record body $file_record_body - - set file_record [punkcheck::recordlist::file_record_prune $file_record] - - set getresult [punkcheck::recordlist::get_file_record $targetlist $record_list] - set old_posn [dict get $getresult position] - if {$old_posn == -1} { - lappend record_list $file_record - } else { - lset record_list $old_posn $file_record - } - - save_records_to_file $record_list $punkcheck_file "installfile_skipped_install [llength $targetlist] targets" - return $file_record - } #----------------------------------------------- #then: file_record_add_installrecord @@ -1391,13 +1286,13 @@ namespace eval punkcheck { is generated automatically. Pass a specific ID to group multiple install calls under the same event in the .punkcheck file." -punkcheck-records -default "" -help\ - "Empty string or a parsed TDL records structure. - e.g - {tag FILEINFO - ... body { - {tag INSTALL-RECORD - ... body {}} - ... - }... - }" + "DEPRECATED/ignored (G-094) - retained for call compatibility. + The working recordset lives in a punkcheck::installtrack instance for + the duration of the install walk; the passed value is not consulted + (pre-G-094 behaviour also replaced it from the file at depth 0)." + -punkcheck-eventobj -default "" -help\ + "(internal recursion carrier - the installevent object shared by all + levels of the install walk)" -installer -default "punkcheck::install" -help\ "A user nominated string that is stored in the .punkcheck file This might be the name of a script or installation process." @@ -1481,6 +1376,7 @@ namespace eval punkcheck { -punkcheck-folder target -punkcheck-eventid "\uFFFF" -punkcheck-records "" + -punkcheck-eventobj "" -installer punkcheck::install -progresschannel none }] @@ -1643,54 +1539,52 @@ namespace eval punkcheck { #review - leave empty? use pwd? } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- - set punkcheck_records [dict get $opts -punkcheck-records] - set punkcheck_records_init $punkcheck_records ;#change-detection + #-punkcheck-records is vestigial as of G-094 (the working recordset lives in the installtrack + #instance) - the option remains accepted for call compatibility. Pre-G-094 behaviour also + #discarded the passed value at depth 0 (replaced from the file via start_installer_event). # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_installer [dict get $opts -installer] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_punkcheck_eventid [dict get $opts -punkcheck-eventid] + set opt_punkcheck_eventobj [dict get $opts -punkcheck-eventobj] set punkcheck_file [file join $punkcheck_folder/.punkcheck] if {$CALLDEPTH == 0} { - set punkcheck_eventid "" - if {$punkcheck_folder ne ""} { - set config $opts - dict unset config -call-depth-internal - dict unset config -max-depth - dict unset config -subdirlist - dict unset config -progresschannel - tcl::dict::for {k v} $config { - if {$v eq "\uFFFF"} { - dict unset config $k - } - } - lassign [punkcheck::start_installer_event $punkcheck_file $opt_installer $srcdir $tgtdir $config] _eventid punkcheck_eventid _recordset punkcheck_records - } + if {$punkcheck_folder eq ""} { + #pre-G-094 code silently skipped event setup for an empty -punkcheck-folder and then failed + #during path relativization - fail early and clearly instead. (folder resolution above yields + #"" only when the caller explicitly passes -punkcheck-folder "") + error "punkcheck::install requires a resolvable -punkcheck-folder (target|source|project|) - received an empty value" + } + set config $opts + dict unset config -call-depth-internal + dict unset config -max-depth + dict unset config -subdirlist + dict unset config -progresschannel + dict unset config -punkcheck-eventobj + tcl::dict::for {k v} $config { + if {$v eq "\uFFFF"} { + dict unset config $k + } + } + #G-094: batch install is a tree-walking consumer of the OO layer in deferred persistence mode. + #The installtrack working recordset accumulates finalized records in memory; one flush at the + #end of the walk persists them (event-scoped save), preserving the original batch write pattern. + set punkcheck_installer [punkcheck::installtrack new $opt_installer $punkcheck_file] + $punkcheck_installer set_source_target $srcdir $tgtdir + set punkcheck_eventobj [$punkcheck_installer start_event $config -persistence deferred] + set punkcheck_eventid [$punkcheck_eventobj get_id] } else { + set punkcheck_eventobj $opt_punkcheck_eventobj + set punkcheck_installer [$punkcheck_eventobj get_installer] set punkcheck_eventid $opt_punkcheck_eventid } - if {$opt_source_checksum != 0} { - #we need to read the file even if only set to store (or we would overwrite entries) - set compare_cksums 1 - } else { - set compare_cksums 0 - } - - if {[string match *store* $opt_source_checksum]} { - set store_source_cksums 1 - } else { - set store_source_cksums 0 - } - - - - if {[llength $subdirlist] == 0} { set current_source_dir $srcdir @@ -1717,6 +1611,12 @@ namespace eval punkcheck { #puts "testing folder - globmatchpath $unpub $relative_source_dir" if {[punk::path::globmatchpath $unpub $relative_source_dir]} { lappend antiglob_paths_matched $current_source_dir + set punkcheck_records [$punkcheck_installer get_recordlist] + if {$CALLDEPTH == 0} { + $punkcheck_eventobj end + $punkcheck_eventobj destroy + $punkcheck_installer destroy + } return [list files_copied {} files_skipped {} sources_unchanged {} punkcheck_records $punkcheck_records antiglob_paths_matched $antiglob_paths_matched srcdir $srcdir tgtdir $tgtdir punkcheck_folder $punkcheck_folder] } } @@ -1760,11 +1660,11 @@ namespace eval punkcheck { #FILEINFO -targets jjjetc-0.1.0.tm -keep_installrecords 2 -keep_skipped 1 -keep_inprogress 2 { # INSTALL-RECORD -tsiso 2023-09-20T07:30:30 -ts 1695159030266610 -installer punk::mix::cli::build_modules_from_source_to_base -metadata_us 18426 -ts_start_transfer 1695159030285036 -transfer_us 10194 -elapsed_us 28620 { # SOURCE -type file -path ../src/modules/jjjetc-buildversion.txt -cksum c7c71839c36b3d21c8370fed106192fcd659eca9 -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 1 -metadata_us 3423 - # SOURCE -type file -path ../src/modules/jjjetc-0.4.0.tm -cksum b646fc2ee88cbd068d2e946fe929b7aea96bd39d -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 1 -metadata_us 3413 + # SOURCE -type file -path ../src/modules/jjjetc-0.5.0.tm -cksum b646fc2ee88cbd068d2e946fe929b7aea96bd39d -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 1 -metadata_us 3413 # } # INSTALL-SKIPPED -tsiso 2023-09-20T08:14:26 -ts 1695161666087880 -installer punk::mix::cli::build_modules_from_source_to_base -elapsed_us 18914 { # SOURCE -type file -path ../src/modules/jjjetc-buildversion.txt -cksum c7c71839c36b3d21c8370fed106192fcd659eca9 -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 0 -metadata_us 3435 - # SOURCE -type file -path ../src/modules/jjjetc-0.4.0.tm -cksum b646fc2ee88cbd068d2e946fe929b7aea96bd39d -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 0 -metadata_us 3338 + # SOURCE -type file -path ../src/modules/jjjetc-0.5.0.tm -cksum b646fc2ee88cbd068d2e946fe929b7aea96bd39d -cksum_all_opts {-cksum_content 1 -cksum_meta 0 -cksum_acls 0 -cksum_usetar 0 -cksum_algorithm sha1} -changed 0 -metadata_us 3338 # } #} @@ -1781,7 +1681,6 @@ namespace eval punkcheck { #puts stdout "Current target dir: $current_target_dir" set last_depth "" foreach m $match_list { - set new_tgt_cksum_info [list] set relative_target_path [file join $relative_target_dir $m] set relative_source_path [file join $relative_source_dir $m] set punkcheck_target_relpath [file join $target_relative_to_punkcheck_dir $m] @@ -1798,35 +1697,18 @@ namespace eval punkcheck { continue } - set ts_start [clock microseconds] - set seconds [expr {$ts_start / 1000000}] - set ts_start_iso [clock format $seconds -format "%Y-%m-%dT%H:%M:%S"] - - #puts stdout " rel_target: $punkcheck_target_relpath" - - set fetch_filerec_result [punkcheck::recordlist::get_file_record $punkcheck_target_relpath $punkcheck_records] - #change to use extract_or_create_fileset_record ? - set existing_filerec_posn [dict get $fetch_filerec_result position] - if {$existing_filerec_posn == -1} { - if {$opt_progresschannel ne ""} { + if {$opt_progresschannel ne ""} { + #pre-G-094 output parity: announce first-seen targets when progress output is active + set fetch_filerec_result [punkcheck::recordlist::get_file_record $punkcheck_target_relpath [$punkcheck_installer get_recordlist]] + if {[dict get $fetch_filerec_result position] == -1} { puts stdout "\nNO existing record for $punkcheck_target_relpath" } - set has_filerec 0 - set new_filerec [dict create tag FILEINFO -targets $punkcheck_target_relpath] - set filerec $new_filerec - } else { - set has_filerec 1 - #puts stdout " TDL existing FILEINFO record for $punkcheck_target_relpath" - #puts stdout " $existing_install_record" - set filerec [dict get $fetch_filerec_result record] } - set filerec [punkcheck::recordlist::file_record_set_defaults $filerec] - #new INSTALLREC must be tagged as INSTALL-INPROGRESS to use recordlist::installfile_ method - set new_install_record [dict create tag INSTALL-INPROGRESS -tsiso $ts_start_iso -ts $ts_start -installer $opt_installer -eventid $punkcheck_eventid] - dict lappend filerec body $new_install_record ;#can't use recordlist::file_record_add_installrecord as '*-INPROGRESS' isn't a final tag - so pruning would be mucked up. No need to prune now anyway. - unset new_install_record + #G-094: per-target record lifecycle through the OO layer (deferred persistence - no per-file saves, + #finalized records accumulate in the installtrack working recordset until the end-of-walk flush) + $punkcheck_eventobj targetset_init INSTALL $punkcheck_target_relpath if {$opt_progresschannel ne ""} { @@ -1847,11 +1729,10 @@ namespace eval punkcheck { #REVIEW #different volume or root #} - #Note this isn't a recordlist function - so it doesn't purely operate on the records - #this hits the filesystem for the sourcepath - gets checksums/timestamps depending on config. - #It doesn't save to .punkcheck (the only punkcheck::installfile_ method which doesn't) + #targetset_addsource hits the filesystem for the sourcepath - gets checksums/timestamps + #depending on config - and returns the updated fileset record (no save in deferred mode) set ts1 [clock milliseconds] - set filerec [punkcheck::installfile_add_source_and_fetch_metadata $punkcheck_folder $relative_source_path $filerec] + set filerec [$punkcheck_eventobj targetset_addsource $relative_source_path] set ts2 [clock milliseconds] set diff [expr {$ts2 - $ts1}] if {$diff > 100} { @@ -1874,7 +1755,7 @@ namespace eval punkcheck { #changeinfo comes from last record in body - which is the record we are working on and so will always exist - set changeinfo [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $filerec body] end]] + set changeinfo [$punkcheck_eventobj targetset_source_changes] set changed [dict get $changeinfo changed] set unchanged [dict get $changeinfo unchanged] @@ -1885,6 +1766,7 @@ namespace eval punkcheck { set is_skip 0 set is_new 0 if {$overwrite_what eq "all-targets"} { + $punkcheck_eventobj targetset_started file mkdir $current_target_dir #-------------------------------------------- #sometimes we get the error: 'error copying "file1" to "file2": invalid argument' @@ -1895,27 +1777,24 @@ namespace eval punkcheck { } else { if {![file exists $current_target_dir/$m]} { #puts stderr "punkcheck: first copy to $current_target_dir/$m " + $punkcheck_eventobj targetset_started file mkdir $current_target_dir puts stderr "punkcheck: about to: file copy $current_source_dir/$m $current_target_dir" file copy $current_source_dir/$m $current_target_dir - set new_tgt_cksum_info [punk::mix::base::lib::cksum_path $current_target_dir/$m] lappend files_copied $current_source_dir/$m - incr filecount_new set is_new 1 } else { switch -- $overwrite_what { installedsourcechanged-targets { if {[llength $changed]} { #An unrecorded installation is considered a source change (from unknown/unrecorded source to recorded) + $punkcheck_eventobj targetset_started puts -nonewline stderr "punkcheck: about to: file copy -force $current_source_dir/$m $current_target_dir" set ts1 [clock milliseconds] file mkdir $current_target_dir file copy -force $current_source_dir/$m $current_target_dir set ts2 [clock milliseconds] - puts -nonewline stderr " (copy time [expr {$ts2 - $ts1}] ms)" - set new_tgt_cksum_info [punk::mix::base::lib::cksum_path $current_target_dir/$m] - set ts3 [clock milliseconds] - puts stderr " (cksum time [expr {$ts2 - $ts1}] ms)" + puts stderr " (copy time [expr {$ts2 - $ts1}] ms)" lappend files_copied $current_source_dir/$m } else { set is_skip 1 @@ -1943,10 +1822,10 @@ namespace eval punkcheck { set target_cksum_compare "norecord" } if {$is_target_unmodified_since_install} { + $punkcheck_eventobj targetset_started file mkdir $current_target_dir puts stderr "punkcheck: synced-targets about to: file copy -force $current_source_dir/$m $current_target_dir" file copy -force $current_source_dir/$m $current_target_dir - set new_tgt_cksum_info [punk::mix::base::lib::cksum_path $current_target_dir/$m] lappend files_copied $current_source_dir/$m } else { #either cksum is different or we were unable to verify the record. Either way we can't know if the target is in sync so we must skip it @@ -1977,34 +1856,13 @@ namespace eval punkcheck { - set ts_now [clock microseconds] - set elapsed_us [expr {$ts_now - $ts_start}] - - #if {$store_source_cksums} { - #} - - set install_records [dict get $filerec body] - set current_install_record [lindex $install_records end] - #change the tag from *-INPROGRESS to INSTALL-RECORD/SKIPPED + #finalize the record: targetset_end sets the tag, -elapsed_us, and computes/stores post-install + #target cksums (-targets_cksums/-cksum_all_opts/-cksum_us) for successful installs, then merges + #the finalized record into the installtrack working recordset (in-memory - deferred mode) if {$is_skip} { - set tag INSTALL-SKIPPED + $punkcheck_eventobj targetset_end SKIPPED } else { - set tag INSTALL-RECORD - } - dict set current_install_record tag $tag - dict set current_install_record -elapsed_us $elapsed_us - if {[llength $new_tgt_cksum_info]} { - dict set current_install_record -targets_cksums [list [dict get $new_tgt_cksum_info cksum]] - dict set current_install_record -cksum_all_opts [dict get $new_tgt_cksum_info opts] - } - lset install_records end $current_install_record - dict set filerec body $install_records - set filerec [punkcheck::recordlist::file_record_prune $filerec] ;#prune now that tag is finalized - if {!$has_filerec} { - #not found in original recordlist - append - lappend punkcheck_records $filerec - } else { - lset punkcheck_records $existing_filerec_posn $filerec + $punkcheck_eventobj targetset_end OK } @@ -2075,28 +1933,12 @@ namespace eval punkcheck { #} - set sub_opts_1 [list {*}{ - } -call-depth-internal [expr {$CALLDEPTH + 1}] {*}{ - } -subdirlist [list {*}$subdirlist $d] {*}{ - } -glob $fileglob {*}{ - } -exclude-filetails-core $opt_excludefiletail_core {*}{ - } -exclude-filetails $opt_excludefiletail {*}{ - } -exclude-paths-core $opt_excludepaths_core {*}{ - } -exclude-paths $opt_excludepaths {*}{ - } -overwrite $overwrite_what {*}{ - } -source-checksum $opt_source_checksum {*}{ - } -punkcheck-folder $punkcheck_folder {*}{ - } -punkcheck-eventid $punkcheck_eventid {*}{ - } -punkcheck-records $punkcheck_records {*}{ - } -installer $opt_installer {*}{ - } - ] set sub_opts [list {*}{ } -call-depth-internal [expr {$CALLDEPTH + 1}] {*}{ } -subdirlist [list {*}$subdirlist $d] {*}{ } -punkcheck-folder $punkcheck_folder {*}{ } -punkcheck-eventid $punkcheck_eventid {*}{ - } -punkcheck-records $punkcheck_records {*}{ + } -punkcheck-eventobj $punkcheck_eventobj {*}{ } -progresschannel $opt_progresschannel {*}{ } ] @@ -2107,25 +1949,24 @@ namespace eval punkcheck { lappend files_skipped {*}[dict get $sub_result files_skipped] lappend sources_unchanged {*}[dict get $sub_result sources_unchanged] lappend antiglob_paths_matched {*}[dict get $sub_result antiglob_paths_matched] - set punkcheck_records [dict get $sub_result punkcheck_records] + #the evolving recordset is shared state in the installtrack instance - nothing to carry back } - if {[string match *store* $opt_source_checksum]} { - #puts "subdirlist: $subdirlist" - if {$CALLDEPTH == 0} { - if {[llength $files_copied] || [llength $files_skipped]} { - #puts stdout ">>>>>>>>>>>>>>>>>>>" - set saveresult [punkcheck::save_records_to_file $punkcheck_records $punkcheck_file "install $srcdir to $tgtdir"] - puts stdout "\npunkcheck::install [dict get $saveresult recordcount] records saved as [dict get $saveresult linecount] lines to $punkcheck_file copied: [llength $files_copied] skipped: [llength $files_skipped]" - #puts stdout ">>>>>>>>>>>>>>>>>>>" - } else { - #todo - write db INSTALLER record if -debug true - - } - #puts stdout "sources_unchanged" - #puts stdout "$sources_unchanged" - #puts stdout "- -- --- --- --- ---" - } + if {$CALLDEPTH == 0} { + #event-scoped deferred flush: one save persists the whole walk's finalized records. + #compare-only runs (-source-checksum compare) never flush - the in-memory updates are + #deliberately discarded, matching the pre-G-094 batch behaviour (the start_event save + #already registered the event itself in both cases). + if {[string match *store* $opt_source_checksum] && ([llength $files_copied] || [llength $files_skipped])} { + set saveresult [$punkcheck_installer flush "install $srcdir to $tgtdir"] + puts stdout "\npunkcheck::install [dict get $saveresult recordcount] records saved as [dict get $saveresult linecount] lines to $punkcheck_file copied: [llength $files_copied] skipped: [llength $files_skipped]" + } + set punkcheck_records [$punkcheck_installer get_recordlist] + $punkcheck_eventobj end + $punkcheck_eventobj destroy + $punkcheck_installer destroy + } else { + set punkcheck_records [$punkcheck_installer get_recordlist] } return [list files_copied $files_copied files_skipped $files_skipped sources_unchanged $sources_unchanged antiglob_paths_matched $antiglob_paths_matched punkcheck_records $punkcheck_records punkcheck_folder $punkcheck_folder srcdir $srcdir tgtdir $tgtdir] @@ -2486,12 +2327,12 @@ namespace eval punkcheck { } #extract new or existing filerecord for path given - #REVIEW - locking/concurrency + #locking/concurrency: G-095 (this is a pure record transform - no file access; the isnew key + #tells the caller whether a record existed, callers own any reporting) proc extract_or_create_fileset_record {relative_target_paths recordset} { set fetch_record_result [punkcheck::recordlist::get_file_record $relative_target_paths $recordset] set existing_posn [dict get $fetch_record_result position] if {$existing_posn == -1} { - puts stdout "punkcheck NO existing record for $relative_target_paths" set isnew 1 set fileset_record [dict create tag FILEINFO -targets $relative_target_paths body {}] } else { @@ -2528,6 +2369,6 @@ namespace eval ::punk::args::register { package provide punkcheck [namespace eval punkcheck { set pkg punkcheck variable version - set version 0.4.0 + set version 0.5.0 }] return