From 1370484f5ef4544da471af6d7fdb36ca0f829459 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Tue, 14 Jul 2026 04:16:52 +1000 Subject: [PATCH] make.tcl: self-build guard for kit builds under a built punk exe; quieten libunknown double-init Self-build guard (user-identified gap): when make.tcl is driven by a built punk executable, the kit whose deployed executable is the running interpreter is now skipped before any build work, with guidance to rerun under tclsh or a different kit. Previously the pre-deploy process sweep matched processes by executable name only and would taskkill the build's own process mid-run with -k (suicide: remaining kits unbuilt, no recap, dangling punkcheck event); without -k only the polite kill's refusal on console processes saved the run, and the deploy delete of the running image would fail with a cryptic message. The sweep additionally excludes the build's own pid unconditionally, covering a same-named copy driving the build from a non-deploy path. Informational and update subcommands were already safe (they exit before the kit machinery); building other kits from a punk exe remains supported. Verified live: 'punk91 src/make.tcl vfs -confirm 0' skips punk91.exe with the warning and processes the other kits normally; plain tclsh runs unaffected. libunknown quietening: 'punkexe src/make.tcl ' emitted "punk::libunknown::init already done - unnecessary call? info frame -1: ..." because punk_main.tcl had already initialised libunknown (renaming ::package) and make.tcl unconditionally re-sourced the bootsupport copy and called init again, tripping init's load-bearing rename guard. make.tcl now leaves an already-active libunknown in place (no re-source - also avoids silently swapping the running copy for the bootsupport copy when versions diverge); plain tclsh runs source+init exactly as before. Verified: no diagnostic from 'punk91 src/make.tcl projectversion'. src/AGENTS.md build guidance updated (punk-exe-driven builds and the self-kit skip); layout make.tcl copies re-synced via the punkcheck channel. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 4 ++ punkproject.toml | 2 +- src/AGENTS.md | 2 +- src/make.tcl | 44 +++++++++++++++++-- .../custom/_project/punk.basic/src/make.tcl | 44 +++++++++++++++++-- .../_project/punk.project-0.1/src/make.tcl | 44 +++++++++++++++++-- .../_project/punk.shell-0.1/src/make.tcl | 44 +++++++++++++++++-- 7 files changed, 170 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c39f25e..67b57713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.12.26] - 2026-07-14 + +- make.tcl under a built punk executable: kit builds gain a self-build guard - the kit whose deployed executable is running the build is skipped with a clear warning (previously the pre-deploy process sweep would taskkill the build's own process mid-run with -k, since it matched processes by executable name only; without -k the polite kill's failure was the only thing saving the run). The sweep now also excludes the build's own pid unconditionally (covers a same-named copy driving the build from a non-deploy path). Informational/update subcommands were already safe (they exit before the kit machinery); building *other* kits from a punk exe remains supported. Also quietened the 'punk::libunknown::init already done - unnecessary call?' stderr diagnostic when a punk exe runs make.tcl: make.tcl now leaves an already-active libunknown in place instead of re-sourcing the bootsupport copy over it and tripping init's rename guard. src/AGENTS.md build guidance updated. + ## [0.12.25] - 2026-07-14 - G-030 follow-up (user review): make.tcl's constructed punk::args definitions adopt the G-045 authoring mechanisms - @cmd -help bodies converted to block-form values with a bare @normalize per definition, fixing the constructed-definition indent leak (first Description line rendered less indented than continuations; the braced option fragments never leaked because literal block-shaped parts get file-style normalization). Added a define-time capability probe (get_spec on representative ids) so a stale punk::args that accepts the text but cannot resolve a mechanism degrades to the plain fallback immediately. AGENTS infrastructure for future documentation work: new "punk::args definition authoring ergonomics" subsection in src/modules/AGENTS.md (constructed-def @normalize + block form, -unindentedfields, -& record continuation, container quoting rules, explicit @form -synopsis for script-level ids, the optional-choice error-quality workaround), cross-referenced from src/AGENTS.md with ::punkboot::argdoc named as the in-tree exemplar. Layout make.tcl copies followed via the punkcheck channel. diff --git a/punkproject.toml b/punkproject.toml index bc821e1f..23b05aa9 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.12.25" +version = "0.12.26" license = "BSD-2-Clause" diff --git a/src/AGENTS.md b/src/AGENTS.md index 371e3dc1..24a1e16b 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -69,7 +69,7 @@ Recovery after a wrong path guess: - Use `tclsh src/make.tcl vfscommonupdate` to rebuild `_vfscommon.vfs`. The REPLACE confirmation prompts on an interactive terminal; unattended/agent runs must pass `-confirm 0` (with the default `-confirm 1` and a piped/closed stdin, make.tcl aborts fast with guidance instead of reading stdin - do not pipe `y`). - make.tcl subcommands and flags are punk::args-declared (G-030): `tclsh src/make.tcl help ?subcommand?` or ` -help` shows tabled usage; invalid arguments produce punk::args usage errors. The dispatch degrades to plain scan/help when the bootsupport punk::args (or its rendering stack) is stale or unavailable; `PUNKBOOT_PLAIN=1` forces the degraded mode for troubleshooting. The `::punkboot::argdoc` definitions are constructed (string-built) and use the G-045 authoring mechanisms — block-form help values + `@normalize`, explicit `@form -synopsis` — see src/modules/AGENTS.md "punk::args definition authoring ergonomics" before editing them or authoring similar definitions. - Uncommitted `make.tcl`-generated outputs may be batched into one commit, regardless of how many `make.tcl` invocations produced them. This covers punkcheck-managed build outputs that are VCS-tracked: `src/bootsupport/`, `_vfscommon.vfs/modules` + `/lib`, and declared per-kit `*.vfs/lib_tcl/` subfolders. (Root `modules/`, `lib/`, `modules_tcl/`, `lib_tcl/` are gitignored and never committed.) Each module's old→new version rename is atomic; a regeneration reflects one build-output refresh, not independent hand-edits. This is a carve-out from generic "split aggressively" commit guidance for punkcheck-managed build outputs only — it does not cover user-curated VFS content (`_config/`, per-kit `*.vfs/` outside declared `lib_tcl` subfolders, `_aside/`, `mkzipfix.vfs`, `_vfscommon.vfs/doc`), which commits separately per its own concerns. -- Use `punk make.tcl project` or `punk902z make.tcl project` inside Punk shell when building binaries through Punk. +- Use `punk make.tcl project` or `punk902z make.tcl project` inside Punk shell when building binaries through Punk. Driving make.tcl from a built punk executable is supported for informational/update subcommands and for kit builds of *other* kits — the kit whose deployed executable is running the build is skipped with a warning (it cannot be replaced while running, and the pre-deploy process sweep must not kill the build itself; the sweep also excludes the build's own pid in all cases). Rebuild that kit from tclsh or a different kit. - Binary images are platform-specific; build on each target platform rather than expecting a cross-platform flag. - Remove `_build/` artifacts only when a clean/resync is needed, then rerun the relevant `make.tcl` command. Avoid partial cleans that break boot modules. - Superseded module intermediates are pruned automatically so payload wrapping inherits clean trees: diff --git a/src/make.tcl b/src/make.tcl index 4fa7b856..02ced58b 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -508,9 +508,17 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { } } if {$libunknown ne ""} { - source $libunknown - if {[catch {punk::libunknown::init -caller make.tcl} errM]} { - puts stderr "error initialising punk::libunknown\n$errM" + if {[info commands ::punk::libunknown::package] ne ""} { + #libunknown already active in this interp - e.g make.tcl driven by a built punk + #executable whose boot (punk_main.tcl) initialised it. init's rename of ::package + #must not be repeated (its guard would emit an 'init already done' diagnostic and + #return), and re-sourcing would silently swap the running copy for the bootsupport + #copy - leave the active one in place. + } else { + source $libunknown + if {[catch {punk::libunknown::init -caller make.tcl} errM]} { + puts stderr "error initialising punk::libunknown\n$errM" + } } #puts stdout " *** [package names]" #puts stdout " **** [dict get $::punk::libunknown::epoch pkg untracked]" @@ -3917,6 +3925,24 @@ foreach vfstail $vfs_tails { foreach targetkit_info $targetkits { puts stdout " processing targetkit: $targetkit_info" lassign $targetkit_info targetkit target_kit_type + #Self-build guard: never process the kit whose deployed executable is running this + #build - the process sweep below would kill this very process, and the deploy step + #cannot replace a running image anyway. Informational/update subcommands are + #unaffected (they exit before the kit machinery), so driving make.tcl from a built + #punk executable remains supported for everything except rebuilding that executable. + set self_exe [file normalize [info nameofexecutable]] + set self_deploy_target [file normalize $binfolder/$targetkit] + if {$::tcl_platform(platform) eq "windows"} { + set is_self_build [string equal -nocase $self_exe $self_deploy_target] + } else { + set is_self_build [string equal $self_exe $self_deploy_target] + } + if {$is_self_build} { + puts stderr "SKIPPING kit $targetkit - its deployed executable is the one running this build ($self_exe)" + puts stderr " Rerun 'make.tcl $::punkboot::command' under tclsh or a different built kit to rebuild $targetkit." + lappend skipped_kits [list kit $targetkit reason "deployed executable is running this build"] + continue + } # -- ---------- set vfs_installer [punkcheck::installtrack new $installername $basedir/.punkcheck] $vfs_installer set_source_target $sourcefolder $buildfolder @@ -4480,6 +4506,18 @@ foreach vfstail $vfs_tails { foreach ln [split [string trim $ps_lines] \n] { if {[string match "* grep *" $ln]} {continue} if {[string match "*$targetkit*" $ln]} { + #never target the process running this build. Name-only matching + #cannot distinguish an unrelated same-named executable elsewhere on + #the system (see TODO above) - but the build must at least not kill + #itself (e.g a copy of the target executable driving this build from + #a path other than the deploy target, which the self-build guard + #above doesn't catch). + if {$::tcl_platform(platform) eq "windows"} { + set ln_pid [lindex $ln 1] + } else { + set ln_pid [lindex $ln 0] + } + if {$ln_pid eq [pid]} {continue} lappend still_running_lines $ln } } diff --git a/src/project_layouts/custom/_project/punk.basic/src/make.tcl b/src/project_layouts/custom/_project/punk.basic/src/make.tcl index 4fa7b856..02ced58b 100644 --- a/src/project_layouts/custom/_project/punk.basic/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.basic/src/make.tcl @@ -508,9 +508,17 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { } } if {$libunknown ne ""} { - source $libunknown - if {[catch {punk::libunknown::init -caller make.tcl} errM]} { - puts stderr "error initialising punk::libunknown\n$errM" + if {[info commands ::punk::libunknown::package] ne ""} { + #libunknown already active in this interp - e.g make.tcl driven by a built punk + #executable whose boot (punk_main.tcl) initialised it. init's rename of ::package + #must not be repeated (its guard would emit an 'init already done' diagnostic and + #return), and re-sourcing would silently swap the running copy for the bootsupport + #copy - leave the active one in place. + } else { + source $libunknown + if {[catch {punk::libunknown::init -caller make.tcl} errM]} { + puts stderr "error initialising punk::libunknown\n$errM" + } } #puts stdout " *** [package names]" #puts stdout " **** [dict get $::punk::libunknown::epoch pkg untracked]" @@ -3917,6 +3925,24 @@ foreach vfstail $vfs_tails { foreach targetkit_info $targetkits { puts stdout " processing targetkit: $targetkit_info" lassign $targetkit_info targetkit target_kit_type + #Self-build guard: never process the kit whose deployed executable is running this + #build - the process sweep below would kill this very process, and the deploy step + #cannot replace a running image anyway. Informational/update subcommands are + #unaffected (they exit before the kit machinery), so driving make.tcl from a built + #punk executable remains supported for everything except rebuilding that executable. + set self_exe [file normalize [info nameofexecutable]] + set self_deploy_target [file normalize $binfolder/$targetkit] + if {$::tcl_platform(platform) eq "windows"} { + set is_self_build [string equal -nocase $self_exe $self_deploy_target] + } else { + set is_self_build [string equal $self_exe $self_deploy_target] + } + if {$is_self_build} { + puts stderr "SKIPPING kit $targetkit - its deployed executable is the one running this build ($self_exe)" + puts stderr " Rerun 'make.tcl $::punkboot::command' under tclsh or a different built kit to rebuild $targetkit." + lappend skipped_kits [list kit $targetkit reason "deployed executable is running this build"] + continue + } # -- ---------- set vfs_installer [punkcheck::installtrack new $installername $basedir/.punkcheck] $vfs_installer set_source_target $sourcefolder $buildfolder @@ -4480,6 +4506,18 @@ foreach vfstail $vfs_tails { foreach ln [split [string trim $ps_lines] \n] { if {[string match "* grep *" $ln]} {continue} if {[string match "*$targetkit*" $ln]} { + #never target the process running this build. Name-only matching + #cannot distinguish an unrelated same-named executable elsewhere on + #the system (see TODO above) - but the build must at least not kill + #itself (e.g a copy of the target executable driving this build from + #a path other than the deploy target, which the self-build guard + #above doesn't catch). + if {$::tcl_platform(platform) eq "windows"} { + set ln_pid [lindex $ln 1] + } else { + set ln_pid [lindex $ln 0] + } + if {$ln_pid eq [pid]} {continue} lappend still_running_lines $ln } } diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl index 4fa7b856..02ced58b 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl @@ -508,9 +508,17 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { } } if {$libunknown ne ""} { - source $libunknown - if {[catch {punk::libunknown::init -caller make.tcl} errM]} { - puts stderr "error initialising punk::libunknown\n$errM" + if {[info commands ::punk::libunknown::package] ne ""} { + #libunknown already active in this interp - e.g make.tcl driven by a built punk + #executable whose boot (punk_main.tcl) initialised it. init's rename of ::package + #must not be repeated (its guard would emit an 'init already done' diagnostic and + #return), and re-sourcing would silently swap the running copy for the bootsupport + #copy - leave the active one in place. + } else { + source $libunknown + if {[catch {punk::libunknown::init -caller make.tcl} errM]} { + puts stderr "error initialising punk::libunknown\n$errM" + } } #puts stdout " *** [package names]" #puts stdout " **** [dict get $::punk::libunknown::epoch pkg untracked]" @@ -3917,6 +3925,24 @@ foreach vfstail $vfs_tails { foreach targetkit_info $targetkits { puts stdout " processing targetkit: $targetkit_info" lassign $targetkit_info targetkit target_kit_type + #Self-build guard: never process the kit whose deployed executable is running this + #build - the process sweep below would kill this very process, and the deploy step + #cannot replace a running image anyway. Informational/update subcommands are + #unaffected (they exit before the kit machinery), so driving make.tcl from a built + #punk executable remains supported for everything except rebuilding that executable. + set self_exe [file normalize [info nameofexecutable]] + set self_deploy_target [file normalize $binfolder/$targetkit] + if {$::tcl_platform(platform) eq "windows"} { + set is_self_build [string equal -nocase $self_exe $self_deploy_target] + } else { + set is_self_build [string equal $self_exe $self_deploy_target] + } + if {$is_self_build} { + puts stderr "SKIPPING kit $targetkit - its deployed executable is the one running this build ($self_exe)" + puts stderr " Rerun 'make.tcl $::punkboot::command' under tclsh or a different built kit to rebuild $targetkit." + lappend skipped_kits [list kit $targetkit reason "deployed executable is running this build"] + continue + } # -- ---------- set vfs_installer [punkcheck::installtrack new $installername $basedir/.punkcheck] $vfs_installer set_source_target $sourcefolder $buildfolder @@ -4480,6 +4506,18 @@ foreach vfstail $vfs_tails { foreach ln [split [string trim $ps_lines] \n] { if {[string match "* grep *" $ln]} {continue} if {[string match "*$targetkit*" $ln]} { + #never target the process running this build. Name-only matching + #cannot distinguish an unrelated same-named executable elsewhere on + #the system (see TODO above) - but the build must at least not kill + #itself (e.g a copy of the target executable driving this build from + #a path other than the deploy target, which the self-build guard + #above doesn't catch). + if {$::tcl_platform(platform) eq "windows"} { + set ln_pid [lindex $ln 1] + } else { + set ln_pid [lindex $ln 0] + } + if {$ln_pid eq [pid]} {continue} lappend still_running_lines $ln } } diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl index 4fa7b856..02ced58b 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl @@ -508,9 +508,17 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { } } if {$libunknown ne ""} { - source $libunknown - if {[catch {punk::libunknown::init -caller make.tcl} errM]} { - puts stderr "error initialising punk::libunknown\n$errM" + if {[info commands ::punk::libunknown::package] ne ""} { + #libunknown already active in this interp - e.g make.tcl driven by a built punk + #executable whose boot (punk_main.tcl) initialised it. init's rename of ::package + #must not be repeated (its guard would emit an 'init already done' diagnostic and + #return), and re-sourcing would silently swap the running copy for the bootsupport + #copy - leave the active one in place. + } else { + source $libunknown + if {[catch {punk::libunknown::init -caller make.tcl} errM]} { + puts stderr "error initialising punk::libunknown\n$errM" + } } #puts stdout " *** [package names]" #puts stdout " **** [dict get $::punk::libunknown::epoch pkg untracked]" @@ -3917,6 +3925,24 @@ foreach vfstail $vfs_tails { foreach targetkit_info $targetkits { puts stdout " processing targetkit: $targetkit_info" lassign $targetkit_info targetkit target_kit_type + #Self-build guard: never process the kit whose deployed executable is running this + #build - the process sweep below would kill this very process, and the deploy step + #cannot replace a running image anyway. Informational/update subcommands are + #unaffected (they exit before the kit machinery), so driving make.tcl from a built + #punk executable remains supported for everything except rebuilding that executable. + set self_exe [file normalize [info nameofexecutable]] + set self_deploy_target [file normalize $binfolder/$targetkit] + if {$::tcl_platform(platform) eq "windows"} { + set is_self_build [string equal -nocase $self_exe $self_deploy_target] + } else { + set is_self_build [string equal $self_exe $self_deploy_target] + } + if {$is_self_build} { + puts stderr "SKIPPING kit $targetkit - its deployed executable is the one running this build ($self_exe)" + puts stderr " Rerun 'make.tcl $::punkboot::command' under tclsh or a different built kit to rebuild $targetkit." + lappend skipped_kits [list kit $targetkit reason "deployed executable is running this build"] + continue + } # -- ---------- set vfs_installer [punkcheck::installtrack new $installername $basedir/.punkcheck] $vfs_installer set_source_target $sourcefolder $buildfolder @@ -4480,6 +4506,18 @@ foreach vfstail $vfs_tails { foreach ln [split [string trim $ps_lines] \n] { if {[string match "* grep *" $ln]} {continue} if {[string match "*$targetkit*" $ln]} { + #never target the process running this build. Name-only matching + #cannot distinguish an unrelated same-named executable elsewhere on + #the system (see TODO above) - but the build must at least not kill + #itself (e.g a copy of the target executable driving this build from + #a path other than the deploy target, which the self-build guard + #above doesn't catch). + if {$::tcl_platform(platform) eq "windows"} { + set ln_pid [lindex $ln 1] + } else { + set ln_pid [lindex $ln 0] + } + if {$ln_pid eq [pid]} {continue} lappend still_running_lines $ln } }