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. - **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`. - **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`. - **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". - **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. - **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. - **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` Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy. "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 ## [0.30.0] - 2026-07-29
- Kit builds carry a configurable icon (G-057): every baked kit ships a - 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/`). 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 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 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 PE RT_ICON/RT_GROUP_ICON resources (stub stamped before the payload is
payload is appended so the vfs payload is never at risk; runtime store appended so the vfs payload is never at risk; runtime store originals are
originals are never modified). Named "resources", not "icon", so the parked never modified). Two mechanisms share one semantic and yield equivalent
RT_VERSION stamping extends the same file. 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` - 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 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); path of the chosen icon); `icon_hash` (`sha256:`-prefixed);
`icon_provenance_source` / `icon_provenance_source_hash` (lifted from the `icon_provenance_source` / `icon_provenance_source_hash` (lifted from the
chosen icon's `.assetorigin.toml` when one exists - the record then reaches 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` | `embedding_status` = `embedded` | `not-applicable` | `unavailable` |
`failed`; `embedding_reason` (empty when embedded). `failed`; `embedding_reason` (empty when embedded).
- Skip semantics: a target with no PE resources (linux, freebsd, macosx - Skip semantics: a target with no PE resources (linux, freebsd, macosx
targets, runtimeless `.kit` artifacts) is NOT APPLICABLE - not degradation; targets, runtimeless `.kit` artifacts) is NOT APPLICABLE - not degradation;
a win32 target on a host that cannot write resources (twapi unloadable, or a win32 target on a host where NEITHER mechanism serves (punkres not built,
a non-windows build host) records `unavailable` with the distinct reason, and twapi unloadable or a non-windows build host) records `unavailable`
and a portable post-hoc stamper is the G-128 remedy. In every case the with a combined reason naming both gaps and the punkres build remedy
sidecar is still written and the build completes. (`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 - Byte-stable at unchanged input and host capability: no timestamps, fixed
field order, rewritten only when bytes change. The default icon is part of field order, rewritten only when bytes change. The default icon is part of
the kit's punkcheck source set, so changing it rebuilds kits. 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), - 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 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. 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 - 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 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 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' - 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 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 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 of that shape falls to the opaque-overlay taxonomy; stub-first stamping at
attachment survives is to be measured in this goal's work); stub-first stamping at the G-057 seam is shape-independent either way. MEASURED 2026-07-29 (see
the G-057 seam is shape-independent either way. 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] [project]
name = "punkshell" name = "punkshell"
version = "0.30.0" version = "0.30.1"
license = "BSD-2-Clause" 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, #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 #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 #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 #when this host can write them. Two mechanisms share one semantic (delete ALL
#ReplaceIconInStub (twapi resource-update APIs; Ashok P. Nadkarni) generalized #icon/group entries, write the .ico images as ids 1..N under the first
#to arbitrary stubs: enumerate, delete ALL icon/group entries, write fresh - #pre-existing group's name+lang - idempotent by construction, parity recorded
#idempotent by construction. Ordering is stub-first: a per-kit copy of the #in the G-128 detail file): the vendored punkres portable stamper (G-128,
#payload-free raw runtime prefix is stamped BEFORE any payload is appended, so #src/tools/punkres, built by 'make.tcl tool build punkres') is selected when
#the resource rewrite can never corrupt an overlay. kit_icon_process is the #bin/punkres(.exe) exists and works from ANY build host; otherwise the twapi
#single internal entry point - mechanism selection lives inside it (and #arm (tcl-sfe sfe-0.2.tm ReplaceIconInStub lineage; Ashok P. Nadkarni) serves
#kit_icon_embed_twapi) so a portable stamper (G-128) is a substitution, not a #windows hosts with nothing built. Ordering is stub-first either way: a per-kit
#caller rewrite. Non-PE targets skip as NOT APPLICABLE; a win32 target on an #copy of the payload-free raw runtime prefix is stamped BEFORE any payload is
#incapable host skips with a distinct notice; both still write the sidecar. #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: #Decisions and skip taxonomy: goals G-057 detail file. Sidecar format doc:
#bin/AGENTS.md. #bin/AGENTS.md.
#sha256 is self-contained (same algorithm as src/assets/logo/make-ico.tcl and #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 variable sha256_cache
if {![info exists sha256_cache]} {set sha256_cache [dict create]} if {![info exists sha256_cache]} {set sha256_cache [dict create]}
variable twapi_state "" variable twapi_state ""
variable punkres_state ""
variable K { variable K {
0x428a2f98 0x71374491 0xb5c0fbcf 0xe9b5dba5 0x3956c25b 0x59f111f1 0x923f82a4 0xab1c5ed5 0x428a2f98 0x71374491 0xb5c0fbcf 0xe9b5dba5 0x3956c25b 0x59f111f1 0x923f82a4 0xab1c5ed5
0xd807aa98 0x12835b01 0x243185be 0x550c7dc3 0x72be5d74 0x80deb1fe 0x9bdc06a7 0xc19bf174 0xd807aa98 0x12835b01 0x243185be 0x550c7dc3 0x72be5d74 0x80deb1fe 0x9bdc06a7 0xc19bf174
@ -6510,6 +6515,50 @@ proc ::punkboot::kiticon::twapi_available {} {
set twapi_state ok set twapi_state ok
return [list 1 {}] 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 #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 #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 #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 status unavailable
set reason "extraction produced no raw runtime prefix to stamp" set reason "extraction produced no raw runtime prefix to stamp"
} else { } else {
lassign [::punkboot::kiticon::twapi_available] twapi_ok twapi_reason #mechanism selection (G-128): the portable stamper when built, else the
if {!$twapi_ok} { #twapi arm; only when neither serves does the step skip - the combined
set status unavailable #reason names both gaps and the punkres build remedy
set reason $twapi_reason 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 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 set stamped $buildfolder/iconed_$targetkit
file copy -force $raw_runtime $stamped file copy -force $raw_runtime $stamped
catch {exec chmod +w $stamped} catch {exec chmod +w $stamped}
if {[catch {::punkboot::kit_icon_embed_twapi $stamped $icon_path} embed_result]} { if {$punkres_ok} {
::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"] set embedding punkres
set embed_cmd [list ::punkboot::kit_icon_embed_punkres $punkres_info $stamped $icon_path]
} else {
set embedding twapi 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 status failed
set reason $embed_result set reason $embed_result
catch {file delete $stamped} catch {file delete $stamped}
} else { } else {
set embedding twapi
set status embedded set status embedded
set wrap_runtime $stamped 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"} { } elseif {$status eq "not-applicable"} {
puts stdout " kit icon (G-057): embedding NOT APPLICABLE for $targetkit - $reason (sidecar still written)" 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