18 KiB
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=<name> <url> 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-<lib>=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_<lib>.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/<lib>.log(full output) andout/testreports/<lib>.summary(line-record evidence summary - shape documented ingoals/G-107-buildsuite-library-tests.md, consumed by the G-103 artifact metadata; the core gate writestclcore.summarythere 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-<lib>="..." (extra tcltest/driver args) and
-Dtestnotfiles-<lib>="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 <v>
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<patchlevel>.exe(e.gtclsh9.0.5.exe) - stock shell (no piperepl patch) + batteries: loadable Thread, tclvfs with the vfs::* packages, tcllib + tcllibc (critcl accelerators). - punk
tclsh<patchlevel>-punk.exe- the same batteries on the TCLSH_PIPEREPL-patched shell, gate ENABLED BY DEFAULT (opt out withTCLSH_PIPEREPL=0; users wanting stock semantics take the plain kit). - punk-bi
tclsh<patchlevel>-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/<ver>/ beside it (tm.tcl
anchors at the dirname of tcl_library), batteries under lib/<pkg>/ 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<N>-named copies (revision via -Dfamilyrev=N, suite.tcl
-zigargs {-Dfamilyrev=N}; default 1) plus per-artifact <name>.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-<version> 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 whysuite.tclstages 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 theirtcldirparam (a hardcoded../tcl90/genericinclude, marked "todo - fix" upstream, was fixed when these copies were made).build_libtommath.zigstill carries the hardcoded path but is not called by the current recipe (tcl's in-tree tommath is compiled into the core; thelibtommath.dllseen 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 scriptstest_gate.tcl/suite_smoke.tcl/pkg_smoke.tcl(G-102) plusfamily_check.tcl/family_artifacts.tcl(G-103: kit-family self-containment verification; punkbin-layout artifact + metadata emission).test_gate.tclis 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 (seepatches/README.md) - compiled into the SEPARATEtclsh90spr/tclsh90sprzipproducts; 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=<stage> 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:
- Copy the whole tree:
src/buildsuites/suite_tcl90->src/buildsuites/<yourname>. - Edit
<yourname>/sources.config(urls, refs, pins). tclsh suite.tcl build- the stage is derived from the suite folder name (_build/<yourname>), 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'smanifest.uuid(wrapfiletofile Run steps; consumed via overlay include dirs).tclsh.exe.manifest/wish.exe.manifest- from their.intemplates (*_WIN_VERSIONper 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).