You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

13 KiB

G-128 Portable PE resource stamping

Status: active Scope: src/tools/punkres/ (vendored zig source - tool name settled in the work - with provenance and licence records); src/make.tcl (mechanism selection at the G-057 seam, sharing G-126's tool build step); bin/tools/zig* (pinned toolchain as consumed); bin/punkres.exe (untracked build output); src/tests/ (characterization - icon replaced, payload intact, idempotent, overlay-unsafe refusal); TEMP_REFERENCE/tcl-sfe (read-only reference for the resource structures) Goal: punkshell can stamp windows resources into a FINISHED kit executable from any build host - no twapi, no windows host, no disturbing the appended vfs payload - so a cross-target win32 kit is not condemned to its runtime's icon, and resource stamping becomes a post-hoc operation on a built artifact rather than something wired into the middle of the kit wrap. G-057's mechanism seam selects this implementation when it is present and keeps its twapi path otherwise. Acceptance: the vendored tool, built by the same make.tcl tool step G-126 establishes, replaces RT_ICON/RT_GROUP_ICON in a pre-built PE that already carries an appended zip overlay, for the kit shapes we build (kit, zip, zipcat per mapvfs.config): the new icon is confirmed by reading the resources back with the tool ITSELF (so verification does not require a windows host or twapi) and cross-checked once on windows with twapi::extract_resources; the stamped kit still boots to a working punk shell reading its payload; re-stamping converges with no resource accumulation; it works where .rsrc is NOT the last section - the shape our own runtimes have (tclsfe-x64/punk91: .text .rdata .data .pdata .rsrc .reloc; suite-built tclsh9.0.5-punk: .text .rdata .buildid .data .pdata .tls .rsrc .reloc) - by appending a section and repointing the resource data directory rather than growing .rsrc in place; the appended payload survives the file-offset shift this causes, which is safe for an ARCHIVE-relative zip and not for a file-relative one; a file-relative payload is REFUSED BY DEFAULT with an error naming the reason and the rewrap remedy, but an explicit consent flag (tool CLI and the G-057 seam, never an interactive prompt in non-interactive runs) stamps it by shifting the overlay AND rewriting every central-directory local-header offset plus the EOCD directory offset by the shift delta - deliberately PRESERVING the file-relative convention, with a notification that the format is non-standard - verified by re-reading the stamped artifact (offsetstyle still file, members crc-verified); zip64 and multi-disk archives are refused even with consent; one end-to-end run from a NON-WINDOWS host produces a stamped win32 kit whose icon a windows machine confirms (recorded here: which host); G-057's seam selects the tool when present and falls back to its twapi path otherwise, with a parity check showing both mechanisms yield equivalent resource content from the same input, and zig stays optional - with no tool built, G-057 behaves exactly as it does without this goal; the tool's distribution is settled and recorded - buildable from the vendored source with the pinned toolchain AND publishable as a punkbin artifact through the G-123/G-006 channels, so a no-toolchain user has a route that does not depend on the committed twapi; the tool's structure handling is written so the parked RT_VERSION stamping needs no second tool; licensing and upstream provenance of the vendored tree are recorded per G-063 and G-026.

Context

G-057 embeds icons with twapi, which is the right mechanism to ship first (demonstrated prior art in tcl-sfe, no new build infrastructure, and the vendored twapi makes it work on a fresh windows checkout with nothing built). It has two limits that are structural rather than incidental:

  • Host-bound. twapi is a Windows extension, so a windows-target kit cross-baked from a linux or FreeBSD host cannot be stamped at all. G-122 made cross-target bakes routine and G-127 makes them first-class, so this stops being hypothetical.
  • Ordering-bound. A Windows resource update rewrites the PE image and corrupts an already-appended vfs payload, which is why G-057 must either icon the stub BEFORE appending or do sfe's split/update/reattach dance. Stamping cannot be applied to a finished kit.

Feasibility measured 2026-07-26. The zig toolchain we already vendor carries both halves of the problem's prior art: zig rc is resinator, a full Windows RC compiler bundled at lib/compiler/resinator/, which is the reference for building correct RT_GROUP_ICON / RT_ICON structures from an .ico image directory; and std.coff parses PE. Note what resinator is NOT: a compiler produces a .res blob for a LINKER to embed into an executable it is building. Our runtimes are pre-built third-party binaries we never link, so the patch-an-existing-image half is the work this goal owns.

Section layout of our actual runtimes, via llvm-objdump:

tclsfe-x64.exe / punk91.exe    .text .rdata .data .pdata .rsrc .reloc
tclsh9.0.5-punk.exe            .text .rdata .buildid .data .pdata .tls .rsrc .reloc

.rsrc is not last - .reloc follows it - so growing it in place is unavailable and the implementation must append a section and repoint IMAGE_DIRECTORY_ENTRY_RESOURCE (the approach rcedit takes). That shifts the overlay's file offset, which is exactly the archive-relative vs file-relative distinction that G-122 and G-124 keep surfacing: an archive-relative zip survives being moved because its internal offsets are relative to its own start, a file-relative one does not. punkshell's own kits are archive-relative (measured: punk91.exe, preamble 5267968), so post-hoc stamping is viable for them - and the unsafe case must be detected and refused, not attempted.

The payoff is therefore larger than portability: a tool that treats the overlay as a first-class thing to preserve removes G-057's ordering constraint entirely, which is what the parked RT_VERSION stamping wants, since the project version is known late.

Approach

  • Vendor and build through the pattern G-126 establishes - same tool build step, same optional-zig posture, same provenance and licence recording. This is the second instance of that pattern, not a new one.
  • Read-back is part of the tool, not an afterthought: without it the acceptance cannot be checked from a non-windows host, which is the whole point.
  • Refuse rather than risk: a payload whose offsets would break if the archive moves is a hard error naming the reason.
  • Build the resource structures once, general enough that the parked RT_VERSION work extends the same tool rather than starting another.

Alternatives considered

  • Shell out to rcedit - rejected: a third-party Node/C++ binary, against the vendored-source direction, and it would be another committed or fetched executable with no in-tree provenance.
  • Keep twapi as the only mechanism - rejected as the END state: it leaves cross-host kits icon-less and keeps stamping wired into the middle of the wrap. Note it is NOT rejected as a path - G-057's twapi arm is explicitly permanent, and this goal must not retire it.
  • Build resources at link time - impossible here: we do not link the runtimes, we receive them as finished binaries.
  • Write it in Tcl instead - rejected: PE section surgery on multi-megabyte images is exactly the file-format work the zig tooling direction exists for, and unlike the punk::zip reader (G-124) there is no module-level need that would justify a pure-Tcl floor.

Notes

  • Depends on: G-057 (achieved 2026-07-29, archived - see goals/archive/G-057-kit-icon-embedding.md) - supplies the mechanism seam this plugs into, and stays the no-toolchain windows path. This goal adds a mechanism; it does not remove one. The seam now EXISTS: ::punkboot::kit_icon_process in src/make.tcl is the single entry point with mechanism selection inside it (::punkboot::kit_icon_embed_twapi is the twapi arm), the per-kit <kitname>.resources.toml sidecar records embedding/status/reason (bin/AGENTS.md documents the format), and the cross-host skip notice names this goal as the remedy - the WSL linux cross-bake of a win32 kit (2026-07-29) is the live demonstration of the gap this goal closes. Note the bake seam stamps stub-first (pre-payload); this goal's post-hoc split/update/reattach operates on FINISHED kits, so its integration point can be either the same seam (replacing the twapi call for cross-host bakes) or a standalone post-hoc invocation - both stay substitutions behind kit_icon_process.
  • Depends on: G-126 (achieved 2026-07-27 - see goals/archive/G-126-punkzip-accelerator.md) - the vendored-zig-tool build step, optional-zig posture and provenance pattern are shared rather than duplicated. Everything this goal needs from it now EXISTS: vendor under src/tools// with a PROVENANCE.md (record format + re-vendor procedure per src/tools/AGENTS.md), declare the zig floor in build.zig.zon minimum_zig_version, and make.tcl tool discovers/builds/tests/installs it with zero make.tcl edits. Traps recorded in the archived detail worth reading before the port: zig build run-steps need their own install-step dependency, msys pipes hand OVERLAPPED stdout handles (probe pattern in punkzip's stdout_file), and 0.16 Io openFile defaults allow_directory=true (breaks file-vs-dir probes) and cannot stamp directory timestamps (direct SetFileTime route).
  • Related: G-123 / G-006 - publishing the built tool as a punkbin artifact is the route that gives a no-toolchain user portable stamping without depending on the committed twapi, which is what makes the G-004 tension resolvable rather than permanent.
  • Related: G-004 - the tool is built or fetched, never committed; and it is the mechanism by which the vendored twapi could eventually stop being load-bearing.
  • Related: G-127 - a win32 kit cross-baked from a non-windows host is precisely the case twapi cannot serve; this is what makes those kits complete rather than icon-less.
  • Related: G-124 (achieved 2026-07-26 - see goals/archive/G-124-punkzip-reader.md) - shares the archive-relative vs file-relative offset distinction: that goal reads a zip at a derived base offset, this one moves one and must not break it. It leaves this goal a ready-made instrument for both halves of that requirement: punk::zip::archive_info <file> answers "is this artifact safe to move the overlay of?" in one call (offsetstyle archive vs file, plus dataoffset), and re-reading the stamped artifact with punk::zip::members/unzip proves the payload survived. Survey recorded there 2026-07-26 over the whole runtime store: 15 archive-relative, 1 file-relative (tclsh90b4_piperepl.exe), 4 with no attached zip.
  • Related: G-023 / G-025 / G-117 (achieved) - the parked RT_VERSION stamping this tool is structured for would carry the project version and become a further provenance surface; G-057's Notes already require those to be reconciled into one story.
  • Related: G-028 - a locked kit exe blocks a resource update exactly as it blocks deploy.
  • Related: G-063 / G-026 - licence and provenance recording for the vendored tree.
  • Recorded 2026-07-27 (user decision): a file-relative payload is no longer a straight refusal - default refuse with a consent-flag preserve, per the amended Acceptance. Rationale: the format works and serves naive zip-tooling/SFX-repair interop and external zip pipelines that treat recorded offsets as file positions. The consented path costs one central-directory offset fix-up pass (the shift delta added to every local-header offset and to the EOCD directory offset) - no decompression, member data untouched; a zip digital-signature record carries no offsets and survives. In-place CONVERSION between conventions falls out of the same machinery with the delta direction reversed - parked, not part of this goal.
  • Stamping invalidates any Authenticode signature regardless of zip convention: the certificate table is located via the PE security directory (a file offset) and the image hash covers the appended payload - sign AFTER stamping; the sign-last rule is orthogonal to the archive/file-relative question.
  • 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.
  • 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 G-126 recorded for punkzip), and its WSL-first verification environment is the natural host for this goal's e2e run.
  • 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.