Browse Source

G-128: seam selects punkres when built; characterization tests; punkshell 0.30.1

src/make.tcl kit icon seam (G-057): mechanism selection is now punkres-first -
kiticon::punkres_available (memoized; binary presence at bin/punkres(.exe) is
the selection signal) + kit_icon_embed_punkres (exec wrapper; punkres verifies
its own output). twapi arm unchanged as fallback; when NEITHER serves the skip
notice combines both reasons and names the 'make.tcl tool build punkres'
remedy. Sidecar embedding value gains 'punkres' (bin/AGENTS.md format doc,
ARCHITECTURE.md kit-icon bullet updated; architecture_lint clean).

Live-bake fallback matrix on punkluck86 (win32-ix86 = PE32 arm live): punkres
present -> embedded via punkres, kit boots + smoke OK; punkres absent + twapi
loadable -> via twapi; both absent -> unavailable with combined notice,
embedding none (exact pre-G-128 degradation). Live parity: both arms' baked
kits listing-identical (codepage host-ACP noise finding recorded in the goal
detail) with byte-identical extracted icons.

New src/tests/shell/testsuites/punkexe/punkres.test (7/7 under the
tclsh9.0.5-punk runner; self-gates on bin/punkres + store fixture): overlay-
unsafe refusal exit 3 with untouched input, consented file-relative shift with
convention preserved, payload intact via punk::zip members + crc-verified
extraction, icon replaced (extract-ico == punkshell.ico), idempotent re-stamp
byte-identical, make.tcl tool info punkres provenance records. Full punkexe
suite 83 tests 0 failed (no collateral).

Goal detail: Notes findings (codepage noise, root-vendorlib twapi fragility,
nested tclsfe payload, metakit move-safety -> G-101 pointer updated) +
Progress increment 1 with the two remaining acceptance items (non-windows-host
e2e, punkbin publication route) and one acceptance-wording flag for the user
(metakit 'kit' shape vs 'zip overlay' clause). punkshell 0.30.1 + CHANGELOG.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 2 days ago
parent
commit
5e9f1c75fc
  1. 2
      ARCHITECTURE.md
  2. 16
      CHANGELOG.md
  3. 23
      bin/AGENTS.md
  4. 111
      goals/G-128-portable-pe-resource-stamping.md
  5. 2
      punkproject.toml
  6. 96
      src/make.tcl
  7. 234
      src/tests/shell/testsuites/punkexe/punkres.test

2
ARCHITECTURE.md

@ -87,7 +87,7 @@ Two layers with a deliberate dependency direction (the class never depends on th
- **Provenance gates.** Build/promotion commands warn on uncommitted `src/` changes (column-0 `PROVENANCE-WARNING:` token, `-dirty-abort` for strict mode); `vendorupdate` warns for dirty source-project checkouts.
- **Boot-precondition gate (G-125).** A bake refuses a kit whose merged `.vfs` supplies no tcl library rather than deploying an artifact that cannot initialise: the kit lands in `FAILED KITS` and nothing is written, so the previously deployed `bin/<kit>` survives. Structural and non-executing (so cross-target kits are covered), reading the merged tree rather than the extraction outcome. The predicate is `punkboot::utils::vfs_boot_library_report`, called through the same guarded require as the provenance check so a stale bootsupport snapshot degrades it to a notice; `make.tcl check` reports ACTIVE/UNAVAILABLE. Sources: `src/AGENTS.md`, `src/modules/punkboot/utils-999999.0a1.0.tm`.
- **Payload/target consistency checks (G-133), both advisory.** At the same post-merge seam, a bake classifies each binary library in the merged tree by header (`punkboot::utils::binary_arch_classify` - PE/ELF/Mach-O, honest unknowns) against the kit's target platform; wrong-arch libraries outside platform-discriminated subdirs (canonical `<os>-<cpu>` names, vendor spellings like `win-x64`) earn recapped `BUILD-WARNING`s. After assembly, a kit declaring smoke-require packages (`mapvfs.config` 5th entry element) has each one plain-`package require`d inside the freshly built artifact via its tclsh subcommand - the only check that sees resolution-order defects (wrong-arch version shadowing); cross-target kits skip with a stated reason. Same guarded-require degradation; `make.tcl check` reports both. Neither guarantees statics, pure-tcl packages with binary deps, or version preference beyond the declared smoke set. Sources: `src/AGENTS.md`, `src/runtime/AGENTS.md`, `src/modules/punkboot/utils-999999.0a1.0.tm`.
- **Kit icon step (G-057).** Every kit a bake builds gets a `<kitname>.resources.toml` sidecar (deployed beside `bin/<kit>`) recording the build-time icon choice - default `src/runtime/punkshell.ico`, overridable by a root `punkshell.ico` in the kit's own custom `.vfs` folder (pre-merge) - with sha256 identity and provenance lifted from the icon's G-135 assetorigin record. win32-target kits on a twapi-capable host get the icon embedded as PE `RT_ICON`/`RT_GROUP_ICON`: a per-kit copy of the payload-free raw runtime prefix is stamped BEFORE payload attach (the tcl-sfe mechanism behind a single seam, `::punkboot::kit_icon_process`, so a portable stamper - G-128 - substitutes without caller changes). Non-PE targets skip as not applicable; incapable hosts (no twapi / non-windows build host) skip with distinct notices; the sidecar is written in every case. Sources: `src/AGENTS.md`, `bin/AGENTS.md`, `src/vfs/AGENTS.md`.
- **Kit icon step (G-057 + G-128).** Every kit a bake builds gets a `<kitname>.resources.toml` sidecar (deployed beside `bin/<kit>`) recording the build-time icon choice - default `src/runtime/punkshell.ico`, overridable by a root `punkshell.ico` in the kit's own custom `.vfs` folder (pre-merge) - with sha256 identity and provenance lifted from the icon's G-135 assetorigin record. win32-target kits get the icon embedded as PE `RT_ICON`/`RT_GROUP_ICON` behind a single seam (`::punkboot::kit_icon_process`): the vendored punkres portable stamper (`src/tools/punkres`, built to `bin/punkres(.exe)` by the make.tcl tool step) is selected when present and works from any build host; otherwise the twapi arm (tcl-sfe mechanism) serves windows hosts with nothing built. Both arms share one semantic (delete all icon/group entries, write ids 1..N under the first pre-existing group's name+lang) and stamp a per-kit copy of the payload-free raw runtime prefix BEFORE payload attach; punkres can also stamp a FINISHED kit post-hoc, preserving an appended zip payload (archive-relative moved verbatim; file-relative refused by default, shifted with a consent flag). Non-PE targets skip as not applicable; a host where neither mechanism serves skips with a combined notice naming the punkres build remedy; the sidecar is written in every case. Sources: `src/AGENTS.md`, `bin/AGENTS.md`, `src/vfs/AGENTS.md`, `src/tools/AGENTS.md`.
- **Buildsuites and the kit family.** `src/buildsuites/suite_tcl90/` builds Tcl/Tk/tcllib from source with a pinned zig toolchain and produces the runtime kit family (plain / punk / bi) plus artifact metadata (G-096-G-117 era: see archived goals G-096, G-098, G-102, G-103, G-107); artifacts publish to the punkbin repo that `bin/punk-runtime.cmd` fetches from. `src/buildsuites/suite_tcl86/` is the 8.6 sibling (G-099 + G-100, both achieved): a Tcl 8.6 windows runtime (static + dynamic shells, `tcl86t.dll`, on-disk lib tree - no zipfs, so no self-contained kit) with thread 2.8 + tclvfs + Tk 8.6 + tklib + tcllib(+tcllibc critcl accelerators) companions, gated against the 8.6 core/thread/tclvfs testsuites with tcllib/tklib on a record tier and an opt-in `test-tk`; punkshell's own runtests is censused on that runtime at parity with a same-day native-8.6 baseline. Suite child shells scrub `TCL<major>_<minor>_TM_PATH` as well as TCLLIBPATH/TCL_LIBRARY/TK_LIBRARY - without it a census measures the machine's module trees. The 8.6 kit container strategy remains in-flux - see "In-flux areas".
- **Project generation.** `dev project.new` composes thin layouts from `src/project_layouts/` (overlay chain with `.anti` deletion markers and `name@base` derivation - G-087, achieved) and injects bootsupport modules from the generating shell at generation time.
- **Workflow overview.** `tclsh src/make.tcl workflow` prints the embedded ASCII data-flow overview of the build/release pipeline, with its own update contract in `src/AGENTS.md`. This section deliberately summarises rather than copies it.

16
CHANGELOG.md

@ -5,6 +5,22 @@ 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.30.1] - 2026-07-29
- Portable PE resource stamping (G-128, first increment): new vendored zig tool
`src/tools/punkres` (upstream c:/repo/jn/zig/punkres, BSD-2-Clause) builds to
`bin/punkres(.exe)` via the existing `make.tcl tool` step with zero make.tcl
edits. punkres stamps RT_ICON/RT_GROUP_ICON into a FINISHED PE from any build
host and reads its own work back (info/list/extract-ico + built-in set-icon
verification), preserving an appended zip payload: archive-relative moved
verbatim, file-relative refused by default and shifted (offsets rewritten,
convention preserved) with `-allow-file-relative-shift`, zip64/multi-disk
always refused, non-zip overlays moved only with `-allow-opaque-overlay`.
- The G-057 kit icon seam now selects punkres when built (any host), with the
twapi arm as the unchanged fallback; sidecar `embedding` records which
mechanism ran (`punkres` | `twapi` | `none`). Parity verified: both
mechanisms produce byte-identical icon resources from the same input.
## [0.30.0] - 2026-07-29
- Kit builds carry a configurable icon (G-057): every baked kit ships a

23
bin/AGENTS.md

@ -162,10 +162,14 @@ Every kit a bake builds ships with a `bin/<kitname>.resources.toml` text
sidecar (deployed beside the kit; the build copy sits in `src/_build/`).
It records the build-time icon choice for EVERY target, and on win32 targets
baked by a capable host the same icon is embedded into the kit executable as
PE RT_ICON/RT_GROUP_ICON resources (twapi mechanism, stub stamped before the
payload is appended so the vfs payload is never at risk; runtime store
originals are never modified). Named "resources", not "icon", so the parked
RT_VERSION stamping extends the same file.
PE RT_ICON/RT_GROUP_ICON resources (stub stamped before the payload is
appended so the vfs payload is never at risk; runtime store originals are
never modified). Two mechanisms share one semantic and yield equivalent
resource content (G-128 parity evidence): `bin/punkres(.exe)` - the vendored
portable stamper, built via `make.tcl tool build punkres`, host-independent -
is selected when present; otherwise the twapi arm serves windows hosts with
nothing built. Named "resources", not "icon", so the parked RT_VERSION
stamping extends the same file.
- Icon choice: a root-level `punkshell.ico` in the kit's own custom `.vfs`
folder (consulted pre-merge, so the `_vfscommon.vfs` copy never reads as an
@ -179,15 +183,16 @@ RT_VERSION stamping extends the same file.
path of the chosen icon); `icon_hash` (`sha256:`-prefixed);
`icon_provenance_source` / `icon_provenance_source_hash` (lifted from the
chosen icon's `.assetorigin.toml` when one exists - the record then reaches
back to the SVG-derived master); `embedding` = `twapi` | `none`;
back to the SVG-derived master); `embedding` = `punkres` | `twapi` | `none`;
`embedding_status` = `embedded` | `not-applicable` | `unavailable` |
`failed`; `embedding_reason` (empty when embedded).
- Skip semantics: a target with no PE resources (linux, freebsd, macosx
targets, runtimeless `.kit` artifacts) is NOT APPLICABLE - not degradation;
a win32 target on a host that cannot write resources (twapi unloadable, or
a non-windows build host) records `unavailable` with the distinct reason,
and a portable post-hoc stamper is the G-128 remedy. In every case the
sidecar is still written and the build completes.
a win32 target on a host where NEITHER mechanism serves (punkres not built,
and twapi unloadable or a non-windows build host) records `unavailable`
with a combined reason naming both gaps and the punkres build remedy
(`make.tcl tool build punkres` - G-128). In every case the sidecar is still
written and the build completes.
- Byte-stable at unchanged input and host capability: no timestamps, fixed
field order, rewritten only when bytes change. The default icon is part of
the kit's punkcheck source set, so changing it rebuilds kits.

111
goals/G-128-portable-pe-resource-stamping.md

@ -138,6 +138,28 @@ the parked RT_VERSION stamping wants, since the project version is known late.
- Related: G-134 - pins make.tcl's own kit outputs archive-relative (advisory probe),
so a default-refusal case reaching the stamper reads as a provenance signal (the
artifact did not come through the bake pipeline), not a pipeline regression.
- Codepage finding (2026-07-29): the IMAGE_RESOURCE_DATA_ENTRY CodePage field is
host-locale NOISE under the twapi arm - Windows EndUpdateResource rebuilds the
whole resource section stamping the host ANSI codepage (1252 here) on every
entry INCLUDING untouched types (observed: RT_VERSION/RT_MANIFEST of the
zig-built luck stub flipped 0->1252 under twapi). punkres instead inherits the
codepage of the entries it replaces (deterministic, input-derived; 0 for a
stub with no prior icons) and preserves untouched types byte-exactly. Parity
is therefore full listing identity on MSVC-lineage stubs (tclsfe family,
1252) and a codepage-only metadata divergence on zig-built stubs (punkres 0
vs twapi host-ACP) - image bytes identical in all cases.
- Environment finding (2026-07-29): the twapi arm is environmentally fragile on
this tree - the ROOT vendorlib_tcl9/ tier has no win32-x86_64/ subfolder, so
neither the Git tclsh nor the native tclsh9.0.5-punk could load the vendored
twapi during a bake (it loads fine when TCLLIBPATH points at
src/vendorlib_tcl9/win32-x86_64). punkres embeds regardless of the driving
tclsh, which is precisely the host-independence this goal exists for.
- Nested-payload finding (2026-07-29): raw tclsfe stubs carry their OWN
tcl_library zip payload, so a zipcat construction over a raw stub yields
zip-after-non-zip-overlay - the derived archive base sits after the overlay
start and moves by exactly the shift delta. punkres handles this (verify
invariant base2 == base1 + delta) and reports the leading-byte distance via
an overlay_note line.
- Related: G-105 - recorded at activation (2026-07-29 overlap survey): the end-to-end
non-windows-host acceptance clause needs a host-native punkres build; cross-target
zig builds and target naming live in G-105 rather than here (the same division
@ -146,6 +168,89 @@ the parked RT_VERSION stamping wants, since the project version is known late.
- Related: G-101 - recorded at activation (2026-07-29 overlap survey): the 'kit'
shape named in this goal's acceptance is today the sdx/metakit container whose
Tcl 8.6 future G-101 decides. A metakit payload is not a zip, so POST-HOC stamping
of that shape falls to the opaque-overlay taxonomy (whether a moved metakit
attachment survives is to be measured in this goal's work); stub-first stamping at
the G-057 seam is shape-independent either way.
of that shape falls to the opaque-overlay taxonomy; stub-first stamping at
the G-057 seam is shape-independent either way. MEASURED 2026-07-29 (see
Progress): the metakit attachment is MOVE-SAFE - punksys.exe stamped post-hoc
via -allow-opaque-overlay boots and loads punk::lib from the moved payload
(metakit self-locates from the file end), so the consent flag serves the kit
shape today.
## Progress
- 2026-07-29 increment 1 (activation day): the tool EXISTS, is vendored, and the
seam selects it. Landed:
- UPSTREAM punkres v0.1.0 at c:/repo/jn/zig/punkres (git; gitea remote
recorded in the vendored PROVENANCE.md; commits 8e47767 initial, 09d5ec5
codepage-inheritance + verify fix). zig 0.16.0 floor (comptime gate + zon).
Modules: pe.zig (PE32/PE32+ parse, overlay boundary, MS checksum,
append-vs-rebuild placement with convergence), rsrc.zig (generic 3-level
resource tree parse + deterministic sorted serialize - RT_VERSION needs no
second tool), ico.zig (.ico <-> group/icon resources, reassembly),
overlay.zig (EOCD/CD analysis, derived-base archive/file detection,
zip64/multi-disk refusal, file-relative offset fix-up), synthpe.zig (all
test fixtures synthesized in memory - no committed binaries). CLI:
info/list (pinned porcelain v1), extract-ico, set-icon with BUILT-IN
read-back verification; exit taxonomy 0 ok / 1 error / 2 hard refusal
(zip64, multi-disk, odd signature) / 3 consent-liftable refusal
(-allow-file-relative-shift, -allow-opaque-overlay, -strip-signature).
twapi-arm-equivalent semantics (all icon/group entries deleted, ids 1..N
under first pre-existing group's name+lang, fallback 1/1033). msys
overlapped-stdout probe carried from punkzip preemptively. zig build test
31/31 across 5 suites.
- VENDORED src/tools/punkres @ upstream 09d5ec5 with PROVENANCE.md (origin,
re-vendor procedure, G-063 licence records: BSD-2-Clause first-party);
src/tools/AGENTS.md indexed. make.tcl tool step discovered it with ZERO
make.tcl edits: test gate + ReleaseSafe build installed bin/punkres.exe.
- SEAM (G-057) wired in src/make.tcl: kiticon::punkres_available (memoized;
binary presence = selection signal) + kit_icon_embed_punkres; selection =
punkres when built (any host), else twapi, else skip with a COMBINED
notice naming both gaps and the build remedy; sidecar embedding value
punkres|twapi|none (bin/AGENTS.md + ARCHITECTURE.md updated). Live-bake
fallback matrix on punkluck86 (win32-ix86 - the PE32 arm live): punkres
present -> "via punkres" embedded/sidecar punkres/kit boots + smoke OK;
punkres absent + twapi loadable (TCLLIBPATH) -> "via twapi"; both absent
-> unavailable + combined reason + embedding none (exact pre-G-128
degradation). Live-bake parity: twapi-baked vs punkres-baked punkluck86
listings identical except the codepage-noise finding (Notes); extracted
icons byte-identical.
- VERIFIED on real artifacts (this windows host):
- punk91.exe copy (49MB, 6-section shape, 3634-member archive-relative
zip): post-hoc stamp, overlay byte-identical at new offset, kit BOOTS
and package-requires punk::lib from the payload, re-stamp
byte-identical (convergence), FULL twapi parity - listing identity,
extract-ico byte-identical between mechanisms AND to the source
punkshell.ico, twapi::extract_resources cross-check identical (8
resources, run inside the stamped kit itself).
- tclsh9.0.5-punk.exe copy (8-section .buildid/.tls suite shape,
1615-member attached image): stamped, boots.
- tclsh90b4_piperepl.exe copy (FILE-relative, 841 members): refused by
default exit 3 naming reason + both remedies; with consent flag 841 CD
offsets + EOCD rewritten, punk::zip::archive_info offsetstyle STILL
file, punkzip extracted 841/841 members crc-verified.
- punksys.exe copy (sdx/metakit kit shape, 17 sections): classified
opaque data, refused by default, consent-moved verbatim, BOOTS loading
punk::lib 0.5.1 - metakit is move-safe (G-101 pointer updated).
- zipcat shape (raw tclsfe stub + zipfs-mkzip append): stamped with the
nested-payload note, appended payload mounts and sources, icon
byte-identical.
- CHARACTERIZATION src/tests/shell/testsuites/punkexe/punkres.test (7/7
under the tclsh9.0.5-punk runner, self-gating on bin/punkres + the store
fixture): file-relative refusal exit 3 + untouched input, consent shift +
convention preserved, payload intact (punk::zip members + crc-verified
extraction), icon replaced (extract-ico == punkshell.ico), idempotent
re-stamp byte-identical, make.tcl tool info punkres provenance records.
- punkshell 0.30.1 (CHANGELOG entry).
- REMAINS for acceptance:
- End-to-end run from a NON-WINDOWS host (WSL linux per G-105's direction):
build punkres on that host, stamp a win32 kit there, confirm the icon on
a windows machine, and record which host here.
- Distribution second leg: publication of the built tool as a punkbin
artifact through the G-123/G-006 channels (the no-toolchain route). The
buildable-from-vendored-source leg is done.
- FLAG for user (acceptance wording, not editable by agent): the acceptance
reads "a pre-built PE that already carries an appended ZIP overlay, for
the kit shapes we build (kit, zip, zipcat)" - the 'kit' shape's payload
is metakit, not zip. Measured behaviour: it stamps post-hoc via the
consented opaque-overlay path and survives (boots). Confirm whether that
satisfies the clause as written or the clause should name the metakit
case explicitly.

2
punkproject.toml

@ -1,4 +1,4 @@
[project]
name = "punkshell"
version = "0.30.0"
version = "0.30.1"
license = "BSD-2-Clause"

96
src/make.tcl

@ -6311,16 +6311,20 @@ proc ::punkboot::assemble_zipcat_image {raw_runtime wrapvfs outfile zipfile} {
#overridable by a root-level punkshell.ico in the kit's own custom .vfs folder,
#consulted PRE-merge so the _vfscommon copy never reads as an override), and
#win32-target kits get that icon embedded as PE RT_ICON/RT_GROUP_ICON resources
#when this host can write them. The mechanism follows tcl-sfe's sfe-0.2.tm
#ReplaceIconInStub (twapi resource-update APIs; Ashok P. Nadkarni) generalized
#to arbitrary stubs: enumerate, delete ALL icon/group entries, write fresh -
#idempotent by construction. Ordering is stub-first: a per-kit copy of the
#payload-free raw runtime prefix is stamped BEFORE any payload is appended, so
#the resource rewrite can never corrupt an overlay. kit_icon_process is the
#single internal entry point - mechanism selection lives inside it (and
#kit_icon_embed_twapi) so a portable stamper (G-128) is a substitution, not a
#caller rewrite. Non-PE targets skip as NOT APPLICABLE; a win32 target on an
#incapable host skips with a distinct notice; both still write the sidecar.
#when this host can write them. Two mechanisms share one semantic (delete ALL
#icon/group entries, write the .ico images as ids 1..N under the first
#pre-existing group's name+lang - idempotent by construction, parity recorded
#in the G-128 detail file): the vendored punkres portable stamper (G-128,
#src/tools/punkres, built by 'make.tcl tool build punkres') is selected when
#bin/punkres(.exe) exists and works from ANY build host; otherwise the twapi
#arm (tcl-sfe sfe-0.2.tm ReplaceIconInStub lineage; Ashok P. Nadkarni) serves
#windows hosts with nothing built. Ordering is stub-first either way: a per-kit
#copy of the payload-free raw runtime prefix is stamped BEFORE any payload is
#appended (punkres could also stamp post-hoc - the bake keeps stub-first so
#both arms stay interchangeable). kit_icon_process is the single internal
#entry point - mechanism selection lives inside it. Non-PE targets skip as NOT
#APPLICABLE; a win32 target on an incapable host skips with a distinct notice;
#both still write the sidecar.
#Decisions and skip taxonomy: goals G-057 detail file. Sidecar format doc:
#bin/AGENTS.md.
#sha256 is self-contained (same algorithm as src/assets/logo/make-ico.tcl and
@ -6331,6 +6335,7 @@ namespace eval ::punkboot::kiticon {
variable sha256_cache
if {![info exists sha256_cache]} {set sha256_cache [dict create]}
variable twapi_state ""
variable punkres_state ""
variable K {
0x428a2f98 0x71374491 0xb5c0fbcf 0xe9b5dba5 0x3956c25b 0x59f111f1 0x923f82a4 0xab1c5ed5
0xd807aa98 0x12835b01 0x243185be 0x550c7dc3 0x72be5d74 0x80deb1fe 0x9bdc06a7 0xc19bf174
@ -6510,6 +6515,50 @@ proc ::punkboot::kiticon::twapi_available {} {
set twapi_state ok
return [list 1 {}]
}
#Memoized once per run: locate the built punkres portable stamper (G-128).
#Returns {1 <exepath>} or {0 <reason>}. Presence of the binary IS the
#mechanism-selection signal: with no tool built the twapi arm and its skip
#taxonomy behave exactly as they did before G-128 (zig stays optional).
proc ::punkboot::kiticon::punkres_available {repo_root} {
variable punkres_state
if {$punkres_state ne ""} {
return $punkres_state
}
if {[punkboot::lib::platform_process_family $::punkboot::host_platform] eq "windows"} {
set exe $repo_root/bin/punkres.exe
} else {
set exe $repo_root/bin/punkres
}
if {[file isfile $exe]} {
set punkres_state [list 1 $exe]
} else {
set punkres_state [list 0 "portable stamper not present at $exe - 'tclsh src/make.tcl tool build punkres' builds it from vendored source (G-128)"]
}
return $punkres_state
}
#G-128 punkres arm: exec the vendored portable stamper on the payload-free stub
#copy. punkres itself deletes all icon/group entries, writes the images as ids
#1..N under the first pre-existing group's name+lang, and re-reads its own
#output (resources AND any overlay) before reporting success - equivalent
#resource content to the twapi arm from the same input (parity evidence in the
#G-128 detail file). Returns the number of images embedded; raises an error
#carrying punkres's output on any nonzero exit.
proc ::punkboot::kit_icon_embed_punkres {punkres_exe exepath icopath} {
set f [open $icopath rb]
set icondata [read $f]
close $f
binary scan $icondata ttt ico_reserved ico_type ico_count
if {$ico_reserved != 0 || $ico_type != 1} {
error "$icopath is not recognisable as a .ico file"
}
if {$ico_count == 0} {
error "no images in $icopath"
}
if {[catch {exec [file nativename $punkres_exe] set-icon [file nativename $exepath] [file nativename $icopath] 2>@1} output]} {
error "punkres set-icon failed: $output"
}
return $ico_count
}
#Replace RT_ICON (type 3) and RT_GROUP_ICON (type 14) in the PE at exepath with
#the images of icopath. Follows sfe-0.2.tm ReplaceIconInStub generalized: all
#existing icon/group entries (any name, any lang) are deleted and the new set
@ -6619,10 +6668,16 @@ proc ::punkboot::kit_icon_process {targetkit rt_target rtname kit_type sourcefol
set status unavailable
set reason "extraction produced no raw runtime prefix to stamp"
} else {
lassign [::punkboot::kiticon::twapi_available] twapi_ok twapi_reason
if {!$twapi_ok} {
set status unavailable
set reason $twapi_reason
#mechanism selection (G-128): the portable stamper when built, else the
#twapi arm; only when neither serves does the step skip - the combined
#reason names both gaps and the punkres build remedy
lassign [::punkboot::kiticon::punkres_available $repo_root] punkres_ok punkres_info
if {!$punkres_ok} {
lassign [::punkboot::kiticon::twapi_available] twapi_ok twapi_reason
if {!$twapi_ok} {
set status unavailable
set reason "$twapi_reason; $punkres_info"
}
}
}
set wrap_runtime $raw_runtime
@ -6630,17 +6685,22 @@ proc ::punkboot::kit_icon_process {targetkit rt_target rtname kit_type sourcefol
set stamped $buildfolder/iconed_$targetkit
file copy -force $raw_runtime $stamped
catch {exec chmod +w $stamped}
if {[catch {::punkboot::kit_icon_embed_twapi $stamped $icon_path} embed_result]} {
::punkboot::print_build_warnings [list "kit icon embedding FAILED for $targetkit ($icon_origin icon $icon_path): $embed_result - kit keeps its runtime's own icon"]
if {$punkres_ok} {
set embedding punkres
set embed_cmd [list ::punkboot::kit_icon_embed_punkres $punkres_info $stamped $icon_path]
} else {
set embedding twapi
set embed_cmd [list ::punkboot::kit_icon_embed_twapi $stamped $icon_path]
}
if {[catch $embed_cmd embed_result]} {
::punkboot::print_build_warnings [list "kit icon embedding FAILED for $targetkit ($icon_origin icon $icon_path via $embedding): $embed_result - kit keeps its runtime's own icon"]
set status failed
set reason $embed_result
catch {file delete $stamped}
} else {
set embedding twapi
set status embedded
set wrap_runtime $stamped
puts stdout " kit icon (G-057): embedded $icon_origin icon into $targetkit stub via twapi ($embed_result images from [file tail $icon_path])"
puts stdout " kit icon (G-057): embedded $icon_origin icon into $targetkit stub via $embedding ($embed_result images from [file tail $icon_path])"
}
} elseif {$status eq "not-applicable"} {
puts stdout " kit icon (G-057): embedding NOT APPLICABLE for $targetkit - $reason (sidecar still written)"

234
src/tests/shell/testsuites/punkexe/punkres.test

@ -0,0 +1,234 @@
package require tcltest
#Characterization of the vendored punkres portable PE resource stamper (goal
#G-128) against a REAL artifact: the file-relative prefixed runtime
#bin/runtime/win32-x86_64/tclsh90b4_piperepl.exe (the same store fixture the
#punk::zip accelerator parity suite uses). Pins the G-128 acceptance behaviours
#on scratch COPIES (store originals are never modified):
# - overlay-unsafe refusal: a file-relative zip payload is REFUSED by default
# (exit 3) with the reason and both remedies named
# - consented stamp: -allow-file-relative-shift stamps, the offset convention
# is PRESERVED (punk::zip::archive_info offsetstyle still 'file'), the
# member count is unchanged and member data crc-verifies (payload intact)
# - icon replaced: extract-ico of the stamped copy is byte-identical to
# src/runtime/punkshell.ico
# - idempotent: re-stamping the stamped copy is byte-identical (convergence)
# - make.tcl tool discovery: 'tool info punkres' reports the PROVENANCE.md
# records (run via the punk executable's script subcommand, as
# maketcltool.test does for punkzip)
#All punkres tests self-gate on bin/punkres(.exe) being built (zig stays
#OPTIONAL - an unbuilt tree skips them) and on the store fixture being present.
#
#Target punk executable for the make.tcl test resolved from
#env(PUNK_SHELL_TEST_EXE), else <projectroot>/bin/punk902z.exe then
#<projectroot>/bin/punkshell902.
namespace eval ::testspace {
namespace import ::tcltest::*
variable testdir [file dirname [file normalize [info script]]]
#<projectroot>/src/tests/shell/testsuites/punkexe -> 5 levels up to <projectroot>
variable projectroot [file normalize [file join $testdir .. .. .. .. ..]]
variable maketcl [file join $projectroot src make.tcl]
variable punkresexe ""
foreach candidate [list [file join $projectroot bin punkres.exe] [file join $projectroot bin punkres]] {
if {[file isfile $candidate]} {
set punkresexe $candidate
break
}
}
variable fixture [file join $projectroot bin runtime win32-x86_64 tclsh90b4_piperepl.exe]
variable srcicon [file join $projectroot src runtime punkshell.ico]
testConstraint punkresavailable [expr {$punkresexe ne ""}]
testConstraint punkresfixture [expr {$punkresexe ne "" && [file isfile $fixture] && [file isfile $srcicon]}]
testConstraint punkzipmodule [expr {![catch {package require punk::zip}]}]
variable punkexe ""
if {[info exists ::env(PUNK_SHELL_TEST_EXE)] && $::env(PUNK_SHELL_TEST_EXE) ne ""} {
set punkexe [file normalize $::env(PUNK_SHELL_TEST_EXE)]
} else {
foreach candidate [list [file join $projectroot bin punk902z.exe] [file join $projectroot bin punkshell902]] {
if {[file exists $candidate]} {
set punkexe $candidate
break
}
}
}
testConstraint punkexeavailable [expr {$punkexe ne "" && [file exists $punkexe]}]
#run punkres with stderr merged; returns {exitcode output}. exec raises on
#nonzero exit with CHILDSTATUS carrying the code.
proc run_punkres {args} {
variable punkresexe
set rc 0
set out ""
if {[catch {exec [file nativename $punkresexe] {*}$args 2>@1} out errdict]} {
if {[lindex [dict get $errdict -errorcode] 0] eq "CHILDSTATUS"} {
set rc [lindex [dict get $errdict -errorcode] end]
} else {
set rc -1
}
}
return [list $rc $out]
}
proc readbin {path} {
set f [open $path rb]
set data [read $f]
close $f
return $data
}
variable workdir [temporaryDirectory]
variable frcopy [file join $workdir punkres_frcopy.exe]
variable frcopy2 [file join $workdir punkres_frcopy2.exe]
variable exico [file join $workdir punkres_extracted.ico]
#added 2026-07-29 (agent, G-128)
test punkres_info_fixture {info -porcelain classifies the store piperepl runtime: zip payload, file-relative offsets} -constraints {punkresfixture} -body {
variable fixture
lassign [run_punkres info -porcelain [file nativename $fixture]] rc out
set result [list rc $rc]
lappend result versionline [regexp {(?n)^punkres_info_v1$} $out]
lappend result kind [regexp {(?n)^overlay_kind zip$} $out]
lappend result style [regexp {(?n)^zip_style file$} $out]
lappend result base0 [regexp {(?n)^zip_base 0$} $out]
lappend result members [regexp {(?n)^zip_members [1-9][0-9]*$} $out]
set result
} -result {rc 0 versionline 1 kind 1 style 1 base0 1 members 1}
#added 2026-07-29 (agent, G-128)
test punkres_refuse_file_relative {set-icon on a file-relative payload copy is refused by default: exit 3, reason + both remedies} -constraints {punkresfixture} -body {
variable fixture; variable frcopy; variable srcicon
file copy -force $fixture $frcopy
lassign [run_punkres set-icon [file nativename $frcopy] [file nativename $srcicon]] rc out
set result [list rc $rc]
lappend result refused [regexp {REFUSED: appended zip payload records FILE-relative member offsets} $out]
lappend result remedyflag [regexp -- {-allow-file-relative-shift} $out]
lappend result remedyrewrap [regexp {rewrap the artifact archive-relative} $out]
#refusal happened before any write: the copy is untouched
lappend result untouched [string equal [readbin $frcopy] [readbin $fixture]]
set result
} -result {rc 3 refused 1 remedyflag 1 remedyrewrap 1 untouched 1}
#added 2026-07-29 (agent, G-128)
test punkres_consent_shift {consented stamp preserves the file-relative convention with all offsets rewritten} -constraints {punkresfixture} -body {
variable frcopy; variable srcicon
lassign [run_punkres set-icon -allow-file-relative-shift [file nativename $frcopy] [file nativename $srcicon]] rc out
set result [list rc $rc]
lappend result moved [regexp {overlay: zip file-relative, [0-9]+ bytes moved by [0-9]+; [0-9]+ central-directory offsets and the EOCD directory offset rewritten \(non-standard convention PRESERVED\)} $out]
lappend result verified [regexp {verify: resources re-read OK; zip overlay file-relative preserved, [0-9]+ members, all local headers at rewritten offsets} $out]
set result
} -result {rc 0 moved 1 verified 1}
#added 2026-07-29 (agent, G-128)
test punkres_payload_intact {stamped copy's payload: offsetstyle still file, member count unchanged, member data crc-verifies} -constraints {punkresfixture punkzipmodule} -body {
variable fixture; variable frcopy; variable workdir
set orig_info [punk::zip::archive_info $fixture]
set new_info [punk::zip::archive_info $frcopy]
set result [list]
lappend result offsetstyle [dict get $new_info offsetstyle]
lappend result members_match [expr {[llength [punk::zip::members $frcopy]] == [llength [punk::zip::members $fixture]]}]
#crc-verified extraction of a member subset proves data intact through
#the shift (-verify defaults to 1: a crc mismatch raises)
set exdir [file join $workdir punkres_exdir]
file delete -force $exdir
file mkdir $exdir
punk::zip::unzip $frcopy $exdir tcl_library/init.tcl
lappend result extracted [file isfile [file join $exdir tcl_library init.tcl]]
file delete -force $exdir
set result
} -result {offsetstyle file members_match 1 extracted 1}
#added 2026-07-29 (agent, G-128)
test punkres_icon_replaced {extract-ico of the stamped copy is byte-identical to the source punkshell.ico} -constraints {punkresfixture} -body {
variable frcopy; variable exico; variable srcicon
file delete -force $exico
lassign [run_punkres extract-ico [file nativename $frcopy] [file nativename $exico]] rc out
set result [list rc $rc]
lappend result identical [string equal [readbin $exico] [readbin $srcicon]]
set result
} -cleanup {
file delete -force $exico
} -result {rc 0 identical 1}
#added 2026-07-29 (agent, G-128)
test punkres_idempotent {re-stamping the stamped copy converges byte-identically (rebuild mode, no accumulation)} -constraints {punkresfixture} -body {
variable frcopy; variable frcopy2; variable srcicon
lassign [run_punkres set-icon -o [file nativename $frcopy2] -allow-file-relative-shift [file nativename $frcopy] [file nativename $srcicon]] rc out
set result [list rc $rc]
lappend result rebuildmode [regexp {mode: rebuild section \.prsrc in place} $out]
lappend result identical [string equal [readbin $frcopy] [readbin $frcopy2]]
set result
} -cleanup {
file delete -force $frcopy $frcopy2
} -result {rc 0 rebuildmode 1 identical 1}
# -- make.tcl tool discovery (same piped pattern as maketcltool.test) --
variable maketcl_run_timeout_ms 60000
variable runstate
array set runstate {}
proc maketcl_run_read {chan} {
variable runstate
append runstate(output) [read $chan]
if {[chan eof $chan]} {
chan event $chan readable {}
set runstate(done) eof
}
}
proc maketcl_run {cmdargs} {
variable runstate
variable maketcl_run_timeout_ms
variable punkexe
variable maketcl
array unset runstate
set runstate(output) ""
set runstate(done) ""
set chan [open |[list $punkexe script $maketcl {*}$cmdargs 2>@1] r+]
chan configure $chan -blocking 0 -translation binary
catch {chan close $chan write} ;#no stdin for the child - immediate EOF
set timerid [after $maketcl_run_timeout_ms [list set [namespace current]::runstate(done) timeout]]
chan event $chan readable [list [namespace current]::maketcl_run_read $chan]
while {$runstate(done) eq ""} {
vwait [namespace current]::runstate(done)
}
after cancel $timerid
set timedout [expr {$runstate(done) eq "timeout"}]
set exitcode ""
if {$timedout} {
catch {exec {*}[auto_execok taskkill] /F /T /PID [lindex [pid $chan] 0]}
catch {chan close $chan}
} else {
chan configure $chan -blocking 1
if {[catch {chan close $chan} errdata errdict]} {
set exitcode [lindex [dict get $errdict -errorcode] end]
} else {
set exitcode 0
}
}
return [dict create timedout $timedout exitcode $exitcode output $runstate(output)]
}
#added 2026-07-29 (agent, G-128)
test maketcl_tool_info_punkres {tool info punkres reports the PROVENANCE.md records (zero-edit discovery of the second tool)} -constraints {punkexeavailable} -body {
set r [maketcl_run {tool info punkres}]
set out [dict get $r output]
set result [list]
lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode]
lappend result toolline [regexp {(?n)^tool: punkres\s*$} $out]
lappend result upstream [regexp {(?n)^ upstream:\s+\S+} $out]
lappend result commit [regexp {(?n)^ vendored commit: [0-9a-f]{40}\s*$} $out]
lappend result provfile [regexp {(?n)^ provenance:\s+\S+PROVENANCE\.md\s*$} $out]
lappend result floor [regexp {(?n)^ zig floor:\s+>= 0\.16\.0\s*$} $out]
set result
} -result {timedout 0 exitcode 0 toolline 1 upstream 1 commit 1 provfile 1 floor 1}
cleanupTests
}
namespace delete ::testspace
Loading…
Cancel
Save