Browse Source

G-031 increment 1: startup-script collision gate - bake refuses two root-level main.tcl suppliers (0.44.0)

A source .vfs carrying an actual root main.tcl AND a root fauxlink resolving
to main.tcl (or several such fauxlinks) baked whichever boot script merge
traversal met last - merge_over's fauxlink branch overwrites without warning.
New punkboot::utils::vfs_startup_script_report (0.7.0) censuses root
startup-script suppliers by RESOLVED fauxlink name (empty-nominalname links
included; nested app/main.tcl exempt per G-129). make.tcl consumes it with
the same guarded-require degradation as the G-125/G-133/G-134 checks and
refuses affected kits per-kit under FAILED KITS before any build product is
written - kits from other folders proceed, deployed binaries untouched.
Fixture characterization: startupscript.test (12 tests incl. a
real-source-tree sweep proving today's folders are collision-free);
bootlibrary.test re-run green. Outputs (mint 0.7.0 + bootsupport/vfscommon
promotion) follow in a separate commit.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
1d08333721
  1. 18
      CHANGELOG.md
  2. 2
      punkproject.toml
  3. 41
      src/make.tcl
  4. 88
      src/modules/punkboot/utils-999999.0a1.0.tm
  5. 3
      src/modules/punkboot/utils-buildversion.txt
  6. 187
      src/tests/modules/punkboot/utils/testsuites/utils/startupscript.test

18
CHANGELOG.md

@ -5,6 +5,24 @@ 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.44.0] - 2026-08-02
- `make.tcl bake` refuses a kit whose source .vfs carries two root-level
suppliers of the startup-script name main.tcl - an actual main.tcl plus a
fauxlink resolving to that name, or several such fauxlinks (G-031 collision
gate). Previously merge_over materialised whichever supplier traversal met
last (its fauxlink branch overwrites with no warning), so the built kit's
boot script depended silently on directory order. Refusal is per-kit under
FAILED KITS, before any build product is written: kits from other .vfs
folders proceed and a previously deployed bin/<kit> is never replaced over
the ambiguity. Folders with NO startup script stay legal (warning only, as
before); nested app/main.tcl links (the undroidwish convention) are not
root suppliers. Verdict predicate:
punkboot::utils::vfs_startup_script_report (0.7.0), fixture-characterized
in src/tests/modules/punkboot/utils/testsuites/utils/startupscript.test;
same guarded-require degradation as the G-125/G-133/G-134 checks when the
bootsupport snapshot is stale.
## [0.43.0] - 2026-08-02
- Launch package_mode `dev` renamed `minted` (clean break, NO alias): the mode

2
punkproject.toml

@ -1,6 +1,6 @@
[project]
name = "punkshell"
version = "0.43.0"
version = "0.44.0"
license = "BSD-2-Clause"
url = "https://www.gitea1.intx.com.au/jn/punkshell"
#packager: declared identity for published artifacts (declarative, not proof -

41
src/make.tcl

@ -4435,6 +4435,20 @@ proc ::punkboot::get_vfs_boot_library_report {vfsfolder} {
}
return [list 1 [::punkboot::utils::vfs_boot_library_report $vfsfolder]]
}
#Availability probe + root startup-script census of a kit SOURCE .vfs folder (G-031
#startup-script collision gate), consumed by the bake loop's per-kit refusal. Same
#guarded-require treatment as the boot-precondition check above: a stale bootsupport
#snapshot degrades the gate to a NOTE. Returns {available report}.
proc ::punkboot::get_vfs_startup_script_report {vfsfolder} {
set available [expr {\
![catch {package require punkboot::utils}]\
&& [llength [info commands ::punkboot::utils::vfs_startup_script_report]]\
}]
if {!$available} {
return [list 0 [dict create]]
}
return [list 1 [::punkboot::utils::vfs_startup_script_report $vfsfolder]]
}
#Availability probe + advisory payload/target binary-arch scan of an assembled kit vfs
#(G-133), shared by the bake loop and the 'check' command report. Same guarded-require
#treatment as the boot-precondition check above: a stale bootsupport snapshot degrades
@ -8421,6 +8435,8 @@ foreach vfstail $vfs_tails {
#(suffixed with .exe for windows-family targets, whether or not mapvfs.config spelled it that way)
puts " vfs: $vfstail runtimes to process ([llength $runtimes]): $runtimes"
set startup_collision 0
set startup_collision_report [dict create]
if {[llength $runtimes]} {
#only check startup script for .vfs folders that will actually be built against a runtime
#(e.g _vfscommon.vfs is a merge overlay with no runtime mapping and legitimately has no main.tcl)
@ -8428,6 +8444,19 @@ foreach vfstail $vfs_tails {
if {$startup_warning ne ""} {
::punkboot::print_bake_warnings [list $startup_warning]
}
#G-031 startup-script collision gate verdict for this SOURCE .vfs, consumed
#per-kit below (every kit built from a colliding folder is refused; kits from
#other folders proceed). merge_over's fauxlink branch overwrites without
#warning, so a collision lets traversal order silently decide which boot
#script the built kit carries.
lassign [::punkboot::get_vfs_startup_script_report $sourcefolder/vfs/$vfstail] startup_check_available startup_collision_report
if {!$startup_check_available} {
puts stderr "NOTE: startup-script collision check unavailable (punkboot::utils vfs_startup_script_report not loadable from bootsupport) - continuing without it"
} elseif {[dict get $startup_collision_report status] eq "unchecked"} {
puts stderr "NOTE: startup-script collision check could not run ([dict get $startup_collision_report reason]) - continuing without it"
} elseif {![dict get $startup_collision_report ok]} {
set startup_collision 1
}
}
#todo - non kit based - zipkit?
# $runtimes may now include a dash entry "-" (from mapvfs.config file)
@ -8544,6 +8573,18 @@ foreach vfstail $vfs_tails {
lappend skipped_kits [list kit $targetkit reason "deployed executable is running this build"]
continue
}
#G-031 startup-script collision gate: two root-level suppliers of the name
#main.tcl in the source .vfs mean the baked boot script depends on merge
#traversal order. Refuse the kit BEFORE any build product is written - as
#with the G-125 gate, a previously deployed bin/<kit> must never be
#replaced over an ambiguity; kits from other .vfs folders proceed.
if {$startup_collision} {
set failmsg "startup-script collision in src/vfs/$vfstail - [dict get $startup_collision_report reason]. NOT BUILT and NOT DEPLOYED: any previously deployed $targetkit is untouched"
puts stderr "startup-script collision gate FAILED for kit $targetkit - refusing to bake an ambiguous boot script"
puts stderr " [dict get $startup_collision_report reason]"
lappend failed_kits [list kit $targetkit reason $failmsg]
continue
}
# -- ----------
set vfs_installer [punkcheck::installtrack new $installername $basedir/.punkcheck]
$vfs_installer set_source_target $sourcefolder $bakefolder

88
src/modules/punkboot/utils-999999.0a1.0.tm

@ -23,7 +23,7 @@ package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export binary_arch_classify kit_offsetstyle_report parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report
namespace export binary_arch_classify kit_offsetstyle_report parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report vfs_startup_script_report
namespace eval argdoc {
@ -336,6 +336,92 @@ namespace eval punkboot::utils {
return $report
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vfs_startup_script_report
@cmd -name "::punkboot::utils::vfs_startup_script_report"\
-summary\
"Report what supplies a kit source .vfs folder's root startup script"\
-help\
"Structural census of the root-level startup script of a kit SOURCE
.vfs folder (pre-merge): an actual main.tcl file, root fauxlinks
whose resolved nominal name is main.tcl (target existence is not
examined - presence of the link is what matters), or neither.
Reads directory names only - nothing is executed and no fauxlink
target is opened.
ok is 0 only for the G-031 startup-script collision: more than one
root-level supplier of the name main.tcl (an actual main.tcl file
plus one or more fauxlinks resolving to that name, or several such
fauxlinks). A merge materialises whichever supplier it meets last,
so a collision lets traversal order silently decide the kit's boot
script - make.tcl's bake refuses such a kit rather than build an
ambiguous artifact. A folder with NO startup script is ok here
(legal bare kit - the bake's separate missing-startup warning owns
that case), as is a nested fauxlink such as app/main.tcl (the
undroidwish convention, G-129) - root level only.
Returned dict keys:
ok 0 only on collision (or unreadable folder)
status one of: main fauxlink none collision unchecked
mainfile main.tcl when the actual file is present, else \"\"
mainlinks root fauxlink filenames resolving to main.tcl
unresolvable root fauxlink filenames fauxlink::resolve rejects
reason why ok is 0 (or unchecked); empty string when clean"
@leaders
vfsfolder -type string -optional 0 -help\
"Path of the kit source .vfs folder to inspect"
}]
}
proc vfs_startup_script_report {vfsfolder} {
set report [dict create ok 1 status none mainfile "" mainlinks {} unresolvable {} reason ""]
if {![file isdirectory $vfsfolder]} {
dict set report ok 0
dict set report reason "no such directory: $vfsfolder"
return $report
}
if {[catch {package require fauxlink} errM]} {
#cannot examine links - report unchecked rather than guessing (callers
#degrade to a notice; the gate must not refuse kits it could not check)
dict set report status unchecked
dict set report reason "cannot check: fauxlink package unavailable ($errM)"
return $report
}
if {[file isfile [file join $vfsfolder main.tcl]]} {
dict set report mainfile main.tcl
}
set mainlinks [list]
set unresolvable [list]
foreach link [lsort [glob -nocomplain -dir $vfsfolder -types f *.fxlnk *.fauxlink]] {
if {[catch {fauxlink::resolve $link} linkinfo]} {
lappend unresolvable [file tail $link]
continue
}
#resolved nominal name - covers empty-nominalname links whose effective
#name is the target's tail (filename-prefix parsing would miss them)
if {[dict get $linkinfo name] eq "main.tcl"} {
lappend mainlinks [file tail $link]
}
}
dict set report mainlinks $mainlinks
dict set report unresolvable $unresolvable
set suppliers $mainlinks
if {[dict get $report mainfile] ne ""} {
set suppliers [linsert $mainlinks 0 main.tcl]
}
if {[llength $suppliers] > 1} {
dict set report ok 0
dict set report status collision
dict set report reason "[llength $suppliers] root-level suppliers of the startup-script name main.tcl coexist in [file tail $vfsfolder]: [join $suppliers { AND }] - a merge materialises whichever it meets last, so traversal order silently decides the kit's boot script. Keep exactly one (the fauxlink into _config/ is the punkshell convention)"
} elseif {[dict get $report mainfile] ne ""} {
dict set report status main
} elseif {[llength $mainlinks]} {
dict set report status fauxlink
}
return $report
}
#G-133 binary-arch scan support. Recognition data is deliberately plain namespace
#variables so extending it (a new vendor spelling, a new cpu token) is a visible
#one-line edit rather than a code change.

3
src/modules/punkboot/utils-buildversion.txt

@ -1,6 +1,7 @@
0.6.2
0.7.0
#First line must be a semantic version number
#all other lines are ignored.
#0.7.0 - added vfs_startup_script_report (root startup-script census of a kit SOURCE .vfs folder: actual main.tcl vs fauxlink-resolved main.tcl suppliers, collision verdict; backs make.tcl's G-031 startup-script collision gate)
#0.6.2 - vcs_dirty_warnings wording: "artifacts produced from a dirty tree" (G-155 stage vocabulary; was "built")
#0.6.1 - doc-only: kit_offsetstyle_report argdoc names the recapped tag BAKE-WARNING (G-155 stage vocabulary; tag renamed from BUILD-WARNING in make.tcl)
#0.6.0 - added kit_offsetstyle_report (advisory zip offset-style probe of an assembled kit image via punk::zip::archive_info; backs make.tcl's G-134 archive-relative output pin)

187
src/tests/modules/punkboot/utils/testsuites/utils/startupscript.test

@ -0,0 +1,187 @@
# -*- tcl -*-
# Tests for punkboot::utils::vfs_startup_script_report - the structural startup-script
# census behind make.tcl's G-031 startup-script collision gate:
# - the three clean shapes a kit SOURCE .vfs folder may have (actual root main.tcl,
# root fauxlink resolving to main.tcl, none at all - the last is legal and owned by
# the bake's separate missing-startup warning)
# - the collision verdict: more than one root-level supplier of the name main.tcl
# (actual file + fauxlink, or several fauxlinks) - the case merge_over materialises
# in traversal order with no overwrite warning from its fauxlink branch
# - resolved-NAME detection (an empty-nominalname link whose target tail is main.tcl
# collides; filename-prefix parsing would miss it), nested app/main.tcl exemption
# (G-129 undroidwish convention), unresolvable-link reporting, and the real source
# .vfs folders of this checkout (which must all be collision-free)
# Directory fixtures only - the check reads names, never opens targets, executes nothing.
# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punkboot/utils/*** startupscript.test
package require tcltest
package require punkboot::utils
#added 2026-08-02 (agent, G-031)
namespace eval ::testspace {
namespace import ::tcltest::*
variable BASE [makeDirectory g031_startupscript]
#<projectroot>/src/tests/modules/punkboot/utils/testsuites/utils -> 7 levels up
variable projectroot [file normalize [file join [file dirname [info script]] .. .. .. .. .. .. ..]]
variable srcvfsfolder [file join $projectroot src vfs]
proc mkfile {path} {
file mkdir [file dirname $path]
set fd [open $path w]
puts -nonewline $fd "#fixture\n"
close $fd
}
#build a fresh fixture tree under BASE and return its path
proc mktree {name args} {
variable BASE
set root [file join $BASE $name]
file delete -force $root
file mkdir $root
foreach rel $args {
if {[string index $rel end] eq "/"} {
file mkdir [file join $root [string trimright $rel /]]
} else {
mkfile [file join $root $rel]
}
}
return $root
}
proc report {root} {
return [punkboot::utils::vfs_startup_script_report $root]
}
#the layout/punkshell convention form: name # target # tagset # (empty comment)
variable FXMAIN {main.tcl#..+_config+kit_main.tcl#@punk%3a%3aboot,merge_over#.fxlnk}
#minimal form: name # target
variable FXMAIN2 {main.tcl#..+_config+other_main.tcl.fxlnk}
#empty nominal name - effective name is the decoded target's tail (main.tcl)
variable FXNONAME {#..+_config+main.tcl.fxlnk}
#resolves to a name that is not main.tcl
variable FXOTHER {other.tcl#..+_config+kit_main.tcl.fxlnk}
variable common {
set result [list]
}
# -- --- --- the clean shapes --- --- --
test startup_main_only {an actual root main.tcl alone is clean - status main}\
-setup $common -body {
set r [report [mktree mainonly main.tcl lib/ modules/]]
lappend result [dict get $r ok] [dict get $r status] [dict get $r mainfile] [dict get $r mainlinks]
}\
-result {1 main main.tcl {}}
test startup_fauxlink_only {a root fauxlink resolving to main.tcl alone is clean - status fauxlink}\
-setup $common -body {
variable FXMAIN
set r [report [mktree linkonly $FXMAIN modules/]]
lappend result [dict get $r ok] [dict get $r status] [dict get $r mainfile] [dict get $r mainlinks]
}\
-result [list 1 fauxlink {} [list $FXMAIN]]
test startup_none {no startup script at all is NOT a collision - the missing case belongs to the bake warning, not this gate}\
-setup $common -body {
set r [report [mktree bare lib/ modules/]]
lappend result [dict get $r ok] [dict get $r status] [expr {[dict get $r reason] eq ""}]
}\
-result {1 none 1}
# -- --- --- the collisions --- --- --
test startup_collision_main_plus_fauxlink {an actual main.tcl AND a fauxlink resolving to main.tcl collide - both named in the reason}\
-setup $common -body {
variable FXMAIN
set r [report [mktree collide1 main.tcl $FXMAIN]]
lappend result [dict get $r ok] [dict get $r status]
lappend result [string match "*main.tcl AND *$FXMAIN*" [dict get $r reason]]
lappend result [string match "*traversal order*" [dict get $r reason]]
}\
-result {0 collision 1 1}
test startup_collision_two_fauxlinks {two root fauxlinks both resolving to main.tcl collide even with no actual file}\
-setup $common -body {
variable FXMAIN
variable FXMAIN2
set r [report [mktree collide2 $FXMAIN $FXMAIN2]]
lappend result [dict get $r ok] [dict get $r status] [llength [dict get $r mainlinks]]
}\
-result {0 collision 2}
test startup_collision_empty_nominalname {an empty-nominalname link whose target tail is main.tcl collides - detection is by RESOLVED name}\
-setup $common -body {
variable FXNONAME
set r [report [mktree collide3 main.tcl $FXNONAME]]
lappend result [dict get $r ok] [dict get $r status] [dict get $r mainlinks]
}\
-result [list 0 collision [list $FXNONAME]]
# -- --- --- what must NOT collide --- --- --
test startup_other_fauxlink_no_collision {a fauxlink resolving to a different name does not collide with main.tcl}\
-setup $common -body {
variable FXOTHER
set r [report [mktree othername main.tcl $FXOTHER]]
lappend result [dict get $r ok] [dict get $r status] [dict get $r mainlinks]
}\
-result {1 main {}}
test startup_nested_fauxlink_exempt {a nested app/main.tcl fauxlink (undroidwish convention, G-129) is not a root supplier}\
-setup $common -body {
variable FXMAIN
set r [report [mktree nested main.tcl app/$FXMAIN]]
lappend result [dict get $r ok] [dict get $r status] [dict get $r mainlinks]
}\
-result {1 main {}}
test startup_unresolvable_listed {a structurally invalid fauxlink is reported unresolvable and does not affect the verdict}\
-setup $common -body {
set r [report [mktree badlink main.tcl notalink.fxlnk]]
lappend result [dict get $r ok] [dict get $r status] [dict get $r unresolvable]
}\
-result {1 main notalink.fxlnk}
# -- --- --- robustness --- --- --
test startup_missing_directory {a nonexistent folder is refused by name rather than erroring}\
-setup $common -body {
variable BASE
set r [report [file join $BASE no_such_vfs]]
lappend result [dict get $r ok] [string match "no such directory:*" [dict get $r reason]]
}\
-result {0 1}
test startup_does_not_execute_or_write {the check writes nothing into the tree it inspects}\
-setup $common -body {
variable FXMAIN
set root [mktree readonlycheck $FXMAIN modules/]
set before [lsort [glob -nocomplain -directory $root -tails *]]
report $root
set after [lsort [glob -nocomplain -directory $root -tails *]]
lappend result [expr {$before eq $after}]
}\
-result {1}
# -- --- --- the trees the gate will actually see --- --- --
test startup_real_source_vfs_folders {every kit source .vfs folder in this checkout is collision-free - the gate must not refuse kits that bake today}\
-setup $common -body {
variable srcvfsfolder
set failures [list]
set checked 0
foreach d [lsort [glob -nocomplain -type d -directory $srcvfsfolder *.vfs]] {
incr checked
set r [report $d]
if {![dict get $r ok]} {
lappend failures [list [file tail $d] [dict get $r reason]]
}
}
lappend result [expr {$checked > 0}] $failures
}\
-result {1 {}}
cleanupTests
}
Loading…
Cancel
Save