diff --git a/src/bootsupport/modules/punk/mix/templates-0.2.0.tm b/src/bootsupport/modules/punk/mix/templates-0.2.0.tm index cdac3694..839bb06d 100644 Binary files a/src/bootsupport/modules/punk/mix/templates-0.2.0.tm and b/src/bootsupport/modules/punk/mix/templates-0.2.0.tm differ diff --git a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl index df240826..894dcba5 100644 --- a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl +++ b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl @@ -3287,6 +3287,60 @@ proc ::punkboot::get_src_provenance_warnings {projectroot label} { set checked_vcs_roots [dict create] return [list 1 [::punkboot::utils::vcs_dirty_warnings $projectroot checked_vcs_roots $label src]] } +#Replace a file that may be MEMORY-MAPPED by another process (or by this one). +# +#On windows an ordinary 'file copy -force' onto a mapped file is refused: a zipfs mount +#maps its archive (tclZipfs.c CreateFileMappingW + MapViewOfFile), so a zip-based .tm +#modpod that any running shell - including this build - has mounted cannot be overwritten +#in place. Windows reports ERROR_USER_MAPPED_FILE (1224); Tcl's Tcl_WinConvertError maps +#only Win32 codes 0..267 and sends everything above them to EINVAL, so it surfaces as the +#uninformative "invalid argument" (same code in 8.6 and 9.x). +# +#UNLINKING a mapped file IS allowed - the holder keeps reading its own mapping and new +#content lands at the name - so delete-then-place succeeds where overwrite cannot. The new +#content goes to a sibling temp FIRST so a mid-sequence failure can never leave the target +#missing. +# +#Returns one of: ok (plain copy worked), replaced (needed the delete-then-place path), +#failed (target untouched), broken (target was unlinked and could not be restored - the +#caller must treat this as serious). The first copy's error message is written to the +#variable named by firsterrvar. +proc ::punkboot::replace_possibly_mapped_file {srcfile tgtfile firsterrvar} { + upvar 1 $firsterrvar firsterr + set firsterr "" + if {![catch {file copy -force $srcfile $tgtfile} firsterr]} { + return ok + } + if {![file exists $tgtfile]} { + #nothing to unlink - whatever failed, it was not an in-place overwrite + return failed + } + set tmpfile $tgtfile.punkboot-new + catch {file delete -force -- $tmpfile} + if {[catch {file copy -- $srcfile $tmpfile}]} { + catch {file delete -force -- $tmpfile} + return failed + } + if {[catch {file delete -- $tgtfile}]} { + catch {file delete -force -- $tmpfile} + return failed + } + if {[catch {file rename -- $tmpfile $tgtfile}]} { + return broken + } + return replaced +} +#Actionable text for a file-operation failure whose message is windows' catch-all. +#Returns "" when the error does not look like that case. +proc ::punkboot::mapped_file_hint {errmsg tgtfile} { + if {![string match -nocase "*invalid argument*" $errmsg]} { + return "" + } + return "'invalid argument' is Tcl's catch-all for any windows error above 267 (Tcl_WinConvertError), not a bad argument.\ + For a file replace the usual cause is ERROR_USER_MAPPED_FILE: '$tgtfile' is memory-mapped by a process that has it mounted -\ + a zipfs/modpod mount does this, so a running punk shell (or this build) holding that .tm blocks an in-place overwrite.\ + Close shells holding it, or rerun - the update now replaces such files by delete-then-place rather than overwrite." +} #Availability probe + boot-precondition check of an assembled kit vfs (G-125), shared by the #kit gate in the bake loop and the 'check' command report. Same guarded-require treatment as #the provenance check above: a stale or missing punkboot::utils snapshot must not brick the @@ -4370,31 +4424,47 @@ if {$::punkboot::command eq "bootsupport"} { $boot_event targetset_started # -- --- --- --- --- --- puts "\nBOOTSUPPORT module$which update: $srcfile -> $tgtfile" - if {[catch { - file copy -force $srcfile $tgtfile - } errM]} { - #a copy onto a modpod this make.tcl process has mounted (e.g punk::mix::templates) - #fails on windows even when the target content is already current (a prior failed - #record forces a retry). If content is already identical, record OK so the record - #converges instead of churning FAILED forever. - set already_current 0 - if {[file exists $tgtfile] && [file size $tgtfile] == [file size $srcfile]} { - catch { - set src_ck [dict get [punk::mix::base::lib::cksum_path $srcfile] cksum] - set tgt_ck [dict get [punk::mix::base::lib::cksum_path $tgtfile] cksum] - if {$src_ck ne "" && $src_ck eq $tgt_ck} { - set already_current 1 - } - } + #An in-place overwrite is refused when the target is memory-mapped - + #which is what a mounted zip-based .tm modpod (e.g punk::mix::templates) + #is. replace_possibly_mapped_file falls back to delete-then-place, which + #windows does allow. See its comment for why the error reads "invalid + #argument" and why unlinking is safe for the process holding the mapping. + switch -- [::punkboot::replace_possibly_mapped_file $srcfile $tgtfile errM] { + ok { + $boot_event targetset_end OK + } + replaced { + puts stdout " (in-place overwrite refused - replaced by delete-then-place; target was memory-mapped, e.g a mounted modpod)" + $boot_event targetset_end OK -note "overwrite refused ($errM) - replaced via delete-then-place (target memory-mapped)" } - if {$already_current} { - $boot_event targetset_end OK -note "copy failed ($errM) but target content already identical (self-mounted modpod?)" - } else { - puts stderr "BOOTSUPPORT module$which update FAILED: $tgtfile ($errM)" + broken { + puts stderr "BOOTSUPPORT module$which update FAILED: $tgtfile was unlinked and could not be replaced - restore it from $srcfile by hand ($errM)" $boot_event targetset_end FAILED } - } else { - $boot_event targetset_end OK + default { + #target untouched. If its content is already identical, record OK so a + #prior failed record converges instead of churning FAILED forever. + set already_current 0 + if {[file exists $tgtfile] && [file size $tgtfile] == [file size $srcfile]} { + catch { + set src_ck [dict get [punk::mix::base::lib::cksum_path $srcfile] cksum] + set tgt_ck [dict get [punk::mix::base::lib::cksum_path $tgtfile] cksum] + if {$src_ck ne "" && $src_ck eq $tgt_ck} { + set already_current 1 + } + } + } + if {$already_current} { + $boot_event targetset_end OK -note "copy failed ($errM) but target content already identical" + } else { + puts stderr "BOOTSUPPORT module$which update FAILED: $tgtfile ($errM)" + set _hint [::punkboot::mapped_file_hint $errM $tgtfile] + if {$_hint ne ""} { + puts stderr " $_hint" + } + $boot_event targetset_end FAILED + } + } } # -- --- --- --- --- --- } else { diff --git a/src/project_layouts/vendor/punk/basic/src/make.tcl b/src/project_layouts/vendor/punk/basic/src/make.tcl index df240826..894dcba5 100644 --- a/src/project_layouts/vendor/punk/basic/src/make.tcl +++ b/src/project_layouts/vendor/punk/basic/src/make.tcl @@ -3287,6 +3287,60 @@ proc ::punkboot::get_src_provenance_warnings {projectroot label} { set checked_vcs_roots [dict create] return [list 1 [::punkboot::utils::vcs_dirty_warnings $projectroot checked_vcs_roots $label src]] } +#Replace a file that may be MEMORY-MAPPED by another process (or by this one). +# +#On windows an ordinary 'file copy -force' onto a mapped file is refused: a zipfs mount +#maps its archive (tclZipfs.c CreateFileMappingW + MapViewOfFile), so a zip-based .tm +#modpod that any running shell - including this build - has mounted cannot be overwritten +#in place. Windows reports ERROR_USER_MAPPED_FILE (1224); Tcl's Tcl_WinConvertError maps +#only Win32 codes 0..267 and sends everything above them to EINVAL, so it surfaces as the +#uninformative "invalid argument" (same code in 8.6 and 9.x). +# +#UNLINKING a mapped file IS allowed - the holder keeps reading its own mapping and new +#content lands at the name - so delete-then-place succeeds where overwrite cannot. The new +#content goes to a sibling temp FIRST so a mid-sequence failure can never leave the target +#missing. +# +#Returns one of: ok (plain copy worked), replaced (needed the delete-then-place path), +#failed (target untouched), broken (target was unlinked and could not be restored - the +#caller must treat this as serious). The first copy's error message is written to the +#variable named by firsterrvar. +proc ::punkboot::replace_possibly_mapped_file {srcfile tgtfile firsterrvar} { + upvar 1 $firsterrvar firsterr + set firsterr "" + if {![catch {file copy -force $srcfile $tgtfile} firsterr]} { + return ok + } + if {![file exists $tgtfile]} { + #nothing to unlink - whatever failed, it was not an in-place overwrite + return failed + } + set tmpfile $tgtfile.punkboot-new + catch {file delete -force -- $tmpfile} + if {[catch {file copy -- $srcfile $tmpfile}]} { + catch {file delete -force -- $tmpfile} + return failed + } + if {[catch {file delete -- $tgtfile}]} { + catch {file delete -force -- $tmpfile} + return failed + } + if {[catch {file rename -- $tmpfile $tgtfile}]} { + return broken + } + return replaced +} +#Actionable text for a file-operation failure whose message is windows' catch-all. +#Returns "" when the error does not look like that case. +proc ::punkboot::mapped_file_hint {errmsg tgtfile} { + if {![string match -nocase "*invalid argument*" $errmsg]} { + return "" + } + return "'invalid argument' is Tcl's catch-all for any windows error above 267 (Tcl_WinConvertError), not a bad argument.\ + For a file replace the usual cause is ERROR_USER_MAPPED_FILE: '$tgtfile' is memory-mapped by a process that has it mounted -\ + a zipfs/modpod mount does this, so a running punk shell (or this build) holding that .tm blocks an in-place overwrite.\ + Close shells holding it, or rerun - the update now replaces such files by delete-then-place rather than overwrite." +} #Availability probe + boot-precondition check of an assembled kit vfs (G-125), shared by the #kit gate in the bake loop and the 'check' command report. Same guarded-require treatment as #the provenance check above: a stale or missing punkboot::utils snapshot must not brick the @@ -4370,31 +4424,47 @@ if {$::punkboot::command eq "bootsupport"} { $boot_event targetset_started # -- --- --- --- --- --- puts "\nBOOTSUPPORT module$which update: $srcfile -> $tgtfile" - if {[catch { - file copy -force $srcfile $tgtfile - } errM]} { - #a copy onto a modpod this make.tcl process has mounted (e.g punk::mix::templates) - #fails on windows even when the target content is already current (a prior failed - #record forces a retry). If content is already identical, record OK so the record - #converges instead of churning FAILED forever. - set already_current 0 - if {[file exists $tgtfile] && [file size $tgtfile] == [file size $srcfile]} { - catch { - set src_ck [dict get [punk::mix::base::lib::cksum_path $srcfile] cksum] - set tgt_ck [dict get [punk::mix::base::lib::cksum_path $tgtfile] cksum] - if {$src_ck ne "" && $src_ck eq $tgt_ck} { - set already_current 1 - } - } + #An in-place overwrite is refused when the target is memory-mapped - + #which is what a mounted zip-based .tm modpod (e.g punk::mix::templates) + #is. replace_possibly_mapped_file falls back to delete-then-place, which + #windows does allow. See its comment for why the error reads "invalid + #argument" and why unlinking is safe for the process holding the mapping. + switch -- [::punkboot::replace_possibly_mapped_file $srcfile $tgtfile errM] { + ok { + $boot_event targetset_end OK + } + replaced { + puts stdout " (in-place overwrite refused - replaced by delete-then-place; target was memory-mapped, e.g a mounted modpod)" + $boot_event targetset_end OK -note "overwrite refused ($errM) - replaced via delete-then-place (target memory-mapped)" } - if {$already_current} { - $boot_event targetset_end OK -note "copy failed ($errM) but target content already identical (self-mounted modpod?)" - } else { - puts stderr "BOOTSUPPORT module$which update FAILED: $tgtfile ($errM)" + broken { + puts stderr "BOOTSUPPORT module$which update FAILED: $tgtfile was unlinked and could not be replaced - restore it from $srcfile by hand ($errM)" $boot_event targetset_end FAILED } - } else { - $boot_event targetset_end OK + default { + #target untouched. If its content is already identical, record OK so a + #prior failed record converges instead of churning FAILED forever. + set already_current 0 + if {[file exists $tgtfile] && [file size $tgtfile] == [file size $srcfile]} { + catch { + set src_ck [dict get [punk::mix::base::lib::cksum_path $srcfile] cksum] + set tgt_ck [dict get [punk::mix::base::lib::cksum_path $tgtfile] cksum] + if {$src_ck ne "" && $src_ck eq $tgt_ck} { + set already_current 1 + } + } + } + if {$already_current} { + $boot_event targetset_end OK -note "copy failed ($errM) but target content already identical" + } else { + puts stderr "BOOTSUPPORT module$which update FAILED: $tgtfile ($errM)" + set _hint [::punkboot::mapped_file_hint $errM $tgtfile] + if {$_hint ne ""} { + puts stderr " $_hint" + } + $boot_event targetset_end FAILED + } + } } # -- --- --- --- --- --- } else { diff --git a/src/project_layouts/vendor/punk/project-0.1/src/make.tcl b/src/project_layouts/vendor/punk/project-0.1/src/make.tcl index df240826..894dcba5 100644 --- a/src/project_layouts/vendor/punk/project-0.1/src/make.tcl +++ b/src/project_layouts/vendor/punk/project-0.1/src/make.tcl @@ -3287,6 +3287,60 @@ proc ::punkboot::get_src_provenance_warnings {projectroot label} { set checked_vcs_roots [dict create] return [list 1 [::punkboot::utils::vcs_dirty_warnings $projectroot checked_vcs_roots $label src]] } +#Replace a file that may be MEMORY-MAPPED by another process (or by this one). +# +#On windows an ordinary 'file copy -force' onto a mapped file is refused: a zipfs mount +#maps its archive (tclZipfs.c CreateFileMappingW + MapViewOfFile), so a zip-based .tm +#modpod that any running shell - including this build - has mounted cannot be overwritten +#in place. Windows reports ERROR_USER_MAPPED_FILE (1224); Tcl's Tcl_WinConvertError maps +#only Win32 codes 0..267 and sends everything above them to EINVAL, so it surfaces as the +#uninformative "invalid argument" (same code in 8.6 and 9.x). +# +#UNLINKING a mapped file IS allowed - the holder keeps reading its own mapping and new +#content lands at the name - so delete-then-place succeeds where overwrite cannot. The new +#content goes to a sibling temp FIRST so a mid-sequence failure can never leave the target +#missing. +# +#Returns one of: ok (plain copy worked), replaced (needed the delete-then-place path), +#failed (target untouched), broken (target was unlinked and could not be restored - the +#caller must treat this as serious). The first copy's error message is written to the +#variable named by firsterrvar. +proc ::punkboot::replace_possibly_mapped_file {srcfile tgtfile firsterrvar} { + upvar 1 $firsterrvar firsterr + set firsterr "" + if {![catch {file copy -force $srcfile $tgtfile} firsterr]} { + return ok + } + if {![file exists $tgtfile]} { + #nothing to unlink - whatever failed, it was not an in-place overwrite + return failed + } + set tmpfile $tgtfile.punkboot-new + catch {file delete -force -- $tmpfile} + if {[catch {file copy -- $srcfile $tmpfile}]} { + catch {file delete -force -- $tmpfile} + return failed + } + if {[catch {file delete -- $tgtfile}]} { + catch {file delete -force -- $tmpfile} + return failed + } + if {[catch {file rename -- $tmpfile $tgtfile}]} { + return broken + } + return replaced +} +#Actionable text for a file-operation failure whose message is windows' catch-all. +#Returns "" when the error does not look like that case. +proc ::punkboot::mapped_file_hint {errmsg tgtfile} { + if {![string match -nocase "*invalid argument*" $errmsg]} { + return "" + } + return "'invalid argument' is Tcl's catch-all for any windows error above 267 (Tcl_WinConvertError), not a bad argument.\ + For a file replace the usual cause is ERROR_USER_MAPPED_FILE: '$tgtfile' is memory-mapped by a process that has it mounted -\ + a zipfs/modpod mount does this, so a running punk shell (or this build) holding that .tm blocks an in-place overwrite.\ + Close shells holding it, or rerun - the update now replaces such files by delete-then-place rather than overwrite." +} #Availability probe + boot-precondition check of an assembled kit vfs (G-125), shared by the #kit gate in the bake loop and the 'check' command report. Same guarded-require treatment as #the provenance check above: a stale or missing punkboot::utils snapshot must not brick the @@ -4370,31 +4424,47 @@ if {$::punkboot::command eq "bootsupport"} { $boot_event targetset_started # -- --- --- --- --- --- puts "\nBOOTSUPPORT module$which update: $srcfile -> $tgtfile" - if {[catch { - file copy -force $srcfile $tgtfile - } errM]} { - #a copy onto a modpod this make.tcl process has mounted (e.g punk::mix::templates) - #fails on windows even when the target content is already current (a prior failed - #record forces a retry). If content is already identical, record OK so the record - #converges instead of churning FAILED forever. - set already_current 0 - if {[file exists $tgtfile] && [file size $tgtfile] == [file size $srcfile]} { - catch { - set src_ck [dict get [punk::mix::base::lib::cksum_path $srcfile] cksum] - set tgt_ck [dict get [punk::mix::base::lib::cksum_path $tgtfile] cksum] - if {$src_ck ne "" && $src_ck eq $tgt_ck} { - set already_current 1 - } - } + #An in-place overwrite is refused when the target is memory-mapped - + #which is what a mounted zip-based .tm modpod (e.g punk::mix::templates) + #is. replace_possibly_mapped_file falls back to delete-then-place, which + #windows does allow. See its comment for why the error reads "invalid + #argument" and why unlinking is safe for the process holding the mapping. + switch -- [::punkboot::replace_possibly_mapped_file $srcfile $tgtfile errM] { + ok { + $boot_event targetset_end OK + } + replaced { + puts stdout " (in-place overwrite refused - replaced by delete-then-place; target was memory-mapped, e.g a mounted modpod)" + $boot_event targetset_end OK -note "overwrite refused ($errM) - replaced via delete-then-place (target memory-mapped)" } - if {$already_current} { - $boot_event targetset_end OK -note "copy failed ($errM) but target content already identical (self-mounted modpod?)" - } else { - puts stderr "BOOTSUPPORT module$which update FAILED: $tgtfile ($errM)" + broken { + puts stderr "BOOTSUPPORT module$which update FAILED: $tgtfile was unlinked and could not be replaced - restore it from $srcfile by hand ($errM)" $boot_event targetset_end FAILED } - } else { - $boot_event targetset_end OK + default { + #target untouched. If its content is already identical, record OK so a + #prior failed record converges instead of churning FAILED forever. + set already_current 0 + if {[file exists $tgtfile] && [file size $tgtfile] == [file size $srcfile]} { + catch { + set src_ck [dict get [punk::mix::base::lib::cksum_path $srcfile] cksum] + set tgt_ck [dict get [punk::mix::base::lib::cksum_path $tgtfile] cksum] + if {$src_ck ne "" && $src_ck eq $tgt_ck} { + set already_current 1 + } + } + } + if {$already_current} { + $boot_event targetset_end OK -note "copy failed ($errM) but target content already identical" + } else { + puts stderr "BOOTSUPPORT module$which update FAILED: $tgtfile ($errM)" + set _hint [::punkboot::mapped_file_hint $errM $tgtfile] + if {$_hint ne ""} { + puts stderr " $_hint" + } + $boot_event targetset_end FAILED + } + } } # -- --- --- --- --- --- } else { diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm index cdac3694..839bb06d 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm and b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm differ