From 5e9f1c75fc081d80b00aa272a660b9e82880b840 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Wed, 29 Jul 2026 02:52:10 +1000 Subject: [PATCH] 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 --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 16 ++ bin/AGENTS.md | 23 +- goals/G-128-portable-pe-resource-stamping.md | 111 ++++++++- punkproject.toml | 2 +- src/make.tcl | 96 +++++-- .../shell/testsuites/punkexe/punkres.test | 234 ++++++++++++++++++ 7 files changed, 452 insertions(+), 32 deletions(-) create mode 100644 src/tests/shell/testsuites/punkexe/punkres.test diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5f654da4..7f70afc1 100644 --- a/ARCHITECTURE.md +++ b/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/` 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 `-` 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 `.resources.toml` sidecar (deployed beside `bin/`) 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 `.resources.toml` sidecar (deployed beside `bin/`) 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__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. diff --git a/CHANGELOG.md b/CHANGELOG.md index bd309e52..547a43f1 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/bin/AGENTS.md b/bin/AGENTS.md index d92eb998..11e01d4b 100644 --- a/bin/AGENTS.md +++ b/bin/AGENTS.md @@ -162,10 +162,14 @@ Every kit a bake builds ships with a `bin/.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. diff --git a/goals/G-128-portable-pe-resource-stamping.md b/goals/G-128-portable-pe-resource-stamping.md index dc7da624..6ecd8b9a 100644 --- a/goals/G-128-portable-pe-resource-stamping.md +++ b/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. diff --git a/punkproject.toml b/punkproject.toml index 79c84b5a..0fd8452b 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.30.0" +version = "0.30.1" license = "BSD-2-Clause" diff --git a/src/make.tcl b/src/make.tcl index 7366093d..3e3169ff 100644 --- a/src/make.tcl +++ b/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 } or {0 }. 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)" diff --git a/src/tests/shell/testsuites/punkexe/punkres.test b/src/tests/shell/testsuites/punkexe/punkres.test new file mode 100644 index 00000000..8982d828 --- /dev/null +++ b/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 /bin/punk902z.exe then +#/bin/punkshell902. + +namespace eval ::testspace { + namespace import ::tcltest::* + + variable testdir [file dirname [file normalize [info script]]] + #/src/tests/shell/testsuites/punkexe -> 5 levels up to + 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