# suite_tcl90 - zig build of the Tcl 9.0 windows runtime (G-096, G-102, G-103) The first real, tracked buildsuite: reproducibly builds `tclsh90s.exe` (static) and `tclsh90szip.exe` (self-contained zipfs) plus the TCLSH_PIPEREPL variants and the thread/vfs/tk extension dlls, tklib/tcllib installs and critcl-built tcllibc, from Tcl core-9-0-branch sources, using zig only (no MS toolchain, per the project toolchain policy) - and assembles them into the G-103 RUNTIME KIT FAMILY: verified self-contained batteries-attached runtimes (see "Runtime kit family" below). (vqtcl/vlerq was removed 2026-07-20 - an old-tclkit experiment; future tclkit investigation has TEMP_REFERENCE/metakit and TEMP_REFERENCE/KitCreator as reference material.) Everything transient lives under `src/buildsuites/_build/suite_tcl90/` (VCS-ignored via the repo's existing `_build` globs): staged recipe, source trees, zig cache, and the `out/` install prefix. The tracked content here is the recipe and its drivers. ## Two flows, one pipeline (G-102) The PIPELINE - configure-product generation (overlays, source trees never written), compile, package installs, smokes, test gate - lives entirely in the zig recipe as build steps. Two ways to arrange sources and drive it: **No-tclsh bootstrap flow** (pinned sources; nothing but the pinned zig required): zig build stage # fetch build.zig.zon-pinned sources + stage recipe -> ../_build/suite_tcl90 zig build bootstrap # stage + run the staged pipeline end-to-end run from THIS directory with the pinned zig. Sources come from `build.zig.zon` - content-hashed per-checkin tarball pins ('refresh' = bump a pin; `zig fetch --save= ` edits the manifest). Steps for the nested staged build: `-Dsteps=...` (repeat the flag), default install install-libraries make-zipfs smoke tklib tcllib tcllibc kit-family kit-family-artifacts. The test gate: `zig build bootstrap -Dsteps=test-gate` (or invoke the staged build file directly, as suite.tcl does). zig extracts fetched packages into `zig-pkg/` beside the manifest and `.zig-cache/` for the outer invocation - both VCS-ignored. **Fossil dev flow** (live checkouts; a plain tclsh drives fetch+staging): tclsh suite.tcl build ?options? tclsh suite.tcl test ?-testargs {...}? tclsh suite.tcl clean `suite.tcl` arranges LIVE fossil/git checkouts per `sources.config` (branch work, `-refresh`, `-tclbranch` overrides) into the same stage layout, then delegates to the same staged zig steps. An existing fossil/git checkout in the stage is never touched by the bootstrap staging (guarded twice), so the flows can be mixed. See `suite.tcl`'s header for options (`-zig`, `-optimize`, `-tclbranch`, `-refresh`, `-steps`). Zig resolution: `-zig` > `$env(PUNK_ZIG)` > the pinned toolchain at `bin/tools/zig-x86_64-windows-0.16.0/zig.exe` > `zig` on PATH. ## Library test runs (G-107) The external libraries the suite builds and ships run their OWN testsuites under the suite-built shell, as named recipe steps with a per-library policy tier - evidence for the exact zig-built combinations nobody upstream tests: tclsh suite.tcl test # core gate (test-gate), as before tclsh suite.tcl test -steps {test-libraries} # thread+tclvfs GATED, tcllib+tklib RECORDED tclsh suite.tcl test -steps {test-thread} # individual: test-thread test-tclvfs # test-tcllib test-tklib test-tk tclsh suite.tcl test -steps {test-tcllib} -zigargs {-Dtestpolicy-tcllib=gate} Policy tiers (`-Dtestpolicy-=gate|record|skip`, defaults in the recipe, overridable at invocation without recipe edits): - **gate** (default: thread, tclvfs) - parsed totals diffed against the tracked dispositioned baseline `expected_test_failures_.txt` (core-gate semantics including stale-entry notes). Small, load-bearing for every kit. - **record** (default: tcllib, tklib, tk) - the run must complete; failures are recorded, not fatal. Emits `out/testreports/.log` (full output) and `out/testreports/.summary` (line-record evidence summary - shape documented in `goals/G-107-buildsuite-library-tests.md`, consumed by the G-103 artifact metadata; the core gate writes `tclcore.summary` there too). Promote a library to gate once its census is dispositioned and two-run deterministic. - **skip** - the named step becomes a no-op (composite tuning). EVERY mode fails the step when the run produces no parseable totals line - infrastructure breakage must never masquerade as recorded results. Per-library invocation overrides (recorded in the evidence summary): `-Dtestargs-="..."` (extra tcltest/driver args) and `-Dtestnotfiles-="pat ..."` (test-FILE glob patterns excluded from the run; a dedicated knob because `-notfile` needs a single list value that a flat `-testargs` cannot carry through the option chain). `test-libraries` runs thread/tclvfs/tcllib/tklib. The full Tk suite is deliberately OUTSIDE it: **`test-tk` is opt-in** (needs an interactive desktop, maps real windows, steals focus, runs tens of minutes) - intended before PUBLISHING bi-family artifacts, not on every build. Tk excludes the **native-dialog suites** `winDialog.test` (72 cases) and `winMsgbox.test` (19) by default (`default_notfiles`): their message-injection automation does not drive the real OS dialogs under the batch/suite-built shell, so each case blocks waiting on a human and the census cannot complete unattended. To run them, `-Dtestnotfiles-tk=""` and dismiss each dialog by hand. Drivers: thread/tclvfs/tk use their trees' own `tests/all.tcl`. tcllib/tklib are driven via their own `support/devel/all.tcl` (sak's underlying driver, bypassing sak's log format): per-file child interps, aggregated standard totals, and - because the installed `tcllibc` is on the shell's package path - the tcllib suites run WITH the critcl accelerators engaged (`E tcllibc ` markers in the log). A test file that errors out is recorded in the summary (`errored_files` / `errorexit_files`) as evidence; the gate criterion itself stays totals-vs-baseline. Known residual: the old-style thread/tclvfs drivers catch a mid-file abort without a structured marker, so such an abort shows up only as a failure-count drop plus stale-baseline notes. Timing-sensitive suites (thread) must not run in parallel with heavy ones: `suite.tcl test` passes `-j1` so combined step invocations serialize (zig otherwise runs independent steps concurrently). ## Runtime kit family (G-103) The `kit-family` step (in the default build pipeline) assembles the project's runtime kit family - runnable, self-contained executables whose info library AND core batteries live in the initially attached zip, depending on no external filesystem tree: - **plain** `tclsh.exe` (e.g `tclsh9.0.5.exe`) - stock shell (no piperepl patch) + batteries: loadable Thread, tclvfs with the vfs::* packages, tcllib + tcllibc (critcl accelerators). - **punk** `tclsh-punk.exe` - the same batteries on the TCLSH_PIPEREPL-patched shell, gate ENABLED BY DEFAULT (opt out with `TCLSH_PIPEREPL=0`; users wanting stock semantics take the plain kit). - **punk-bi** `tclsh-punk-bi.exe` - punk + the batteries-included tier we build: Tk (dll + script library) and tklib. (tcltls joins when its zig-built crypto backend lands - see the G-103 goal notes.) Attached-image layout (also the make.tcl kit contract - a future kit build extracts this image and merges its .vfs payload over it): `tcl_library/` at the app root (the C-level zipfs boot looks only for `/app/main.tcl` and `/app/tcl_library`; no main.tcl is included, so the stock boot falls through to the ordinary interactive shell), tm modules at `tcl9//` beside it (tm.tcl anchors at the dirname of tcl_library), batteries under `lib//` with their installed-shape pkgIndexes (`$dir/../../bin` dll references resolve to `/app/bin`), dlls under `bin/`, and a one-line stock pkgIndex hook at `lib/pkgIndex.tcl` joining `lib/` to the package search (auto_path is `[tcl_library, /app]`, and tclPkgUnknown re-scans auto_path growth mid-scan - the same mechanism tcllib's own top-level index uses). Battery dlls load out of zipfs via the core's copy-to-temp fallback - the TIP-741 temp-dir accumulation wart is characterized in the G-103 goal notes. Every member is verified by `tools/family_check.tcl` before the step passes: the kit is copied ALONE into a scratch dir and probed from there with a scrubbed environment (no external Tcl visible) - patchlevel, zipfs tcl_library, tzdata/encodings, tm modules, functional Thread (cross-thread eval), tclvfs (vfs::zip mount round-trip), tcllib md5 with the tcllibc accelerator engaged, piperepl default/opt-out behaviour per variant, and Tk create/destroy + tklib for bi. Products land in `out/family/`. The `kit-family-artifacts` step (also default; depends on the checks) emits the punkbin ARTIFACT tier into `out/family/punkbin/win32-x86_64/`: immutable `-r`-named copies (revision via `-Dfamilyrev=N`, suite.tcl `-zigargs {-Dfamilyrev=N}`; default 1) plus per-artifact `.toml` metadata (variant, working name, sha1/size, tcl patchlevel, piperepl policy, attached battery versions, source-checkout provenance, toolchain, G-107 test-evidence result lines) and a punkbin-format `sha1sums.txt`. The emission deliberately runs UNDER the plain family kit itself (sha1 via its attached tcllib) - every run re-proves the family runtime executes real tooling self-contained. Publication to the real punkbin repo remains a deliberate user step, deferred per the goal notes until the family shape is accepted. Consumers: working-name runtimes are copied into `bin/runtime/win32-x86_64/` and referenced by `src/runtime/mapvfs.config` (punk -> punk9wintk905.vfs as `punk9_beta`, punk-bi -> punk9win_for_tkruntime.vfs as `punk9bi_beta`, per the *_beta trial convention). ## Pinned zig The suite pin is **zig 0.16.0** (current official release; adopted 2026-07-20 - the recipe was ported to the 0.16 build API and 0.14 support is deprecated; the 0.14-era recipe remains in git history and 0.14.1 remains archived on punkbin). The pinned archive `zig-x86_64-windows-0.16.0.zip` (+`.minisig`) is hosted on the punkbin artifact repo (`win32-x86_64/tools/`, sha1sums recorded) - the same mirror punk-getzig consults - and was minisign-verified against the zig release key before hosting. punk-getzig's own pin is the same 0.16.0 (installed to `bin/tools/zig`); a per-version fetch argument remains the intended enhancement for materializing arbitrary pinned versions into versioned dirs. Zig object caches are kept **per zig version** (`.zig-cache-` inside the staged build dir): caches must never be shared across zig versions (observed producing stale/duplicate artifacts at link time). ## Layout - `build905.zig` - the zig recipe (verbatim lineage copy - see Lineage). Expects its build root's SIBLING dirs to hold sources (`../tcl905`, `../zlib`, `../tclthread`, `../tclvfs`), which is why `suite.tcl` stages it into `_build/suite_tcl90/build/` rather than building in place. - `build_libtommath/ build_zlib/ build_tclvfs/ build_tclthread/` - helper build modules. The four tcl-dependent helpers take the tcl source dir as their `tcldir` param (a hardcoded `../tcl90/generic` include, marked "todo - fix" upstream, was fixed when these copies were made). `build_libtommath.zig` still carries the hardcoded path but is not called by the current recipe (tcl's in-tree tommath is compiled into the core; the `libtommath.dll` seen in old output dirs was a stale cache-install artifact). - `src/main.zig` - stub root module that disables zig's own start symbols so the mingw CRT provides the exe entry. - `tools/` - `wrapfiletofile.zig` (uuid-header overlay generation), `stagetree.zig` (bootstrap-mode source/recipe materializer), `zipfs_mkzip.tcl` / `zipfs_mkimg.tcl` (make-zipfs + kit-family wrap drivers), and the suite-built-shell step scripts `test_gate.tcl` / `suite_smoke.tcl` / `pkg_smoke.tcl` (G-102) plus `family_check.tcl` / `family_artifacts.tcl` (G-103: kit-family self-containment verification; punkbin-layout artifact + metadata emission). `test_gate.tcl` is the shared testsuite engine (G-107): gate/record modes, driver selection, evidence summaries. - `expected_test_failures.txt` (tcl core) / `expected_test_failures_thread.txt` / `expected_test_failures_tclvfs.txt` - tracked dispositioned gate baselines. - `patches/` - recovered 2024 experiment patches, including TCLSH_PIPEREPL (see `patches/README.md`) - compiled into the SEPARATE `tclsh90spr`/ `tclsh90sprzip` products; the stock shells stay unpatched. - `build.zig.zon` - pinned source manifest for the bootstrap flow. - `suite.tcl` - the fossil dev driver: stage recipe -> arrange live checkouts -> delegate to the staged recipe's steps. ## Sources Source repositories, refs and staged checkout dirs are declared in **`sources.config`** (tcl, tclthread, tclvfs, tk, tklib, tcllib via fossil; critcl via git, version-pinned). That file is the tweak surface - its header documents the record format, the copy-and-tweak workflow, and how deep a tweak can go before it touches the zig recipe. One source is not in the config: `zlib` is copied from the tcl checkout's own `compat/zlib` (version-matched, includes contrib/minizip). The `tcl` ref can be overridden per-run with `-tclbranch`. Fossil clones live in a punkshell-owned MACHINE-LEVEL store, `~/.punkshell/fossils` (G-098 decisions 2026-07-20: hermetic - independent of any personal fossil collection - and outside deletable `_build`-class dirs; clone dbs are caches, not build output). Missing clones are seeded by READ-ONLY file copy from a local collection when available (`-seedfossils`, default `~/.fossils` - the seed is never written; the copy is pulled current), else network-cloned. Store override: `-repofolder` or `PUNK_FOSSIL_STORE` (punk::config-registered - see `help env`). `-refresh 1` pulls the store clones and updates checkouts. Checkouts use `FOSSIL_HOME=` so the user's global fossil config db records nothing (stage-local `_fossil` db). `clean` wipes the stage only; the store lives outside it. ## Retargeting / copying the suite For a developer (no punkshell knowledge required - the driver is dependency-free plain tclsh) who wants this build with different sources: 1. Copy the whole tree: `src/buildsuites/suite_tcl90` -> `src/buildsuites/`. 2. Edit `/sources.config` (urls, refs, pins). 3. `tclsh suite.tcl build` - the stage is derived from the suite folder name (`_build/`), so copies build isolated from the original. Same-major url/ref tweaks (mirror, fork, release tag) are config-only. Version-class changes additionally touch `build905.zig` and the driver's install steps (checkout dir names, version-derived tm/dll/manifest names) - see the depth-limit note in sources.config. ## Fresh-checkout preparations (configure-products) Normally products of `configure`/`nmake` that a bare checkout lacks (G-076 findings). Since G-102 these are generated INSIDE the zig recipe as build-cache OVERLAYS - source trees are never written (overlay include dirs added first so stale tree copies can never shadow; rc files that reference generated resources compile from overlay copies where rc-file-relative lookup would otherwise win): - `tclUuid.h` / `tkUuid.h` / `threadUuid.h` - from each tree's `manifest.uuid` (wrapfiletofile Run steps; consumed via overlay include dirs). - `tclsh.exe.manifest` / `wish.exe.manifest` - from their `.in` templates (`*_WIN_VERSION` per the trees' own formula: `VERSION . releaselevel . patchlevel-stripped-of-'ab.'`). - `thread.rc` - overlay copy gains `#include "tcl.h"` resolved via rc include paths (the zig rc compile does not receive the makefile's include paths; historically an uncommitted checkout tweak, later a scripted edit, now an overlay - checkouts stay pristine). ## Lineage `build905.zig` descends from the validated ad-hoc builds in `c:/buildtcl/2024zig/build_tcl90` (reference snapshot: `TEMP_REFERENCE/2024zig`): `build90.zig` (tcl 9.0.1 era, last built 2024-11-27, zig 0.14.0-dev.2074) plus the 2026-07-20 G-076 deltas - source folder `../tcl905`, tclUuid.h copy-to-source targets derived from `tcl_source_folder`, and `mingw_unicode_entry_point = true` (9.0.5 sources dropped `TCL_BROKEN_MAINARGS`; tclAppInit's `_tmain` is `wmain` under `-DUNICODE`). Full provenance: `goals/archive/G-076-tcl9-deadconsole-fix-adoption.md` (2026-07-20 entries). Goal contract for this suite: `goals/G-096-zig-buildsuite-piperepl.md`. zig 0.14.1 compat deltas (2026-07-20): `b.host` -> `b.graph.host`; the redundant second UNICODE tclMain add into the objs lib removed; the exe's ansi tclMain copy compiles with `-UUNICODE -U_UNICODE -DTCL_ASCII_MAIN` (the zig cc driver can supply windows-default defines, which made the "ansi" copy a duplicate UNICODE compile - upstream's TCL_ASCII_MAIN guard is the intended mechanism for the ascii variant). zig 0.16.0 port (2026-07-20, adopted - recipe is now 0.16-API only): creation calls take `.root_module = b.createModule(...)` (addLibrary with `.linkage` replaces addShared/StaticLibrary); Compile method forwarders are gone - all addCSourceFile(s)/addIncludePath/addObjectFile/addLibraryPath/linkLibrary/ addWin32ResourceFile calls go through `.root_module.`, `linkLibC()` becomes `root_module.link_libc = true`, `linkSystemLibrary` takes an options arg (linkSystemLibrary2 merged); `std.ArrayList(T).init` becomes `std.array_list.Managed(T).init`; build-script dir walks use `std.Io.Dir.cwd().openDir(b.graph.io, ...)` / `it.next(b.graph.io)`; `tools/wrapfiletofile.zig` uses the 0.16 entry idiom (`main(init: std.process.Init.Minimal)`, `init.args.toSlice`, an `std.Io.Threaded` io instance, `Io.Dir.readFileAlloc`/`createFile`, `File.writeStreamingAll`, `cleanExit(io)`). `mingw_unicode_entry_point` is unchanged in 0.16. Verified: full build PASS, shells report 9.0.5, tcl-test parity identical to the 0.14.1 build (557/357/200/0).