diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 95a13bd1..8619619f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -86,7 +86,7 @@ Two layers with a deliberate dependency direction (the class never depends on th - **punkcheck** records every install/delete as events in per-folder `.punkcheck` directories - the basis for skip/copy change detection, superseded-module pruning and provenance. Single OO record lifecycle (G-094, achieved) with atomic saves and advisory event-scoped locking for concurrent writers (G-095, achieved). - **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`. +- **Payload/target consistency checks (G-133 + G-134), all 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. The assembled image is also probed for its zip offset convention (G-134, `punkboot::utils::kit_offsetstyle_report` over `punk::zip::archive_info`): a FILE-relative attached payload warns (the pipeline emits archive-relative; the G-128 stamper refuses file-relative by default), no-zip/plain results stay silent. Same guarded-require degradation; `make.tcl check` reports all three. None 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 + 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; the group is NAMED from the icon file's uppercased rootname - `PUNKSHELL` - with language adopted from the replaced group) 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f93b6d9..680919da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ 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.7] - 2026-07-29 + +- Baked kits pinned archive-relative (G-134): every assembled kit image is + probed post-assembly with `punk::zip::archive_info` + (`punkboot::utils::kit_offsetstyle_report`, punkboot::utils 0.6.0) and a + FILE-relative attached zip payload earns a recapped BUILD-WARNING - + advisory, the kit still builds and deploys. No-zip results (the metakit + kit shape) and bare zips stay silent; today's bakes warn on nothing + (measured baseline). `make.tcl check` reports the pin + ACTIVE/UNAVAILABLE with the same guarded-require degradation as the + G-125/G-133 checks. `punk::zip::mkzip -offsettype file` remains available + for deliberate manual file-relative artifacts - the pin asserts only what + the pipeline emits. + ## [0.30.6] - 2026-07-29 - punkres is publishable as a punkbin artifact (G-128 distribution route): diff --git a/GOALS-archive.md b/GOALS-archive.md index e08e26b4..bf2ba3be 100644 --- a/GOALS-archive.md +++ b/GOALS-archive.md @@ -234,3 +234,7 @@ Acceptance: the `.assetorigin.toml` sidecar format is documented ### G-128 [achieved 2026-07-29] Portable PE resource stamping: a vendored zig tool that icons a finished kit from any host → detail: goals/archive/G-128-portable-pe-resource-stamping.md 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) 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. + +### G-134 [achieved 2026-07-29] Baked kits pinned archive-relative: advisory offset-style probe on assembled kit images → detail: goals/archive/G-134-baked-kits-pinned-archive-relative.md +Scope: src/make.tcl (post-assembly offset-style probe at the G-133 advisory-check seam, recapped reporting); src/modules/punkboot/utils-999999.0a1.0.tm (probe predicate beside the G-133 scan helpers, guarded require; bootsupport copies via established sync channels); src/tests/modules/punkboot/utils/ + src/tests/shell/testsuites/punkexe/ (characterization); src/AGENTS.md (what the pin does and does not guarantee) +Acceptance: each assembled kit image is probed with punk::zip::archive_info and a recapped BUILD-WARNING naming the kit is emitted when offsetstyle is file (advisory per the G-133 posture - the kit still builds and deploys, unlike the G-125 hard gate); plain and none results (no zip attached - the metakit kit shape G-131 targets) are silence, not warnings: the pin covers zip offset convention, not the presence of a zip; today's bakes produce no warning (G-124's measured baseline: every built kit archive-relative); the probe predicate lives in punkboot::utils beside the G-133 scan helpers, called through the same guarded require so a stale bootsupport snapshot degrades it to a NOTE, and tclsh src/make.tcl check reports its ACTIVE/UNAVAILABLE state; characterization pins the warning on a file-relative kit fixture and silence on the archive-relative kit set; scope is make.tcl's own assembly paths only - punk::zip::mkzip -offsettype file and modpod's equivalent REMAIN available for deliberate manual creation of file-relative artifacts (user decision 2026-07-27: the format works and serves naive zip-tooling/SFX-repair interop, it is simply not what our pipeline emits), and external zip-repair tools' file-relative output remains tolerated on input by the G-124 reader and normalized away by any re-bake. diff --git a/GOALS.md b/GOALS.md index bc5aaa08..63d9a90e 100644 --- a/GOALS.md +++ b/GOALS.md @@ -399,10 +399,6 @@ Detail: goals/G-131-boot-payload-autodetection.md Scope: TEMP_REFERENCE/tcl-windows-errno-catchall-TICKET-DRAFT.md (submission-ready ticket text - STAGING ONLY, unversioned, and the agent does not submit it); this detail file (the tracked record: finding, verbatim reproducer, the submitted URL once the user files it, and the upstream disposition); src/bootsupport/AGENTS.md and src/make.tcl (the existing workaround and its warning - dated, not removed, if upstream fixes it) Detail: goals/G-132-tcl-windows-errno-catchall-report.md -### G-134 [active] Baked kits pinned archive-relative: advisory offset-style probe on assembled kit images -Scope: src/make.tcl (post-assembly offset-style probe at the G-133 advisory-check seam, recapped reporting); src/modules/punkboot/utils-999999.0a1.0.tm (probe predicate beside the G-133 scan helpers, guarded require; bootsupport copies via established sync channels); src/tests/modules/punkboot/utils/ + src/tests/shell/testsuites/punkexe/ (characterization); src/AGENTS.md (what the pin does and does not guarantee) -Detail: goals/G-134-baked-kits-pinned-archive-relative.md - ### G-136 [proposed] Generated projects are seeded with a default icon from a single punkshell-held master Scope: src/assets/logo/project-default/ (the single held seed artwork); src/modules/punk/mix/commandset/project-999999.0a1.0.tm (generation-time asset injection beside bootsupport_inject); src/project_layouts/ (per-layout icon-slot declaration with no artwork stored in layout trees); src/modules/punkcheck-999999.0a1.0.tm (install records for injected assets) Detail: goals/G-136-generated-project-icon-seeding.md diff --git a/bin/AGENTS.md b/bin/AGENTS.md index 9c52600f..f6de5aa9 100644 --- a/bin/AGENTS.md +++ b/bin/AGENTS.md @@ -149,7 +149,7 @@ and `tclsh9.1b0-tclsfe.exe` (modern `//zipfs:/app`; apnadkarni/tcl-sfe releases) wrapped as `punkluck86` / `punk91ix86` in `src/runtime/mapvfs.config`. Characterization: `src/tests/shell/testsuites/punkexe/kitmountpoint.test`. -### What a bake verifies about a deployed kit (G-125, G-133) +### What a bake verifies about a deployed kit (G-125, G-133, G-134) A `bin/` that a bake deployed has passed the G-125 boot-precondition gate (its merged vfs supplies a tcl library - a kit failing that is never deployed) and the @@ -158,7 +158,11 @@ wrong-arch `.dll`/`.so`/`.dylib` outside a platform-discriminated subdir earned recapped `BUILD-WARNING` - advisory, so heed the bake recap before trusting a warned kit) and, when its `src/runtime/mapvfs.config` entry declares smoke-require packages and the kit is host-runnable, a plain `package require` of each inside the built -artifact via its tclsh subcommand. What that does NOT guarantee: statically linked +artifact via its tclsh subcommand. The assembled image was also probed for its zip +offset convention (G-134, advisory): a FILE-relative attached payload earned a +recapped `BUILD-WARNING` (the pipeline emits archive-relative; the G-128 stamper +refuses file-relative by default), while kits with no attached zip (metakit shapes) +probe as `none` and stay silent. What that does NOT guarantee: statically linked packages and pure-tcl packages with binary dependencies are invisible to the arch scan; package-resolution outcomes (a wrong-arch higher-versioned copy shadowing a working one) are visible only to the smoke probe and only for the declared package diff --git a/goals/G-134-baked-kits-pinned-archive-relative.md b/goals/G-134-baked-kits-pinned-archive-relative.md index 1f93372f..9d4cda03 100644 --- a/goals/G-134-baked-kits-pinned-archive-relative.md +++ b/goals/G-134-baked-kits-pinned-archive-relative.md @@ -1,6 +1,6 @@ # G-134 Baked kits pinned archive-relative: advisory offset-style probe on assembled kit images -Status: active +Status: achieved 2026-07-29 Scope: src/make.tcl (post-assembly offset-style probe at the G-133 advisory-check seam, recapped reporting); src/modules/punkboot/utils-999999.0a1.0.tm (probe predicate beside the G-133 scan helpers, guarded require; bootsupport copies via established sync channels); src/tests/modules/punkboot/utils/ + src/tests/shell/testsuites/punkexe/ (characterization); src/AGENTS.md (what the pin does and does not guarantee) Goal: every kit image make.tcl assembles is verified to carry an ARCHIVE-relative zip payload - probed with punk::zip::archive_info post-assembly - so the bake pipeline's archive-relative output property becomes a checked contract rather than a by-construction accident, and any future assembly path (new kit type, modpod or accelerator change) that emits file-relative offsets surfaces at bake time instead of when G-128's stamper refuses punkshell's own kits. Acceptance: each assembled kit image is probed with punk::zip::archive_info and a recapped BUILD-WARNING naming the kit is emitted when offsetstyle is file (advisory per the G-133 posture - the kit still builds and deploys, unlike the G-125 hard gate); plain and none results (no zip attached - the metakit kit shape G-131 targets) are silence, not warnings: the pin covers zip offset convention, not the presence of a zip; today's bakes produce no warning (G-124's measured baseline: every built kit archive-relative); the probe predicate lives in punkboot::utils beside the G-133 scan helpers, called through the same guarded require so a stale bootsupport snapshot degrades it to a NOTE, and tclsh src/make.tcl check reports its ACTIVE/UNAVAILABLE state; characterization pins the warning on a file-relative kit fixture and silence on the archive-relative kit set; scope is make.tcl's own assembly paths only - punk::zip::mkzip -offsettype file and modpod's equivalent REMAIN available for deliberate manual creation of file-relative artifacts (user decision 2026-07-27: the format works and serves naive zip-tooling/SFX-repair interop, it is simply not what our pipeline emits), and external zip-repair tools' file-relative output remains tolerated on input by the G-124 reader and normalized away by any re-bake. @@ -71,3 +71,34 @@ asserts only what OUR pipeline emits by default. semantic overlap with an offset-style probe). `goals_xref report` reviewed: no unlinked pair concerns an output offset-convention check. The G-124/G-133 anchors are archived; the relations above are from reading. + +## Progress + +- 2026-07-29 implemented and ACHIEVED same day (flip evidence): predicate + punkboot::utils::kit_offsetstyle_report landed (punkboot::utils 0.6.0, full + PUNKARGS argdoc; thin classification wrapper over punk::zip::archive_info - + offsetstyle archive|file|plain|none|unreadable + dataoffset + detail), + synced through make.tcl modules + bootsupport (a stale + src/bootsupport/.punkcheck.lock from a dead pid was removed per the + recorded recovery procedure en route) and promoted into _vfscommon via + vfscommonupdate (kits carry 0.6.0 from their next bake). make.tcl gained + get_kit_offsetstyle_report (same guarded-require shape as the G-125/G-133 + getters), the post-assembly probe beside the smoke-require block (recapped + BUILD-WARNING only when offsetstyle is file; plain/none/unreadable silent; + NOTE when bootsupport is stale) and the 'check' report row ("kit + offset-style pin (G-134): ACTIVE (advisory)" with the contract lines). + Characterization: offsetstyle.test 7/7 (plain/archive/file fixtures built + hermetically with punk::zip::mkzip -runtime/-offsettype - the deliberately + available manual file-relative path exercised as the fixture generator - + plus none on text and metakit-magic binary, unreadable-with-detail on a + missing path, and the src/_build baseline sweep asserting no kit image + probes file); maketclpayloadcheck.test gains maketcl_check_offsetstyle_pin + (4/4 - status line + contract lines, ESC-free harness). Full utils suite + 69/69; full punkexe suite 81 passed 0 failed. Live bake (punkluck86 + rebuilt): probe silent - no NOTE (available), no BUILD-WARNING + (archive-relative), matching the acceptance's today's-bakes-produce-no- + warning clause. Docs per the acceptance: src/AGENTS.md gains the pin + bullet with the does/does-not-guarantee statement, bin/AGENTS.md's + deployed-kit section and ARCHITECTURE.md's check-family bullet extended. + punkshell 0.30.7. Every acceptance clause satisfied; remaining manual + items: none. diff --git a/punkproject.toml b/punkproject.toml index 602217e3..b07bd4f1 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.30.6" +version = "0.30.7" license = "BSD-2-Clause" diff --git a/src/AGENTS.md b/src/AGENTS.md index f46b13bb..f3abea2d 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -73,6 +73,7 @@ Recovery after a wrong path guess: - Uncommitted `make.tcl`-generated outputs may be batched into one commit, regardless of how many `make.tcl` invocations produced them. This covers punkcheck-managed build outputs that are VCS-tracked: `src/bootsupport/`, `_vfscommon.vfs/modules` + `/lib`, declared per-kit `*.vfs/lib_tcl/` subfolders, the thin-layout sync copies (`src/project_layouts/vendor/punk//src/{make.tcl,build.tcl}` + bootsupport manifest copies), and the module-shipped layout payload under `src/modules/punk/mix/#modpod-templates-*/templates/project_layouts/` (G-087: synced from `src/project_layouts` - never hand-edit the modpod copy). (Root `modules/`, `lib/`, `modules_tcl/`, `lib_tcl/` are gitignored and never committed.) Each module's old→new version rename is atomic; a regeneration reflects one build-output refresh, not independent hand-edits. This is a carve-out from generic "split aggressively" commit guidance for punkcheck-managed build outputs only — it does not cover user-curated VFS content (`_config/`, per-kit `*.vfs/` outside declared `lib_tcl` subfolders, `_aside/`, `mkzipfix.vfs`, `_vfscommon.vfs/doc`), which commits separately per its own concerns. - **Boot-precondition gate (G-125).** A bake refuses any kit whose merged `.vfs` supplies no tcl library: the kit is listed under `FAILED KITS` with a reason naming the cause, and NOTHING is written - no `src/_build/`, no deploy, and the previously deployed `bin/` is left byte-identical. This is deliberately a failure rather than a warning: the deploy step deletes the old kit before copying the new one, so the pre-gate behaviour replaced a working shell with an artifact that died at startup with `application-specific initialization failed: Cannot find a usable init.tcl`. The check is structural and non-executing (so it covers cross-target kits too): `init.tcl` in `tcl_library/`, `lib/tcl./` or `tcl./` - the third for runtimes whose archive mounts at the executable's own path rather than `//zipfs:/app`, so `[info library]` is `/tcl8.6` (the androwish/undroidwish zipfs backport for 8.6 does this) - plus one companion file a real Tcl library carries beside it (`tm.tcl`, `package.tcl`, `auto.tcl`, `clock.tcl`, `history.tcl`, `word.tcl`) or an `encoding/` directory - without that second test the `lib/BWidget1.10.1/init.tcl` every punkshell kit carries would answer for a tcl library it cannot provide. It reads the MERGED tree rather than asking whether extraction ran, so a `.vfs` that legitimately supplies its own tcl library (`src/vfs/punk8_statictwapi.vfs`, `src/vfs/punk9test.vfs`) builds and deploys unchanged. **Remedy when a kit is refused:** the usual cause is that the runtime's own payload could not be extracted - read the recapped `BUILD-WARNING` naming what was tried, then either fix extraction (a driving tclsh with no zipfs needs punk::zip >= 0.2.0 in bootsupport - see the extraction bullet above), point the kit at a runtime that carries a payload, or have the kit's `.vfs` supply a tcl library itself. The predicate is `punkboot::utils::vfs_boot_library_report` called through a guarded require, exactly as the dirty-src provenance check is: a stale or missing bootsupport snapshot degrades the gate to a `NOTE` rather than failing every kit. `tclsh src/make.tcl check` reports whether the gate is `ACTIVE` or `UNAVAILABLE`. - **Payload/target consistency checks (G-133), both ADVISORY** - warnings are recapped `BUILD-WARNING`s and the kit still builds and deploys (unlike the G-125 gate). (a) **Binary-arch scan**, at the same post-merge seam as the gate: every binary library (`*.dll`/`*.so`/`*.dylib`) in the merged tree is classified by header bytes (`punkboot::utils::binary_arch_classify` - PE Machine field, ELF e_machine, Mach-O incl. universal; unclassifiable content is honestly `unknown` and never warned about) against the kit's target platform. Structural and non-executing, so it runs for cross-target kits too (measured ~30-140ms per kit, avg ~93ms across the 12 assembled trees). Libraries under a platform-discriminated subdir - canonical `-` platform-dir names, the universal `macosx` folder, or a recognised vendor spelling (`win-x64` etc; the lists are namespace variables in `punkboot::utils`) - are exempt: multi-arch payloads are legitimate there. First real sweep (2026-07-27) found a true latent instance: `zint.dll` 2.13.0 in `src/vfs/punk8win.vfs/lib_tcl8/` is 32-bit (i386) and can never load in the x64 tcl8 kits (punk86/punkbi/punksys) that carry it - those bakes warn until the payload is fixed. (b) **Smoke-require probe**: a kit declaring packages in `src/runtime/mapvfs.config` (5th entry element - see `src/runtime/AGENTS.md`) has each one plain-`package require`d INSIDE the freshly built artifact via its tclsh subcommand, which exits cleanly even on runtimes whose full repl teardown is fragile. This is the only check that observes actual package RESOLUTION - the punkluck86 2026-07-27 incident class (a higher-versioned wrong-arch Thread shadowing the runtime's working copy on plain require) is invisible to structural checks because the working file exists too. Failures are recapped naming kit, package and the actual error; cross-target kits skip with a stated reason; undeclared kits run nothing new; the probe only runs when the kit actually rebuilds. **What the checks do NOT guarantee:** statically linked packages, pure-tcl packages whose binary dependency lives elsewhere, and version-preference outcomes are visible only to the smoke probe - and only for the declared package set; the arch scan proves nothing about loadability beyond architecture, and an `unknown` classification is silence, not a pass. Both degrade to a NOTE when the bootsupport `punkboot::utils` snapshot is stale (same guarded require as the G-125 gate); `tclsh src/make.tcl check` reports the scan's ACTIVE/UNAVAILABLE state and the currently declared smoke-require kits. +- **Kit offset-style pin (G-134), ADVISORY** - at the same seam family, every ASSEMBLED kit image is probed with `punk::zip::archive_info` (via `punkboot::utils::kit_offsetstyle_report`, same guarded-require degradation to a NOTE; `make.tcl check` reports ACTIVE/UNAVAILABLE): a FILE-relative attached zip payload earns a recapped `BUILD-WARNING` naming the kit, because the pipeline emits ARCHIVE-relative by construction and the G-128 stamper refuses file-relative by default - a warning here means an assembly path regressed the output convention. **What the pin does NOT cover:** `plain` and `none` results (a bare zip, or no attached zip at all - the metakit kit shape, and any unreadable input) are silence, never warnings - the pin asserts the offset CONVENTION of an attached zip, not the presence of one; and it asserts only what make.tcl's own assembly paths EMIT - `punk::zip::mkzip -offsettype file` (and modpod's equivalent) remain available for deliberate manual creation of file-relative artifacts, and file-relative INPUT artifacts remain tolerated by the G-124 reader and normalized by any re-bake. - Kit bakes consume the punk-runtime WORKING COPIES under `bin/runtime//`: `make.tcl bake`/`bakehouse` emit a `BUILD-WARNING:` (recapped at end of run) when a wrapped runtime's beside-toml revision is older than an `-r` artifact present in the same folder - the forgot-to-switch-back guard for deliberate `punk-runtime use ` testing excursions. Heed it before trusting freshly baked family kits; `bin/punk-runtime.cmd use >` (`.exe` suffix optional) re-materializes the working copy. - `tclsh src/make.tcl bakelist ?kitname ...?` (G-121) reports the kit outputs configured in `src/runtime/mapvfs.config`: kit name, kit type, runtime (with presence in the runtime store), vfs folder, and the deployed state of `bin/` vs the `src/_build` build product (`current|stale|absent|nobuild`), with anomalies (`runtime=missing`, `vfs=missing`, `rtrev=r` materialization staleness) and the cross-target marker `target=` in a trailing notes column. A nonexistent store tier is flagged loudly per tier actually referenced (header `(FOLDER MISSING)` for the default tier + stderr derivation; bake's no-runtimes exit self-diagnoses the same way). Name arguments filter the report and add a per-kit detail block (resolved store tier, target and provenance of the target, paths/sizes/mtimes). `make.tcl bake ?kitname ...?` bakes and deploys only the named kits - other kits' `_build`/`bin` artifacts and punkcheck records are untouched and the vfslibs phase narrows to the named kits' vfs folders; an unknown name errors before any build, listing the configured names; flags go before kit names (`make.tcl bake -confirm 0 punk91`). Bare `bake` processes all configured kits as before. Both surfaces consume the shared parsed-mapping helpers (`::punkboot::lib::mapvfs_*`) rather than the file format, so the G-024 config-format work swaps the reader underneath. Piped characterization: `src/tests/shell/testsuites/punkexe/maketclbakelist.test`. - **Host vs target platform (G-122).** Everything a bake EMITS is keyed by the artifact's TARGET platform, never by the driving tclsh's personality: the `bin/runtime/` store the runtime is read from, `.exe` suffixing of runtime files and kit outputs, the presence checks behind `runtime=missing`, and whether the pre-deploy process sweep uses `tasklist`/`taskkill` or `ps`/`kill`. Host semantics - copy commands, path handling, filesystem case rules, prompts - stay keyed to the host. The default target is the host's own platform canon, EXCEPT a cygwin-family host (an msys2/cygwin-runtime tclsh, which reports `tcl_platform(platform)` `unix` on windows and canonizes as `msys-x86_64`/`cygwin-x86_64`), which targets `win32-x86_64`: such a host now drives the identical kit set, names and store addressing as a native tclsh. `tclsh src/make.tcl check` prints the derivation on one `platform (G-122): host=... target=... store=... exe-suffix=... process-tooling=...` line. A mapvfs entry may declare its own target platform (4th element - see `src/runtime/AGENTS.md`), which is then read from that platform's tier, named with that platform's executable convention, and skipped by the process sweep (its processes are not visible to this host). Traps this closes, all field-verified 2026-07-26: msys `ps` cannot see a natively-launched kit (only `tasklist` can), and msys2 rewrites arguments that look like absolute posix paths when spawning a native windows program - so `taskkill /PID ` arrived as `taskkill C://PID `; native-windows command lines therefore go through `::punkboot::exec_nativeargs` (sets `MSYS2_ARG_CONV_EXCL=*`, inert elsewhere). Also note a cygwin-family tclsh is a POSIX Tcl: name the script the way that shell spells paths (`tclsh /c/repo/.../src/make.tcl`, not `C:/repo/...`, which it resolves relative to the cwd) - make.tcl diagnoses the windows-spelling case. Piped characterization: `src/tests/shell/testsuites/punkexe/maketclplatform.test` (the cygwin-host tests self-gate on finding and probing a real msys/cygwin tclsh; `PUNK_MSYS_TCLSH` names one explicitly). diff --git a/src/bootsupport/modules/punk/mix/templates-0.2.0.tm b/src/bootsupport/modules/punk/mix/templates-0.2.0.tm index 664ca687..132f49d4 100644 Binary files a/src/bootsupport/modules/punk/mix/templates-0.2.0.tm and b/src/bootsupport/modules/punk/mix/templates-0.2.0.tm differ diff --git a/src/bootsupport/modules/punkboot/utils-0.5.0.tm b/src/bootsupport/modules/punkboot/utils-0.6.0.tm similarity index 91% rename from src/bootsupport/modules/punkboot/utils-0.5.0.tm rename to src/bootsupport/modules/punkboot/utils-0.6.0.tm index 1cd5324c..43372880 100644 --- a/src/bootsupport/modules/punkboot/utils-0.5.0.tm +++ b/src/bootsupport/modules/punkboot/utils-0.6.0.tm @@ -7,7 +7,7 @@ # (C) 2023 # # @@ Meta Begin -# Application punkboot::utils 0.5.0 +# Application punkboot::utils 0.6.0 # Meta platform tcl # Meta license BSD # @@ Meta End @@ -23,7 +23,7 @@ package require Tcl 8.6- # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ namespace eval punkboot::utils { variable version 0.1.0 - namespace export binary_arch_classify parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report + namespace export binary_arch_classify kit_offsetstyle_report parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report namespace eval argdoc { @@ -736,6 +736,65 @@ namespace eval punkboot::utils { dict set report ok [expr {[llength $mismatches] == 0}] return $report } + + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::punkboot::utils::kit_offsetstyle_report + @cmd -name "::punkboot::utils::kit_offsetstyle_report"\ + -summary\ + "Advisory zip offset-style probe of an assembled kit image (G-134)"\ + -help\ + "Probes a finished kit image file with punk::zip::archive_info and + reports the offset convention of any attached zip payload. The + punkshell bake pipeline emits ARCHIVE-relative payloads by + construction; this probe makes that a checked contract - a + 'file' result is the pipeline-regression signal make.tcl + surfaces as a recapped BUILD-WARNING (advisory: the kit still + builds and deploys). 'plain' (the file is a bare zip), 'none' + (no zip attached - e.g the metakit kit shape, or any non-zip + file) and 'unreadable' are silence, not warnings: the pin + covers the offset convention of an attached zip, never the + presence of one. Purely structural: one EOCD scan plus a + central-directory walk, nothing executed, so cross-target kit + images are covered. + + Returned dict keys: + offsetstyle archive | file | plain | none | unreadable + dataoffset payload base offset when archive_info supplies + one (\"\" otherwise) + detail diagnostic text for unreadable (missing file, + punk::zip unavailable, archive_info error); + empty otherwise" + @leaders + kitpath -type string -optional 0 -help\ + "Path of the assembled kit image file to probe" + }] + } + proc kit_offsetstyle_report {kitpath} { + set report [dict create offsetstyle unreadable dataoffset "" detail ""] + if {![file isfile $kitpath]} { + dict set report detail "no such file: $kitpath" + return $report + } + if {[catch {package require punk::zip} errM]} { + dict set report detail "punk::zip not available: $errM" + return $report + } + if {[catch {punk::zip::archive_info $kitpath} zinfo]} { + dict set report detail "archive_info error: $zinfo" + return $report + } + if {[dict exists $zinfo offsetstyle]} { + dict set report offsetstyle [dict get $zinfo offsetstyle] + } else { + dict set report detail "archive_info returned no offsetstyle" + return $report + } + if {[dict exists $zinfo dataoffset]} { + dict set report dataoffset [dict get $zinfo dataoffset] + } + return $report + } } namespace eval ::punk::args::register { @@ -746,8 +805,8 @@ namespace eval ::punk::args::register { ## Ready package provide punkboot::utils [tcl::namespace::eval punkboot::utils { variable version - #- this version number, exactly 0.5.0, is a literal used in src module folders + #- this version number, exactly 0.6.0, is a literal used in src module folders #- we refer to this sometimes as the magic version number - set version 0.5.0 + set version 0.6.0 }] return diff --git a/src/make.tcl b/src/make.tcl index 07121b28..1a4aafe3 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -3478,6 +3478,20 @@ proc ::punkboot::get_vfs_binary_arch_report {vfsfolder targetplatform} { } return [list 1 [::punkboot::utils::vfs_binary_arch_report $vfsfolder $targetplatform]] } +#Availability probe + advisory zip offset-style check of an assembled kit image +#(G-134), shared by the bake loop and the 'check' command report. Same guarded- +#require treatment as the checks above: a stale bootsupport snapshot degrades +#the pin to a NOTE. Returns {available report}. +proc ::punkboot::get_kit_offsetstyle_report {kitpath} { + set available [expr {\ + ![catch {package require punkboot::utils}]\ + && [llength [info commands ::punkboot::utils::kit_offsetstyle_report]]\ + }] + if {!$available} { + return [list 0 [dict create]] + } + return [list 1 [::punkboot::utils::kit_offsetstyle_report $kitpath]] +} #G-133 smoke-require probe: execute a freshly built kit artifact via its 'tclsh' #subcommand and plain-'package require' each declared package inside it. This is the #only check that observes actual package RESOLUTION in the artifact - the punkluck86 @@ -3823,6 +3837,20 @@ if {$::punkboot::command eq "check"} { puts stdout "payload/target arch scan (G-133): UNAVAILABLE (punkboot::utils vfs_binary_arch_report not loadable from bootsupport)" puts stdout " bake will proceed with a NOTE and cannot flag wrong-arch payload libraries - run 'make.tcl modules' then 'make.tcl bootsupport'." } + # Kit offset-style pin (G-134) - advisory archive-relative output contract + puts stdout $sep + #availability probe only - the path deliberately does not exist + lassign [::punkboot::get_kit_offsetstyle_report [file join $projectroot __punkboot_offsetstyle_probe__]] _off_available _off_report + if {$_off_available} { + puts stdout "kit offset-style pin (G-134): ACTIVE (advisory)" + puts stdout " bake probes each assembled kit image with punk::zip::archive_info and emits a recapped" + puts stdout " BUILD-WARNING when an attached zip payload records FILE-relative offsets (the pipeline" + puts stdout " emits archive-relative by construction; the G-128 stamper refuses file-relative by default)." + puts stdout " plain/none results (no attached zip - e.g the metakit kit shape) are silence, not warnings." + } else { + puts stdout "kit offset-style pin (G-134): UNAVAILABLE (punkboot::utils kit_offsetstyle_report not loadable from bootsupport)" + puts stdout " bake will proceed with a NOTE and cannot flag a file-relative kit payload - run 'make.tcl modules' then 'make.tcl bootsupport'." + } set _smoke_declared [list] if {[file exists $sourcefolder/runtime/mapvfs.config]} { set _smokemodel [punkboot::lib::mapvfs_parse $sourcefolder/runtime/mapvfs.config [file dirname $sourcefolder]/bin/runtime $sourcefolder $::punkboot::target_platform] @@ -7662,6 +7690,21 @@ foreach vfstail $vfs_tails { # -- --- --- --- --- --- $vfs_event targetset_end OK + #G-134 post-assembly offset-style pin: the pipeline emits + #ARCHIVE-relative zip payloads by construction - this probe makes + #that a checked contract. Advisory per the G-133 posture: a + #file-relative image earns a recapped BUILD-WARNING (the G-128 + #stamper would refuse it by default) and the kit still builds and + #deploys; plain/none (no attached zip - the metakit kit shape) and + #unreadable results are silence. One EOCD scan + CD walk per + #REBUILT kit only. + lassign [::punkboot::get_kit_offsetstyle_report $buildfolder/$targetkit] offprobe_available offprobe + if {!$offprobe_available} { + puts stderr "NOTE: kit offset-style pin unavailable (punkboot::utils kit_offsetstyle_report not loadable from bootsupport) - continuing without it" + } elseif {[dict get $offprobe offsetstyle] eq "file"} { + ::punkboot::print_build_warnings [list "kit $targetkit assembled with a FILE-relative zip payload (G-134 pin: the pipeline emits archive-relative by construction; the G-128 stamper refuses file-relative by default) - investigate this kit's assembly path"] + } + #G-133 post-build smoke-require probe. Only an actual 'package require' #inside the built artifact observes package RESOLUTION - the wrong-arch #version-shadowing class is invisible to every structural check because diff --git a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl index 0658f3c4..1a4aafe3 100644 --- a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl +++ b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl @@ -2300,15 +2300,24 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase) (gate) --> refused: kit listed under FAILED KITS, nothing | built, nothing deployed, bin/ untouched | - | zip image appended to the runtime exe: tcl::zipfs::mkimg, - | or (driving tcl without zipfs, e.g 8.6) raw-runtime split - | + punk::zip::mkzip + concatenation - both mount the same + | kit icon step (G-057): .resources.toml sidecar records + | the icon choice for every target (default src/runtime/ + | punkshell.ico; override = punkshell.ico in the kit's own .vfs + | folder, pre-merge); win32 targets on a twapi-capable host get + | the icon stamped into a per-kit copy of the raw runtime stub + | BEFORE the payload attach (non-PE target: not applicable; + | incapable host: distinct notice - sidecar written either way) + | + | zip image appended to the (stamped) runtime exe: tcl::zipfs:: + | mkimg, or (driving tcl without zipfs, e.g 8.6) raw-runtime + | split + punk::zip::mkzip + concatenation - both mount the same v - src/_build/.exe + src/_build/.exe (+ .exe.resources.toml) | | smoke-require probe (host-runnable kits with declared | packages): plain 'package require' inside the artifact [K11] | deploy step: delete old bin/.exe, copy new one in + | (the resource sidecar deploys beside the kit) v bin/.exe <-- FAILS (kit listed under FAILED KITS) if a running shell still holds the old exe [K5] @@ -3469,6 +3478,20 @@ proc ::punkboot::get_vfs_binary_arch_report {vfsfolder targetplatform} { } return [list 1 [::punkboot::utils::vfs_binary_arch_report $vfsfolder $targetplatform]] } +#Availability probe + advisory zip offset-style check of an assembled kit image +#(G-134), shared by the bake loop and the 'check' command report. Same guarded- +#require treatment as the checks above: a stale bootsupport snapshot degrades +#the pin to a NOTE. Returns {available report}. +proc ::punkboot::get_kit_offsetstyle_report {kitpath} { + set available [expr {\ + ![catch {package require punkboot::utils}]\ + && [llength [info commands ::punkboot::utils::kit_offsetstyle_report]]\ + }] + if {!$available} { + return [list 0 [dict create]] + } + return [list 1 [::punkboot::utils::kit_offsetstyle_report $kitpath]] +} #G-133 smoke-require probe: execute a freshly built kit artifact via its 'tclsh' #subcommand and plain-'package require' each declared package inside it. This is the #only check that observes actual package RESOLUTION in the artifact - the punkluck86 @@ -3814,6 +3837,20 @@ if {$::punkboot::command eq "check"} { puts stdout "payload/target arch scan (G-133): UNAVAILABLE (punkboot::utils vfs_binary_arch_report not loadable from bootsupport)" puts stdout " bake will proceed with a NOTE and cannot flag wrong-arch payload libraries - run 'make.tcl modules' then 'make.tcl bootsupport'." } + # Kit offset-style pin (G-134) - advisory archive-relative output contract + puts stdout $sep + #availability probe only - the path deliberately does not exist + lassign [::punkboot::get_kit_offsetstyle_report [file join $projectroot __punkboot_offsetstyle_probe__]] _off_available _off_report + if {$_off_available} { + puts stdout "kit offset-style pin (G-134): ACTIVE (advisory)" + puts stdout " bake probes each assembled kit image with punk::zip::archive_info and emits a recapped" + puts stdout " BUILD-WARNING when an attached zip payload records FILE-relative offsets (the pipeline" + puts stdout " emits archive-relative by construction; the G-128 stamper refuses file-relative by default)." + puts stdout " plain/none results (no attached zip - e.g the metakit kit shape) are silence, not warnings." + } else { + puts stdout "kit offset-style pin (G-134): UNAVAILABLE (punkboot::utils kit_offsetstyle_report not loadable from bootsupport)" + puts stdout " bake will proceed with a NOTE and cannot flag a file-relative kit payload - run 'make.tcl modules' then 'make.tcl bootsupport'." + } set _smoke_declared [list] if {[file exists $sourcefolder/runtime/mapvfs.config]} { set _smokemodel [punkboot::lib::mapvfs_parse $sourcefolder/runtime/mapvfs.config [file dirname $sourcefolder]/bin/runtime $sourcefolder $::punkboot::target_platform] @@ -6296,6 +6333,472 @@ proc ::punkboot::assemble_zipcat_image {raw_runtime wrapvfs outfile zipfile} { return $zipbytes } +# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- +#G-057 kit icon step. Every built kit gets a .resources.toml sidecar +#recording the build-time icon choice (default src/runtime/punkshell.ico, +#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. Two mechanisms share one semantic (delete ALL +#icon/group entries, write the .ico images as ids 1..N; the RT_GROUP_ICON is +#NAMED from the icon file - uppercased rootname, so punkshell.ico gives +#PUNKSHELL (user decision 2026-07-29, RC uppercase convention) - with language +#adopted from the first replaced group; 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 +#scriptlib/developer/assetorigin_check.tcl - each stays standalone: make.tcl +#must not depend on scriptlib, and this step must not depend on bootsupport +#freshness; bootsupport carries only sha1/md5). Memoized per path per run. +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 + 0xe49b69c1 0xefbe4786 0x0fc19dc6 0x240ca1cc 0x2de92c6f 0x4a7484aa 0x5cb0a9dc 0x76f988da + 0x983e5152 0xa831c66d 0xb00327c8 0xbf597fc7 0xc6e00bf3 0xd5a79147 0x06ca6351 0x14292967 + 0x27b70a85 0x2e1b2138 0x4d2c6dfc 0x53380d13 0x650a7354 0x766a0abb 0x81c2c92e 0x92722c85 + 0xa2bfe8a1 0xa81a664b 0xc24b8b70 0xc76c51a3 0xd192e819 0xd6990624 0xf40e3585 0x106aa070 + 0x19a4c116 0x1e376c08 0x2748774c 0x34b0bcb5 0x391c0cb3 0x4ed8aa4a 0x5b9cca4f 0x682e6ff3 + 0x748f82ee 0x78a5636f 0x84c87814 0x8cc70208 0x90befffa 0xa4506ceb 0xbef9a3f7 0xc67178f2 + } +} +proc ::punkboot::kiticon::sha256_data {data} { + variable K + set bitlen [expr {wide([string length $data]) * 8}] + append data \x80 + set pad [expr {(56 - [string length $data] % 64 + 64) % 64}] + append data [string repeat \x00 $pad] [binary format W $bitlen] + set h0 0x6a09e667; set h1 0xbb67ae85; set h2 0x3c6ef372; set h3 0xa54ff53a + set h4 0x510e527f; set h5 0x9b05688c; set h6 0x1f83d9ab; set h7 0x5be0cd19 + set n [string length $data] + for {set off 0} {$off < $n} {incr off 64} { + binary scan [string range $data $off [expr {$off + 63}]] Iu16 w + for {set t 16} {$t < 64} {incr t} { + set x [lindex $w [expr {$t - 15}]] + set s0 [expr {((($x >> 7) | ($x << 25)) ^ (($x >> 18) | ($x << 14)) ^ ($x >> 3)) & 0xffffffff}] + set x [lindex $w [expr {$t - 2}]] + set s1 [expr {((($x >> 17) | ($x << 15)) ^ (($x >> 19) | ($x << 13)) ^ ($x >> 10)) & 0xffffffff}] + lappend w [expr {([lindex $w [expr {$t - 16}]] + $s0 + [lindex $w [expr {$t - 7}]] + $s1) & 0xffffffff}] + } + set a $h0; set b $h1; set c $h2; set d $h3 + set e $h4; set f $h5; set g $h6; set h $h7 + for {set t 0} {$t < 64} {incr t} { + set S1 [expr {((($e >> 6) | ($e << 26)) ^ (($e >> 11) | ($e << 21)) ^ (($e >> 25) | ($e << 7))) & 0xffffffff}] + set ch [expr {(($e & $f) ^ (~$e & $g)) & 0xffffffff}] + set T1 [expr {($h + $S1 + $ch + [lindex $K $t] + [lindex $w $t]) & 0xffffffff}] + set S0 [expr {((($a >> 2) | ($a << 30)) ^ (($a >> 13) | ($a << 19)) ^ (($a >> 22) | ($a << 10))) & 0xffffffff}] + set maj [expr {($a & $b) ^ ($a & $c) ^ ($b & $c)}] + set T2 [expr {($S0 + $maj) & 0xffffffff}] + set h $g; set g $f; set f $e; set e [expr {($d + $T1) & 0xffffffff}] + set d $c; set c $b; set b $a; set a [expr {($T1 + $T2) & 0xffffffff}] + } + set h0 [expr {($h0 + $a) & 0xffffffff}]; set h1 [expr {($h1 + $b) & 0xffffffff}] + set h2 [expr {($h2 + $c) & 0xffffffff}]; set h3 [expr {($h3 + $d) & 0xffffffff}] + set h4 [expr {($h4 + $e) & 0xffffffff}]; set h5 [expr {($h5 + $f) & 0xffffffff}] + set h6 [expr {($h6 + $g) & 0xffffffff}]; set h7 [expr {($h7 + $h) & 0xffffffff}] + } + return [format %08x%08x%08x%08x%08x%08x%08x%08x $h0 $h1 $h2 $h3 $h4 $h5 $h6 $h7] +} +proc ::punkboot::kiticon::sha256_file {path} { + variable sha256_cache + set key [file normalize $path] + if {[dict exists $sha256_cache $key]} { + return [dict get $sha256_cache $key] + } + set f [open $path rb] + set data [read $f] + close $f + set hex [sha256_data $data] + dict set sha256_cache $key $hex + return $hex +} +proc ::punkboot::kiticon::toml_escape {s} { + return [string map {\\ \\\\ \" \\\"} $s] +} +#Tolerant subset reader for the chosen icon's G-135 assetorigin sidecar - lifts +#source/source_hash so the kit record reaches back to the SVG-derived master. +#Missing sidecar, unknown schema or malformed content simply yields no +#provenance fields (never an error). +proc ::punkboot::kiticon::assetorigin_read {assetpath} { + set sc $assetpath.assetorigin.toml + set out [dict create] + if {![file isfile $sc]} { + return $out + } + if {[catch { + set f [open $sc rb] + set raw [read $f] + close $f + }]} { + return $out + } + foreach line [split [encoding convertfrom utf-8 $raw] \n] { + set line [string trim [string map [list \r {}] $line]] + if {$line eq "" || [string index $line 0] eq "#"} continue + if {[string index $line 0] eq "\["} break + set eq [string first = $line] + if {$eq < 1} continue + set key [string trim [string range $line 0 [expr {$eq - 1}]]] + if {$key ni {schema source source_hash}} continue + set val [string trim [string range $line [expr {$eq + 1}] end]] + if {[string index $val 0] eq "\""} { + set parsed "" + set ok 0 + set len [string length $val] + for {set i 1} {$i < $len} {incr i} { + set chr [string index $val $i] + if {$chr eq "\\"} { + incr i + append parsed [string index $val $i] + } elseif {$chr eq "\""} { + set ok 1 + break + } else { + append parsed $chr + } + } + if {!$ok} continue + set val $parsed + } else { + set cmt [string first # $val] + if {$cmt >= 0} { + set val [string trim [string range $val 0 [expr {$cmt - 1}]]] + } + } + dict set out $key $val + } + if {![dict exists $out schema] || [dict get $out schema] ne "1"} { + return [dict create] + } + return $out +} +proc ::punkboot::kiticon::writefile_ifchanged {path content} { + if {[file exists $path]} { + set f [open $path rb] + set existing [read $f] + close $f + if {$existing eq $content} { + return unchanged + } + } + set f [open $path wb] + puts -nonewline $f $content + close $f + return written +} +proc ::punkboot::kiticon::copy_ifchanged {from to} { + if {![file isfile $from]} { + return absent + } + set f [open $from rb] + set fromdata [read $f] + close $f + if {[file exists $to]} { + set f [open $to rb] + set todata [read $f] + close $f + if {$todata eq $fromdata} { + return unchanged + } + } + file copy -force $from $to + return copied +} +#Memoized once per run: can this host's tclsh write PE resources? Returns +#{1 {}} or {0 reason} with the reason distinguishing cross-host from +#twapi-unavailable (the acceptance requires distinct notices). +proc ::punkboot::kiticon::twapi_available {} { + variable twapi_state + if {$twapi_state eq "ok"} { + return [list 1 {}] + } + if {$twapi_state ne ""} { + return [list 0 $twapi_state] + } + if {[punkboot::lib::platform_process_family $::punkboot::host_platform] ne "windows"} { + set twapi_state "cross-host: win32 target baked on a $::punkboot::host_platform host cannot run the twapi resource APIs (portable post-hoc stamping is the G-128 remedy)" + return [list 0 $twapi_state] + } + if {[catch {package require twapi_resource}] && [catch {package require twapi}]} { + set twapi_state "twapi resource APIs not loadable in this tclsh ([info nameofexecutable]) - the vendored twapi under vendorlib_tcl/win32-x86_64 serves native windows tclsh builds" + return [list 0 $twapi_state] + } + if {![llength [info commands ::twapi::begin_resource_update]]} { + set twapi_state "twapi loaded but its resource-update APIs are absent" + return [list 0 $twapi_state] + } + 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, or fetch the prebuilt punkbin artifact (win32-x86_64/tools/punkres-*; route: src/tools/punkres/PROVENANCE.md) (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 (a non-empty +#group_override renames the group via 'set-icon -group' - the seam's naming +#policy; language and codepage adoption unchanged), 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 {group_override {}}} { + 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" + } + set cmd [list exec [file nativename $punkres_exe] set-icon] + if {$group_override ne ""} { + lappend cmd -group $group_override + } + #-drop-opaque-overlay: the stub contract is a payload-free prefix, so any + #non-zip trailing bytes are build detritus - concretely the COFF + #symbol/debug tables mingw-built runtimes carry after their sections, + #which ride into the extraction head. The twapi arm's EndUpdateResource + #has always silently stripped exactly those bytes (measured 2026-07-29, + #G-128 detail); punkres drops them deliberately and zeroes the dangling + #symbol-table header pointer. A REAL zip payload on a stub is unaffected + #(the flag is inert for zip-classified overlays, which punkres preserves). + lappend cmd -drop-opaque-overlay + lappend cmd [file nativename $exepath] [file nativename $icopath] 2>@1 + if {[catch $cmd 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 +#written as ids 1..N under the first pre-existing group's name+lang (fallback +#name 1, lang 1033), in one resource-update transaction. A non-empty +#group_override replaces the group NAME only (language adoption unchanged) - +#the same override punkres's 'set-icon -group' applies, so the seam's naming +#policy keeps the two arms in parity (G-128). Raises an error on failure +#(after -discard); returns the number of images embedded. The file at exepath +#must carry NO appended payload - a resource update rewrites the image. +proc ::punkboot::kit_icon_embed_twapi {exepath icopath {group_override {}}} { + 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" + } + set old_icons [list] + set old_groups [list] + set libh [twapi::load_library $exepath -datafile] + try { + set resources [twapi::extract_resources $libh] + foreach {restype var} {3 old_icons 14 old_groups} { + if {[dict exists $resources $restype]} { + foreach rname [dict keys [dict get $resources $restype]] { + foreach rlang [dict keys [dict get $resources $restype $rname]] { + lappend $var [list $rname $rlang] + } + } + } + } + } trap {TWAPI_WIN32 1812} {} { + #no resource section at all - nothing to enumerate + } finally { + twapi::free_library $libh + } + set group_name 1 + set group_lang 1033 + if {[llength $old_groups]} { + lassign [lindex $old_groups 0] group_name group_lang + } + if {$group_override ne ""} { + set group_name $group_override + } + set update_h [twapi::begin_resource_update $exepath] + try { + foreach entry $old_icons { + lassign $entry rname rlang + twapi::delete_resource $update_h 3 $rname $rlang + } + foreach entry $old_groups { + lassign $entry rname rlang + twapi::delete_resource $update_h 14 $rname $rlang + } + set grouphdr [binary format ttt 0 1 $ico_count] + for {set i 0} {$i < $ico_count} {incr i} { + #.ico ICONDIRENTRY is 16 bytes; GRPICONDIRENTRY is 14 - the trailing + #4-byte image offset becomes a 2-byte icon resource id. Raw width and + #height bytes are carried as-is (0 means 256 in both formats). + set off [expr {6 + 16 * $i}] + binary scan $icondata "@${off} cu cu cu cu tu tu nu nu" w h cc rsv planes bits bytesinres imgoff + set iconid [expr {$i + 1}] + append grouphdr [binary format "cu cu cu cu tu tu nu tu" $w $h $cc $rsv $planes $bits $bytesinres $iconid] + twapi::update_resource $update_h 3 $iconid $group_lang [string range $icondata $imgoff [expr {$imgoff + $bytesinres - 1}]] + } + twapi::update_resource $update_h 14 $group_name $group_lang $grouphdr + } on error {msg ropts} { + catch {twapi::end_resource_update $update_h -discard} + return -options $ropts $msg + } + twapi::end_resource_update $update_h + return $ico_count +} +#G-057 single internal entry point. Decides the kit's icon (override: root +#punkshell.ico in the kit's own custom .vfs folder, pre-merge; else the project +#default src/runtime/punkshell.ico), writes/refreshes the kit's +#.resources.toml sidecar in the build folder, and - when the target +#has PE resources and this host can write them - stamps a per-kit copy of the +#payload-free raw runtime prefix. Returns {wrap_runtime sidecarpath}: +#wrap_runtime is the runtime prefix the assembly step must consume (the stamped +#copy when embedding happened, the untouched raw prefix otherwise). +proc ::punkboot::kit_icon_process {targetkit rt_target rtname kit_type sourcefolder vfstail raw_runtime buildfolder} { + set repo_root [file dirname $sourcefolder] + set override_icon $sourcefolder/vfs/$vfstail/punkshell.ico + if {[file isfile $override_icon]} { + set icon_origin override + set icon_path $override_icon + } else { + set icon_origin default + set icon_path $sourcefolder/runtime/punkshell.ico + } + set have_icon [file isfile $icon_path] + #RT_GROUP_ICON name policy (G-128, user decision 2026-07-29): derived from + #the icon FILE's rootname, uppercased per the RC convention - punkshell.ico + #gives PUNKSHELL for the default icon and for per-kit overrides alike + #(overrides are by convention also named punkshell.ico). Applied by + #whichever mechanism embeds; language stays adopted from the replaced group. + set icon_group "" + if {$have_icon} { + set icon_group [string toupper [file rootname [file tail $icon_path]]] + } + set embedding none + set status "" + set reason "" + if {![string match "win32-*" $rt_target]} { + set status not-applicable + set reason "target $rt_target has no PE resources" + } elseif {$rtname eq "-"} { + set status not-applicable + set reason "runtimeless .kit artifact has no PE container" + } elseif {$kit_type in {cookit cookfs}} { + set status unavailable + set reason "kit type $kit_type edits the payload in place inside the mounted runtime - no payload-free stub moment to stamp" + } elseif {!$have_icon} { + set status unavailable + set reason "icon file not found at $icon_path" + } elseif {$raw_runtime in {{} -} || ![file isfile $raw_runtime]} { + set status unavailable + set reason "extraction produced no raw runtime prefix to stamp" + } else { + #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 + if {$status eq ""} { + set stamped $buildfolder/iconed_$targetkit + file copy -force $raw_runtime $stamped + catch {exec chmod +w $stamped} + if {$punkres_ok} { + set embedding punkres + set embed_cmd [list ::punkboot::kit_icon_embed_punkres $punkres_info $stamped $icon_path $icon_group] + } else { + set embedding twapi + set embed_cmd [list ::punkboot::kit_icon_embed_twapi $stamped $icon_path $icon_group] + } + 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 status embedded + set wrap_runtime $stamped + 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)" + } else { + puts stdout " kit icon (G-057): embedding UNAVAILABLE for $targetkit - $reason (sidecar still written)" + } + set esc ::punkboot::kiticon::toml_escape + set lines [list] + lappend lines "# kit resource record (schema 1) for the kit artifact named by this file" + lappend lines "# with the .resources.toml suffix stripped. Emitted by the make.tcl bake" + lappend lines "# icon step (G-057). Optional and advisory; format: bin/AGENTS.md." + lappend lines "schema = 1" + lappend lines "kit = \"[$esc $targetkit]\"" + lappend lines "target = \"[$esc $rt_target]\"" + lappend lines "kit_type = \"[$esc $kit_type]\"" + lappend lines "icon_origin = \"$icon_origin\"" + lappend lines "icon_source = \"[$esc [punkcheck::lib::path_relative $repo_root $icon_path]]\"" + if {$have_icon} { + lappend lines "icon_hash = \"sha256:[::punkboot::kiticon::sha256_file $icon_path]\"" + set ao [::punkboot::kiticon::assetorigin_read $icon_path] + if {[dict exists $ao source]} { + lappend lines "icon_provenance_source = \"[$esc [dict get $ao source]]\"" + } + if {[dict exists $ao source_hash]} { + lappend lines "icon_provenance_source_hash = \"[$esc [dict get $ao source_hash]]\"" + } + lappend lines "icon_group = \"[$esc $icon_group]\"" + } + lappend lines "embedding = \"$embedding\"" + lappend lines "embedding_status = \"$status\"" + lappend lines "embedding_reason = \"[$esc $reason]\"" + set sidecar $buildfolder/$targetkit.resources.toml + ::punkboot::kiticon::writefile_ifchanged $sidecar "[join $lines \n]\n" + return [list $wrap_runtime $sidecar] +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + set startdir [pwd] puts stdout "Found [llength $vfs_tails] .vfs folders - checking each for executables that may need to be built" cd [file dirname $buildfolder] @@ -6461,6 +6964,11 @@ foreach vfstail $vfs_tails { $vfs_event targetset_addsource $sourcefolder/vfs/_config ;#some files linked via fauxlink - need to detect change $vfs_event targetset_addsource $sourcefolder/vfs/_vfscommon.vfs $vfs_event targetset_addsource $sourcefolder/vfs/$vfstail + if {[file isfile $sourcefolder/runtime/punkshell.ico]} { + #G-057: the default kit icon is a build input - a changed icon rebuilds + #kits (an override icon lives inside the .vfs folder, already a source) + $vfs_event targetset_addsource $sourcefolder/runtime/punkshell.ico + } if {$rtname ne "-"} { set building_runtime $buildfolder/build_$runtime_fullname ;#working copy of runtime executable - (possibly with kit/zipfs/cookfs etc attached!) $vfs_event targetset_addsource $building_runtime @@ -6857,6 +7365,13 @@ foreach vfstail $vfs_tails { continue } + #G-057 kit icon step: sidecar for every kit; PE icon embedding where + #the target has PE resources and this host can write them. Stub-first: + #the assemblers below consume wrap_runtime - the stamped per-kit copy + #of the payload-free raw prefix when embedding happened, the untouched + #raw prefix otherwise. Runtime store originals are never modified. + lassign [::punkboot::kit_icon_process $targetkit $rt_target $rtname $target_kit_type $sourcefolder $vfstail $raw_runtime $buildfolder] wrap_runtime kiticon_sidecar + set wrapvfs $targetvfs switch -- $target_kit_type { zip { @@ -6886,14 +7401,14 @@ foreach vfstail $vfs_tails { } if {[info commands ::tcl::zipfs::mkimg] ne ""} { #note - as at 2024-08 - there is some discussion about the interface to mkimg - it is considered unstable (may change to -option value syntax) - puts stderr "calling: tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs \"\" $raw_runtime" - tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs "" $raw_runtime + puts stderr "calling: tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs \"\" $wrap_runtime" + tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs "" $wrap_runtime } else { #G-122: the driving tcl has no zipfs (8.6) - assemble the same #kind of image by concatenation. The result mounts on any #zipfs-capable runtime (archive-start-relative offsets). puts stderr "WARNING: tcl shell '[info nameofexecutable]' has no zipfs - assembling zip kit $targetkit by concatenation (raw runtime + mkzip)" - ::punkboot::assemble_zipcat_image $raw_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip + ::punkboot::assemble_zipcat_image $wrap_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip } } result ]} { set failmsg "zipfs mkimg failed with msg: $result" @@ -6930,7 +7445,7 @@ foreach vfstail $vfs_tails { } #'archive' based zip offsets - editable in 7z,peazip - ::punkboot::assemble_zipcat_image $raw_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip + ::punkboot::assemble_zipcat_image $wrap_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip } result ]} { set failmsg "creating zipcat image failed with msg: $result" puts stderr "creating image (zipcat) $targetkit failed" @@ -7005,13 +7520,13 @@ foreach vfstail $vfs_tails { cd [file dirname $building_runtime] ;#make sure SDX is called with the same working dir as the files we're working on. if {[catch { if {$rtname ne "-"} { - exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $raw_runtime {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $wrap_runtime {*}$verbose } else { exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose } } result]} { if {$rtname ne "-"} { - set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $raw_runtime {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $wrap_runtime {*}$verbose failed with msg: $result" } else { set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" } @@ -7175,6 +7690,21 @@ foreach vfstail $vfs_tails { # -- --- --- --- --- --- $vfs_event targetset_end OK + #G-134 post-assembly offset-style pin: the pipeline emits + #ARCHIVE-relative zip payloads by construction - this probe makes + #that a checked contract. Advisory per the G-133 posture: a + #file-relative image earns a recapped BUILD-WARNING (the G-128 + #stamper would refuse it by default) and the kit still builds and + #deploys; plain/none (no attached zip - the metakit kit shape) and + #unreadable results are silence. One EOCD scan + CD walk per + #REBUILT kit only. + lassign [::punkboot::get_kit_offsetstyle_report $buildfolder/$targetkit] offprobe_available offprobe + if {!$offprobe_available} { + puts stderr "NOTE: kit offset-style pin unavailable (punkboot::utils kit_offsetstyle_report not loadable from bootsupport) - continuing without it" + } elseif {[dict get $offprobe offsetstyle] eq "file"} { + ::punkboot::print_build_warnings [list "kit $targetkit assembled with a FILE-relative zip payload (G-134 pin: the pipeline emits archive-relative by construction; the G-128 stamper refuses file-relative by default) - investigate this kit's assembly path"] + } + #G-133 post-build smoke-require probe. Only an actual 'package require' #inside the built artifact observes package RESOLUTION - the wrong-arch #version-shadowing class is invisible to every structural check because @@ -7249,6 +7779,10 @@ foreach vfstail $vfs_tails { puts stdout "$deployment_folder/$targetkit" after 300 file copy $buildfolder/$targetkit $deployment_folder/$targetkit + #G-057: the kit resource sidecar ships with the kit + if {[file isfile $buildfolder/$targetkit.resources.toml]} { + file copy -force $buildfolder/$targetkit.resources.toml $deployment_folder/$targetkit.resources.toml + } lappend installed_kits $targetkit # -- ---------- $bin_event targetset_end OK @@ -7257,6 +7791,12 @@ foreach vfstail $vfs_tails { set skipped_kit_install 1 puts stderr "." puts stdout "Skipping kit install for $targetkit with vfs $vfstail runtime $rtname - no change detected" + #G-057: keep the deployed resource sidecar current even when the kit + #itself is unchanged (the record can be newer than the kit rebuild - + #e.g the sidecar feature arriving, or a host-capability change) + if {[::punkboot::kiticon::copy_ifchanged $buildfolder/$targetkit.resources.toml $deployment_folder/$targetkit.resources.toml] eq "copied"} { + puts stdout " kit icon (G-057): refreshed deployed sidecar $deployment_folder/$targetkit.resources.toml" + } lappend skipped_kit_installs [list kit $targetkit reason "no change detected"] $bin_event targetset_end SKIPPED } diff --git a/src/modules/punkboot/utils-999999.0a1.0.tm b/src/modules/punkboot/utils-999999.0a1.0.tm index 5b188262..1073b1d5 100644 --- a/src/modules/punkboot/utils-999999.0a1.0.tm +++ b/src/modules/punkboot/utils-999999.0a1.0.tm @@ -23,7 +23,7 @@ package require Tcl 8.6- # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ namespace eval punkboot::utils { variable version 0.1.0 - namespace export binary_arch_classify parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report + namespace export binary_arch_classify kit_offsetstyle_report parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report namespace eval argdoc { @@ -736,6 +736,65 @@ namespace eval punkboot::utils { dict set report ok [expr {[llength $mismatches] == 0}] return $report } + + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::punkboot::utils::kit_offsetstyle_report + @cmd -name "::punkboot::utils::kit_offsetstyle_report"\ + -summary\ + "Advisory zip offset-style probe of an assembled kit image (G-134)"\ + -help\ + "Probes a finished kit image file with punk::zip::archive_info and + reports the offset convention of any attached zip payload. The + punkshell bake pipeline emits ARCHIVE-relative payloads by + construction; this probe makes that a checked contract - a + 'file' result is the pipeline-regression signal make.tcl + surfaces as a recapped BUILD-WARNING (advisory: the kit still + builds and deploys). 'plain' (the file is a bare zip), 'none' + (no zip attached - e.g the metakit kit shape, or any non-zip + file) and 'unreadable' are silence, not warnings: the pin + covers the offset convention of an attached zip, never the + presence of one. Purely structural: one EOCD scan plus a + central-directory walk, nothing executed, so cross-target kit + images are covered. + + Returned dict keys: + offsetstyle archive | file | plain | none | unreadable + dataoffset payload base offset when archive_info supplies + one (\"\" otherwise) + detail diagnostic text for unreadable (missing file, + punk::zip unavailable, archive_info error); + empty otherwise" + @leaders + kitpath -type string -optional 0 -help\ + "Path of the assembled kit image file to probe" + }] + } + proc kit_offsetstyle_report {kitpath} { + set report [dict create offsetstyle unreadable dataoffset "" detail ""] + if {![file isfile $kitpath]} { + dict set report detail "no such file: $kitpath" + return $report + } + if {[catch {package require punk::zip} errM]} { + dict set report detail "punk::zip not available: $errM" + return $report + } + if {[catch {punk::zip::archive_info $kitpath} zinfo]} { + dict set report detail "archive_info error: $zinfo" + return $report + } + if {[dict exists $zinfo offsetstyle]} { + dict set report offsetstyle [dict get $zinfo offsetstyle] + } else { + dict set report detail "archive_info returned no offsetstyle" + return $report + } + if {[dict exists $zinfo dataoffset]} { + dict set report dataoffset [dict get $zinfo dataoffset] + } + return $report + } } namespace eval ::punk::args::register { diff --git a/src/modules/punkboot/utils-buildversion.txt b/src/modules/punkboot/utils-buildversion.txt index c85f47cb..02323dd1 100644 --- a/src/modules/punkboot/utils-buildversion.txt +++ b/src/modules/punkboot/utils-buildversion.txt @@ -1,6 +1,7 @@ -0.5.0 +0.6.0 #First line must be a semantic version number #all other lines are ignored. +#0.6.0 - added kit_offsetstyle_report (advisory zip offset-style probe of an assembled kit image via punk::zip::archive_info; backs make.tcl's G-134 archive-relative output pin) #0.5.0 - added binary_arch_classify (PE/ELF/Mach-O header classifier, honest unknowns), platform_expected_binary, platform_discriminated_segment and vfs_binary_arch_report (advisory payload/target binary-arch scan of a merged .vfs tree with platform-subdir exemption; backs make.tcl's G-133 payload/target consistency checks) #0.4.0 - vfs_boot_library_report recognises a third tcl-library convention: tcl./ at the vfs root, used by runtimes whose archive mounts at the executable's own path rather than //zipfs:/app (the androwish/undroidwish zipfs backport for 8.6). Such a runtime was being refused as unbootable. 'checked' gains the new location. #0.3.0 - added vfs_boot_library_report (structural boot-precondition check of an assembled .vfs tree; backs make.tcl's G-125 unbootable-kit deploy gate) diff --git a/src/project_layouts/vendor/punk/basic/src/make.tcl b/src/project_layouts/vendor/punk/basic/src/make.tcl index 0658f3c4..1a4aafe3 100644 --- a/src/project_layouts/vendor/punk/basic/src/make.tcl +++ b/src/project_layouts/vendor/punk/basic/src/make.tcl @@ -2300,15 +2300,24 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase) (gate) --> refused: kit listed under FAILED KITS, nothing | built, nothing deployed, bin/ untouched | - | zip image appended to the runtime exe: tcl::zipfs::mkimg, - | or (driving tcl without zipfs, e.g 8.6) raw-runtime split - | + punk::zip::mkzip + concatenation - both mount the same + | kit icon step (G-057): .resources.toml sidecar records + | the icon choice for every target (default src/runtime/ + | punkshell.ico; override = punkshell.ico in the kit's own .vfs + | folder, pre-merge); win32 targets on a twapi-capable host get + | the icon stamped into a per-kit copy of the raw runtime stub + | BEFORE the payload attach (non-PE target: not applicable; + | incapable host: distinct notice - sidecar written either way) + | + | zip image appended to the (stamped) runtime exe: tcl::zipfs:: + | mkimg, or (driving tcl without zipfs, e.g 8.6) raw-runtime + | split + punk::zip::mkzip + concatenation - both mount the same v - src/_build/.exe + src/_build/.exe (+ .exe.resources.toml) | | smoke-require probe (host-runnable kits with declared | packages): plain 'package require' inside the artifact [K11] | deploy step: delete old bin/.exe, copy new one in + | (the resource sidecar deploys beside the kit) v bin/.exe <-- FAILS (kit listed under FAILED KITS) if a running shell still holds the old exe [K5] @@ -3469,6 +3478,20 @@ proc ::punkboot::get_vfs_binary_arch_report {vfsfolder targetplatform} { } return [list 1 [::punkboot::utils::vfs_binary_arch_report $vfsfolder $targetplatform]] } +#Availability probe + advisory zip offset-style check of an assembled kit image +#(G-134), shared by the bake loop and the 'check' command report. Same guarded- +#require treatment as the checks above: a stale bootsupport snapshot degrades +#the pin to a NOTE. Returns {available report}. +proc ::punkboot::get_kit_offsetstyle_report {kitpath} { + set available [expr {\ + ![catch {package require punkboot::utils}]\ + && [llength [info commands ::punkboot::utils::kit_offsetstyle_report]]\ + }] + if {!$available} { + return [list 0 [dict create]] + } + return [list 1 [::punkboot::utils::kit_offsetstyle_report $kitpath]] +} #G-133 smoke-require probe: execute a freshly built kit artifact via its 'tclsh' #subcommand and plain-'package require' each declared package inside it. This is the #only check that observes actual package RESOLUTION in the artifact - the punkluck86 @@ -3814,6 +3837,20 @@ if {$::punkboot::command eq "check"} { puts stdout "payload/target arch scan (G-133): UNAVAILABLE (punkboot::utils vfs_binary_arch_report not loadable from bootsupport)" puts stdout " bake will proceed with a NOTE and cannot flag wrong-arch payload libraries - run 'make.tcl modules' then 'make.tcl bootsupport'." } + # Kit offset-style pin (G-134) - advisory archive-relative output contract + puts stdout $sep + #availability probe only - the path deliberately does not exist + lassign [::punkboot::get_kit_offsetstyle_report [file join $projectroot __punkboot_offsetstyle_probe__]] _off_available _off_report + if {$_off_available} { + puts stdout "kit offset-style pin (G-134): ACTIVE (advisory)" + puts stdout " bake probes each assembled kit image with punk::zip::archive_info and emits a recapped" + puts stdout " BUILD-WARNING when an attached zip payload records FILE-relative offsets (the pipeline" + puts stdout " emits archive-relative by construction; the G-128 stamper refuses file-relative by default)." + puts stdout " plain/none results (no attached zip - e.g the metakit kit shape) are silence, not warnings." + } else { + puts stdout "kit offset-style pin (G-134): UNAVAILABLE (punkboot::utils kit_offsetstyle_report not loadable from bootsupport)" + puts stdout " bake will proceed with a NOTE and cannot flag a file-relative kit payload - run 'make.tcl modules' then 'make.tcl bootsupport'." + } set _smoke_declared [list] if {[file exists $sourcefolder/runtime/mapvfs.config]} { set _smokemodel [punkboot::lib::mapvfs_parse $sourcefolder/runtime/mapvfs.config [file dirname $sourcefolder]/bin/runtime $sourcefolder $::punkboot::target_platform] @@ -6296,6 +6333,472 @@ proc ::punkboot::assemble_zipcat_image {raw_runtime wrapvfs outfile zipfile} { return $zipbytes } +# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- +#G-057 kit icon step. Every built kit gets a .resources.toml sidecar +#recording the build-time icon choice (default src/runtime/punkshell.ico, +#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. Two mechanisms share one semantic (delete ALL +#icon/group entries, write the .ico images as ids 1..N; the RT_GROUP_ICON is +#NAMED from the icon file - uppercased rootname, so punkshell.ico gives +#PUNKSHELL (user decision 2026-07-29, RC uppercase convention) - with language +#adopted from the first replaced group; 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 +#scriptlib/developer/assetorigin_check.tcl - each stays standalone: make.tcl +#must not depend on scriptlib, and this step must not depend on bootsupport +#freshness; bootsupport carries only sha1/md5). Memoized per path per run. +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 + 0xe49b69c1 0xefbe4786 0x0fc19dc6 0x240ca1cc 0x2de92c6f 0x4a7484aa 0x5cb0a9dc 0x76f988da + 0x983e5152 0xa831c66d 0xb00327c8 0xbf597fc7 0xc6e00bf3 0xd5a79147 0x06ca6351 0x14292967 + 0x27b70a85 0x2e1b2138 0x4d2c6dfc 0x53380d13 0x650a7354 0x766a0abb 0x81c2c92e 0x92722c85 + 0xa2bfe8a1 0xa81a664b 0xc24b8b70 0xc76c51a3 0xd192e819 0xd6990624 0xf40e3585 0x106aa070 + 0x19a4c116 0x1e376c08 0x2748774c 0x34b0bcb5 0x391c0cb3 0x4ed8aa4a 0x5b9cca4f 0x682e6ff3 + 0x748f82ee 0x78a5636f 0x84c87814 0x8cc70208 0x90befffa 0xa4506ceb 0xbef9a3f7 0xc67178f2 + } +} +proc ::punkboot::kiticon::sha256_data {data} { + variable K + set bitlen [expr {wide([string length $data]) * 8}] + append data \x80 + set pad [expr {(56 - [string length $data] % 64 + 64) % 64}] + append data [string repeat \x00 $pad] [binary format W $bitlen] + set h0 0x6a09e667; set h1 0xbb67ae85; set h2 0x3c6ef372; set h3 0xa54ff53a + set h4 0x510e527f; set h5 0x9b05688c; set h6 0x1f83d9ab; set h7 0x5be0cd19 + set n [string length $data] + for {set off 0} {$off < $n} {incr off 64} { + binary scan [string range $data $off [expr {$off + 63}]] Iu16 w + for {set t 16} {$t < 64} {incr t} { + set x [lindex $w [expr {$t - 15}]] + set s0 [expr {((($x >> 7) | ($x << 25)) ^ (($x >> 18) | ($x << 14)) ^ ($x >> 3)) & 0xffffffff}] + set x [lindex $w [expr {$t - 2}]] + set s1 [expr {((($x >> 17) | ($x << 15)) ^ (($x >> 19) | ($x << 13)) ^ ($x >> 10)) & 0xffffffff}] + lappend w [expr {([lindex $w [expr {$t - 16}]] + $s0 + [lindex $w [expr {$t - 7}]] + $s1) & 0xffffffff}] + } + set a $h0; set b $h1; set c $h2; set d $h3 + set e $h4; set f $h5; set g $h6; set h $h7 + for {set t 0} {$t < 64} {incr t} { + set S1 [expr {((($e >> 6) | ($e << 26)) ^ (($e >> 11) | ($e << 21)) ^ (($e >> 25) | ($e << 7))) & 0xffffffff}] + set ch [expr {(($e & $f) ^ (~$e & $g)) & 0xffffffff}] + set T1 [expr {($h + $S1 + $ch + [lindex $K $t] + [lindex $w $t]) & 0xffffffff}] + set S0 [expr {((($a >> 2) | ($a << 30)) ^ (($a >> 13) | ($a << 19)) ^ (($a >> 22) | ($a << 10))) & 0xffffffff}] + set maj [expr {($a & $b) ^ ($a & $c) ^ ($b & $c)}] + set T2 [expr {($S0 + $maj) & 0xffffffff}] + set h $g; set g $f; set f $e; set e [expr {($d + $T1) & 0xffffffff}] + set d $c; set c $b; set b $a; set a [expr {($T1 + $T2) & 0xffffffff}] + } + set h0 [expr {($h0 + $a) & 0xffffffff}]; set h1 [expr {($h1 + $b) & 0xffffffff}] + set h2 [expr {($h2 + $c) & 0xffffffff}]; set h3 [expr {($h3 + $d) & 0xffffffff}] + set h4 [expr {($h4 + $e) & 0xffffffff}]; set h5 [expr {($h5 + $f) & 0xffffffff}] + set h6 [expr {($h6 + $g) & 0xffffffff}]; set h7 [expr {($h7 + $h) & 0xffffffff}] + } + return [format %08x%08x%08x%08x%08x%08x%08x%08x $h0 $h1 $h2 $h3 $h4 $h5 $h6 $h7] +} +proc ::punkboot::kiticon::sha256_file {path} { + variable sha256_cache + set key [file normalize $path] + if {[dict exists $sha256_cache $key]} { + return [dict get $sha256_cache $key] + } + set f [open $path rb] + set data [read $f] + close $f + set hex [sha256_data $data] + dict set sha256_cache $key $hex + return $hex +} +proc ::punkboot::kiticon::toml_escape {s} { + return [string map {\\ \\\\ \" \\\"} $s] +} +#Tolerant subset reader for the chosen icon's G-135 assetorigin sidecar - lifts +#source/source_hash so the kit record reaches back to the SVG-derived master. +#Missing sidecar, unknown schema or malformed content simply yields no +#provenance fields (never an error). +proc ::punkboot::kiticon::assetorigin_read {assetpath} { + set sc $assetpath.assetorigin.toml + set out [dict create] + if {![file isfile $sc]} { + return $out + } + if {[catch { + set f [open $sc rb] + set raw [read $f] + close $f + }]} { + return $out + } + foreach line [split [encoding convertfrom utf-8 $raw] \n] { + set line [string trim [string map [list \r {}] $line]] + if {$line eq "" || [string index $line 0] eq "#"} continue + if {[string index $line 0] eq "\["} break + set eq [string first = $line] + if {$eq < 1} continue + set key [string trim [string range $line 0 [expr {$eq - 1}]]] + if {$key ni {schema source source_hash}} continue + set val [string trim [string range $line [expr {$eq + 1}] end]] + if {[string index $val 0] eq "\""} { + set parsed "" + set ok 0 + set len [string length $val] + for {set i 1} {$i < $len} {incr i} { + set chr [string index $val $i] + if {$chr eq "\\"} { + incr i + append parsed [string index $val $i] + } elseif {$chr eq "\""} { + set ok 1 + break + } else { + append parsed $chr + } + } + if {!$ok} continue + set val $parsed + } else { + set cmt [string first # $val] + if {$cmt >= 0} { + set val [string trim [string range $val 0 [expr {$cmt - 1}]]] + } + } + dict set out $key $val + } + if {![dict exists $out schema] || [dict get $out schema] ne "1"} { + return [dict create] + } + return $out +} +proc ::punkboot::kiticon::writefile_ifchanged {path content} { + if {[file exists $path]} { + set f [open $path rb] + set existing [read $f] + close $f + if {$existing eq $content} { + return unchanged + } + } + set f [open $path wb] + puts -nonewline $f $content + close $f + return written +} +proc ::punkboot::kiticon::copy_ifchanged {from to} { + if {![file isfile $from]} { + return absent + } + set f [open $from rb] + set fromdata [read $f] + close $f + if {[file exists $to]} { + set f [open $to rb] + set todata [read $f] + close $f + if {$todata eq $fromdata} { + return unchanged + } + } + file copy -force $from $to + return copied +} +#Memoized once per run: can this host's tclsh write PE resources? Returns +#{1 {}} or {0 reason} with the reason distinguishing cross-host from +#twapi-unavailable (the acceptance requires distinct notices). +proc ::punkboot::kiticon::twapi_available {} { + variable twapi_state + if {$twapi_state eq "ok"} { + return [list 1 {}] + } + if {$twapi_state ne ""} { + return [list 0 $twapi_state] + } + if {[punkboot::lib::platform_process_family $::punkboot::host_platform] ne "windows"} { + set twapi_state "cross-host: win32 target baked on a $::punkboot::host_platform host cannot run the twapi resource APIs (portable post-hoc stamping is the G-128 remedy)" + return [list 0 $twapi_state] + } + if {[catch {package require twapi_resource}] && [catch {package require twapi}]} { + set twapi_state "twapi resource APIs not loadable in this tclsh ([info nameofexecutable]) - the vendored twapi under vendorlib_tcl/win32-x86_64 serves native windows tclsh builds" + return [list 0 $twapi_state] + } + if {![llength [info commands ::twapi::begin_resource_update]]} { + set twapi_state "twapi loaded but its resource-update APIs are absent" + return [list 0 $twapi_state] + } + 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, or fetch the prebuilt punkbin artifact (win32-x86_64/tools/punkres-*; route: src/tools/punkres/PROVENANCE.md) (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 (a non-empty +#group_override renames the group via 'set-icon -group' - the seam's naming +#policy; language and codepage adoption unchanged), 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 {group_override {}}} { + 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" + } + set cmd [list exec [file nativename $punkres_exe] set-icon] + if {$group_override ne ""} { + lappend cmd -group $group_override + } + #-drop-opaque-overlay: the stub contract is a payload-free prefix, so any + #non-zip trailing bytes are build detritus - concretely the COFF + #symbol/debug tables mingw-built runtimes carry after their sections, + #which ride into the extraction head. The twapi arm's EndUpdateResource + #has always silently stripped exactly those bytes (measured 2026-07-29, + #G-128 detail); punkres drops them deliberately and zeroes the dangling + #symbol-table header pointer. A REAL zip payload on a stub is unaffected + #(the flag is inert for zip-classified overlays, which punkres preserves). + lappend cmd -drop-opaque-overlay + lappend cmd [file nativename $exepath] [file nativename $icopath] 2>@1 + if {[catch $cmd 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 +#written as ids 1..N under the first pre-existing group's name+lang (fallback +#name 1, lang 1033), in one resource-update transaction. A non-empty +#group_override replaces the group NAME only (language adoption unchanged) - +#the same override punkres's 'set-icon -group' applies, so the seam's naming +#policy keeps the two arms in parity (G-128). Raises an error on failure +#(after -discard); returns the number of images embedded. The file at exepath +#must carry NO appended payload - a resource update rewrites the image. +proc ::punkboot::kit_icon_embed_twapi {exepath icopath {group_override {}}} { + 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" + } + set old_icons [list] + set old_groups [list] + set libh [twapi::load_library $exepath -datafile] + try { + set resources [twapi::extract_resources $libh] + foreach {restype var} {3 old_icons 14 old_groups} { + if {[dict exists $resources $restype]} { + foreach rname [dict keys [dict get $resources $restype]] { + foreach rlang [dict keys [dict get $resources $restype $rname]] { + lappend $var [list $rname $rlang] + } + } + } + } + } trap {TWAPI_WIN32 1812} {} { + #no resource section at all - nothing to enumerate + } finally { + twapi::free_library $libh + } + set group_name 1 + set group_lang 1033 + if {[llength $old_groups]} { + lassign [lindex $old_groups 0] group_name group_lang + } + if {$group_override ne ""} { + set group_name $group_override + } + set update_h [twapi::begin_resource_update $exepath] + try { + foreach entry $old_icons { + lassign $entry rname rlang + twapi::delete_resource $update_h 3 $rname $rlang + } + foreach entry $old_groups { + lassign $entry rname rlang + twapi::delete_resource $update_h 14 $rname $rlang + } + set grouphdr [binary format ttt 0 1 $ico_count] + for {set i 0} {$i < $ico_count} {incr i} { + #.ico ICONDIRENTRY is 16 bytes; GRPICONDIRENTRY is 14 - the trailing + #4-byte image offset becomes a 2-byte icon resource id. Raw width and + #height bytes are carried as-is (0 means 256 in both formats). + set off [expr {6 + 16 * $i}] + binary scan $icondata "@${off} cu cu cu cu tu tu nu nu" w h cc rsv planes bits bytesinres imgoff + set iconid [expr {$i + 1}] + append grouphdr [binary format "cu cu cu cu tu tu nu tu" $w $h $cc $rsv $planes $bits $bytesinres $iconid] + twapi::update_resource $update_h 3 $iconid $group_lang [string range $icondata $imgoff [expr {$imgoff + $bytesinres - 1}]] + } + twapi::update_resource $update_h 14 $group_name $group_lang $grouphdr + } on error {msg ropts} { + catch {twapi::end_resource_update $update_h -discard} + return -options $ropts $msg + } + twapi::end_resource_update $update_h + return $ico_count +} +#G-057 single internal entry point. Decides the kit's icon (override: root +#punkshell.ico in the kit's own custom .vfs folder, pre-merge; else the project +#default src/runtime/punkshell.ico), writes/refreshes the kit's +#.resources.toml sidecar in the build folder, and - when the target +#has PE resources and this host can write them - stamps a per-kit copy of the +#payload-free raw runtime prefix. Returns {wrap_runtime sidecarpath}: +#wrap_runtime is the runtime prefix the assembly step must consume (the stamped +#copy when embedding happened, the untouched raw prefix otherwise). +proc ::punkboot::kit_icon_process {targetkit rt_target rtname kit_type sourcefolder vfstail raw_runtime buildfolder} { + set repo_root [file dirname $sourcefolder] + set override_icon $sourcefolder/vfs/$vfstail/punkshell.ico + if {[file isfile $override_icon]} { + set icon_origin override + set icon_path $override_icon + } else { + set icon_origin default + set icon_path $sourcefolder/runtime/punkshell.ico + } + set have_icon [file isfile $icon_path] + #RT_GROUP_ICON name policy (G-128, user decision 2026-07-29): derived from + #the icon FILE's rootname, uppercased per the RC convention - punkshell.ico + #gives PUNKSHELL for the default icon and for per-kit overrides alike + #(overrides are by convention also named punkshell.ico). Applied by + #whichever mechanism embeds; language stays adopted from the replaced group. + set icon_group "" + if {$have_icon} { + set icon_group [string toupper [file rootname [file tail $icon_path]]] + } + set embedding none + set status "" + set reason "" + if {![string match "win32-*" $rt_target]} { + set status not-applicable + set reason "target $rt_target has no PE resources" + } elseif {$rtname eq "-"} { + set status not-applicable + set reason "runtimeless .kit artifact has no PE container" + } elseif {$kit_type in {cookit cookfs}} { + set status unavailable + set reason "kit type $kit_type edits the payload in place inside the mounted runtime - no payload-free stub moment to stamp" + } elseif {!$have_icon} { + set status unavailable + set reason "icon file not found at $icon_path" + } elseif {$raw_runtime in {{} -} || ![file isfile $raw_runtime]} { + set status unavailable + set reason "extraction produced no raw runtime prefix to stamp" + } else { + #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 + if {$status eq ""} { + set stamped $buildfolder/iconed_$targetkit + file copy -force $raw_runtime $stamped + catch {exec chmod +w $stamped} + if {$punkres_ok} { + set embedding punkres + set embed_cmd [list ::punkboot::kit_icon_embed_punkres $punkres_info $stamped $icon_path $icon_group] + } else { + set embedding twapi + set embed_cmd [list ::punkboot::kit_icon_embed_twapi $stamped $icon_path $icon_group] + } + 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 status embedded + set wrap_runtime $stamped + 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)" + } else { + puts stdout " kit icon (G-057): embedding UNAVAILABLE for $targetkit - $reason (sidecar still written)" + } + set esc ::punkboot::kiticon::toml_escape + set lines [list] + lappend lines "# kit resource record (schema 1) for the kit artifact named by this file" + lappend lines "# with the .resources.toml suffix stripped. Emitted by the make.tcl bake" + lappend lines "# icon step (G-057). Optional and advisory; format: bin/AGENTS.md." + lappend lines "schema = 1" + lappend lines "kit = \"[$esc $targetkit]\"" + lappend lines "target = \"[$esc $rt_target]\"" + lappend lines "kit_type = \"[$esc $kit_type]\"" + lappend lines "icon_origin = \"$icon_origin\"" + lappend lines "icon_source = \"[$esc [punkcheck::lib::path_relative $repo_root $icon_path]]\"" + if {$have_icon} { + lappend lines "icon_hash = \"sha256:[::punkboot::kiticon::sha256_file $icon_path]\"" + set ao [::punkboot::kiticon::assetorigin_read $icon_path] + if {[dict exists $ao source]} { + lappend lines "icon_provenance_source = \"[$esc [dict get $ao source]]\"" + } + if {[dict exists $ao source_hash]} { + lappend lines "icon_provenance_source_hash = \"[$esc [dict get $ao source_hash]]\"" + } + lappend lines "icon_group = \"[$esc $icon_group]\"" + } + lappend lines "embedding = \"$embedding\"" + lappend lines "embedding_status = \"$status\"" + lappend lines "embedding_reason = \"[$esc $reason]\"" + set sidecar $buildfolder/$targetkit.resources.toml + ::punkboot::kiticon::writefile_ifchanged $sidecar "[join $lines \n]\n" + return [list $wrap_runtime $sidecar] +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + set startdir [pwd] puts stdout "Found [llength $vfs_tails] .vfs folders - checking each for executables that may need to be built" cd [file dirname $buildfolder] @@ -6461,6 +6964,11 @@ foreach vfstail $vfs_tails { $vfs_event targetset_addsource $sourcefolder/vfs/_config ;#some files linked via fauxlink - need to detect change $vfs_event targetset_addsource $sourcefolder/vfs/_vfscommon.vfs $vfs_event targetset_addsource $sourcefolder/vfs/$vfstail + if {[file isfile $sourcefolder/runtime/punkshell.ico]} { + #G-057: the default kit icon is a build input - a changed icon rebuilds + #kits (an override icon lives inside the .vfs folder, already a source) + $vfs_event targetset_addsource $sourcefolder/runtime/punkshell.ico + } if {$rtname ne "-"} { set building_runtime $buildfolder/build_$runtime_fullname ;#working copy of runtime executable - (possibly with kit/zipfs/cookfs etc attached!) $vfs_event targetset_addsource $building_runtime @@ -6857,6 +7365,13 @@ foreach vfstail $vfs_tails { continue } + #G-057 kit icon step: sidecar for every kit; PE icon embedding where + #the target has PE resources and this host can write them. Stub-first: + #the assemblers below consume wrap_runtime - the stamped per-kit copy + #of the payload-free raw prefix when embedding happened, the untouched + #raw prefix otherwise. Runtime store originals are never modified. + lassign [::punkboot::kit_icon_process $targetkit $rt_target $rtname $target_kit_type $sourcefolder $vfstail $raw_runtime $buildfolder] wrap_runtime kiticon_sidecar + set wrapvfs $targetvfs switch -- $target_kit_type { zip { @@ -6886,14 +7401,14 @@ foreach vfstail $vfs_tails { } if {[info commands ::tcl::zipfs::mkimg] ne ""} { #note - as at 2024-08 - there is some discussion about the interface to mkimg - it is considered unstable (may change to -option value syntax) - puts stderr "calling: tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs \"\" $raw_runtime" - tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs "" $raw_runtime + puts stderr "calling: tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs \"\" $wrap_runtime" + tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs "" $wrap_runtime } else { #G-122: the driving tcl has no zipfs (8.6) - assemble the same #kind of image by concatenation. The result mounts on any #zipfs-capable runtime (archive-start-relative offsets). puts stderr "WARNING: tcl shell '[info nameofexecutable]' has no zipfs - assembling zip kit $targetkit by concatenation (raw runtime + mkzip)" - ::punkboot::assemble_zipcat_image $raw_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip + ::punkboot::assemble_zipcat_image $wrap_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip } } result ]} { set failmsg "zipfs mkimg failed with msg: $result" @@ -6930,7 +7445,7 @@ foreach vfstail $vfs_tails { } #'archive' based zip offsets - editable in 7z,peazip - ::punkboot::assemble_zipcat_image $raw_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip + ::punkboot::assemble_zipcat_image $wrap_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip } result ]} { set failmsg "creating zipcat image failed with msg: $result" puts stderr "creating image (zipcat) $targetkit failed" @@ -7005,13 +7520,13 @@ foreach vfstail $vfs_tails { cd [file dirname $building_runtime] ;#make sure SDX is called with the same working dir as the files we're working on. if {[catch { if {$rtname ne "-"} { - exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $raw_runtime {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $wrap_runtime {*}$verbose } else { exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose } } result]} { if {$rtname ne "-"} { - set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $raw_runtime {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $wrap_runtime {*}$verbose failed with msg: $result" } else { set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" } @@ -7175,6 +7690,21 @@ foreach vfstail $vfs_tails { # -- --- --- --- --- --- $vfs_event targetset_end OK + #G-134 post-assembly offset-style pin: the pipeline emits + #ARCHIVE-relative zip payloads by construction - this probe makes + #that a checked contract. Advisory per the G-133 posture: a + #file-relative image earns a recapped BUILD-WARNING (the G-128 + #stamper would refuse it by default) and the kit still builds and + #deploys; plain/none (no attached zip - the metakit kit shape) and + #unreadable results are silence. One EOCD scan + CD walk per + #REBUILT kit only. + lassign [::punkboot::get_kit_offsetstyle_report $buildfolder/$targetkit] offprobe_available offprobe + if {!$offprobe_available} { + puts stderr "NOTE: kit offset-style pin unavailable (punkboot::utils kit_offsetstyle_report not loadable from bootsupport) - continuing without it" + } elseif {[dict get $offprobe offsetstyle] eq "file"} { + ::punkboot::print_build_warnings [list "kit $targetkit assembled with a FILE-relative zip payload (G-134 pin: the pipeline emits archive-relative by construction; the G-128 stamper refuses file-relative by default) - investigate this kit's assembly path"] + } + #G-133 post-build smoke-require probe. Only an actual 'package require' #inside the built artifact observes package RESOLUTION - the wrong-arch #version-shadowing class is invisible to every structural check because @@ -7249,6 +7779,10 @@ foreach vfstail $vfs_tails { puts stdout "$deployment_folder/$targetkit" after 300 file copy $buildfolder/$targetkit $deployment_folder/$targetkit + #G-057: the kit resource sidecar ships with the kit + if {[file isfile $buildfolder/$targetkit.resources.toml]} { + file copy -force $buildfolder/$targetkit.resources.toml $deployment_folder/$targetkit.resources.toml + } lappend installed_kits $targetkit # -- ---------- $bin_event targetset_end OK @@ -7257,6 +7791,12 @@ foreach vfstail $vfs_tails { set skipped_kit_install 1 puts stderr "." puts stdout "Skipping kit install for $targetkit with vfs $vfstail runtime $rtname - no change detected" + #G-057: keep the deployed resource sidecar current even when the kit + #itself is unchanged (the record can be newer than the kit rebuild - + #e.g the sidecar feature arriving, or a host-capability change) + if {[::punkboot::kiticon::copy_ifchanged $buildfolder/$targetkit.resources.toml $deployment_folder/$targetkit.resources.toml] eq "copied"} { + puts stdout " kit icon (G-057): refreshed deployed sidecar $deployment_folder/$targetkit.resources.toml" + } lappend skipped_kit_installs [list kit $targetkit reason "no change detected"] $bin_event targetset_end SKIPPED } diff --git a/src/project_layouts/vendor/punk/project-0.1/src/make.tcl b/src/project_layouts/vendor/punk/project-0.1/src/make.tcl index 0658f3c4..1a4aafe3 100644 --- a/src/project_layouts/vendor/punk/project-0.1/src/make.tcl +++ b/src/project_layouts/vendor/punk/project-0.1/src/make.tcl @@ -2300,15 +2300,24 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase) (gate) --> refused: kit listed under FAILED KITS, nothing | built, nothing deployed, bin/ untouched | - | zip image appended to the runtime exe: tcl::zipfs::mkimg, - | or (driving tcl without zipfs, e.g 8.6) raw-runtime split - | + punk::zip::mkzip + concatenation - both mount the same + | kit icon step (G-057): .resources.toml sidecar records + | the icon choice for every target (default src/runtime/ + | punkshell.ico; override = punkshell.ico in the kit's own .vfs + | folder, pre-merge); win32 targets on a twapi-capable host get + | the icon stamped into a per-kit copy of the raw runtime stub + | BEFORE the payload attach (non-PE target: not applicable; + | incapable host: distinct notice - sidecar written either way) + | + | zip image appended to the (stamped) runtime exe: tcl::zipfs:: + | mkimg, or (driving tcl without zipfs, e.g 8.6) raw-runtime + | split + punk::zip::mkzip + concatenation - both mount the same v - src/_build/.exe + src/_build/.exe (+ .exe.resources.toml) | | smoke-require probe (host-runnable kits with declared | packages): plain 'package require' inside the artifact [K11] | deploy step: delete old bin/.exe, copy new one in + | (the resource sidecar deploys beside the kit) v bin/.exe <-- FAILS (kit listed under FAILED KITS) if a running shell still holds the old exe [K5] @@ -3469,6 +3478,20 @@ proc ::punkboot::get_vfs_binary_arch_report {vfsfolder targetplatform} { } return [list 1 [::punkboot::utils::vfs_binary_arch_report $vfsfolder $targetplatform]] } +#Availability probe + advisory zip offset-style check of an assembled kit image +#(G-134), shared by the bake loop and the 'check' command report. Same guarded- +#require treatment as the checks above: a stale bootsupport snapshot degrades +#the pin to a NOTE. Returns {available report}. +proc ::punkboot::get_kit_offsetstyle_report {kitpath} { + set available [expr {\ + ![catch {package require punkboot::utils}]\ + && [llength [info commands ::punkboot::utils::kit_offsetstyle_report]]\ + }] + if {!$available} { + return [list 0 [dict create]] + } + return [list 1 [::punkboot::utils::kit_offsetstyle_report $kitpath]] +} #G-133 smoke-require probe: execute a freshly built kit artifact via its 'tclsh' #subcommand and plain-'package require' each declared package inside it. This is the #only check that observes actual package RESOLUTION in the artifact - the punkluck86 @@ -3814,6 +3837,20 @@ if {$::punkboot::command eq "check"} { puts stdout "payload/target arch scan (G-133): UNAVAILABLE (punkboot::utils vfs_binary_arch_report not loadable from bootsupport)" puts stdout " bake will proceed with a NOTE and cannot flag wrong-arch payload libraries - run 'make.tcl modules' then 'make.tcl bootsupport'." } + # Kit offset-style pin (G-134) - advisory archive-relative output contract + puts stdout $sep + #availability probe only - the path deliberately does not exist + lassign [::punkboot::get_kit_offsetstyle_report [file join $projectroot __punkboot_offsetstyle_probe__]] _off_available _off_report + if {$_off_available} { + puts stdout "kit offset-style pin (G-134): ACTIVE (advisory)" + puts stdout " bake probes each assembled kit image with punk::zip::archive_info and emits a recapped" + puts stdout " BUILD-WARNING when an attached zip payload records FILE-relative offsets (the pipeline" + puts stdout " emits archive-relative by construction; the G-128 stamper refuses file-relative by default)." + puts stdout " plain/none results (no attached zip - e.g the metakit kit shape) are silence, not warnings." + } else { + puts stdout "kit offset-style pin (G-134): UNAVAILABLE (punkboot::utils kit_offsetstyle_report not loadable from bootsupport)" + puts stdout " bake will proceed with a NOTE and cannot flag a file-relative kit payload - run 'make.tcl modules' then 'make.tcl bootsupport'." + } set _smoke_declared [list] if {[file exists $sourcefolder/runtime/mapvfs.config]} { set _smokemodel [punkboot::lib::mapvfs_parse $sourcefolder/runtime/mapvfs.config [file dirname $sourcefolder]/bin/runtime $sourcefolder $::punkboot::target_platform] @@ -6296,6 +6333,472 @@ proc ::punkboot::assemble_zipcat_image {raw_runtime wrapvfs outfile zipfile} { return $zipbytes } +# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- +#G-057 kit icon step. Every built kit gets a .resources.toml sidecar +#recording the build-time icon choice (default src/runtime/punkshell.ico, +#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. Two mechanisms share one semantic (delete ALL +#icon/group entries, write the .ico images as ids 1..N; the RT_GROUP_ICON is +#NAMED from the icon file - uppercased rootname, so punkshell.ico gives +#PUNKSHELL (user decision 2026-07-29, RC uppercase convention) - with language +#adopted from the first replaced group; 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 +#scriptlib/developer/assetorigin_check.tcl - each stays standalone: make.tcl +#must not depend on scriptlib, and this step must not depend on bootsupport +#freshness; bootsupport carries only sha1/md5). Memoized per path per run. +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 + 0xe49b69c1 0xefbe4786 0x0fc19dc6 0x240ca1cc 0x2de92c6f 0x4a7484aa 0x5cb0a9dc 0x76f988da + 0x983e5152 0xa831c66d 0xb00327c8 0xbf597fc7 0xc6e00bf3 0xd5a79147 0x06ca6351 0x14292967 + 0x27b70a85 0x2e1b2138 0x4d2c6dfc 0x53380d13 0x650a7354 0x766a0abb 0x81c2c92e 0x92722c85 + 0xa2bfe8a1 0xa81a664b 0xc24b8b70 0xc76c51a3 0xd192e819 0xd6990624 0xf40e3585 0x106aa070 + 0x19a4c116 0x1e376c08 0x2748774c 0x34b0bcb5 0x391c0cb3 0x4ed8aa4a 0x5b9cca4f 0x682e6ff3 + 0x748f82ee 0x78a5636f 0x84c87814 0x8cc70208 0x90befffa 0xa4506ceb 0xbef9a3f7 0xc67178f2 + } +} +proc ::punkboot::kiticon::sha256_data {data} { + variable K + set bitlen [expr {wide([string length $data]) * 8}] + append data \x80 + set pad [expr {(56 - [string length $data] % 64 + 64) % 64}] + append data [string repeat \x00 $pad] [binary format W $bitlen] + set h0 0x6a09e667; set h1 0xbb67ae85; set h2 0x3c6ef372; set h3 0xa54ff53a + set h4 0x510e527f; set h5 0x9b05688c; set h6 0x1f83d9ab; set h7 0x5be0cd19 + set n [string length $data] + for {set off 0} {$off < $n} {incr off 64} { + binary scan [string range $data $off [expr {$off + 63}]] Iu16 w + for {set t 16} {$t < 64} {incr t} { + set x [lindex $w [expr {$t - 15}]] + set s0 [expr {((($x >> 7) | ($x << 25)) ^ (($x >> 18) | ($x << 14)) ^ ($x >> 3)) & 0xffffffff}] + set x [lindex $w [expr {$t - 2}]] + set s1 [expr {((($x >> 17) | ($x << 15)) ^ (($x >> 19) | ($x << 13)) ^ ($x >> 10)) & 0xffffffff}] + lappend w [expr {([lindex $w [expr {$t - 16}]] + $s0 + [lindex $w [expr {$t - 7}]] + $s1) & 0xffffffff}] + } + set a $h0; set b $h1; set c $h2; set d $h3 + set e $h4; set f $h5; set g $h6; set h $h7 + for {set t 0} {$t < 64} {incr t} { + set S1 [expr {((($e >> 6) | ($e << 26)) ^ (($e >> 11) | ($e << 21)) ^ (($e >> 25) | ($e << 7))) & 0xffffffff}] + set ch [expr {(($e & $f) ^ (~$e & $g)) & 0xffffffff}] + set T1 [expr {($h + $S1 + $ch + [lindex $K $t] + [lindex $w $t]) & 0xffffffff}] + set S0 [expr {((($a >> 2) | ($a << 30)) ^ (($a >> 13) | ($a << 19)) ^ (($a >> 22) | ($a << 10))) & 0xffffffff}] + set maj [expr {($a & $b) ^ ($a & $c) ^ ($b & $c)}] + set T2 [expr {($S0 + $maj) & 0xffffffff}] + set h $g; set g $f; set f $e; set e [expr {($d + $T1) & 0xffffffff}] + set d $c; set c $b; set b $a; set a [expr {($T1 + $T2) & 0xffffffff}] + } + set h0 [expr {($h0 + $a) & 0xffffffff}]; set h1 [expr {($h1 + $b) & 0xffffffff}] + set h2 [expr {($h2 + $c) & 0xffffffff}]; set h3 [expr {($h3 + $d) & 0xffffffff}] + set h4 [expr {($h4 + $e) & 0xffffffff}]; set h5 [expr {($h5 + $f) & 0xffffffff}] + set h6 [expr {($h6 + $g) & 0xffffffff}]; set h7 [expr {($h7 + $h) & 0xffffffff}] + } + return [format %08x%08x%08x%08x%08x%08x%08x%08x $h0 $h1 $h2 $h3 $h4 $h5 $h6 $h7] +} +proc ::punkboot::kiticon::sha256_file {path} { + variable sha256_cache + set key [file normalize $path] + if {[dict exists $sha256_cache $key]} { + return [dict get $sha256_cache $key] + } + set f [open $path rb] + set data [read $f] + close $f + set hex [sha256_data $data] + dict set sha256_cache $key $hex + return $hex +} +proc ::punkboot::kiticon::toml_escape {s} { + return [string map {\\ \\\\ \" \\\"} $s] +} +#Tolerant subset reader for the chosen icon's G-135 assetorigin sidecar - lifts +#source/source_hash so the kit record reaches back to the SVG-derived master. +#Missing sidecar, unknown schema or malformed content simply yields no +#provenance fields (never an error). +proc ::punkboot::kiticon::assetorigin_read {assetpath} { + set sc $assetpath.assetorigin.toml + set out [dict create] + if {![file isfile $sc]} { + return $out + } + if {[catch { + set f [open $sc rb] + set raw [read $f] + close $f + }]} { + return $out + } + foreach line [split [encoding convertfrom utf-8 $raw] \n] { + set line [string trim [string map [list \r {}] $line]] + if {$line eq "" || [string index $line 0] eq "#"} continue + if {[string index $line 0] eq "\["} break + set eq [string first = $line] + if {$eq < 1} continue + set key [string trim [string range $line 0 [expr {$eq - 1}]]] + if {$key ni {schema source source_hash}} continue + set val [string trim [string range $line [expr {$eq + 1}] end]] + if {[string index $val 0] eq "\""} { + set parsed "" + set ok 0 + set len [string length $val] + for {set i 1} {$i < $len} {incr i} { + set chr [string index $val $i] + if {$chr eq "\\"} { + incr i + append parsed [string index $val $i] + } elseif {$chr eq "\""} { + set ok 1 + break + } else { + append parsed $chr + } + } + if {!$ok} continue + set val $parsed + } else { + set cmt [string first # $val] + if {$cmt >= 0} { + set val [string trim [string range $val 0 [expr {$cmt - 1}]]] + } + } + dict set out $key $val + } + if {![dict exists $out schema] || [dict get $out schema] ne "1"} { + return [dict create] + } + return $out +} +proc ::punkboot::kiticon::writefile_ifchanged {path content} { + if {[file exists $path]} { + set f [open $path rb] + set existing [read $f] + close $f + if {$existing eq $content} { + return unchanged + } + } + set f [open $path wb] + puts -nonewline $f $content + close $f + return written +} +proc ::punkboot::kiticon::copy_ifchanged {from to} { + if {![file isfile $from]} { + return absent + } + set f [open $from rb] + set fromdata [read $f] + close $f + if {[file exists $to]} { + set f [open $to rb] + set todata [read $f] + close $f + if {$todata eq $fromdata} { + return unchanged + } + } + file copy -force $from $to + return copied +} +#Memoized once per run: can this host's tclsh write PE resources? Returns +#{1 {}} or {0 reason} with the reason distinguishing cross-host from +#twapi-unavailable (the acceptance requires distinct notices). +proc ::punkboot::kiticon::twapi_available {} { + variable twapi_state + if {$twapi_state eq "ok"} { + return [list 1 {}] + } + if {$twapi_state ne ""} { + return [list 0 $twapi_state] + } + if {[punkboot::lib::platform_process_family $::punkboot::host_platform] ne "windows"} { + set twapi_state "cross-host: win32 target baked on a $::punkboot::host_platform host cannot run the twapi resource APIs (portable post-hoc stamping is the G-128 remedy)" + return [list 0 $twapi_state] + } + if {[catch {package require twapi_resource}] && [catch {package require twapi}]} { + set twapi_state "twapi resource APIs not loadable in this tclsh ([info nameofexecutable]) - the vendored twapi under vendorlib_tcl/win32-x86_64 serves native windows tclsh builds" + return [list 0 $twapi_state] + } + if {![llength [info commands ::twapi::begin_resource_update]]} { + set twapi_state "twapi loaded but its resource-update APIs are absent" + return [list 0 $twapi_state] + } + 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, or fetch the prebuilt punkbin artifact (win32-x86_64/tools/punkres-*; route: src/tools/punkres/PROVENANCE.md) (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 (a non-empty +#group_override renames the group via 'set-icon -group' - the seam's naming +#policy; language and codepage adoption unchanged), 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 {group_override {}}} { + 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" + } + set cmd [list exec [file nativename $punkres_exe] set-icon] + if {$group_override ne ""} { + lappend cmd -group $group_override + } + #-drop-opaque-overlay: the stub contract is a payload-free prefix, so any + #non-zip trailing bytes are build detritus - concretely the COFF + #symbol/debug tables mingw-built runtimes carry after their sections, + #which ride into the extraction head. The twapi arm's EndUpdateResource + #has always silently stripped exactly those bytes (measured 2026-07-29, + #G-128 detail); punkres drops them deliberately and zeroes the dangling + #symbol-table header pointer. A REAL zip payload on a stub is unaffected + #(the flag is inert for zip-classified overlays, which punkres preserves). + lappend cmd -drop-opaque-overlay + lappend cmd [file nativename $exepath] [file nativename $icopath] 2>@1 + if {[catch $cmd 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 +#written as ids 1..N under the first pre-existing group's name+lang (fallback +#name 1, lang 1033), in one resource-update transaction. A non-empty +#group_override replaces the group NAME only (language adoption unchanged) - +#the same override punkres's 'set-icon -group' applies, so the seam's naming +#policy keeps the two arms in parity (G-128). Raises an error on failure +#(after -discard); returns the number of images embedded. The file at exepath +#must carry NO appended payload - a resource update rewrites the image. +proc ::punkboot::kit_icon_embed_twapi {exepath icopath {group_override {}}} { + 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" + } + set old_icons [list] + set old_groups [list] + set libh [twapi::load_library $exepath -datafile] + try { + set resources [twapi::extract_resources $libh] + foreach {restype var} {3 old_icons 14 old_groups} { + if {[dict exists $resources $restype]} { + foreach rname [dict keys [dict get $resources $restype]] { + foreach rlang [dict keys [dict get $resources $restype $rname]] { + lappend $var [list $rname $rlang] + } + } + } + } + } trap {TWAPI_WIN32 1812} {} { + #no resource section at all - nothing to enumerate + } finally { + twapi::free_library $libh + } + set group_name 1 + set group_lang 1033 + if {[llength $old_groups]} { + lassign [lindex $old_groups 0] group_name group_lang + } + if {$group_override ne ""} { + set group_name $group_override + } + set update_h [twapi::begin_resource_update $exepath] + try { + foreach entry $old_icons { + lassign $entry rname rlang + twapi::delete_resource $update_h 3 $rname $rlang + } + foreach entry $old_groups { + lassign $entry rname rlang + twapi::delete_resource $update_h 14 $rname $rlang + } + set grouphdr [binary format ttt 0 1 $ico_count] + for {set i 0} {$i < $ico_count} {incr i} { + #.ico ICONDIRENTRY is 16 bytes; GRPICONDIRENTRY is 14 - the trailing + #4-byte image offset becomes a 2-byte icon resource id. Raw width and + #height bytes are carried as-is (0 means 256 in both formats). + set off [expr {6 + 16 * $i}] + binary scan $icondata "@${off} cu cu cu cu tu tu nu nu" w h cc rsv planes bits bytesinres imgoff + set iconid [expr {$i + 1}] + append grouphdr [binary format "cu cu cu cu tu tu nu tu" $w $h $cc $rsv $planes $bits $bytesinres $iconid] + twapi::update_resource $update_h 3 $iconid $group_lang [string range $icondata $imgoff [expr {$imgoff + $bytesinres - 1}]] + } + twapi::update_resource $update_h 14 $group_name $group_lang $grouphdr + } on error {msg ropts} { + catch {twapi::end_resource_update $update_h -discard} + return -options $ropts $msg + } + twapi::end_resource_update $update_h + return $ico_count +} +#G-057 single internal entry point. Decides the kit's icon (override: root +#punkshell.ico in the kit's own custom .vfs folder, pre-merge; else the project +#default src/runtime/punkshell.ico), writes/refreshes the kit's +#.resources.toml sidecar in the build folder, and - when the target +#has PE resources and this host can write them - stamps a per-kit copy of the +#payload-free raw runtime prefix. Returns {wrap_runtime sidecarpath}: +#wrap_runtime is the runtime prefix the assembly step must consume (the stamped +#copy when embedding happened, the untouched raw prefix otherwise). +proc ::punkboot::kit_icon_process {targetkit rt_target rtname kit_type sourcefolder vfstail raw_runtime buildfolder} { + set repo_root [file dirname $sourcefolder] + set override_icon $sourcefolder/vfs/$vfstail/punkshell.ico + if {[file isfile $override_icon]} { + set icon_origin override + set icon_path $override_icon + } else { + set icon_origin default + set icon_path $sourcefolder/runtime/punkshell.ico + } + set have_icon [file isfile $icon_path] + #RT_GROUP_ICON name policy (G-128, user decision 2026-07-29): derived from + #the icon FILE's rootname, uppercased per the RC convention - punkshell.ico + #gives PUNKSHELL for the default icon and for per-kit overrides alike + #(overrides are by convention also named punkshell.ico). Applied by + #whichever mechanism embeds; language stays adopted from the replaced group. + set icon_group "" + if {$have_icon} { + set icon_group [string toupper [file rootname [file tail $icon_path]]] + } + set embedding none + set status "" + set reason "" + if {![string match "win32-*" $rt_target]} { + set status not-applicable + set reason "target $rt_target has no PE resources" + } elseif {$rtname eq "-"} { + set status not-applicable + set reason "runtimeless .kit artifact has no PE container" + } elseif {$kit_type in {cookit cookfs}} { + set status unavailable + set reason "kit type $kit_type edits the payload in place inside the mounted runtime - no payload-free stub moment to stamp" + } elseif {!$have_icon} { + set status unavailable + set reason "icon file not found at $icon_path" + } elseif {$raw_runtime in {{} -} || ![file isfile $raw_runtime]} { + set status unavailable + set reason "extraction produced no raw runtime prefix to stamp" + } else { + #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 + if {$status eq ""} { + set stamped $buildfolder/iconed_$targetkit + file copy -force $raw_runtime $stamped + catch {exec chmod +w $stamped} + if {$punkres_ok} { + set embedding punkres + set embed_cmd [list ::punkboot::kit_icon_embed_punkres $punkres_info $stamped $icon_path $icon_group] + } else { + set embedding twapi + set embed_cmd [list ::punkboot::kit_icon_embed_twapi $stamped $icon_path $icon_group] + } + 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 status embedded + set wrap_runtime $stamped + 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)" + } else { + puts stdout " kit icon (G-057): embedding UNAVAILABLE for $targetkit - $reason (sidecar still written)" + } + set esc ::punkboot::kiticon::toml_escape + set lines [list] + lappend lines "# kit resource record (schema 1) for the kit artifact named by this file" + lappend lines "# with the .resources.toml suffix stripped. Emitted by the make.tcl bake" + lappend lines "# icon step (G-057). Optional and advisory; format: bin/AGENTS.md." + lappend lines "schema = 1" + lappend lines "kit = \"[$esc $targetkit]\"" + lappend lines "target = \"[$esc $rt_target]\"" + lappend lines "kit_type = \"[$esc $kit_type]\"" + lappend lines "icon_origin = \"$icon_origin\"" + lappend lines "icon_source = \"[$esc [punkcheck::lib::path_relative $repo_root $icon_path]]\"" + if {$have_icon} { + lappend lines "icon_hash = \"sha256:[::punkboot::kiticon::sha256_file $icon_path]\"" + set ao [::punkboot::kiticon::assetorigin_read $icon_path] + if {[dict exists $ao source]} { + lappend lines "icon_provenance_source = \"[$esc [dict get $ao source]]\"" + } + if {[dict exists $ao source_hash]} { + lappend lines "icon_provenance_source_hash = \"[$esc [dict get $ao source_hash]]\"" + } + lappend lines "icon_group = \"[$esc $icon_group]\"" + } + lappend lines "embedding = \"$embedding\"" + lappend lines "embedding_status = \"$status\"" + lappend lines "embedding_reason = \"[$esc $reason]\"" + set sidecar $buildfolder/$targetkit.resources.toml + ::punkboot::kiticon::writefile_ifchanged $sidecar "[join $lines \n]\n" + return [list $wrap_runtime $sidecar] +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + set startdir [pwd] puts stdout "Found [llength $vfs_tails] .vfs folders - checking each for executables that may need to be built" cd [file dirname $buildfolder] @@ -6461,6 +6964,11 @@ foreach vfstail $vfs_tails { $vfs_event targetset_addsource $sourcefolder/vfs/_config ;#some files linked via fauxlink - need to detect change $vfs_event targetset_addsource $sourcefolder/vfs/_vfscommon.vfs $vfs_event targetset_addsource $sourcefolder/vfs/$vfstail + if {[file isfile $sourcefolder/runtime/punkshell.ico]} { + #G-057: the default kit icon is a build input - a changed icon rebuilds + #kits (an override icon lives inside the .vfs folder, already a source) + $vfs_event targetset_addsource $sourcefolder/runtime/punkshell.ico + } if {$rtname ne "-"} { set building_runtime $buildfolder/build_$runtime_fullname ;#working copy of runtime executable - (possibly with kit/zipfs/cookfs etc attached!) $vfs_event targetset_addsource $building_runtime @@ -6857,6 +7365,13 @@ foreach vfstail $vfs_tails { continue } + #G-057 kit icon step: sidecar for every kit; PE icon embedding where + #the target has PE resources and this host can write them. Stub-first: + #the assemblers below consume wrap_runtime - the stamped per-kit copy + #of the payload-free raw prefix when embedding happened, the untouched + #raw prefix otherwise. Runtime store originals are never modified. + lassign [::punkboot::kit_icon_process $targetkit $rt_target $rtname $target_kit_type $sourcefolder $vfstail $raw_runtime $buildfolder] wrap_runtime kiticon_sidecar + set wrapvfs $targetvfs switch -- $target_kit_type { zip { @@ -6886,14 +7401,14 @@ foreach vfstail $vfs_tails { } if {[info commands ::tcl::zipfs::mkimg] ne ""} { #note - as at 2024-08 - there is some discussion about the interface to mkimg - it is considered unstable (may change to -option value syntax) - puts stderr "calling: tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs \"\" $raw_runtime" - tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs "" $raw_runtime + puts stderr "calling: tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs \"\" $wrap_runtime" + tcl::zipfs::mkimg $buildfolder/$vfsname.new $wrapvfs $wrapvfs "" $wrap_runtime } else { #G-122: the driving tcl has no zipfs (8.6) - assemble the same #kind of image by concatenation. The result mounts on any #zipfs-capable runtime (archive-start-relative offsets). puts stderr "WARNING: tcl shell '[info nameofexecutable]' has no zipfs - assembling zip kit $targetkit by concatenation (raw runtime + mkzip)" - ::punkboot::assemble_zipcat_image $raw_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip + ::punkboot::assemble_zipcat_image $wrap_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip } } result ]} { set failmsg "zipfs mkimg failed with msg: $result" @@ -6930,7 +7445,7 @@ foreach vfstail $vfs_tails { } #'archive' based zip offsets - editable in 7z,peazip - ::punkboot::assemble_zipcat_image $raw_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip + ::punkboot::assemble_zipcat_image $wrap_runtime $wrapvfs $buildfolder/$vfsname.new $buildfolder/$vfsname.zip } result ]} { set failmsg "creating zipcat image failed with msg: $result" puts stderr "creating image (zipcat) $targetkit failed" @@ -7005,13 +7520,13 @@ foreach vfstail $vfs_tails { cd [file dirname $building_runtime] ;#make sure SDX is called with the same working dir as the files we're working on. if {[catch { if {$rtname ne "-"} { - exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $raw_runtime {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $wrap_runtime {*}$verbose } else { exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose } } result]} { if {$rtname ne "-"} { - set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $raw_runtime {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $wrap_runtime {*}$verbose failed with msg: $result" } else { set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" } @@ -7175,6 +7690,21 @@ foreach vfstail $vfs_tails { # -- --- --- --- --- --- $vfs_event targetset_end OK + #G-134 post-assembly offset-style pin: the pipeline emits + #ARCHIVE-relative zip payloads by construction - this probe makes + #that a checked contract. Advisory per the G-133 posture: a + #file-relative image earns a recapped BUILD-WARNING (the G-128 + #stamper would refuse it by default) and the kit still builds and + #deploys; plain/none (no attached zip - the metakit kit shape) and + #unreadable results are silence. One EOCD scan + CD walk per + #REBUILT kit only. + lassign [::punkboot::get_kit_offsetstyle_report $buildfolder/$targetkit] offprobe_available offprobe + if {!$offprobe_available} { + puts stderr "NOTE: kit offset-style pin unavailable (punkboot::utils kit_offsetstyle_report not loadable from bootsupport) - continuing without it" + } elseif {[dict get $offprobe offsetstyle] eq "file"} { + ::punkboot::print_build_warnings [list "kit $targetkit assembled with a FILE-relative zip payload (G-134 pin: the pipeline emits archive-relative by construction; the G-128 stamper refuses file-relative by default) - investigate this kit's assembly path"] + } + #G-133 post-build smoke-require probe. Only an actual 'package require' #inside the built artifact observes package RESOLUTION - the wrong-arch #version-shadowing class is invisible to every structural check because @@ -7249,6 +7779,10 @@ foreach vfstail $vfs_tails { puts stdout "$deployment_folder/$targetkit" after 300 file copy $buildfolder/$targetkit $deployment_folder/$targetkit + #G-057: the kit resource sidecar ships with the kit + if {[file isfile $buildfolder/$targetkit.resources.toml]} { + file copy -force $buildfolder/$targetkit.resources.toml $deployment_folder/$targetkit.resources.toml + } lappend installed_kits $targetkit # -- ---------- $bin_event targetset_end OK @@ -7257,6 +7791,12 @@ foreach vfstail $vfs_tails { set skipped_kit_install 1 puts stderr "." puts stdout "Skipping kit install for $targetkit with vfs $vfstail runtime $rtname - no change detected" + #G-057: keep the deployed resource sidecar current even when the kit + #itself is unchanged (the record can be newer than the kit rebuild - + #e.g the sidecar feature arriving, or a host-capability change) + if {[::punkboot::kiticon::copy_ifchanged $buildfolder/$targetkit.resources.toml $deployment_folder/$targetkit.resources.toml] eq "copied"} { + puts stdout " kit icon (G-057): refreshed deployed sidecar $deployment_folder/$targetkit.resources.toml" + } lappend skipped_kit_installs [list kit $targetkit reason "no change detected"] $bin_event targetset_end SKIPPED } diff --git a/src/tests/modules/punkboot/utils/testsuites/utils/offsetstyle.test b/src/tests/modules/punkboot/utils/testsuites/utils/offsetstyle.test new file mode 100644 index 00000000..87b991c4 --- /dev/null +++ b/src/tests/modules/punkboot/utils/testsuites/utils/offsetstyle.test @@ -0,0 +1,119 @@ +# -*- tcl -*- +# Tests for punkboot::utils::kit_offsetstyle_report - the G-134 advisory +# archive-relative output pin's probe predicate (a thin classification wrapper +# over punk::zip::archive_info, in punkboot::utils precisely so it is testable +# without a bake): +# - plain zip (no prefix) -> plain +# - runtime-prefixed zip, archive-relative offsets (the pipeline's shape) -> archive +# - runtime-prefixed zip, FILE-relative offsets (mkzip -offsettype file - the +# deliberately-available manual convention; the shape that earns the bake's +# BUILD-WARNING) -> file +# - non-zip files (text, binary junk - the metakit kit shape's class) -> none +# - missing path / unusable input -> unreadable with a detail message +# - baseline: every assembled kit image in src/_build (when a build has run) +# probes archive or none, never file - the G-124-measured property the pin +# asserts +# All fixtures are GENERATED here with punk::zip::mkzip / binary writes - no +# committed binaries, nothing executed. +# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punkboot/utils/*** offsetstyle.test + +package require tcltest +package require punkboot::utils +package require punk::zip + +#added 2026-07-29 (agent, G-134) + +namespace eval ::testspace { + namespace import ::tcltest::* + + variable BASE [makeDirectory g134_offsetstyle] + + #/src/tests/modules/punkboot/utils/testsuites/utils -> 7 levels up + variable projectroot [file normalize [file join [file dirname [info script]] .. .. .. .. .. .. ..]] + variable buildfolder [file join $projectroot src _build] + testConstraint builtkitsavailable [expr {[llength [glob -nocomplain -type f -directory $buildfolder *.exe]] > 0}] + + proc writefile {path content} { + set f [open $path wb] + puts -nonewline $f $content + close $f + return $path + } + + #shared fixture set: a tiny payload tree, a fake runtime prefix, and the + #three zip shapes built from them + variable payloaddir [file join $BASE payload] + file mkdir $payloaddir + writefile [file join $payloaddir main.tcl] "puts {offsetstyle fixture payload}\n" + writefile [file join $payloaddir data.txt] [string repeat "0123456789" 20] + variable stub [writefile [file join $BASE fakestub.bin] "FAKE-RUNTIME-PREFIX-[string repeat X 200]"] + + variable zip_plain [file join $BASE plain.zip] + punk::zip::mkzip -directory $payloaddir -base $payloaddir $zip_plain * + variable kit_archive [file join $BASE kit_archive.exe] + punk::zip::mkzip -runtime $stub -directory $payloaddir -base $payloaddir $kit_archive * + variable kit_file [file join $BASE kit_file.exe] + punk::zip::mkzip -runtime $stub -offsettype file -directory $payloaddir -base $payloaddir $kit_file * + + test kit_offsetstyle_plain {a bare zip probes as plain} -body { + variable zip_plain + set r [punkboot::utils::kit_offsetstyle_report $zip_plain] + list offsetstyle [dict get $r offsetstyle] detail [dict get $r detail] + } -result {offsetstyle plain detail {}} + + test kit_offsetstyle_archive {a runtime-prefixed archive-relative image (the pipeline shape) probes as archive} -body { + variable kit_archive + set r [punkboot::utils::kit_offsetstyle_report $kit_archive] + list offsetstyle [dict get $r offsetstyle] dataoffset_positive [expr {[dict get $r dataoffset] > 0}] + } -result {offsetstyle archive dataoffset_positive 1} + + test kit_offsetstyle_file {a runtime-prefixed FILE-relative image (mkzip -offsettype file) probes as file - the warning shape} -body { + variable kit_file + set r [punkboot::utils::kit_offsetstyle_report $kit_file] + dict get $r offsetstyle + } -result {file} + + test kit_offsetstyle_none_text {a non-zip text file probes as none (silence - the pin covers offset convention, not zip presence)} -body { + variable BASE + set f [writefile [file join $BASE not_a_zip.txt] "just text, no archive here\n"] + set r [punkboot::utils::kit_offsetstyle_report $f] + dict get $r offsetstyle + } -result {none} + + test kit_offsetstyle_none_binary {a non-zip binary file (the metakit kit shape's class) probes as none} -body { + variable BASE + set f [file join $BASE not_a_zip.bin] + set fd [open $f wb] + puts -nonewline $fd [binary format a4c* "JL\x1a\x00" {1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16}] + close $fd + set r [punkboot::utils::kit_offsetstyle_report $f] + dict get $r offsetstyle + } -result {none} + + test kit_offsetstyle_missing {a missing path probes as unreadable with a detail message} -body { + variable BASE + set r [punkboot::utils::kit_offsetstyle_report [file join $BASE no_such_file.exe]] + list offsetstyle [dict get $r offsetstyle] has_detail [expr {[dict get $r detail] ne ""}] + } -result {offsetstyle unreadable has_detail 1} + + test kit_offsetstyle_build_baseline {every assembled kit image in src/_build probes archive or none - never file (the G-124 baseline the pin asserts)} -constraints {builtkitsavailable} -body { + variable buildfolder + set bad [list] + foreach kitimage [lsort [glob -nocomplain -type f -directory $buildfolder *.exe]] { + #kit images only: skip runtime prefixes and helper artifacts the + #build folder also holds (raw_/iconed_/build_ prefixes) + set tail [file tail $kitimage] + if {[string match "raw_*" $tail] || [string match "iconed_*" $tail] || [string match "build_*" $tail]} { + continue + } + set r [punkboot::utils::kit_offsetstyle_report $kitimage] + if {[dict get $r offsetstyle] eq "file"} { + lappend bad $tail + } + } + set bad + } -result {} + + cleanupTests +} +namespace delete ::testspace diff --git a/src/tests/shell/testsuites/punkexe/maketclpayloadcheck.test b/src/tests/shell/testsuites/punkexe/maketclpayloadcheck.test index 751cd6a4..6532f3d4 100644 --- a/src/tests/shell/testsuites/punkexe/maketclpayloadcheck.test +++ b/src/tests/shell/testsuites/punkexe/maketclpayloadcheck.test @@ -129,6 +129,27 @@ namespace eval ::testspace { }\ -result {active 1 crosstarget 1 recapped 1 exemption 1 seesresolution 1 skipstated 1 undeclared 1} + #added 2026-07-29 (agent, G-134) + test maketcl_check_offsetstyle_pin {make.tcl check reports the kit offset-style pin with one of the two documented statuses and states its contract (advisory, file-relative warning, plain/none silence)}\ + -constraints {punkexeavailable} -body { + set r [maketcl_run {check}] + set out [dict get $r output] + set result [list] + lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] + lappend result pinline [regexp -line {^kit offset-style pin \(G-134\): (ACTIVE \(advisory\)|UNAVAILABLE)} $out] + #contract lines shown when ACTIVE (the current expected state; an + #UNAVAILABLE snapshot legitimately omits them - gate on the status) + if {[regexp -line {^kit offset-style pin \(G-134\): ACTIVE \(advisory\)\s*$} $out]} { + lappend result probe [regexp {punk::zip::archive_info} $out] + lappend result warnshape [regexp {FILE-relative offsets} $out] + lappend result silence [regexp {plain/none results.*are silence} $out] + } else { + lappend result probe 1 warnshape 1 silence 1 + } + set result + }\ + -result {timedout 0 exitcode 0 pinline 1 probe 1 warnshape 1 silence 1} + #added 2026-07-27 (agent, G-133) - the declared list characterizes the current #src/runtime/mapvfs.config smoke-require matrix (deliberate config changes update it) test maketcl_check_payloadchecks_declared_kits {check lists the kits currently declaring smoke-require packages}\ diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm index 664ca687..132f49d4 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm and b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm differ diff --git a/src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.5.0.tm b/src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.0.tm similarity index 91% rename from src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.5.0.tm rename to src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.0.tm index 1cd5324c..43372880 100644 --- a/src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.5.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.0.tm @@ -7,7 +7,7 @@ # (C) 2023 # # @@ Meta Begin -# Application punkboot::utils 0.5.0 +# Application punkboot::utils 0.6.0 # Meta platform tcl # Meta license BSD # @@ Meta End @@ -23,7 +23,7 @@ package require Tcl 8.6- # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ namespace eval punkboot::utils { variable version 0.1.0 - namespace export binary_arch_classify parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report + namespace export binary_arch_classify kit_offsetstyle_report parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report namespace eval argdoc { @@ -736,6 +736,65 @@ namespace eval punkboot::utils { dict set report ok [expr {[llength $mismatches] == 0}] return $report } + + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::punkboot::utils::kit_offsetstyle_report + @cmd -name "::punkboot::utils::kit_offsetstyle_report"\ + -summary\ + "Advisory zip offset-style probe of an assembled kit image (G-134)"\ + -help\ + "Probes a finished kit image file with punk::zip::archive_info and + reports the offset convention of any attached zip payload. The + punkshell bake pipeline emits ARCHIVE-relative payloads by + construction; this probe makes that a checked contract - a + 'file' result is the pipeline-regression signal make.tcl + surfaces as a recapped BUILD-WARNING (advisory: the kit still + builds and deploys). 'plain' (the file is a bare zip), 'none' + (no zip attached - e.g the metakit kit shape, or any non-zip + file) and 'unreadable' are silence, not warnings: the pin + covers the offset convention of an attached zip, never the + presence of one. Purely structural: one EOCD scan plus a + central-directory walk, nothing executed, so cross-target kit + images are covered. + + Returned dict keys: + offsetstyle archive | file | plain | none | unreadable + dataoffset payload base offset when archive_info supplies + one (\"\" otherwise) + detail diagnostic text for unreadable (missing file, + punk::zip unavailable, archive_info error); + empty otherwise" + @leaders + kitpath -type string -optional 0 -help\ + "Path of the assembled kit image file to probe" + }] + } + proc kit_offsetstyle_report {kitpath} { + set report [dict create offsetstyle unreadable dataoffset "" detail ""] + if {![file isfile $kitpath]} { + dict set report detail "no such file: $kitpath" + return $report + } + if {[catch {package require punk::zip} errM]} { + dict set report detail "punk::zip not available: $errM" + return $report + } + if {[catch {punk::zip::archive_info $kitpath} zinfo]} { + dict set report detail "archive_info error: $zinfo" + return $report + } + if {[dict exists $zinfo offsetstyle]} { + dict set report offsetstyle [dict get $zinfo offsetstyle] + } else { + dict set report detail "archive_info returned no offsetstyle" + return $report + } + if {[dict exists $zinfo dataoffset]} { + dict set report dataoffset [dict get $zinfo dataoffset] + } + return $report + } } namespace eval ::punk::args::register { @@ -746,8 +805,8 @@ namespace eval ::punk::args::register { ## Ready package provide punkboot::utils [tcl::namespace::eval punkboot::utils { variable version - #- this version number, exactly 0.5.0, is a literal used in src module folders + #- this version number, exactly 0.6.0, is a literal used in src module folders #- we refer to this sometimes as the magic version number - set version 0.5.0 + set version 0.6.0 }] return